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,293 @@
"use client";
import React from "react";
import {
Box,
FormControlLabel,
IconButton,
Paper,
Switch,
TextField,
Typography,
Button,
} from "@mui/material";
import { DeleteOutlineOutlined, Add } from "@mui/icons-material";
import { FieldArray, Form, getIn, type FormikProps } from "formik";
import type {
WorkExperienceFormProps,
WorkExperienceFormValues,
WorkExperienceFormItem,
} from "./types";
import {
WORK_EXPERIENCE_EMPTY_ITEM,
WORK_EXPERIENCE_NO_EXPERIENCE_ITEM,
} from "./constant";
type Props = FormikProps<WorkExperienceFormValues> & WorkExperienceFormProps;
export default function InnerWorkExperienceForm(props: Props) {
const {
values,
errors,
touched,
setFieldValue,
handleChange,
isSubmitting,
} = props;
const workExperiences = values.workExperiences || [];
const hasNoExperienceMode =
workExperiences.length === 1 && workExperiences[0]?.hasNoExperience;
const handleBack = () => {
props.update({
workExperiences: values.workExperiences,
});
props.setStep(props.step - 1);
};
return (
<Form>
<FieldArray name="workExperiences">
{({ push, remove, replace }) => (
<>
{workExperiences.map((item: WorkExperienceFormItem, index: number) => {
const itemErrors = getIn(errors, `workExperiences.${index}`) || {};
const itemTouched = getIn(touched, `workExperiences.${index}`) || {};
const setHasNoExperience = (checked: boolean) => {
if (checked) {
replace(0, {
...WORK_EXPERIENCE_NO_EXPERIENCE_ITEM,
id: Date.now(),
});
for (let i = workExperiences.length - 1; i >= 1; i--) {
remove(i);
}
} else {
setFieldValue(`workExperiences.${index}.hasNoExperience`, false);
}
};
return (
<Paper
key={item.id || index}
elevation={0}
sx={{
p: { xs: 2, md: 3 },
borderRadius: "24px",
border: "1px solid #e2e8f0",
backgroundColor: "#fff",
mb: 3,
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
mb: 2,
}}
>
<Typography sx={{ fontWeight: 700 }}>
سابقه کاری {index + 1}
</Typography>
<IconButton
onClick={() => remove(index)}
disabled={workExperiences.length === 1 || hasNoExperienceMode}
size="small"
color="error"
>
<DeleteOutlineOutlined />
</IconButton>
</Box>
<Box
sx={{
display: "grid",
gridTemplateColumns: {
xs: "1fr",
md: "repeat(2, 1fr)",
},
gap: 2,
}}
>
<FormControlLabel
sx={{ gridColumn: "1 / -1" }}
control={
<Switch
checked={item.hasNoExperience}
onChange={(e) => setHasNoExperience(e.target.checked)}
/>
}
label="فاقد سابقه کاری هستم"
/>
<TextField
label="نام شرکت"
name={`workExperiences.${index}.companyName`}
value={item.companyName}
onChange={handleChange}
fullWidth
disabled={item.hasNoExperience}
error={!!itemTouched.companyName && !!itemErrors.companyName}
helperText={
itemTouched.companyName ? itemErrors.companyName : ""
}
/>
<TextField
label="آخرین سمت"
name={`workExperiences.${index}.lastPosition`}
value={item.lastPosition}
onChange={handleChange}
fullWidth
disabled={item.hasNoExperience}
error={!!itemTouched.lastPosition && !!itemErrors.lastPosition}
helperText={
itemTouched.lastPosition ? itemErrors.lastPosition : ""
}
/>
<TextField
label="سال شروع"
name={`workExperiences.${index}.startYear`}
value={item.startYear}
onChange={(e) => {
const onlyDigits = e.target.value.replace(/[^\d]/g, "");
setFieldValue(
`workExperiences.${index}.startYear`,
onlyDigits,
);
}}
fullWidth
disabled={item.hasNoExperience}
inputMode="numeric"
error={!!itemTouched.startYear && !!itemErrors.startYear}
helperText={itemTouched.startYear ? itemErrors.startYear : ""}
/>
<TextField
label="سال پایان"
name={`workExperiences.${index}.endYear`}
value={item.endYear}
onChange={(e) => {
const onlyDigits = e.target.value.replace(/[^\d]/g, "");
setFieldValue(
`workExperiences.${index}.endYear`,
onlyDigits,
);
}}
fullWidth
disabled={item.hasNoExperience}
inputMode="numeric"
error={!!itemTouched.endYear && !!itemErrors.endYear}
helperText={itemTouched.endYear ? itemErrors.endYear : ""}
/>
<TextField
label="علت ترک کار"
name={`workExperiences.${index}.leavingReason`}
value={item.leavingReason}
onChange={handleChange}
fullWidth
disabled={item.hasNoExperience}
error={!!itemTouched.leavingReason && !!itemErrors.leavingReason}
helperText={
itemTouched.leavingReason ? itemErrors.leavingReason : ""
}
sx={{ gridColumn: { md: "1 / -1" } }}
/>
<TextField
label="توضیحات"
name={`workExperiences.${index}.description`}
value={item.description}
onChange={handleChange}
fullWidth
disabled={item.hasNoExperience}
multiline
minRows={3}
error={!!itemTouched.description && !!itemErrors.description}
helperText={
itemTouched.description ? itemErrors.description : ""
}
sx={{ gridColumn: { md: "1 / -1" } }}
/>
</Box>
</Paper>
);
})}
{!hasNoExperienceMode && (
<Button
type="button"
variant="outlined"
startIcon={<Add />}
onClick={() =>
push({
...WORK_EXPERIENCE_EMPTY_ITEM,
id: Date.now(),
})
}
sx={{
borderRadius: "12px",
mb: 3,
fontWeight: 700,
}}
>
افزودن سابقه کاری
</Button>
)}
{typeof errors.workExperiences === "string" && (
<Typography color="error" sx={{ mb: 2 }}>
{errors.workExperiences}
</Typography>
)}
<Box
sx={{
display: "flex",
justifyContent: "space-between",
mt: 4,
width: "100%",
}}
>
<Button
disabled={props.step === 1 || isSubmitting}
type="button"
onClick={handleBack}
sx={{
borderRadius: "12px",
color: "#64748b",
fontWeight: 700,
}}
>
بازگشت
</Button>
<Button
variant="contained"
type="submit"
disabled={isSubmitting}
sx={{
borderRadius: "12px",
px: 4,
py: 1.5,
bgcolor: props.step === 12 ? "green" : "#2563eb",
fontWeight: 700,
}}
>
{props.step === 12 ? "اتمام و ثبت نهایی" : "گام بعدی"}
</Button>
</Box>
</>
)}
</FieldArray>
</Form>
);
}

