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,334 @@
"use client";
import React from "react";
import {
Box,
MenuItem,
TextField,
Typography,
Button,
IconButton,
Paper,
} from "@mui/material";
import DeleteOutlineOutlinedIcon from "@mui/icons-material/DeleteOutlineOutlined";
import AddIcon from "@mui/icons-material/Add";
import { FieldArray, Form, getIn, type FormikProps } from "formik";
import type {
JobRequestFormProps,
JobRequestFormValues,
JobRequestItem,
} from "./types";
import {
JOB_REQUEST_EMPTY_ITEM,
defaultCategories,
defaultJobs,
relationTypes,
shiftTypes,
} from "./constant";
type Props = FormikProps<JobRequestFormValues> & JobRequestFormProps;
export default function InnerJobRequestForm(props: Props) {
const {
values,
errors,
touched,
handleChange,
setFieldValue,
jobCategories = defaultCategories,
jobs = defaultJobs,
isSubmitting,
} = props;
const handleBack = () => {
props.update({
jobRequests: props.values.jobRequests,
});
props.setStep(props.step - 1);
};
return (
<Form>
<FieldArray name="jobRequests">
{({ push, remove }) => (
<>
{values.jobRequests.map((item: JobRequestItem, index: number) => {
const itemErrors = getIn(errors, `jobRequests.${index}`) || {};
const itemTouched = getIn(touched, `jobRequests.${index}`) || {};
const filteredJobs = jobs.filter(
(job) => job.jobCategoryId === item.jobCategoryId,
);
return (
<Paper
key={item.id || index}
elevation={0}
sx={{
borderRadius: "24px",
backgroundColor: "#ffffff",
border: "1px solid #e2e8f0",
p: 3,
mb: 3,
position: "relative",
}}
>
{values.jobRequests.length > 1 && (
<IconButton
onClick={() => remove(index)}
color="error"
sx={{
position: "absolute",
top: 12,
right: 12,
zIndex: 1,
}}
aria-label="remove-job-request"
>
<DeleteOutlineOutlinedIcon />
</IconButton>
)}
<Typography
sx={{
fontWeight: 700,
mb: 3,
color: "#0f172a",
}}
>
درخواست شغلی {index + 1}
</Typography>
<Box
sx={{
display: "grid",
gridTemplateColumns:
"repeat(auto-fit, minmax(340px, 1fr))",
gap: 2,
}}
>
<TextField
select
fullWidth
label="رسته شغلی*"
name={`jobRequests.${index}.jobCategoryId`}
value={item.jobCategoryId}
onChange={(e) => {
const categoryId = e.target.value;
setFieldValue(
`jobRequests.${index}.jobCategoryId`,
categoryId,
);
setFieldValue(`jobRequests.${index}.jobId`, "");
}}
error={
!!itemTouched.jobCategoryId &&
!!itemErrors.jobCategoryId
}
helperText={
itemTouched.jobCategoryId
? itemErrors.jobCategoryId
: " "
}
>
<MenuItem value="">انتخاب...</MenuItem>
{jobCategories.map((category) => (
<MenuItem key={category.id} value={category.id}>
{category.name}
</MenuItem>
))}
</TextField>
<TextField
select
fullWidth
label="شغل درخواستی*"
name={`jobRequests.${index}.jobId`}
value={item.jobId}
onChange={handleChange}
error={!!itemTouched.jobId && !!itemErrors.jobId}
helperText={itemTouched.jobId ? itemErrors.jobId : " "}
disabled={!item.jobCategoryId}
>
<MenuItem value="">انتخاب...</MenuItem>
{filteredJobs.map((job) => (
<MenuItem key={job.id} value={job.id}>
{job.title}
</MenuItem>
))}
</TextField>
<TextField
fullWidth
label="توضیحات شغل درخواست"
name={`jobRequests.${index}.requestedJobDescription`}
value={item.requestedJobDescription}
onChange={handleChange}
error={
!!itemTouched.requestedJobDescription &&
!!itemErrors.requestedJobDescription
}
helperText={
itemTouched.requestedJobDescription
? itemErrors.requestedJobDescription
: ""
}
/>
<TextField
select
fullWidth
label="نوع رابطه کاری*"
name={`jobRequests.${index}.employmentRelationType`}
value={item.employmentRelationType}
onChange={handleChange}
error={
!!itemTouched.employmentRelationType &&
!!itemErrors.employmentRelationType
}
helperText={
itemTouched.employmentRelationType
? itemErrors.employmentRelationType
: " "
}
>
<MenuItem value="">انتخاب...</MenuItem>
{relationTypes.map((relation) => (
<MenuItem key={relation} value={relation}>
{relation}
</MenuItem>
))}
</TextField>
<TextField
select
fullWidth
label="نوع شیفت درخواستی"
name={`jobRequests.${index}.requestedShiftType`}
value={item.requestedShiftType}
onChange={handleChange}
error={
!!itemTouched.requestedShiftType &&
!!itemErrors.requestedShiftType
}
helperText={
itemTouched.requestedShiftType
? itemErrors.requestedShiftType
: ""
}
>
<MenuItem value="">انتخاب...</MenuItem>
{shiftTypes.map((shift) => (
<MenuItem key={shift} value={shift}>
{shift}
</MenuItem>
))}
</TextField>
<TextField
fullWidth
label="حقوق درخواستی (ریال)"
name={`jobRequests.${index}.expectedSalary`}
value={item.expectedSalary}
onChange={(e) => {
const onlyDigits = e.target.value.replace(/[^\d]/g, "");
setFieldValue(
`jobRequests.${index}.expectedSalary`,
onlyDigits,
);
}}
error={
!!itemTouched.expectedSalary &&
!!itemErrors.expectedSalary
}
helperText={
itemTouched.expectedSalary
? itemErrors.expectedSalary
: ""
}
/>
<Box sx={{ gridColumn: { xs: "1", md: "1 / -1" } }}>
<TextField
fullWidth
label="توضیحات"
name={`jobRequests.${index}.description`}
value={item.description}
onChange={handleChange}
multiline
minRows={3}
error={
!!itemTouched.description && !!itemErrors.description
}
helperText={
itemTouched.description ? itemErrors.description : ""
}
/>
</Box>
</Box>
</Paper>
);
})}
<Button
type="button"
variant="outlined"
startIcon={<AddIcon />}
onClick={() =>
push({
...JOB_REQUEST_EMPTY_ITEM,
id: Date.now(),
})
}
sx={{
borderRadius: "12px",
mb: 3,
fontWeight: 700,
}}
>
افزودن درخواست شغلی
</Button>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
mt: 5,
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,42 @@
"use client";
import React from "react";
import { withFormik, type FormikBag } from "formik";
import InnerJobRequestForm from "./InnerJobRequestForm";
import type { JobRequestFormProps, JobRequestFormValues } from "./types";
import { JobRequestValidationSchema } from "./validation";
import { JOB_REQUEST_EMPTY_VALUES } from "./constant";
const JobRequestForm = withFormik<JobRequestFormProps, JobRequestFormValues>({
displayName: "JobRequestForm",
enableReinitialize: true,
mapPropsToValues: (props) => {
return {
jobRequests:
props.data?.jobRequests?.length > 0
? props.data.jobRequests
: JOB_REQUEST_EMPTY_VALUES.jobRequests,
};
},
validationSchema: JobRequestValidationSchema,
handleSubmit: async (
values,
bag: FormikBag<JobRequestFormProps, JobRequestFormValues>,
) => {
const { props, setSubmitting } = bag;
props.update({
jobRequests: values.jobRequests,
});
props.setStep((prev) => prev + 1);
setSubmitting(false);
},
})(InnerJobRequestForm);
export default JobRequestForm;

View File

@@ -0,0 +1,50 @@
import { JobCategoryOption, JobOption, JobRequestFormValues, JobRequestItem } from "../JobRequestForm";
export const relationTypes = [
"تمام وقت",
"پاره وقت",
"پروژه‌ای",
"ساعتی",
"قراردادی",
"کارورزی",
] as const;
export const shiftTypes = [
"ثابت صبح",
"ثابت عصر",
"ثابت شب",
"چرخشی",
"شیفتی",
"فرقی ندارد",
] as const;
export const defaultCategories: JobCategoryOption[] = [
{ id: "1", name: "پاراکلینیک" },
{ id: "2", name: "اداری" },
{ id: "3", name: "درمانی" },
];
export const defaultJobs: JobOption[] = [
{ id: "1", title: "کارشناس آزمایشگاه", jobCategoryId: "1" },
{ id: "2", title: "کارشناس رادیولوژی", jobCategoryId: "1" },
{ id: "3", title: "منشی", jobCategoryId: "2" },
{ id: "4", title: "مسئول بایگانی", jobCategoryId: "2" },
{ id: "5", title: "پرستار", jobCategoryId: "3" },
{ id: "6", title: "کمک پرستار", jobCategoryId: "3" },
];
export const JOB_REQUEST_EMPTY_ITEM: JobRequestItem = {
id: "",
jobCategoryId: "",
jobId: "",
requestedJobDescription: "",
employmentRelationType: "",
description: "",
requestedShiftType: "",
expectedSalary: "",
};
export const JOB_REQUEST_EMPTY_VALUES: JobRequestFormValues = {
jobRequests: [JOB_REQUEST_EMPTY_ITEM],
};

View File

@@ -0,0 +1,44 @@
import type React from "react";
export type JobCategoryOption = {
id: string;
name: string;
};
export type JobOption = {
id: string;
title: string;
jobCategoryId: string;
};
export interface JobRequestItem {
id: string | number;
jobCategoryId: string;
jobId: string;
requestedJobDescription: string;
employmentRelationType: string;
description: string;
requestedShiftType: string;
expectedSalary: string;
}
export interface JobRequestFormValues {
jobRequests: JobRequestItem[];
}
/**
* این type را با مدل اصلی wizard پروژه‌ات هماهنگ کن
*/
export interface WizardFormData {
jobRequests: JobRequestItem[];
// ... سایر step ها
}
export interface JobRequestFormProps {
step: number;
setStep: React.Dispatch<React.SetStateAction<number>>;
data: WizardFormData;
update: (patch: Partial<WizardFormData>) => void;
jobCategories?: JobCategoryOption[];
jobs?: JobOption[];
}

View File

@@ -0,0 +1,35 @@
import * as Yup from "yup";
export const JobRequestValidationSchema = Yup.object({
jobRequests: Yup.array()
.of(
Yup.object({
id: Yup.mixed().optional(),
jobCategoryId: Yup.string().required("رسته شغلی الزامی است"),
jobId: Yup.string().required("شغل درخواستی الزامی است"),
requestedJobDescription: Yup.string()
.max(1000, "توضیحات شغل درخواستی نباید بیشتر از 1000 کاراکتر باشد")
.optional(),
employmentRelationType: Yup.string().required(
"نوع رابطه کاری الزامی است",
),
description: Yup.string()
.max(2000, "توضیحات نباید بیشتر از 2000 کاراکتر باشد")
.optional(),
requestedShiftType: Yup.string().optional(),
expectedSalary: Yup.string()
.matches(/^\d*$/, "حقوق درخواستی فقط باید شامل عدد باشد")
.max(15, "حقوق درخواستی بیش از حد طولانی است")
.optional(),
}),
)
.min(1, "حداقل یک درخواست شغلی باید ثبت شود")
.required("ثبت درخواست شغلی الزامی است"),
});