first commit

This commit is contained in:
2026-05-31 14:22:39 +03:30
commit 98af7d639b
54 changed files with 11545 additions and 0 deletions

336
ui/forms/EducationForm.tsx Normal file
View 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 };