change some files

This commit is contained in:
2026-06-02 17:08:52 +03:30
parent b8dc1d0e1b
commit cfb48c5bb0
76 changed files with 5204 additions and 2555 deletions

View File

@@ -0,0 +1,326 @@
// InnerPersonalInfoForm.tsx
"use client";
import React from "react";
import { Box, Button, MenuItem, TextField } from "@mui/material";
import { Form, type FormikProps } from "formik";
import { MilitaryStatus, PersonalInfoFormValues } from "./types";
import {
EDUCATION_OPTIONS,
HOUSING_OPTIONS,
MILITARY_OPTIONS,
} from "./constants";
import { PersonalInfoFormProps } from "./PersonalInfoForm";
import { handleBack } from "@/core/utils";
type Props = FormikProps<PersonalInfoFormValues> & PersonalInfoFormProps;
export default function InnerPersonalInfoForm(props: Props) {
const { values, errors, touched, setFieldValue, handleChange } = props;
const showSpouseFields = values.maritalStatus === "متاهل";
const showChildrenCount = ["متاهل", "متارکه", "فوت همسر"].includes(
values.maritalStatus,
);
const isPermanentExempt = values.militaryStatus === "معافیت دائم";
const handleMaritalStatusChange = (
e: React.ChangeEvent<HTMLInputElement>,
) => {
const status = e.target.value;
setFieldValue("maritalStatus", status);
// پاکسازی شرطی‌ها
const isMarried = status === "متاهل";
const hasChildren = ["متاهل", "متارکه", "فوت همسر"].includes(status);
if (!isMarried) {
setFieldValue("spouseName", "");
setFieldValue("spouseEducation", "");
setFieldValue("spouseJob", "");
setFieldValue("spouseWorkplace", "");
}
if (!hasChildren) {
setFieldValue("childrenCount", "");
}
};
const handleMilitaryStatusChange = (
e: React.ChangeEvent<HTMLInputElement>,
) => {
const ms = e.target.value as MilitaryStatus;
setFieldValue("militaryStatus", ms);
if (ms !== "معافیت دائم") {
setFieldValue("permanentExemptionReason", "");
}
};
const tf = <K extends keyof PersonalInfoFormValues>(name: K) => ({
name: String(name),
value: values[name] as any,
onChange: handleChange,
fullWidth: true,
error: !!(touched as any)[name] && !!(errors as any)[name],
helperText: (touched as any)[name] ? ((errors as any)[name] as string) : "",
});
return (
<Form>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", md: "repeat(2, 1fr)" },
gap: 2,
}}
>
{/* وضعیت تاهل */}
<TextField
select
label="وضعیت تاهل"
name="maritalStatus"
value={values.maritalStatus}
onChange={handleMaritalStatusChange}
fullWidth
error={!!touched.maritalStatus && !!errors.maritalStatus}
helperText={
touched.maritalStatus ? (errors.maritalStatus as string) : ""
}
>
<MenuItem value="">انتخاب کنید</MenuItem>
<MenuItem value="مجرد">مجرد</MenuItem>
<MenuItem value="متاهل">متاهل</MenuItem>
<MenuItem value="متارکه">متارکه</MenuItem>
<MenuItem value="فوت همسر">فوت همسر</MenuItem>
</TextField>
{/* همسر (شرطی) */}
{showSpouseFields && (
<>
<TextField label="نام و نام خانوادگی همسر" {...tf("spouseName")} />
<TextField label="تحصیلات همسر" {...tf("spouseEducation")} />
<TextField label="شغل همسر" {...tf("spouseJob")} />
<TextField label="محل کار همسر" {...tf("spouseWorkplace")} />
</>
)}
{/* تعداد فرزند (شرطی) */}
{showChildrenCount && (
<TextField
label="تعداد فرزند"
name="childrenCount"
type="number"
value={values.childrenCount}
onChange={(e) =>
setFieldValue(
"childrenCount",
e.target.value === "" ? "" : Number(e.target.value),
)
}
fullWidth
error={!!touched.childrenCount && !!errors.childrenCount}
helperText={
touched.childrenCount ? (errors.childrenCount as string) : ""
}
/>
)}
{/* وضعیت نظام وظیفه */}
<TextField
select
label="وضعیت نظام وظیفه"
name="militaryStatus"
value={values.militaryStatus}
onChange={handleMilitaryStatusChange}
fullWidth
error={!!touched.militaryStatus && !!errors.militaryStatus}
helperText={
touched.militaryStatus ? (errors.militaryStatus as string) : ""
}
>
<MenuItem value="">انتخاب کنید</MenuItem>
{MILITARY_OPTIONS.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</TextField>
{/* علت معافیت دائم */}
{isPermanentExempt && (
<TextField
label="علت معافیت دائم"
{...tf("permanentExemptionReason")}
/>
)}
{/* تحصیلات پدر/مادر */}
<TextField
select
label="تحصیلات پدر"
name="fatherEducation"
value={values.fatherEducation}
onChange={handleChange}
fullWidth
error={!!touched.fatherEducation && !!errors.fatherEducation}
helperText={
touched.fatherEducation ? (errors.fatherEducation as string) : ""
}
>
<MenuItem value="">انتخاب کنید</MenuItem>
{EDUCATION_OPTIONS.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</TextField>
<TextField label="شغل پدر" {...tf("fatherJob")} />
<TextField
select
label="تحصیلات مادر"
name="motherEducation"
value={values.motherEducation}
onChange={handleChange}
fullWidth
error={!!touched.motherEducation && !!errors.motherEducation}
helperText={
touched.motherEducation ? (errors.motherEducation as string) : ""
}
>
<MenuItem value="">انتخاب کنید</MenuItem>
{EDUCATION_OPTIONS.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</TextField>
<TextField label="شغل مادر" {...tf("motherJob")} />
{/* وضعیت مسکن / شهر / آدرس */}
<TextField
select
label="وضعیت مسکن"
name="housingStatus"
value={values.housingStatus}
onChange={handleChange}
fullWidth
error={!!touched.housingStatus && !!errors.housingStatus}
helperText={
touched.housingStatus ? (errors.housingStatus as string) : ""
}
>
<MenuItem value="">انتخاب کنید</MenuItem>
{HOUSING_OPTIONS.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</TextField>{" "}
<TextField label="شهر" {...tf("city")} />
<Box sx={{ gridColumn: { md: "span 2" } }}>
<TextField label="آدرس" {...tf("address")} multiline minRows={2} />
</Box>
{/* تلفن‌ها */}
<TextField label="تلفن منزل" {...tf("homePhone")} />
<TextField label="تلفن همراه" {...tf("mobilePhone")} />
<TextField label="تلفن ضروری" {...tf("emergencyPhone")} />
<TextField label="ایمیل" {...tf("email")} />
{/* مدت سکونت */}
<TextField
label="مدت سکونت (سال)"
name="residenceDuration"
type="number"
value={values.residenceDuration}
onChange={(e) =>
setFieldValue(
"residenceDuration",
e.target.value === "" ? "" : Number(e.target.value),
)
}
fullWidth
error={!!touched.residenceDuration && !!errors.residenceDuration}
helperText={
touched.residenceDuration
? (errors.residenceDuration as string)
: ""
}
/>
{/* ایثارگر */}
<TextField
select
label="ایثارگر"
name="isVeteran"
value={String(values.isVeteran)}
onChange={(e) =>
setFieldValue("isVeteran", e.target.value === "true")
}
fullWidth
error={!!touched.isVeteran && !!errors.isVeteran}
helperText={touched.isVeteran ? (errors.isVeteran as string) : ""}
>
<MenuItem value="false">خیر</MenuItem>
<MenuItem value="true">بله</MenuItem>
</TextField>
{/* سوءپیشینه */}
<TextField
select
label="سابقه کیفری"
name="hasCriminalRecord"
value={String(values.hasCriminalRecord)}
onChange={(e) => {
const next = e.target.value === "true";
setFieldValue("hasCriminalRecord", next);
if (!next) setFieldValue("criminalDescription", "");
}}
fullWidth
error={!!touched.hasCriminalRecord && !!errors.hasCriminalRecord}
helperText={
touched.hasCriminalRecord
? (errors.hasCriminalRecord as string)
: ""
}
>
<MenuItem value="false">خیر</MenuItem>
<MenuItem value="true">بله</MenuItem>
</TextField>
{values.hasCriminalRecord && (
<TextField
label="توضیحات سوء پیشینه"
{...tf("criminalDescription")}
/>
)}
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
mt: 5,
width: "100%",
}}
>
<Button
disabled={props.step === 1}
type="button"
onClick={() => handleBack(props, "personalInfo")}
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>
);
}

