Files
hounam-submit-form-frontend/ui/forms/identity/InnerIdentityForm.tsx
2026-06-03 16:15:30 +03:30

607 lines
20 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import {
Alert,
AlertTitle,
Avatar,
Box,
Button,
IconButton,
MenuItem,
Paper,
TextField,
Typography,
} from "@mui/material";
import { ErrorMessage, Form, FormikProps } from "formik";
import { IdentityFormValues } from "@/core/types";
import { IdentityFormProps } from "./IdentityForm";
import { CheckCircle, Close, Person, UploadFile } from "@mui/icons-material";
import { genderOptions, religionOptions } from "@/core/constant";
import { useRef, useState } from "react";
import { DatePicker } from "@mui/x-date-pickers";
import axios from "axios";
import { toast } from "sonner";
import { useSendIdentityForm } from "@/hooks/identity.hook";
import callAPI from "@/core/caller";
export default function InnerIdentityForm(
props: FormikProps<IdentityFormValues> & IdentityFormProps,
) {
const handleBack = () => {
// قبل از رفتن به عقب، مقادیر فعلی فرم را در استیت والد ذخیره کن
props.update({ identity: props.values });
props.setStep(props.step - 1);
};
const { mutateAsync, isPending } = useSendIdentityForm();
const [profileUploading, setProfileUploading] = useState<boolean>(false);
const [profileUploadProgress, setProfileUploadProgress] = useState<number>(0);
const [profileUploadError, setProfileUploadError] = useState<string>("");
const [profilePreview, setProfilePreview] = useState<string>("");
const [uploadedFileId, setUploadedFileId] = useState<string>("");
const [selectedProfileFile, setSelectedProfileFile] = useState<File | null>(
null,
);
// برای کنسل کردن آپلود
const profileAbortControllerRef = useRef<AbortController | null>(null);
// تابع کمکی اعتبارسنجی (می‌توانید همان تابع قبلی را استفاده کنید)
const validateImageFile = (file: File) => {
if (!file.type.startsWith("image/")) {
return "فقط فایل تصویری مجاز است";
}
const maxSize = 500 * 1024; // 500KB
if (file.size > maxSize) {
return "حجم عکس باید حداکثر ۵۰۰ کیلوبایت باشد";
}
return "";
};
const handleProfilePhotoChange = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const file = event.target.files?.[0];
if (!file) return;
// ۱. پیش‌نمایش موقت با استفاده از خودِ فایل (قبل از آپلود)
// این کار باعث می‌شود کاربر فوراً عکس را ببیند و خطای URL نگیرید
const localPreview = URL.createObjectURL(file);
setProfilePreview(localPreview);
setProfileUploading(true);
setProfileUploadError("");
const formData = new FormData();
formData.append("file", file);
try {
const response = await callAPI.post("/files/upload", formData, {
onUploadProgress: (p) => {
setProfileUploadProgress(
Math.round((p.loaded * 100) / (p.total || 100)),
);
},
});
console.log(response);
// ۲. بررسی دقیق پاسخ بک‌ند
// فرض می‌کنیم بک‌ند شما { id: "...", url: "..." } برمی‌گرداند
const { id, url } = response.data;
if (id) {
props.setFieldValue("profilePhotoId", id);
// اگر بک‌ند URL کامل فرستاده، آن را جایگزین پیش‌نمایش موقت می‌کنیم
if (url) setProfilePreview(url);
} else {
throw new Error("Invalid Response");
}
} catch (error: any) {
console.error("Upload Error:", error);
setProfileUploadError(
error.response?.data?.message || "خطا در آپلود فایل",
);
setProfilePreview(""); // پاک کردن پیش‌نمایش در صورت خطا
props.setFieldValue("profilePhotoId", "");
} finally {
setProfileUploading(false);
}
};
const handleCancelUpload = () => {
if (profileAbortControllerRef.current) {
profileAbortControllerRef.current.abort();
}
setProfileUploading(false);
setProfileUploadProgress(0);
};
const handleRemoveProfilePhoto = async () => {
// اگر نیاز بود به بک‌اند درخواست حذف بزنید (اختیاری)
await callAPI.post("/files/delete", {
fileId: props.values?.profilePhotoId,
});
props.setFieldValue("profilePhotoId", null);
setProfilePreview("");
setSelectedProfileFile(null);
};
const handleNext = async () => {
// اعتبارسنجی دستی کل فرم
const errors = await props.validateForm();
props.update({
identity: props.values,
});
// اگر در گام فعلی خطایی وجود ندارد، برو مرحله بعد
if (Object.keys(errors).length === 0) {
if (props.step === 12) {
props.submitForm(); // ثبت نهایی
} else {
props.setStep(props.step + 1);
}
}
try {
const applicant = await mutateAsync(props.values);
console.log(applicant);
props.update({
identity: props.values,
});
localStorage.setItem(
"applicationDraft",
JSON.stringify({
applicantId: applicant.id,
registrationCenter: props.values,
formStep: applicant.formStep,
}),
);
props.setStep((prev) => prev + 1);
} catch (error: any) {
console.log(error);
toast.error(error?.message || "خطا در ثبت مرکز");
}
};
return (
<Box>
<Alert severity="warning" sx={{ mb: 3, borderRadius: "12px" }}>
<AlertTitle sx={{ fontWeight: 800 }}>توجه</AlertTitle>
پس از تكميل اين گام ، كدملي شما براي ادامه مراحل ذخيره خواهد شد
</Alert>
<Paper
elevation={0}
sx={{
width: "100%",
background: "#ffffff",
}}
>
<Form>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(340px, 1fr))",
gap: "18px",
}}
>
<div>
<TextField
label="نام"
value={props.values.firstName}
onChange={(e) =>
props.setFieldValue("firstName", e.target.value)
}
error={!!props.errors.firstName}
helperText={props.errors.firstName}
fullWidth
required
/>
<ErrorMessage component={"div"} name="firstName" />
</div>
<div>
<TextField
label="نام خانوادگی"
value={props.values.lastName}
onChange={(e) =>
props.setFieldValue("lastName", e.target.value)
}
error={!!props.errors.lastName}
helperText={props.errors.lastName}
fullWidth
required
/>
<ErrorMessage component={"div"} name="lastName" />
</div>
<div>
<TextField
label="نام پدر"
value={props.values.fatherName}
onChange={(e) =>
props.setFieldValue("fatherName", e.target.value)
}
fullWidth
/>
<ErrorMessage component={"div"} name="fatherName" />
</div>
<div>
<TextField
label="کد ملی"
value={props.values.nationalCode}
onChange={(e) =>
props.setFieldValue("nationalCode", e.target.value)
}
error={!!props.errors.nationalCode}
helperText={props.errors.nationalCode}
fullWidth
required
/>
<ErrorMessage component={"div"} name="nationalCode" />
</div>
<DatePicker
label="تاریخ تولد"
value={
props.values.birthDate ? new Date(props.values.birthDate) : null
}
onChange={(newValue) =>
props.setFieldValue("birthDate", newValue)
}
maxDate={new Date()}
slotProps={{
textField: {
fullWidth: true,
error: !!props.errors.birthDate,
helperText: props.errors.birthDate,
},
}}
/>
<div>
<TextField
label="محل تولد"
value={props.values.birthPlace}
onChange={(e) =>
props.setFieldValue("birthPlace", e.target.value)
}
fullWidth
required
error={!!props.errors.birthPlace}
helperText={props.errors.birthPlace}
/>
<ErrorMessage component={"div"} name="birthPlace" />
</div>
<div>
<TextField
select
label="جنسیت"
value={props.values.gender}
onChange={(e) => props.setFieldValue("gender", e.target.value)}
error={!!props.errors.gender}
helperText={props.errors.gender}
fullWidth
required
>
{genderOptions.map((item) => (
<MenuItem key={item.id} value={item.value}>
{item.label}
</MenuItem>
))}
</TextField>
<ErrorMessage component={"div"} name="gender" />
</div>
<div>
<TextField
select
label="دین"
value={props.values.religion}
onChange={(e) =>
props.setFieldValue("religion", e.target.value)
}
fullWidth
required
error={!!props.errors.religion}
helperText={props.errors.religion}
>
{religionOptions.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<ErrorMessage component={"div"} name="religion" />
</div>
<div>
<TextField
label="ملیت"
value={props.values.nationality}
onChange={(e) =>
props.setFieldValue("nationality", e.target.value)
}
error={!!props.errors.nationality}
helperText={props.errors.nationality}
fullWidth
required
/>
<ErrorMessage component={"div"} name="nationality" />
</div>
<Box
sx={{
width: "100%",
border: profileUploadError
? "1px solid #ef4444"
: "1px dashed #cbd5e1",
borderRadius: "18px",
backgroundColor: "#f8fafc",
p: 2,
minHeight: "100%",
transition: "all 0.2s ease",
"&:hover": {
borderColor: profileUploadError ? "#ef4444" : "#2563eb",
backgroundColor: "#f8fbff",
},
}}
>
<Typography
sx={{
fontWeight: 700,
color: "#0f172a",
mb: 1.5,
fontSize: "0.95rem",
}}
>
عکس پرسنلی
</Typography>
<Box
sx={{
display: "flex",
gap: 3,
alignItems: "flex-start",
mb: 2,
}}
>
{/* بخش پیش‌نمایش عکس */}
<Box sx={{ position: "relative" }}>
<Avatar
src={profilePreview || ""} // این استیت را باید در هندلر آپلود ست کنید
variant="rounded"
sx={{
width: 100,
height: 120,
borderRadius: "12px",
bgcolor: "#e2e8f0",
border: "2px solid #fff",
boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)",
}}
>
{!profilePreview && (
<Person sx={{ fontSize: 40, color: "#94a3b8" }} />
)}
</Avatar>
{/* دکمه حذف عکس (فقط اگر عکسی وجود داشت) */}
{profilePreview && !profileUploading && (
<IconButton
onClick={handleRemoveProfilePhoto}
size="small"
sx={{
position: "absolute",
top: -10,
right: -10,
backgroundColor: "#ef4444",
color: "white",
"&:hover": { backgroundColor: "#dc2626" },
boxShadow: "0 2px 4px rgba(0,0,0,0.2)",
}}
>
<Close fontSize="small" />
</IconButton>
)}
</Box>
<Box sx={{ flex: 1 }}>
<Typography
sx={{
color: "#64748b",
fontSize: "0.82rem",
mb: 2,
lineHeight: 1.8,
}}
>
فقط فایل تصویری مجاز است و حجم آن باید حداکثر ۵۰۰ کیلوبایت
باشد.
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1.5,
flexWrap: "wrap",
}}
>
<Button
component="label"
variant="outlined"
startIcon={<UploadFile />}
disabled={profileUploading}
sx={{
borderRadius: "12px",
borderColor: "#cbd5e1",
color: "#2563eb",
fontWeight: 700,
px: 2.5,
"&:hover": {
borderColor: "#2563eb",
backgroundColor: "#eff6ff",
},
"&.Mui-disabled": {
borderColor: "#cbd5e1",
color: "#94a3b8",
backgroundColor: "#f1f5f9",
},
}}
>
{profileUploading ? "در حال آپلود..." : "انتخاب عکس"}
<input
hidden
type="file"
accept="image/*"
onChange={handleProfilePhotoChange}
/>
</Button>
{profileUploading && (
<Button
type="button"
variant="text"
color="error"
onClick={handleCancelUpload}
sx={{
fontWeight: 700,
borderRadius: "12px",
}}
>
لغو آپلود
</Button>
)}
</Box>
</Box>
</Box>
{/* نمایش نام فایل در حال آپلود */}
{selectedProfileFile && profileUploading && (
<Typography
sx={{
mt: 1.5,
fontSize: "0.82rem",
color: "#475569",
wordBreak: "break-word",
}}
>
فایل انتخابشده: {selectedProfileFile.name}
</Typography>
)}
{/* نوار پیشرفت */}
{profileUploading && (
<Box sx={{ mt: 1.5 }}>
<Typography
sx={{
mb: 0.8,
fontSize: "0.8rem",
color: "#475569",
fontWeight: 600,
}}
>
پیشرفت آپلود: {profileUploadProgress}%
</Typography>
<Box
sx={{
width: "100%",
height: "8px",
borderRadius: "999px",
backgroundColor: "#e2e8f0",
overflow: "hidden",
}}
>
<Box
sx={{
width: `${profileUploadProgress}%`,
height: "100%",
backgroundColor: "#2563eb",
transition: "width 0.3s ease",
}}
/>
</Box>
</Box>
)}
{/* پیام موفقیت */}
{!profileUploading &&
props?.values?.profilePhotoId &&
!profileUploadError && (
<Typography
sx={{
mt: 1.5,
color: "#16a34a",
fontSize: "0.8rem",
fontWeight: 600,
display: "flex",
alignItems: "center",
gap: 0.5,
}}
>
<CheckCircle fontSize="small" /> عکس با موفقیت بارگذاری شد.
</Typography>
)}
{/* پیام خطا */}
{profileUploadError && (
<Typography
sx={{
mt: 1.5,
color: "#dc2626",
fontSize: "0.8rem",
fontWeight: 600,
}}
>
{profileUploadError}
</Typography>
)}
<ErrorMessage
component="div"
name="profilePhotoId"
className="text-red-700 text-sm font-semibold mt-4"
/>
</Box>
</div>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
mt: 5,
width: "100%",
}}
>
<Button
disabled={props.step === 1}
type="button"
onClick={handleBack}
sx={{
borderRadius: "12px",
color: "#64748b",
fontWeight: 700,
}}
>
بازگشت
</Button>
<Button
variant="contained"
type="button" // به جای submit از button استفاده کردیم تا با تابع خودمان چک شود
onClick={handleNext}
sx={{
borderRadius: "12px",
px: 4,
py: 1.5,
bgcolor: `${props.step === 12 ? "green" : "#2563eb"}`,
fontWeight: 700,
}}
>
{props.step === 12 ? "اتمام و ثبت نهايي" : "گام بعدی"}
</Button>
</Box>
</Form>
</Paper>
</Box>
);
}