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

405
ui/forms/IdentityForm.tsx Normal file
View File

@@ -0,0 +1,405 @@
"use client";
import React, { useMemo, useState } from "react";
import {
Avatar,
Box,
Button,
MenuItem,
Paper,
TextField,
Typography,
} from "@mui/material";
import { UploadFile } from "@mui/icons-material";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDateFnsJalali } from "@mui/x-date-pickers/AdapterDateFnsJalali";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
type IdentityFormData = {
applicantId: string;
firstName: string;
lastName: string;
fatherName: string;
nationalCode: string;
birthDate: string;
birthPlace: string;
gender: string;
religion: string;
nationality: string;
profilePhotoId: string;
};
type IdentityFormErrors = Partial<Record<keyof IdentityFormData, string>>;
const initialForm: IdentityFormData = {
applicantId: "",
firstName: "",
lastName: "",
fatherName: "",
nationalCode: "",
birthDate: "",
birthPlace: "",
gender: "",
religion: "",
nationality: "",
profilePhotoId: "",
};
export default function IdentityForm() {
const [formData, setFormData] = useState<IdentityFormData>(initialForm);
const [errors, setErrors] = useState<IdentityFormErrors>({});
const [submitted, setSubmitted] = useState(false);
const [profilePhoto, setProfilePhoto] = useState<File | null>(null);
const [profilePhotoPreview, setProfilePhotoPreview] = useState<string>("");
const [profilePhotoError, setProfilePhotoError] = useState<string>("");
const [birthDateValue, setBirthDateValue] = useState<Date | null>(null);
const handleProfilePhotoChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const file = event.target.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
setProfilePhoto(null);
setProfilePhotoPreview("");
setProfilePhotoError("فقط فایل تصویری مجاز است");
return;
}
const maxSize = 500 * 1024; // 500KB
if (file.size > maxSize) {
setProfilePhoto(null);
setProfilePhotoPreview("");
setProfilePhotoError("حجم عکس باید حداکثر ۵۰۰ کیلوبایت باشد");
return;
}
setProfilePhoto(file);
setProfilePhotoError("");
const previewUrl = URL.createObjectURL(file);
setProfilePhotoPreview(previewUrl);
};
const genderOptions = useMemo(() => ["مرد", "زن", "سایر"], []);
const religionOptions = useMemo(
() => ["اسلام", "مسیحیت", "یهودیت", "زرتشتی", "سایر"],
[],
);
const handleChange =
(field: keyof IdentityFormData) =>
(event: React.ChangeEvent<HTMLInputElement>) => {
let value = event.target.value;
if (field === "nationalCode") {
value = value.replace(/\D/g, "").slice(0, 10);
}
setFormData((prev) => ({
...prev,
[field]: value,
}));
if (errors[field]) {
setErrors((prev) => ({
...prev,
[field]: "",
}));
}
};
const validate = () => {
const newErrors: IdentityFormErrors = {};
let hasError = false;
if (!formData.applicantId.trim()) {
newErrors.applicantId = "شناسه متقاضی الزامی است";
hasError = true;
}
if (!formData.firstName.trim()) {
newErrors.firstName = "نام الزامی است";
hasError = true;
}
if (!formData.lastName.trim()) {
newErrors.lastName = "نام خانوادگی الزامی است";
hasError = true;
}
if (!formData.nationalCode.trim()) {
newErrors.nationalCode = "کد ملی الزامی است";
hasError = true;
} else if (!/^\d{10}$/.test(formData.nationalCode)) {
newErrors.nationalCode = "کد ملی باید ۱۰ رقم باشد";
hasError = true;
}
if (!formData.birthDate.trim()) {
newErrors.birthDate = "تاریخ تولد الزامی است";
hasError = true;
}
if (!formData.gender.trim()) {
newErrors.gender = "جنسیت الزامی است";
hasError = true;
}
if (!formData.nationality.trim()) {
newErrors.nationality = "ملیت الزامی است";
hasError = true;
}
if (!profilePhoto) {
setProfilePhotoError("عکس پرسنلی الزامی است");
hasError = true;
} else {
setProfilePhotoError("");
}
setErrors(newErrors);
return !hasError;
};
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
setSubmitted(false);
if (!validate()) return;
const payload = {
...formData,
birthDate: formData.birthDate ? new Date(formData.birthDate) : null,
fatherName: formData.fatherName || null,
birthPlace: formData.birthPlace || null,
religion: formData.religion || null,
profilePhotoId: formData.profilePhotoId || null,
};
console.log("Identity Payload:", payload);
setSubmitted(true);
};
return (
<Box>
<Paper
elevation={0}
sx={{
width: "100%",
background: "#ffffff",
}}
>
<Box component="form" onSubmit={handleSubmit}>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(340px, 1fr))",
gap: "18px",
}}
>
<TextField
label="نام"
value={formData.firstName}
onChange={handleChange("firstName")}
error={!!errors.firstName}
helperText={errors.firstName}
fullWidth
/>
<TextField
label="نام خانوادگی"
value={formData.lastName}
onChange={handleChange("lastName")}
error={!!errors.lastName}
helperText={errors.lastName}
fullWidth
/>
<TextField
label="نام پدر"
value={formData.fatherName}
onChange={handleChange("fatherName")}
fullWidth
/>
<TextField
label="کد ملی"
value={formData.nationalCode}
onChange={handleChange("nationalCode")}
error={!!errors.nationalCode}
helperText={errors.nationalCode}
fullWidth
/>
<DatePicker
label="تاریخ تولد"
value={birthDateValue}
onChange={(newValue) => {
setBirthDateValue(newValue);
setFormData((prev) => ({
...prev,
birthDate: newValue ? newValue.toISOString() : "",
}));
if (errors.birthDate) {
setErrors((prev) => ({
...prev,
birthDate: "",
}));
}
}}
slotProps={{
textField: {
fullWidth: true,
error: !!errors.birthDate,
helperText: errors.birthDate,
},
}}
/>
<TextField
label="محل تولد"
value={formData.birthPlace}
onChange={handleChange("birthPlace")}
fullWidth
/>
<TextField
select
label="جنسیت"
value={formData.gender}
onChange={handleChange("gender")}
error={!!errors.gender}
helperText={errors.gender}
fullWidth
>
{genderOptions.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<TextField
select
label="دین"
value={formData.religion}
onChange={handleChange("religion")}
fullWidth
>
{religionOptions.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<TextField
label="ملیت"
value={formData.nationality}
onChange={handleChange("nationality")}
error={!!errors.nationality}
helperText={errors.nationality}
fullWidth
/>
<Box
sx={{
width: "100%",
border: profilePhotoError
? "1px solid #ef4444"
: "1px dashed #cbd5e1",
borderRadius: "18px",
backgroundColor: "#f8fafc",
p: 2,
minHeight: "100%",
transition: "all 0.2s ease",
"&:hover": {
borderColor: profilePhotoError ? "#ef4444" : "#2563eb",
backgroundColor: "#f8fbff",
},
}}
>
<Typography
sx={{
fontWeight: 700,
color: "#0f172a",
mb: 1.5,
fontSize: "0.95rem",
}}
>
عکس پرسنلی
</Typography>
<Typography
sx={{
color: "#64748b",
fontSize: "0.82rem",
mb: 2,
lineHeight: 1.8,
}}
>
فقط فایل تصویری مجاز است و حجم آن باید حداکثر ۵۰۰ کیلوبایت باشد.
</Typography>
<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>
{profilePhoto && (
<Typography
sx={{
mt: 1.5,
fontSize: "0.82rem",
color: "#475569",
wordBreak: "break-word",
}}
>
فایل انتخابشده: {profilePhoto.name}
</Typography>
)}
{profilePhotoError && (
<Typography
sx={{
mt: 1.5,
color: "#dc2626",
fontSize: "0.8rem",
fontWeight: 600,
}}
>
{profilePhotoError}
</Typography>
)}
</Box>
</div>
</Box>
</Paper>
</Box>
);
}