first commit
This commit is contained in:
336
ui/forms/EducationForm.tsx
Normal file
336
ui/forms/EducationForm.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
// EducationForm.tsx
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Box, Button, MenuItem, TextField, Typography } from "@mui/material";
|
||||
import { UploadFile } from "@mui/icons-material";
|
||||
|
||||
type DegreeValue =
|
||||
| ""
|
||||
| "زیر دیپلم"
|
||||
| "دیپلم"
|
||||
| "کاردانی"
|
||||
| "کارشناسی"
|
||||
| "کارشناسی ارشد"
|
||||
| "دکتری"
|
||||
| "حوزوی"
|
||||
| "سایر";
|
||||
|
||||
export interface EducationFormState {
|
||||
applicantId: string;
|
||||
|
||||
degree: DegreeValue | string; // allow custom too
|
||||
field: string;
|
||||
university: string;
|
||||
|
||||
startYear: number | "";
|
||||
endYear: number | "";
|
||||
|
||||
gpa: number | "";
|
||||
|
||||
description: string;
|
||||
certificateImageId: string; // FK (uuid as string)
|
||||
}
|
||||
|
||||
const initialValues: EducationFormState = {
|
||||
applicantId: "",
|
||||
|
||||
degree: "",
|
||||
field: "",
|
||||
university: "",
|
||||
|
||||
startYear: "",
|
||||
endYear: "",
|
||||
|
||||
gpa: "",
|
||||
|
||||
description: "",
|
||||
certificateImageId: "",
|
||||
};
|
||||
|
||||
function toNumberOrEmpty(v: string): number | "" {
|
||||
if (v === "") return "";
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : "";
|
||||
}
|
||||
|
||||
export default function EducationForm(props: {
|
||||
value?: EducationFormState;
|
||||
onChange?: (next: EducationFormState) => void;
|
||||
applicantId?: string;
|
||||
// اگر آپلودر فایل داری، آیدی خروجیاش رو اینجا ست میکنی
|
||||
// onPickCertificateId?: () => void; // (اختیاری)
|
||||
}) {
|
||||
const { value, onChange, applicantId } = props;
|
||||
|
||||
const [formData, setFormData] = useState<EducationFormState>(
|
||||
value ?? { ...initialValues, applicantId: applicantId ?? "" },
|
||||
);
|
||||
const [profilePhoto, setProfilePhoto] = useState<File | null>(null);
|
||||
const [profilePhotoError, setProfilePhotoError] = useState<string>("");
|
||||
|
||||
const handleProfilePhotoChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const file = event.target.files?.[0];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
if (!file.type.startsWith("image/")) {
|
||||
setProfilePhoto(null);
|
||||
setProfilePhotoError("فقط فایل تصویری مجاز است");
|
||||
return;
|
||||
}
|
||||
|
||||
const maxSize = 500 * 1024; // 500KB
|
||||
if (file.size > maxSize) {
|
||||
setProfilePhoto(null);
|
||||
setProfilePhotoError("حجم عکس باید حداکثر ۵۰۰ کیلوبایت باشد");
|
||||
return;
|
||||
}
|
||||
|
||||
setProfilePhoto(file);
|
||||
setProfilePhotoError("");
|
||||
|
||||
};
|
||||
// controlled sync
|
||||
useEffect(() => {
|
||||
if (value) setFormData(value);
|
||||
}, [value]);
|
||||
|
||||
// applicantId sync when uncontrolled
|
||||
useEffect(() => {
|
||||
if (!value && applicantId) setFormData((p) => ({ ...p, applicantId }));
|
||||
}, [applicantId, value]);
|
||||
|
||||
const setNext = (
|
||||
updater: (prev: EducationFormState) => EducationFormState,
|
||||
) => {
|
||||
setFormData((prev) => {
|
||||
const next = updater(prev);
|
||||
onChange?.(next);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleText =
|
||||
(field: keyof EducationFormState) =>
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const v = e.target.value;
|
||||
setNext((p) => ({ ...p, [field]: v }) as EducationFormState);
|
||||
};
|
||||
|
||||
const handleNumber =
|
||||
(field: keyof EducationFormState) =>
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const v = toNumberOrEmpty(e.target.value);
|
||||
setNext((p) => ({ ...p, [field]: v }) as EducationFormState);
|
||||
};
|
||||
|
||||
// Validation helpers (optional UI only)
|
||||
const startYearError =
|
||||
formData.startYear !== "" &&
|
||||
(formData.startYear < 1300 || formData.startYear > 1600);
|
||||
|
||||
const endYearError =
|
||||
formData.endYear !== "" &&
|
||||
(formData.endYear < 1300 || formData.endYear > 1600);
|
||||
|
||||
const endBeforeStart =
|
||||
formData.startYear !== "" &&
|
||||
formData.endYear !== "" &&
|
||||
Number(formData.endYear) < Number(formData.startYear);
|
||||
|
||||
const gpaError =
|
||||
formData.gpa !== "" &&
|
||||
(Number(formData.gpa) < 0 || Number(formData.gpa) > 20);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", md: "repeat(2, 1fr)" },
|
||||
gap: 2,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
select
|
||||
label="مقطع تحصیلی"
|
||||
value={formData.degree}
|
||||
onChange={handleText("degree")}
|
||||
fullWidth
|
||||
>
|
||||
<MenuItem value="">انتخاب کنید</MenuItem>
|
||||
{(
|
||||
[
|
||||
"زیر دیپلم",
|
||||
"دیپلم",
|
||||
"کاردانی",
|
||||
"کارشناسی",
|
||||
"کارشناسی ارشد",
|
||||
"دکتری",
|
||||
"حوزوی",
|
||||
"سایر",
|
||||
] as const
|
||||
).map((d) => (
|
||||
<MenuItem key={d} value={d}>
|
||||
{d}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
label="رشته تحصیلی"
|
||||
value={formData.field}
|
||||
onChange={handleText("field")}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="دانشگاه / موسسه"
|
||||
value={formData.university}
|
||||
onChange={handleText("university")}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="سال شروع"
|
||||
type="number"
|
||||
value={formData.startYear}
|
||||
onChange={handleNumber("startYear")}
|
||||
fullWidth
|
||||
error={startYearError}
|
||||
helperText={startYearError ? "سال شروع معتبر نیست (مثلاً ۱۳۹۵)" : " "}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="سال پایان"
|
||||
type="number"
|
||||
value={formData.endYear}
|
||||
onChange={handleNumber("endYear")}
|
||||
fullWidth
|
||||
error={endYearError || endBeforeStart}
|
||||
helperText={
|
||||
endBeforeStart
|
||||
? "سال پایان نمیتواند قبل از سال شروع باشد"
|
||||
: endYearError
|
||||
? "سال پایان معتبر نیست (مثلاً ۱۳۹۹)"
|
||||
: " "
|
||||
}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="معدل (از ۲۰)"
|
||||
type="number"
|
||||
value={formData.gpa}
|
||||
onChange={handleNumber("gpa")}
|
||||
fullWidth
|
||||
error={gpaError}
|
||||
helperText={gpaError ? "معدل باید بین ۰ تا ۲۰ باشد" : " "}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
border: profilePhotoError
|
||||
? "1px solid #ef4444"
|
||||
: "1px dashed #cbd5e1",
|
||||
borderRadius: "18px",
|
||||
backgroundColor: "#f8fafc",
|
||||
p: 2,
|
||||
minHeight: "100%",
|
||||
transition: "all 0.2s ease",
|
||||
"&:hover": {
|
||||
borderColor: profilePhotoError ? "#ef4444" : "#2563eb",
|
||||
backgroundColor: "#f8fbff",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: "#0f172a",
|
||||
mb: 1.5,
|
||||
fontSize: "0.95rem",
|
||||
}}
|
||||
>
|
||||
تصوير مدرك تحصيلي
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
color: "#64748b",
|
||||
fontSize: "0.82rem",
|
||||
mb: 2,
|
||||
lineHeight: 1.8,
|
||||
}}
|
||||
>
|
||||
فقط فایل تصویری مجاز است و حجم آن باید حداکثر ۵۰۰ کیلوبایت باشد.
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
component="label"
|
||||
variant="outlined"
|
||||
startIcon={<UploadFile />}
|
||||
sx={{
|
||||
borderRadius: "12px",
|
||||
borderColor: "#cbd5e1",
|
||||
color: "#2563eb",
|
||||
fontWeight: 700,
|
||||
px: 2.5,
|
||||
"&:hover": {
|
||||
borderColor: "#2563eb",
|
||||
backgroundColor: "#eff6ff",
|
||||
},
|
||||
}}
|
||||
>
|
||||
انتخاب عکس
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleProfilePhotoChange}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
{profilePhoto && (
|
||||
<Typography
|
||||
sx={{
|
||||
mt: 1.5,
|
||||
fontSize: "0.82rem",
|
||||
color: "#475569",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
فایل انتخابشده: {profilePhoto.name}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{profilePhotoError && (
|
||||
<Typography
|
||||
sx={{
|
||||
mt: 1.5,
|
||||
color: "#dc2626",
|
||||
fontSize: "0.8rem",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{profilePhotoError}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ gridColumn: { xs: "1", md: "1 / -1" } }}>
|
||||
<TextField
|
||||
label="توضیحات"
|
||||
value={formData.description}
|
||||
onChange={handleText("description")}
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={2}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export { initialValues as educationInitialValues };
|
||||
Reference in New Issue
Block a user