337 lines
8.4 KiB
TypeScript
337 lines
8.4 KiB
TypeScript
// 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 };
|