This commit is contained in:
2026-05-31 18:00:43 +03:30
parent 98af7d639b
commit b241d12ff5
20 changed files with 939 additions and 797 deletions

View File

@@ -0,0 +1,82 @@
import { withFormik } from "formik";
import InnerIdentityForm from "./InnerIdentityForm";
import * as yup from "yup";
export interface IdentityFormValues {
firstName: string;
lastName: string;
birthDate: string;
birthPlace: string;
fatherName: string;
gender: string;
nationalCode: string;
nationality: string;
profilePhotoId: string;
religion: string;
}
export interface WizardFormData {
identity: IdentityFormValues;
}
export interface IdentityFormProps {
step: number;
setStep: React.Dispatch<React.SetStateAction<number>>;
data: WizardFormData;
update: (newData: Partial<WizardFormData>) => void;
}
const IdentityFormValidationSchema = yup.object({
firstName: yup.string().trim().required("نام الزامی است").min(2).max(50),
lastName: yup.string().trim().required("نام خانوادگی الزامی است").min(2).max(50),
birthDate: yup
.string()
.required("تاریخ تولد الزامی است")
.matches(/^\d{4}\/\d{2}\/\d{2}$/, "فرمت تاریخ تولد باید به شکل ۱۴۰۳/۰۱/۲۰ باشد"),
birthPlace: yup.string().trim().required("محل تولد الزامی است").min(2).max(80),
fatherName: yup.string().trim().required("نام پدر الزامی است").min(2).max(50),
gender: yup
.string()
.required("جنسیت الزامی است")
.oneOf(["male", "female", "other"], "جنسیت معتبر نیست"),
nationalCode: yup
.string()
.required("کد ملی الزامی است")
.matches(/^\d{10}$/, "کد ملی باید ۱۰ رقم باشد"),
nationality: yup.string().trim().required("تابعیت الزامی است").min(2).max(50),
profilePhotoId: yup.string().trim().required("عکس پرسنلی الزامی است"),
religion: yup.string().trim().required("دین الزامی است").min(2).max(50),
});
const EMPTY_IDENTITY_VALUES: IdentityFormValues = {
firstName: "",
lastName: "",
birthDate: "",
birthPlace: "",
fatherName: "",
gender: "",
nationalCode: "",
nationality: "",
profilePhotoId: "",
religion: "",
};
const IdentityForm = withFormik<IdentityFormProps, IdentityFormValues>({
enableReinitialize: true,
mapPropsToValues: (props) => {
return props.data?.identity || EMPTY_IDENTITY_VALUES;
},
validationSchema: IdentityFormValidationSchema,
handleSubmit: (values, { props }) => {
props.update({
identity: values,
});
props.setStep((prev) => prev + 1);
},
})(InnerIdentityForm);
export default IdentityForm;

View File

@@ -0,0 +1,310 @@
import {
Box,
Button,
MenuItem,
Paper,
TextField,
Typography,
} from "@mui/material";
import { ErrorMessage, Form, FormikProps } from "formik";
import { IdentityFormValues } from "@/core/types";
import { IdentityFormProps } from "./IdentityForm";
import { UploadFile } from "@mui/icons-material";
import { genderOptions, religionOptions } from "@/core/constant";
import { useState } from "react";
export default function InnerIdentityForm(
props: FormikProps<IdentityFormValues> & IdentityFormProps,
) {
console.log(props.data)
const handleBack = () => {
// قبل از رفتن به عقب، مقادیر فعلی فرم را در استیت والد ذخیره کن
props.update({ identity: props.values });
props.setStep(props.step - 1);
};
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("");
};
return (
<Box>
<Paper
elevation={0}
sx={{
width: "100%",
background: "#ffffff",
}}
>
<Form>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(340px, 1fr))",
gap: "18px",
}}
>
<div>
<TextField
label="نام"
value={props.values.firstName}
onChange={(e) =>
props.setFieldValue("firstName", e.target.value)
}
error={!!props.errors.firstName}
helperText={props.errors.firstName}
fullWidth
required
/>
<ErrorMessage component={"div"} name="firstName" />
</div>
<TextField
label="نام خانوادگی"
value={props.values.lastName}
onChange={(e) => props.setFieldValue("lastName", e.target.value)}
error={!!props.errors.lastName}
helperText={props.errors.lastName}
fullWidth
required
/>
<TextField
label="نام پدر"
value={props.values.fatherName}
onChange={(e) =>
props.setFieldValue("fatherName", e.target.value)
}
fullWidth
/>
<TextField
label="کد ملی"
value={props.values.nationalCode}
onChange={(e) =>
props.setFieldValue("nationalCode", e.target.value)
}
error={!!props.errors.nationalCode}
helperText={props.errors.nationalCode}
fullWidth
required
/>
{/* <DatePicker
label="تاریخ تولد"
value={props.values.birthDate}
onChange={(newValue) =>
props.setFieldValue("birthDate", newValue)
}
slotProps={{
textField: {
fullWidth: true,
error: !!props.errors.birthDate,
helperText: props.errors.birthDate,
},
}}
/> */}
<TextField
label="محل تولد"
value={props.values.birthPlace}
onChange={(e) =>
props.setFieldValue("birthPlace", e.target.value)
}
fullWidth
/>
<TextField
select
label="جنسیت"
value={props.values.gender}
onChange={(e) => props.setFieldValue("gender", e.target.value)}
error={!!props.errors.gender}
helperText={props.errors.gender}
fullWidth
required
>
{genderOptions.map((item) => (
<MenuItem key={item.id} value={item.value}>
{item.label}
</MenuItem>
))}
</TextField>
<TextField
select
label="دین"
value={props.values.religion}
onChange={(e) => props.setFieldValue("religion", e.target.value)}
fullWidth
>
{religionOptions.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<TextField
label="ملیت"
value={props.values.nationality}
onChange={(e) =>
props.setFieldValue("nationality", e.target.value)
}
error={!!props.errors.nationality}
helperText={props.errors.nationality}
fullWidth
required
/>
<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>
</div>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
mt: 5,
width: "100%",
}}
>
<Button
disabled={props.step === 1}
type="button"
onClick={handleBack}
sx={{
borderRadius: "12px",
color: "#64748b",
fontWeight: 700,
}}
>
بازگشت
</Button>
<Button
variant="contained"
type="submit"
sx={{
borderRadius: "12px",
px: 4,
py: 1.5,
bgcolor: `${props.step === 12 ? "green" : "#2563eb"}`,
fontWeight: 700,
}}
>
{props.step === 12 ? "اتمام و ثبت نهايي" : "گام بعدی"}
</Button>
</Box>
</Form>
</Paper>
</Box>
);
}