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

@@ -31,10 +31,8 @@ const IdentityFormValidationSchema = yup.object({
lastName: yup.string().trim().required("نام خانوادگی الزامی است").min(2).max(50),
birthDate: yup
.string()
.required("تاریخ تولد الزامی است")
.matches(/^\d{4}\/\d{2}\/\d{2}$/, "فرمت تاریخ تولد باید به شکل ۱۴۰۳/۰۱/۲۰ باشد"),
.required("تاریخ تولد الزامی است"),
birthPlace: yup.string().trim().required("محل تولد الزامی است").min(2).max(80),
fatherName: yup.string().trim().required("نام پدر الزامی است").min(2).max(50),
gender: yup
.string()
.required("جنسیت الزامی است")
@@ -71,6 +69,7 @@ const IdentityForm = withFormik<IdentityFormProps, IdentityFormValues>({
validationSchema: IdentityFormValidationSchema,
handleSubmit: (values, { props }) => {
console.log('submitted identity')
props.update({
identity: values,
});

View File

@@ -1,6 +1,11 @@
"use client";
import {
Alert,
AlertTitle,
Avatar,
Box,
Button,
IconButton,
MenuItem,
Paper,
TextField,
@@ -9,47 +14,162 @@ import {
import { ErrorMessage, Form, FormikProps } from "formik";
import { IdentityFormValues } from "@/core/types";
import { IdentityFormProps } from "./IdentityForm";
import { UploadFile } from "@mui/icons-material";
import { CheckCircle, Close, Person, UploadFile } from "@mui/icons-material";
import { genderOptions, religionOptions } from "@/core/constant";
import { useState } from "react";
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,
) {
console.log(props.data)
const handleBack = () => {
// قبل از رفتن به عقب، مقادیر فعلی فرم را در استیت والد ذخیره کن
props.update({ identity: props.values });
props.setStep(props.step - 1);
};
const [profilePhoto, setProfilePhoto] = useState<File | null>(null);
const [profilePhotoError, setProfilePhotoError] = useState<string>("");
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 handleProfilePhotoChange = (
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;
if (!file.type.startsWith("image/")) {
setProfilePhoto(null);
setProfilePhotoError("فقط فایل تصویری مجاز است");
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();
}
const maxSize = 500 * 1024; // 500KB
if (file.size > maxSize) {
setProfilePhoto(null);
setProfilePhotoError("حجم عکس باید حداکثر ۵۰۰ کیلوبایت باشد");
return;
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);
}
}
setProfilePhoto(file);
setProfilePhotoError("");
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={{
@@ -80,43 +200,57 @@ export default function InnerIdentityForm(
<ErrorMessage component={"div"} name="firstName" />
</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
/>
<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>
<TextField
label="نام پدر"
value={props.values.fatherName}
onChange={(e) =>
props.setFieldValue("fatherName", e.target.value)
}
fullWidth
/>
<div>
<TextField
label="نام پدر"
value={props.values.fatherName}
onChange={(e) =>
props.setFieldValue("fatherName", e.target.value)
}
fullWidth
/>
<ErrorMessage component={"div"} name="fatherName" />
</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
/>
<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
<DatePicker
label="تاریخ تولد"
value={props.values.birthDate}
onChange={(newValue) =>
props.setFieldValue("birthDate", newValue)
value={
props.values.birthDate ? new Date(props.values.birthDate) : null
}
onChange={(newValue) =>
props.setFieldValue("birthDate", newValue)
}
maxDate={new Date()}
slotProps={{
textField: {
fullWidth: true,
@@ -124,64 +258,82 @@ export default function InnerIdentityForm(
helperText: props.errors.birthDate,
},
}}
/> */}
<TextField
label="محل تولد"
value={props.values.birthPlace}
onChange={(e) =>
props.setFieldValue("birthPlace", e.target.value)
}
fullWidth
/>
<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>
<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>
<TextField
select
label="دین"
value={props.values.religion}
onChange={(e) => props.setFieldValue("religion", e.target.value)}
fullWidth
>
{religionOptions.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<TextField
label="ملیت"
value={props.values.nationality}
onChange={(e) =>
props.setFieldValue("nationality", e.target.value)
}
error={!!props.errors.nationality}
helperText={props.errors.nationality}
fullWidth
required
/>
<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: profilePhotoError
border: profileUploadError
? "1px solid #ef4444"
: "1px dashed #cbd5e1",
borderRadius: "18px",
@@ -190,7 +342,7 @@ export default function InnerIdentityForm(
minHeight: "100%",
transition: "all 0.2s ease",
"&:hover": {
borderColor: profilePhotoError ? "#ef4444" : "#2563eb",
borderColor: profileUploadError ? "#ef4444" : "#2563eb",
backgroundColor: "#f8fbff",
},
}}
@@ -206,43 +358,125 @@ export default function InnerIdentityForm(
عکس پرسنلی
</Typography>
<Typography
<Box
sx={{
color: "#64748b",
fontSize: "0.82rem",
display: "flex",
gap: 3,
alignItems: "flex-start",
mb: 2,
lineHeight: 1.8,
}}
>
فقط فایل تصویری مجاز است و حجم آن باید حداکثر ۵۰۰ کیلوبایت باشد.
</Typography>
{/* بخش پیش‌نمایش عکس */}
<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>
<Button
component="label"
variant="outlined"
startIcon={<UploadFile />}
sx={{
borderRadius: "12px",
borderColor: "#cbd5e1",
color: "#2563eb",
fontWeight: 700,
px: 2.5,
"&:hover": {
borderColor: "#2563eb",
backgroundColor: "#eff6ff",
},
}}
>
انتخاب عکس
<input
hidden
type="file"
accept="image/*"
onChange={handleProfilePhotoChange}
/>
</Button>
{/* دکمه حذف عکس (فقط اگر عکسی وجود داشت) */}
{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>
{profilePhoto && (
<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,
@@ -251,11 +485,66 @@ export default function InnerIdentityForm(
wordBreak: "break-word",
}}
>
فایل انتخابشده: {profilePhoto.name}
فایل انتخابشده: {selectedProfileFile.name}
</Typography>
)}
{profilePhotoError && (
{/* نوار پیشرفت */}
{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,
@@ -264,9 +553,15 @@ export default function InnerIdentityForm(
fontWeight: 600,
}}
>
{profilePhotoError}
{profileUploadError}
</Typography>
)}
<ErrorMessage
component="div"
name="profilePhotoId"
className="text-red-700 text-sm font-semibold mt-4"
/>
</Box>
</div>
<Box
@@ -291,7 +586,8 @@ export default function InnerIdentityForm(
</Button>
<Button
variant="contained"
type="submit"
type="button" // به جای submit از button استفاده کردیم تا با تابع خودمان چک شود
onClick={handleNext}
sx={{
borderRadius: "12px",
px: 4,