View File

@@ -0,0 +1,49 @@
// PersonalInfoForm.tsx
"use client";
import React from "react";
import { withFormik, type FormikBag } from "formik";
import InnerPersonalInfoForm from "./InnerPersonalInfoForm";
import { PersonalInfoFormValues } from "./types";
import { PERSONAL_INFO_EMPTY_VALUES } from "./constants";
import { PersonalInfoValidationSchema } from "./validation/PersonalInfoFormValidation";
/** اینا رو با Wizard خودت هماهنگ کن */
export interface WizardFormData {
personalInfo: PersonalInfoFormValues;
// ... بقیه step ها
}
export type PersonalInfoFormProps = {
step: number;
setStep: React.Dispatch<React.SetStateAction<number>>;
data: WizardFormData;
update: (patch: Partial<WizardFormData>) => void;
};
const PersonalInfoForm = withFormik<PersonalInfoFormProps, PersonalInfoFormValues>({
displayName: "PersonalInfoForm",
enableReinitialize: true,
mapPropsToValues: (props) => {
return props.data?.personalInfo ?? PERSONAL_INFO_EMPTY_VALUES;
},
// validationSchema: PersonalInfoValidationSchema,
handleSubmit: async (values, bag: FormikBag<PersonalInfoFormProps, PersonalInfoFormValues>) => {
const { props, setSubmitting } = bag;
props.update({ personalInfo: values });
// برو مرحله بعد
props.setStep((prev) => prev + 1);
setSubmitting(false);
},
})(InnerPersonalInfoForm);
export default PersonalInfoForm;

