change some files
This commit is contained in:
293
ui/forms/workExperience/InnerWorkExperienceForm.tsx
Normal file
293
ui/forms/workExperience/InnerWorkExperienceForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
46
ui/forms/workExperience/WorkExperienceForm.tsx
Normal file
46
ui/forms/workExperience/WorkExperienceForm.tsx
Normal 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;
|
||||
27
ui/forms/workExperience/constant/index.ts
Normal file
27
ui/forms/workExperience/constant/index.ts
Normal 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 }],
|
||||
};
|
||||
29
ui/forms/workExperience/types/index.ts
Normal file
29
ui/forms/workExperience/types/index.ts
Normal 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;
|
||||
}
|
||||
61
ui/forms/workExperience/validation/index.ts
Normal file
61
ui/forms/workExperience/validation/index.ts
Normal 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("ثبت سابقه کاری الزامی است"),
|
||||
});
|
||||
Reference in New Issue
Block a user