first commit

This commit is contained in:
2026-05-31 14:22:39 +03:30
commit 98af7d639b
54 changed files with 11545 additions and 0 deletions

View File

@@ -0,0 +1,286 @@
import React, { useEffect, useMemo, useState } from "react";
import { Box, MenuItem, TextField } from "@mui/material";
type BloodType = "" | "A+" | "A-" | "B+" | "B-" | "AB+" | "AB-" | "O+" | "O-";
export interface PhysicalInfoFormState {
applicantId: string;
bloodType: BloodType;
height: number | ""; // cm
weight: number | ""; // kg
bmi: number | ""; // auto or manual
hasDisability: boolean;
disabilityDescription: string;
hasChronicDisease: boolean;
chronicDiseaseDescription: string;
surgeryHistory: string;
medications: string;
specialMark: string;
}
const initialValues: PhysicalInfoFormState = {
applicantId: "",
bloodType: "",
height: "",
weight: "",
bmi: "",
hasDisability: false,
disabilityDescription: "",
hasChronicDisease: false,
chronicDiseaseDescription: "",
surgeryHistory: "",
medications: "",
specialMark: "",
};
function toNumberOrEmpty(v: string): number | "" {
if (v === "") return "";
const n = Number(v);
return Number.isFinite(n) ? n : "";
}
function round1(n: number) {
return Math.round(n * 10) / 10;
}
export default function PhysicalInfoForm(props: {
value?: PhysicalInfoFormState;
onChange?: (next: PhysicalInfoFormState) => void;
applicantId?: string; // اگر خواستی از بیرون تزریق کنی
}) {
const { value, onChange, applicantId } = props;
const [formData, setFormData] = useState<PhysicalInfoFormState>(
value ?? { ...initialValues, applicantId: applicantId ?? "" },
);
// اگر value کنترل‌شده بود، همگام‌سازی
useEffect(() => {
if (value) setFormData(value);
}, [value]);
// اگر applicantId از بیرون تغییر کرد
useEffect(() => {
if (!value && applicantId) {
setFormData((p) => ({ ...p, applicantId }));
}
}, [applicantId, value]);
const setNext = (
updater: (prev: PhysicalInfoFormState) => PhysicalInfoFormState,
) => {
setFormData((prev) => {
const next = updater(prev);
onChange?.(next);
return next;
});
};
const handleText =
(field: keyof PhysicalInfoFormState) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
const v = e.target.value;
setNext((p) => ({ ...p, [field]: v }) as PhysicalInfoFormState);
};
const handleNumber =
(field: keyof PhysicalInfoFormState) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
const v = toNumberOrEmpty(e.target.value);
setNext((p) => ({ ...p, [field]: v }) as PhysicalInfoFormState);
};
const handleBoolSelect =
(field: "hasDisability" | "hasChronicDisease") =>
(e: React.ChangeEvent<HTMLInputElement>) => {
const v = e.target.value === "true";
setNext((p) => {
// اگر false شد، توضیحات را پاک می‌کنیم تا داده کثیف نماند
if (field === "hasDisability" && !v) {
return { ...p, hasDisability: false, disabilityDescription: "" };
}
if (field === "hasChronicDisease" && !v) {
return {
...p,
hasChronicDisease: false,
chronicDiseaseDescription: "",
};
}
return { ...p, [field]: v };
});
};
// محاسبه BMI از روی قد و وزن (cm, kg)
const computedBmi = useMemo(() => {
if (formData.height === "" || formData.weight === "") return "";
const hMeters = Number(formData.height) / 100;
if (!hMeters || hMeters <= 0) return "";
const bmi = Number(formData.weight) / (hMeters * hMeters);
return Number.isFinite(bmi) ? round1(bmi) : "";
}, [formData.height, formData.weight]);
// sync bmi (فقط وقتی قد/وزن داریم)
useEffect(() => {
// اگر بخوای دستی BMI وارد کنی، این بخش رو حذف کن.
setNext((p) => ({ ...p, bmi: computedBmi }));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [computedBmi]);
return (
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", md: "repeat(2, 1fr)" },
gap: 2,
width: "100%",
}}
>
{/* bloodType */}
<TextField
select
label="گروه خونی"
value={formData.bloodType}
onChange={handleText("bloodType")}
fullWidth
>
<MenuItem value="">انتخاب کنید</MenuItem>
{(["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"] as const).map(
(bt) => (
<MenuItem key={bt} value={bt}>
{bt}
</MenuItem>
),
)}
</TextField>
{/* height */}
<TextField
label="قد (سانتی‌متر)"
type="number"
value={formData.height}
onChange={handleNumber("height")}
fullWidth
/>
{/* weight */}
<TextField
label="وزن (کیلوگرم)"
type="number"
value={formData.weight}
onChange={handleNumber("weight")}
fullWidth
/>
{/* bmi */}
<TextField
label="BMI"
type="number"
value={formData.bmi}
onChange={handleNumber("bmi")}
fullWidth
disabled // چون خودکار محاسبه می‌کنیم
helperText={
formData.height !== "" && formData.weight !== ""
? "به‌صورت خودکار از قد و وزن محاسبه می‌شود"
: "برای محاسبه BMI، قد و وزن را وارد کنید"
}
/>
{/* specialMark */}
<TextField
label="علامت مشخصه"
value={formData.specialMark}
onChange={handleText("specialMark")}
fullWidth
/>
{/* hasDisability */}
<TextField
select
label="معلولیت دارد؟"
value={String(formData.hasDisability)}
onChange={handleBoolSelect("hasDisability")}
fullWidth
>
<MenuItem value="false">خیر</MenuItem>
<MenuItem value="true">بله</MenuItem>
</TextField>
{/* hasChronicDisease */}
<TextField
select
label="بیماری مزمن دارد؟"
value={String(formData.hasChronicDisease)}
onChange={handleBoolSelect("hasChronicDisease")}
fullWidth
>
<MenuItem value="false">خیر</MenuItem>
<MenuItem value="true">بله</MenuItem>
</TextField>
{/* disabilityDescription */}
{formData.hasDisability && (
<Box sx={{ gridColumn: { xs: "1", md: "1 / -1" } }}>
<TextField
label="توضیحات معلولیت"
value={formData.disabilityDescription}
onChange={handleText("disabilityDescription")}
fullWidth
multiline
minRows={2}
/>
</Box>
)}
{/* chronicDiseaseDescription */}
{formData.hasChronicDisease && (
<Box sx={{ gridColumn: { xs: "1", md: "1 / -1" } }}>
<TextField
label="توضیحات بیماری مزمن"
value={formData.chronicDiseaseDescription}
onChange={handleText("chronicDiseaseDescription")}
fullWidth
multiline
minRows={2}
/>
</Box>
)}
{/* surgeryHistory */}
<Box sx={{ gridColumn: { xs: "1", md: "1 / -1" } }}>
<TextField
label="سابقه جراحی"
value={formData.surgeryHistory}
onChange={handleText("surgeryHistory")}
fullWidth
multiline
minRows={2}
/>
</Box>
{/* medications */}
<Box sx={{ gridColumn: { xs: "1", md: "1 / -1" } }}>
<TextField
label="داروهای مصرفی"
value={formData.medications}
onChange={handleText("medications")}
fullWidth
multiline
minRows={2}
/>
</Box>
</Box>
);
}