607 lines
20 KiB
TypeScript
607 lines
20 KiB
TypeScript
"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>
|
||
);
|
||
}
|