View File

@@ -0,0 +1,46 @@
"use client";
import { withFormik, type FormikBag } from "formik";
import type {
WorkExperienceFormProps,
WorkExperienceFormValues,
} from "./types";
import { WORK_EXPERIENCE_EMPTY_VALUES } from "./constant";
import { WorkExperienceValidationSchema } from "./validation";
import InnerWorkExperienceForm from "./InnerWorkExperienceForm";
const WorkExperienceForm = withFormik<
WorkExperienceFormProps,
WorkExperienceFormValues
>({
displayName: "WorkExperienceForm",
enableReinitialize: true,
mapPropsToValues: (props) => {
return {
workExperiences:
props.data?.workExperiences?.length > 0
? props.data.workExperiences
: WORK_EXPERIENCE_EMPTY_VALUES.workExperiences,
};
},
validationSchema: WorkExperienceValidationSchema,
handleSubmit: async (
values,
bag: FormikBag<WorkExperienceFormProps, WorkExperienceFormValues>,
) => {
const { props, setSubmitting } = bag;
props.update({
workExperiences: values.workExperiences,
});
props.setStep((prev) => prev + 1);
setSubmitting(false);
},
})(InnerWorkExperienceForm);
export default WorkExperienceForm;

View File

@@ -0,0 +1,27 @@
import type { WorkExperienceFormItem, WorkExperienceFormValues } from "../types";
export const WORK_EXPERIENCE_EMPTY_ITEM: WorkExperienceFormItem = {
id: "",
hasNoExperience: false,
companyName: "",
lastPosition: "",
startYear: "",
endYear: "",
leavingReason: "",
description: "",
};
export const WORK_EXPERIENCE_NO_EXPERIENCE_ITEM: WorkExperienceFormItem = {
id: "",
hasNoExperience: true,
companyName: "",
lastPosition: "",
startYear: "",
endYear: "",
leavingReason: "",
description: "",
};
export const WORK_EXPERIENCE_EMPTY_VALUES: WorkExperienceFormValues = {
workExperiences: [{ ...WORK_EXPERIENCE_EMPTY_ITEM }],
};