View File

@@ -0,0 +1,61 @@
import {
EducationLevel,
HousingStatus,
MilitaryStatus,
PersonalInfoFormValues,
} from "../types";
export const PERSONAL_INFO_EMPTY_VALUES: PersonalInfoFormValues = {
maritalStatus: "",
militaryStatus: "",
permanentExemptionReason: "",
fatherEducation: "",
fatherJob: "",
motherEducation: "",
motherJob: "",
housingStatus: "",
city: "",
address: "",
homePhone: "",
mobilePhone: "",
emergencyPhone: "",
email: "",
residenceDuration: "",
isVeteran: false,
hasCriminalRecord: false,
criminalDescription: "",
spouseName: "",
spouseEducation: "",
spouseJob: "",
spouseWorkplace: "",
childrenCount: "",
};
export const MILITARY_OPTIONS: Exclude<MilitaryStatus, "">[] = [
"کارت پایان خدمت",
"در حال خدمت",
"معافیت تحصیلی",
"معافیت دائم",
"انجام نشده",
];
export const EDUCATION_OPTIONS: Exclude<EducationLevel, "">[] = [
"زیر دیپلم",
"دیپلم",
"دانشجو",
"کاردانی",
"کارشناسی",
"کارشناسی ارشد",
"دکترا",
];
export const HOUSING_OPTIONS: Exclude<HousingStatus, "">[] = [
"منزل شخصی",
"منزل والدین",
"منزل استیجاری",
"سایر",
];

View File

@@ -0,0 +1,57 @@
// personal-info.types.ts
export type MilitaryStatus =
| ""
| "کارت پایان خدمت"
| "در حال خدمت"
| "معافیت تحصیلی"
| "معافیت دائم"
| "انجام نشده";
export type EducationLevel =
| ""
| "زیر دیپلم"
| "دیپلم"
| "دانشجو"
| "کاردانی"
| "کارشناسی"
| "کارشناسی ارشد"
| "دکترا";
export type HousingStatus =
| ""
| "منزل شخصی"
| "منزل والدین"
| "منزل استیجاری"
| "سایر";
export interface PersonalInfoFormValues {
maritalStatus: string;
militaryStatus: MilitaryStatus;
permanentExemptionReason: string;
fatherEducation: EducationLevel;
fatherJob: string;
motherEducation: EducationLevel;
motherJob: string;
housingStatus: HousingStatus;
city: string;
address: string;
homePhone: string;
mobilePhone: string;
emergencyPhone: string;
email: string;
residenceDuration: number | "";
isVeteran: boolean;
hasCriminalRecord: boolean;
criminalDescription: string;
spouseName: string;
spouseEducation: string;
spouseJob: string;
spouseWorkplace: string;
childrenCount: number | "";
}

