change some files
This commit is contained in:
297
ui/forms/physicalInfo/InnerPhysicalInfoForm.tsx
Normal file
297
ui/forms/physicalInfo/InnerPhysicalInfoForm.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
// InnerPhysicalInfoForm.tsx
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { Box, Button, MenuItem, TextField } from "@mui/material";
|
||||
import { Form, type FormikProps } from "formik";
|
||||
import { PhysicalInfoFormValues } from "./types";
|
||||
import { PhysicalInfoFormProps } from "./PhysicalInfoForm";
|
||||
import { BLOOD_TYPE_OPTIONS } from "./constants";
|
||||
import { handleBack } from "@/core/utils";
|
||||
|
||||
type Props = FormikProps<PhysicalInfoFormValues> & PhysicalInfoFormProps;
|
||||
|
||||
function round1(n: number) {
|
||||
return Math.round(n * 10) / 10;
|
||||
}
|
||||
|
||||
export default function InnerPhysicalInfoForm(props: Props) {
|
||||
const { values, errors, touched, setFieldValue, handleChange } = props;
|
||||
|
||||
const computedBmi = useMemo(() => {
|
||||
if (values.height === "" || values.weight === "") return "";
|
||||
const hMeters = Number(values.height) / 100;
|
||||
if (!hMeters || hMeters <= 0) return "";
|
||||
const bmi = Number(values.weight) / (hMeters * hMeters);
|
||||
return Number.isFinite(bmi) ? round1(bmi) : "";
|
||||
}, [values.height, values.weight]);
|
||||
|
||||
useEffect(() => {
|
||||
if (values.bmi !== computedBmi) {
|
||||
setFieldValue("bmi", computedBmi, false);
|
||||
}
|
||||
}, [computedBmi, setFieldValue, values.bmi]);
|
||||
|
||||
const tf = <K extends keyof PhysicalInfoFormValues>(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) : "",
|
||||
});
|
||||
|
||||
console.log(props.errors)
|
||||
return (
|
||||
<Form>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", md: "repeat(2, 1fr)" },
|
||||
gap: 2,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
{/* bloodType */}
|
||||
<TextField
|
||||
select
|
||||
label="گروه خونی"
|
||||
name="bloodType"
|
||||
value={values.bloodType}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
error={!!touched.bloodType && !!errors.bloodType}
|
||||
helperText={touched.bloodType ? (errors.bloodType as string) : ""}
|
||||
>
|
||||
<MenuItem value="">انتخاب کنید</MenuItem>
|
||||
{BLOOD_TYPE_OPTIONS.map((bt) => (
|
||||
<MenuItem key={bt} value={bt}>
|
||||
{bt}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
{/* height */}
|
||||
<TextField
|
||||
label="قد (سانتیمتر)"
|
||||
name="height"
|
||||
type="number"
|
||||
value={values.height}
|
||||
onChange={(e) =>
|
||||
setFieldValue(
|
||||
"height",
|
||||
e.target.value === "" ? "" : Number(e.target.value),
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
error={!!touched.height && !!errors.height}
|
||||
helperText={touched.height ? (errors.height as string) : ""}
|
||||
/>
|
||||
|
||||
{/* weight */}
|
||||
<TextField
|
||||
label="وزن (کیلوگرم)"
|
||||
name="weight"
|
||||
type="number"
|
||||
value={values.weight}
|
||||
onChange={(e) =>
|
||||
setFieldValue(
|
||||
"weight",
|
||||
e.target.value === "" ? "" : Number(e.target.value),
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
error={!!touched.weight && !!errors.weight}
|
||||
helperText={touched.weight ? (errors.weight as string) : ""}
|
||||
/>
|
||||
|
||||
{/* bmi */}
|
||||
<TextField
|
||||
label="BMI"
|
||||
name="bmi"
|
||||
type="number"
|
||||
value={values.bmi}
|
||||
fullWidth
|
||||
disabled
|
||||
error={!!touched.bmi && !!errors.bmi}
|
||||
helperText={
|
||||
touched.bmi && errors.bmi
|
||||
? (errors.bmi as string)
|
||||
: values.height !== "" && values.weight !== ""
|
||||
? "بهصورت خودکار از قد و وزن محاسبه میشود"
|
||||
: "برای محاسبه BMI، قد و وزن را وارد کنید"
|
||||
}
|
||||
/>
|
||||
|
||||
{/* specialMark */}
|
||||
<TextField label="علامت مشخصه" {...tf("specialMark")} />
|
||||
|
||||
{/* hasDisability */}
|
||||
<TextField
|
||||
select
|
||||
label="معلولیت دارد؟"
|
||||
name="hasDisability"
|
||||
value={String(values.hasDisability)}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value === "true";
|
||||
setFieldValue("hasDisability", next);
|
||||
if (!next) {
|
||||
setFieldValue("disabilityDescription", "");
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
error={!!touched.hasDisability && !!errors.hasDisability}
|
||||
helperText={
|
||||
touched.hasDisability ? (errors.hasDisability as string) : ""
|
||||
}
|
||||
>
|
||||
<MenuItem value="false">خیر</MenuItem>
|
||||
<MenuItem value="true">بله</MenuItem>
|
||||
</TextField>
|
||||
|
||||
{/* hasChronicDisease */}
|
||||
<TextField
|
||||
select
|
||||
label="بیماری مزمن دارد؟"
|
||||
name="hasChronicDisease"
|
||||
value={String(values.hasChronicDisease)}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value === "true";
|
||||
setFieldValue("hasChronicDisease", next);
|
||||
if (!next) {
|
||||
setFieldValue("chronicDiseaseDescription", "");
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
error={!!touched.hasChronicDisease && !!errors.hasChronicDisease}
|
||||
helperText={
|
||||
touched.hasChronicDisease
|
||||
? (errors.hasChronicDisease as string)
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<MenuItem value="false">خیر</MenuItem>
|
||||
<MenuItem value="true">بله</MenuItem>
|
||||
</TextField>
|
||||
|
||||
{/* disabilityDescription */}
|
||||
{values.hasDisability && (
|
||||
<Box sx={{ gridColumn: { xs: "1", md: "1 / -1" } }}>
|
||||
<TextField
|
||||
label="توضیحات معلولیت"
|
||||
name="disabilityDescription"
|
||||
value={values.disabilityDescription}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={2}
|
||||
error={
|
||||
!!touched.disabilityDescription &&
|
||||
!!errors.disabilityDescription
|
||||
}
|
||||
helperText={
|
||||
touched.disabilityDescription
|
||||
? (errors.disabilityDescription as string)
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* chronicDiseaseDescription */}
|
||||
{values.hasChronicDisease && (
|
||||
<Box sx={{ gridColumn: { xs: "1", md: "1 / -1" } }}>
|
||||
<TextField
|
||||
label="توضیحات بیماری مزمن"
|
||||
name="chronicDiseaseDescription"
|
||||
value={values.chronicDiseaseDescription}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={2}
|
||||
error={
|
||||
!!touched.chronicDiseaseDescription &&
|
||||
!!errors.chronicDiseaseDescription
|
||||
}
|
||||
helperText={
|
||||
touched.chronicDiseaseDescription
|
||||
? (errors.chronicDiseaseDescription as string)
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* surgeryHistory */}
|
||||
<Box sx={{ gridColumn: { xs: "1", md: "1 / -1" } }}>
|
||||
<TextField
|
||||
label="سابقه جراحی"
|
||||
name="surgeryHistory"
|
||||
value={values.surgeryHistory}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={2}
|
||||
error={!!touched.surgeryHistory && !!errors.surgeryHistory}
|
||||
helperText={
|
||||
touched.surgeryHistory ? (errors.surgeryHistory as string) : ""
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* medications */}
|
||||
<Box sx={{ gridColumn: { xs: "1", md: "1 / -1" } }}>
|
||||
<TextField
|
||||
label="داروهای مصرفی"
|
||||
name="medications"
|
||||
value={values.medications}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={2}
|
||||
error={!!touched.medications && !!errors.medications}
|
||||
helperText={
|
||||
touched.medications ? (errors.medications as string) : ""
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
mt: 5,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
disabled={props.step === 1}
|
||||
type="button"
|
||||
onClick={() => handleBack(props,"physicalInfo")}
|
||||
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>
|
||||
);
|
||||
}
|
||||
52
ui/forms/physicalInfo/PhysicalInfoForm.tsx
Normal file
52
ui/forms/physicalInfo/PhysicalInfoForm.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
// PhysicalInfoForm.tsx
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { withFormik, type FormikBag } from "formik";
|
||||
|
||||
import InnerPhysicalInfoForm from "./InnerPhysicalInfoForm";
|
||||
import { PhysicalInfoFormValues } from "./types";
|
||||
import { PHYSICAL_INFO_EMPTY_VALUES } from "./constants";
|
||||
import { PhysicalInfoValidationSchema } from "./validation";
|
||||
|
||||
/** این بخش را با ساختار اصلی WizardFormData پروژه خودت هماهنگ کن */
|
||||
export interface WizardFormData {
|
||||
physicalInfo: PhysicalInfoFormValues;
|
||||
// ... بقیه step ها
|
||||
}
|
||||
|
||||
export type PhysicalInfoFormProps = {
|
||||
step: number;
|
||||
setStep: React.Dispatch<React.SetStateAction<number>>;
|
||||
data: WizardFormData;
|
||||
update: (patch: Partial<WizardFormData>) => void;
|
||||
};
|
||||
|
||||
const PhysicalInfoForm = withFormik<
|
||||
PhysicalInfoFormProps,
|
||||
PhysicalInfoFormValues
|
||||
>({
|
||||
displayName: "PhysicalInfoForm",
|
||||
|
||||
enableReinitialize: true,
|
||||
|
||||
mapPropsToValues: (props) => {
|
||||
return props.data?.physicalInfo ?? PHYSICAL_INFO_EMPTY_VALUES;
|
||||
},
|
||||
|
||||
// validationSchema: PhysicalInfoValidationSchema,
|
||||
|
||||
handleSubmit: async (
|
||||
values,
|
||||
bag: FormikBag<PhysicalInfoFormProps, PhysicalInfoFormValues>,
|
||||
) => {
|
||||
const { props, setSubmitting } = bag;
|
||||
|
||||
props.update({ physicalInfo: values });
|
||||
props.setStep((prev) => prev + 1);
|
||||
|
||||
setSubmitting(false);
|
||||
},
|
||||
})(InnerPhysicalInfoForm);
|
||||
|
||||
export default PhysicalInfoForm;
|
||||
35
ui/forms/physicalInfo/constants/index.ts
Normal file
35
ui/forms/physicalInfo/constants/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// physical-info.constants.ts
|
||||
|
||||
import { BloodType, PhysicalInfoFormValues } from "../types";
|
||||
|
||||
|
||||
export const PHYSICAL_INFO_EMPTY_VALUES: PhysicalInfoFormValues = {
|
||||
applicantId: "",
|
||||
|
||||
bloodType: "",
|
||||
height: "",
|
||||
weight: "",
|
||||
bmi: "",
|
||||
|
||||
hasDisability: false,
|
||||
disabilityDescription: "",
|
||||
|
||||
hasChronicDisease: false,
|
||||
chronicDiseaseDescription: "",
|
||||
|
||||
surgeryHistory: "",
|
||||
medications: "",
|
||||
|
||||
specialMark: "",
|
||||
};
|
||||
|
||||
export const BLOOD_TYPE_OPTIONS: Exclude<BloodType, "">[] = [
|
||||
"A+",
|
||||
"A-",
|
||||
"B+",
|
||||
"B-",
|
||||
"AB+",
|
||||
"AB-",
|
||||
"O+",
|
||||
"O-",
|
||||
];
|
||||
32
ui/forms/physicalInfo/types/index.ts
Normal file
32
ui/forms/physicalInfo/types/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// physical-info.types.ts
|
||||
|
||||
export type BloodType =
|
||||
| ""
|
||||
| "A+"
|
||||
| "A-"
|
||||
| "B+"
|
||||
| "B-"
|
||||
| "AB+"
|
||||
| "AB-"
|
||||
| "O+"
|
||||
| "O-";
|
||||
|
||||
export interface PhysicalInfoFormValues {
|
||||
applicantId: string;
|
||||
|
||||
bloodType: BloodType;
|
||||
height: number | ""; // cm
|
||||
weight: number | ""; // kg
|
||||
bmi: number | ""; // auto calculated
|
||||
|
||||
hasDisability: boolean;
|
||||
disabilityDescription: string;
|
||||
|
||||
hasChronicDisease: boolean;
|
||||
chronicDiseaseDescription: string;
|
||||
|
||||
surgeryHistory: string;
|
||||
medications: string;
|
||||
|
||||
specialMark: string;
|
||||
}
|
||||
68
ui/forms/physicalInfo/validation/index.ts
Normal file
68
ui/forms/physicalInfo/validation/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// PhysicalInfoForm.validation.ts
|
||||
import * as yup from "yup";
|
||||
import { PhysicalInfoFormValues } from "../types";
|
||||
|
||||
export const PhysicalInfoValidationSchema =
|
||||
yup
|
||||
.object({
|
||||
// applicantId: yup.string().trim().required("کد متقاضی الزامی است"),
|
||||
|
||||
bloodType: yup
|
||||
.mixed<PhysicalInfoFormValues["bloodType"]>()
|
||||
.oneOf(["", "A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"])
|
||||
.required("گروه خونی را انتخاب کنید"),
|
||||
|
||||
height: yup
|
||||
.mixed<number | "">()
|
||||
.test(
|
||||
"height-valid",
|
||||
"قد نامعتبر است",
|
||||
(v) => v === "" || (typeof v === "number" && v > 0 && v <= 300),
|
||||
)
|
||||
.required("قد الزامی است"),
|
||||
|
||||
weight: yup
|
||||
.mixed<number | "">()
|
||||
.test(
|
||||
"weight-valid",
|
||||
"وزن نامعتبر است",
|
||||
(v) => v === "" || (typeof v === "number" && v > 0 && v <= 500),
|
||||
)
|
||||
.required("وزن الزامی است"),
|
||||
|
||||
bmi: yup
|
||||
.mixed<number | "">()
|
||||
.test(
|
||||
"bmi-valid",
|
||||
"BMI نامعتبر است يا قد و وزن اشتباه است",
|
||||
(v) => v === "" || (typeof v === "number" && v > 0 && v <= 100),
|
||||
)
|
||||
.notRequired(),
|
||||
|
||||
hasDisability: yup.boolean().required(),
|
||||
|
||||
disabilityDescription: yup
|
||||
.string()
|
||||
.trim()
|
||||
.when("hasDisability", {
|
||||
is: true,
|
||||
then: (s) => s.required("توضیحات معلولیت الزامی است"),
|
||||
otherwise: (s) => s.notRequired(),
|
||||
}),
|
||||
|
||||
hasChronicDisease: yup.boolean().required(),
|
||||
|
||||
chronicDiseaseDescription: yup
|
||||
.string()
|
||||
.trim()
|
||||
.when("hasChronicDisease", {
|
||||
is: true,
|
||||
then: (s) => s.required("توضیحات بیماری مزمن الزامی است"),
|
||||
otherwise: (s) => s.notRequired(),
|
||||
}),
|
||||
|
||||
surgeryHistory: yup.string().trim().notRequired(),
|
||||
medications: yup.string().trim().notRequired(),
|
||||
specialMark: yup.string().trim().notRequired(),
|
||||
})
|
||||
.required();
|
||||
Reference in New Issue
Block a user