View File

@@ -0,0 +1,29 @@
import type React from "react";
export interface WorkExperienceFormItem {
id?: string | number;
hasNoExperience: boolean;
companyName: string;
lastPosition: string;
startYear: string;
endYear: string;
leavingReason: string;
description: string;
}
export interface WorkExperienceFormValues {
workExperiences: WorkExperienceFormItem[];
}
/** با مدل اصلی ویزارد پروژه‌ات هماهنگ شود */
export interface WizardFormData {
workExperiences: WorkExperienceFormItem[];
// ... سایر stepها
}
export interface WorkExperienceFormProps {
step: number;
setStep: React.Dispatch<React.SetStateAction<number>>;
data: WizardFormData;
update: (patch: Partial<WizardFormData>) => void;
}

View File

@@ -0,0 +1,61 @@
import * as Yup from "yup";
export const WorkExperienceValidationSchema = Yup.object({
workExperiences: Yup.array()
.of(
Yup.object({
id: Yup.mixed().optional(),
hasNoExperience: Yup.boolean().required(),
companyName: Yup.string().when("hasNoExperience", {
is: false,
then: (schema) => schema.required("نام شرکت الزامی است"),
otherwise: (schema) => schema.optional(),
}),
lastPosition: Yup.string().when("hasNoExperience", {
is: false,
then: (schema) => schema.required("آخرین سمت الزامی است"),
otherwise: (schema) => schema.optional(),
}),
startYear: Yup.string().when("hasNoExperience", {
is: false,
then: (schema) =>
schema
.required("سال شروع الزامی است")
.matches(/^\d{4}$/, "سال شروع باید 4 رقم باشد"),
otherwise: (schema) => schema.optional(),
}),
endYear: Yup.string().when("hasNoExperience", {
is: false,
then: (schema) =>
schema
.required("سال پایان الزامی است")
.matches(/^\d{4}$/, "سال پایان باید 4 رقم باشد"),
otherwise: (schema) => schema.optional(),
}),
leavingReason: Yup.string().when("hasNoExperience", {
is: false,
then: (schema) => schema.required("علت ترک کار الزامی است"),
otherwise: (schema) => schema.optional(),
}),
description: Yup.string().optional(),
}),
)
.min(1, "حداقل یک رکورد باید وجود داشته باشد")
.test(
"single-no-experience-item",
"در صورت انتخاب فاقد سابقه، فقط یک رکورد مجاز است",
(value) => {
if (!value || value.length === 0) return false;
const hasNoExperience = value.some((item) => item.hasNoExperience);
if (!hasNoExperience) return true;
return value.length === 1 && value[0].hasNoExperience === true;
},
)
.required("ثبت سابقه کاری الزامی است"),
});