View File

@@ -0,0 +1,112 @@
// PersonalInfoForm.validation.ts
import * as yup from "yup";
import { PersonalInfoFormValues } from "../types";
export const PersonalInfoValidationSchema = yup
.object({
maritalStatus: yup.string().trim().required("وضعیت تاهل را انتخاب کنید"),
militaryStatus: yup
.mixed<PersonalInfoFormValues["militaryStatus"]>()
.oneOf([
"",
"کارت پایان خدمت",
"در حال خدمت",
"معافیت تحصیلی",
"معافیت دائم",
"انجام نشده",
])
.required("وضعیت نظام وظیفه را انتخاب کنید"),
permanentExemptionReason: yup
.string()
.trim()
.when("militaryStatus", {
is: "معافیت دائم",
then: (s) => s.required("علت معافیت دائم الزامی است"),
otherwise: (s) => s.notRequired(),
}),
fatherEducation: yup.string().required("تحصیلات پدر را انتخاب کنید"),
fatherJob: yup.string().trim().required("شغل پدر الزامی است"),
motherEducation: yup.string().required("تحصیلات مادر را انتخاب کنید"),
motherJob: yup.string().trim().required("شغل مادر الزامی است"),
housingStatus: yup
.mixed<PersonalInfoFormValues["housingStatus"]>()
.oneOf(["", "منزل شخصی", "منزل والدین", "منزل استیجاری", "سایر"])
.required("وضعیت مسکن را انتخاب کنید"),
city: yup.string().trim().required("شهر الزامی است"),
address: yup.string().trim().required("آدرس الزامی است"),
homePhone: yup.string().trim().notRequired(),
mobilePhone: yup.string().trim().required("تلفن همراه الزامی است"),
emergencyPhone: yup.string().trim().notRequired(),
email: yup.string().trim().email("ایمیل نامعتبر است").notRequired(),
residenceDuration: yup
.mixed<number | "">()
.test(
"residenceDuration",
"مدت سکونت نامعتبر است",
(v) => v === "" || (typeof v === "number" && v >= 0),
)
.notRequired(),
isVeteran: yup.boolean().required(),
hasCriminalRecord: yup.boolean().required(),
criminalDescription: yup
.string()
.trim()
.when("hasCriminalRecord", {
is: true,
then: (s) => s.required("توضیحات سوء پیشینه الزامی است"),
otherwise: (s) => s.notRequired(),
}),
spouseName: yup
.string()
.trim()
.when("maritalStatus", {
is: "متاهل",
then: (s) => s.required("نام همسر الزامی است"),
otherwise: (s) => s.notRequired(),
}),
spouseEducation: yup
.string()
.trim()
.when("maritalStatus", {
is: "متاهل",
then: (s) => s.required("تحصیلات همسر الزامی است"),
otherwise: (s) => s.notRequired(),
}),
spouseJob: yup
.string()
.trim()
.when("maritalStatus", {
is: "متاهل",
then: (s) => s.required("شغل همسر الزامی است"),
otherwise: (s) => s.notRequired(),
}),
spouseWorkplace: yup
.string()
.trim()
.when("maritalStatus", {
is: "متاهل",
then: (s) => s.required("محل کار همسر الزامی است"),
otherwise: (s) => s.notRequired(),
}),
childrenCount: yup.mixed<number | "">().when("maritalStatus", {
is: (ms: string) => ["متاهل", "متارکه", "فوت همسر"].includes(ms),
then: (s) =>
s.test(
"childrenCount",
"تعداد فرزند نامعتبر است",
(v) => v === "" || (typeof v === "number" && v >= 0),
),
otherwise: (s) => s.notRequired(),
}),
})
.required();