Compare commits
2 Commits
98af7d639b
...
b8dc1d0e1b
| Author | SHA1 | Date | |
|---|---|---|---|
| b8dc1d0e1b | |||
| b241d12ff5 |
@@ -9,12 +9,13 @@ import { AdapterDateFnsJalali } from "@mui/x-date-pickers/AdapterDateFnsJalali";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { queryClientOptionsData } from "@/core/constant";
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
const [queryClient] = useState(() => new QueryClient(queryClientOptionsData));
|
||||
|
||||
return (
|
||||
<html
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from "axios";
|
||||
|
||||
const callAPISetting = axios.create({
|
||||
baseURL: "http://localhost:8000/api/v1",
|
||||
baseURL: "http://localhost:4000/api/v1",
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
|
||||
53
core/constant/index.ts
Normal file
53
core/constant/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { QueryClientConfig } from "@tanstack/react-query";
|
||||
|
||||
export const queryClientOptionsData: QueryClientConfig = {
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// مدت زمانی که داده fresh حساب میشود
|
||||
staleTime: 1000 * 60 * 5, // 5 دقیقه
|
||||
|
||||
// مدت نگهداری کش در حافظه بعد از unmount
|
||||
gcTime: 1000 * 60 * 30, // 30 دقیقه
|
||||
|
||||
// چند بار در صورت خطا retry کند
|
||||
retry: (failureCount: any, error: any) => {
|
||||
// برای خطاهای 4xx معمولاً retry منطقی نیست
|
||||
const status = error?.response?.status;
|
||||
|
||||
if (status >= 400 && status < 500) return false;
|
||||
return failureCount < 2;
|
||||
},
|
||||
|
||||
// تاخیر بین retryها
|
||||
retryDelay: (attemptIndex: any) =>
|
||||
Math.min(1000 * 2 ** attemptIndex, 10000),
|
||||
|
||||
// جلوگیری از رفرشهای اضافه
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnMount: false,
|
||||
},
|
||||
|
||||
mutations: {
|
||||
retry: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
export const genderOptions = [
|
||||
{
|
||||
id: 1,
|
||||
label: "مرد",
|
||||
value: "male",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: "زن",
|
||||
value: "female",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: "ساير",
|
||||
value: "other",
|
||||
},
|
||||
];
|
||||
export const religionOptions = ["اسلام", "مسیحیت", "یهودیت", "زرتشتی", "سایر"];
|
||||
45
core/types/index.ts
Normal file
45
core/types/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export type genderType = "male" | "female" | "other";
|
||||
export interface IdentityFormValues {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
fatherName: string;
|
||||
nationalCode: string;
|
||||
birthDate: string;
|
||||
birthPlace: string;
|
||||
gender: string;
|
||||
religion: string;
|
||||
nationality: string;
|
||||
profilePhotoId: string;
|
||||
}
|
||||
export type CenterItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
isUrgent: boolean;
|
||||
};
|
||||
|
||||
export interface WizardFormData {
|
||||
registrationCenter: {
|
||||
selectedCenter: CenterItem | null;
|
||||
};
|
||||
identity: IdentityFormValues; // برای مرحله ۲
|
||||
}
|
||||
|
||||
// مقدار اولیه برای همه مراحل
|
||||
export const INITIAL_WIZARD_DATA: WizardFormData = {
|
||||
registrationCenter: {
|
||||
selectedCenter:null
|
||||
},
|
||||
identity: {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
birthDate: "",
|
||||
birthPlace: "",
|
||||
fatherName: "",
|
||||
gender: "",
|
||||
nationalCode: "",
|
||||
nationality: "",
|
||||
profilePhotoId: "",
|
||||
religion: "",
|
||||
},
|
||||
};
|
||||
10
core/utils/index.ts
Normal file
10
core/utils/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import axios from "axios";
|
||||
|
||||
export function handleAxiosError(error: unknown) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
// اینجا میدونیم که خطا از axios است
|
||||
return error.response?.data?.error?.message;
|
||||
} else {
|
||||
return "Unexpected error";
|
||||
}
|
||||
}
|
||||
8
hooks/center.hook.ts
Normal file
8
hooks/center.hook.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { getAllCenters } from "@/services/apis/center.api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
|
||||
export const useGetAllCenters = () => useQuery({
|
||||
queryKey:["get-all-centers"],
|
||||
queryFn:getAllCenters
|
||||
})
|
||||
7
hooks/identity.hook.ts
Normal file
7
hooks/identity.hook.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { sendIdentityForm } from "@/services/apis/identity.api";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
export const useSendIdentityForm = () =>
|
||||
useMutation({
|
||||
mutationFn: sendIdentityForm,
|
||||
});
|
||||
47
middleware.ts
Normal file
47
middleware.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// middleware.ts
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
const AUTH_COOKIE_KEY = "tid";
|
||||
|
||||
// مسیرهای عمومی
|
||||
const publicPaths = ["/"]; // فرض کردیم "/" صفحه لاگین است
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const token = request.cookies.get(AUTH_COOKIE_KEY)?.value;
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
const isPublicPath = publicPaths.includes(pathname);
|
||||
|
||||
// اگر کاربر لاگین کرده باشد
|
||||
if (token) {
|
||||
// اگر رفت صفحه لاگین، بفرستش داخل پنل
|
||||
if (pathname === "/") {
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/form";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
// بقیه مسیرها مجاز
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// اگر کاربر توکن نداشته باشد
|
||||
if (!token) {
|
||||
// فقط مسیرهای عمومی مجازند
|
||||
if (isPublicPath) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// هر مسیر دیگری => ریدایرکت به لاگین
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
|
||||
};
|
||||
116
package-lock.json
generated
116
package-lock.json
generated
@@ -16,11 +16,13 @@
|
||||
"@tanstack/react-query": "^5.100.14",
|
||||
"axios": "^1.16.1",
|
||||
"date-fns-jalali": "^4.0.0-0",
|
||||
"formik": "^2.4.9",
|
||||
"next": "16.2.6",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"sonner": "^2.0.7",
|
||||
"stylis-plugin-rtl": "^2.1.1"
|
||||
"stylis-plugin-rtl": "^2.1.1",
|
||||
"yup": "^1.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
@@ -2108,6 +2110,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hoist-non-react-statics": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz",
|
||||
"integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@@ -3465,6 +3479,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/deepmerge/-/deepmerge-2.2.1.tgz",
|
||||
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
@@ -4378,6 +4401,31 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/formik": {
|
||||
"version": "2.4.9",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/formik/-/formik-2.4.9.tgz",
|
||||
"integrity": "sha512-5nI94BMnlFDdQRBY4Sz39WkhxajZJ57Fzs8wVbtsQlm5ScKIR1QLYqv/ultBnobObtlUyxpxoLodpixrsf36Og==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://opencollective.com/formik"
|
||||
}
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"deepmerge": "^2.1.1",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react-fast-compare": "^2.0.1",
|
||||
"tiny-warning": "^1.0.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -5625,6 +5673,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/lodash-es/-/lodash-es-4.18.1.tgz",
|
||||
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@@ -6232,6 +6292,12 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/property-expr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/property-expr/-/property-expr-2.0.6.tgz",
|
||||
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
@@ -6293,6 +6359,12 @@
|
||||
"react": "^19.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/react-is/-/react-is-16.13.1.tgz",
|
||||
@@ -6992,6 +7064,18 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-case": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
@@ -7053,6 +7137,12 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toposort": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/toposort/-/toposort-2.0.2.tgz",
|
||||
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||
@@ -7111,6 +7201,18 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/type-fest/-/type-fest-2.19.0.tgz",
|
||||
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-array-buffer": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
||||
@@ -7482,6 +7584,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yup": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/yup/-/yup-1.7.1.tgz",
|
||||
"integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"property-expr": "^2.0.5",
|
||||
"tiny-case": "^1.0.3",
|
||||
"toposort": "^2.0.2",
|
||||
"type-fest": "^2.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/zod/-/zod-4.4.3.tgz",
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
"@tanstack/react-query": "^5.100.14",
|
||||
"axios": "^1.16.1",
|
||||
"date-fns-jalali": "^4.0.0-0",
|
||||
"formik": "^2.4.9",
|
||||
"next": "16.2.6",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"sonner": "^2.0.7",
|
||||
"stylis-plugin-rtl": "^2.1.1"
|
||||
"stylis-plugin-rtl": "^2.1.1",
|
||||
"yup": "^1.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
||||
@@ -2,6 +2,6 @@ import callAPI from "@/core/caller";
|
||||
|
||||
export async function applicantLogin(nationalCode: string) {
|
||||
return await callAPI
|
||||
.post(`/auth/applicant/login`, nationalCode)
|
||||
.post(`/auth/applicant/login`,{nationalCode})
|
||||
.then((res) => res.data);
|
||||
}
|
||||
|
||||
5
services/apis/center.api.ts
Normal file
5
services/apis/center.api.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import callAPI from "@/core/caller";
|
||||
|
||||
export async function getAllCenters() {
|
||||
return await callAPI.get(`/center/all`).then((res) => res.data);
|
||||
}
|
||||
8
services/apis/identity.api.ts
Normal file
8
services/apis/identity.api.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import callAPI from "@/core/caller";
|
||||
import { IdentityFormValues } from "@/core/types";
|
||||
|
||||
export async function sendIdentityForm(data: IdentityFormValues) {
|
||||
return await callAPI
|
||||
.post(`/form/identity/create`, { data })
|
||||
.then((res) => res.data);
|
||||
}
|
||||
@@ -10,13 +10,10 @@ import {
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||
import CenterRegistrationForm from "./forms/register-center/RegistrationCenterForm";
|
||||
import IdentityForm from "./forms/IdentityForm";
|
||||
import IdentityForm from "./forms/identity/IdentityForm";
|
||||
import PersonalInfoForm from "./forms/PersonalInfoForm";
|
||||
import PhysicalInfoForm from "./forms/PhysicalInfoForm";
|
||||
import EducationForm from "./forms/EducationForm";
|
||||
import EducationSection from "./forms/EducationSection";
|
||||
import JobRequestForm from "./forms/JobRequestForm";
|
||||
import JobRequestSection from "./forms/JobRequestSection";
|
||||
import CourseSection from "./forms/CourseSection";
|
||||
import SkillsForm from "./forms/SkillsForm";
|
||||
@@ -24,6 +21,8 @@ import { WorkExperienceSection } from "./forms/WorkExperienceSection";
|
||||
import JobInfoForm from "./forms/JobInfoForm";
|
||||
import { ReferralSection } from "./forms/ReferralForm";
|
||||
import RelationsForm from "./forms/RelationForm";
|
||||
import RegistrationCenterForm from "./forms/register-center/RegistrationCenterForm";
|
||||
import { INITIAL_WIZARD_DATA, WizardFormData } from "@/core/types";
|
||||
|
||||
// کامپوننت پیشفرض برای مراحلی که هنوز نساختید
|
||||
const PlaceholderStep = ({ step }: any) => (
|
||||
@@ -34,8 +33,8 @@ const PlaceholderStep = ({ step }: any) => (
|
||||
|
||||
// --- ۲. نگاشت (Mapping) مراحل به کامپوننتها ---
|
||||
|
||||
const STEP_COMPONENTS: Record<number, React.FC<any>> = {
|
||||
1: CenterRegistrationForm,
|
||||
const STEP_COMPONENTS: Record<number, React.ComponentType<any>> = {
|
||||
1: RegistrationCenterForm,
|
||||
2: IdentityForm,
|
||||
3: PersonalInfoForm,
|
||||
4: PhysicalInfoForm,
|
||||
@@ -70,26 +69,15 @@ const STEP_LABELS = [
|
||||
export default function MultiStepForm() {
|
||||
const [activeStep, setActiveStep] = useState(1);
|
||||
const [maxStepReached, setMaxStepReached] = useState(1);
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
address: "",
|
||||
isUrgent: false,
|
||||
});
|
||||
const [formData, setFormData] = useState<WizardFormData>(INITIAL_WIZARD_DATA);
|
||||
|
||||
const updateFormData = (patch: Partial<WizardFormData>) => {
|
||||
setFormData((prev) => ({ ...prev, ...patch }));
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
const updateFormData = (newData: Partial<typeof formData>) => {
|
||||
setFormData((prev) => ({ ...prev, ...newData }));
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (activeStep < 12) {
|
||||
setActiveStep((prev) => prev + 1);
|
||||
if (activeStep + 1 > maxStepReached) setMaxStepReached(activeStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const ActiveStepComponent = STEP_COMPONENTS[activeStep] || PlaceholderStep;
|
||||
|
||||
return (
|
||||
@@ -160,7 +148,7 @@ export default function MultiStepForm() {
|
||||
{i + 1 < activeStep ? (
|
||||
<CheckCircleIcon sx={{ fontSize: 18 }} color="success" />
|
||||
) : (
|
||||
i + 1
|
||||
Number(i + 1).toLocaleString("fa-IR")
|
||||
)}
|
||||
</Box>
|
||||
<Typography
|
||||
@@ -206,40 +194,12 @@ export default function MultiStepForm() {
|
||||
{/* رندر شدن داینامیک کامپوننت مرحله فعلی */}
|
||||
<div className="w-full">
|
||||
<ActiveStepComponent
|
||||
data={formData}
|
||||
update={updateFormData}
|
||||
data={formData} // کل دیتای فرم
|
||||
update={updateFormData} // تابع آپدیتکننده
|
||||
step={activeStep}
|
||||
setStep={setActiveStep}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Box
|
||||
sx={{ display: "flex", justifyContent: "space-between", mt: 5 }}
|
||||
>
|
||||
<Button
|
||||
disabled={activeStep === 1}
|
||||
onClick={() => setActiveStep((prev) => prev - 1)}
|
||||
sx={{
|
||||
borderRadius: "12px",
|
||||
color: "#64748b",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
بازگشت
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleNext}
|
||||
sx={{
|
||||
borderRadius: "12px",
|
||||
px: 4,
|
||||
py: 1.5,
|
||||
bgcolor: `${activeStep === 12 ? "green" : "#2563eb"}`,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{activeStep === 12 ? "اتمام و ثبت نهايي" : "گام بعدی"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,405 +0,0 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +1,26 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
TextField,
|
||||
Typography,
|
||||
Button,
|
||||
Container,
|
||||
Stack,
|
||||
} from "@mui/material";
|
||||
import { Box, TextField, Typography, Button, Container } from "@mui/material";
|
||||
import { useApplicantLogin } from "@/hooks/auth.hook";
|
||||
import { toast } from "sonner";
|
||||
import { handleAxiosError } from "@/core/utils";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function LoginLayout() {
|
||||
const [nationalId, setNationalId] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
const { mutateAsync, isPending } = useApplicantLogin();
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const { message } = await mutateAsync(nationalId);
|
||||
toast.success(message);
|
||||
await mutateAsync(nationalId);
|
||||
if (isPending) {
|
||||
toast.loading("در حال انتقال به فرم استخدامي");
|
||||
}
|
||||
router.push("/form");
|
||||
} catch (error) {
|
||||
toast.error("خطا رخ داده است");
|
||||
console.log(error);
|
||||
toast.error(handleAxiosError(error));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,6 +59,7 @@ export default function LoginLayout() {
|
||||
fullWidth
|
||||
variant="contained"
|
||||
size="large"
|
||||
loading={isPending}
|
||||
sx={{ py: 1.5, borderRadius: 2, fontSize: "1rem" }}
|
||||
>
|
||||
ورود به سامانه
|
||||
|
||||
82
ui/forms/identity/IdentityForm.tsx
Normal file
82
ui/forms/identity/IdentityForm.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { withFormik } from "formik";
|
||||
import InnerIdentityForm from "./InnerIdentityForm";
|
||||
import * as yup from "yup";
|
||||
|
||||
export interface IdentityFormValues {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
birthDate: string;
|
||||
birthPlace: string;
|
||||
fatherName: string;
|
||||
gender: string;
|
||||
nationalCode: string;
|
||||
nationality: string;
|
||||
profilePhotoId: string;
|
||||
religion: string;
|
||||
}
|
||||
|
||||
export interface WizardFormData {
|
||||
identity: IdentityFormValues;
|
||||
}
|
||||
|
||||
export interface IdentityFormProps {
|
||||
step: number;
|
||||
setStep: React.Dispatch<React.SetStateAction<number>>;
|
||||
data: WizardFormData;
|
||||
update: (newData: Partial<WizardFormData>) => void;
|
||||
}
|
||||
|
||||
const IdentityFormValidationSchema = yup.object({
|
||||
firstName: yup.string().trim().required("نام الزامی است").min(2).max(50),
|
||||
lastName: yup.string().trim().required("نام خانوادگی الزامی است").min(2).max(50),
|
||||
birthDate: yup
|
||||
.string()
|
||||
.required("تاریخ تولد الزامی است")
|
||||
.matches(/^\d{4}\/\d{2}\/\d{2}$/, "فرمت تاریخ تولد باید به شکل ۱۴۰۳/۰۱/۲۰ باشد"),
|
||||
birthPlace: yup.string().trim().required("محل تولد الزامی است").min(2).max(80),
|
||||
fatherName: yup.string().trim().required("نام پدر الزامی است").min(2).max(50),
|
||||
gender: yup
|
||||
.string()
|
||||
.required("جنسیت الزامی است")
|
||||
.oneOf(["male", "female", "other"], "جنسیت معتبر نیست"),
|
||||
nationalCode: yup
|
||||
.string()
|
||||
.required("کد ملی الزامی است")
|
||||
.matches(/^\d{10}$/, "کد ملی باید ۱۰ رقم باشد"),
|
||||
nationality: yup.string().trim().required("تابعیت الزامی است").min(2).max(50),
|
||||
profilePhotoId: yup.string().trim().required("عکس پرسنلی الزامی است"),
|
||||
religion: yup.string().trim().required("دین الزامی است").min(2).max(50),
|
||||
});
|
||||
|
||||
const EMPTY_IDENTITY_VALUES: IdentityFormValues = {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
birthDate: "",
|
||||
birthPlace: "",
|
||||
fatherName: "",
|
||||
gender: "",
|
||||
nationalCode: "",
|
||||
nationality: "",
|
||||
profilePhotoId: "",
|
||||
religion: "",
|
||||
};
|
||||
|
||||
const IdentityForm = withFormik<IdentityFormProps, IdentityFormValues>({
|
||||
enableReinitialize: true,
|
||||
|
||||
mapPropsToValues: (props) => {
|
||||
return props.data?.identity || EMPTY_IDENTITY_VALUES;
|
||||
},
|
||||
|
||||
validationSchema: IdentityFormValidationSchema,
|
||||
|
||||
handleSubmit: (values, { props }) => {
|
||||
props.update({
|
||||
identity: values,
|
||||
});
|
||||
|
||||
props.setStep((prev) => prev + 1);
|
||||
},
|
||||
})(InnerIdentityForm);
|
||||
|
||||
export default IdentityForm;
|
||||
310
ui/forms/identity/InnerIdentityForm.tsx
Normal file
310
ui/forms/identity/InnerIdentityForm.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
MenuItem,
|
||||
Paper,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { ErrorMessage, Form, FormikProps } from "formik";
|
||||
import { IdentityFormValues } from "@/core/types";
|
||||
import { IdentityFormProps } from "./IdentityForm";
|
||||
import { UploadFile } from "@mui/icons-material";
|
||||
import { genderOptions, religionOptions } from "@/core/constant";
|
||||
import { useState } from "react";
|
||||
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 handleProfilePhotoChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const file = event.target.files?.[0];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
if (!file.type.startsWith("image/")) {
|
||||
setProfilePhoto(null);
|
||||
setProfilePhotoError("فقط فایل تصویری مجاز است");
|
||||
return;
|
||||
}
|
||||
|
||||
const maxSize = 500 * 1024; // 500KB
|
||||
if (file.size > maxSize) {
|
||||
setProfilePhoto(null);
|
||||
setProfilePhotoError("حجم عکس باید حداکثر ۵۰۰ کیلوبایت باشد");
|
||||
return;
|
||||
}
|
||||
|
||||
setProfilePhoto(file);
|
||||
setProfilePhotoError("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<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>
|
||||
|
||||
<TextField
|
||||
label="نام خانوادگی"
|
||||
value={props.values.lastName}
|
||||
onChange={(e) => props.setFieldValue("lastName", e.target.value)}
|
||||
error={!!props.errors.lastName}
|
||||
helperText={props.errors.lastName}
|
||||
fullWidth
|
||||
required
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="نام پدر"
|
||||
value={props.values.fatherName}
|
||||
onChange={(e) =>
|
||||
props.setFieldValue("fatherName", e.target.value)
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="کد ملی"
|
||||
value={props.values.nationalCode}
|
||||
onChange={(e) =>
|
||||
props.setFieldValue("nationalCode", e.target.value)
|
||||
}
|
||||
error={!!props.errors.nationalCode}
|
||||
helperText={props.errors.nationalCode}
|
||||
fullWidth
|
||||
required
|
||||
/>
|
||||
|
||||
{/* <DatePicker
|
||||
label="تاریخ تولد"
|
||||
value={props.values.birthDate}
|
||||
onChange={(newValue) =>
|
||||
props.setFieldValue("birthDate", newValue)
|
||||
}
|
||||
slotProps={{
|
||||
textField: {
|
||||
fullWidth: true,
|
||||
error: !!props.errors.birthDate,
|
||||
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>
|
||||
|
||||
<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
|
||||
/>
|
||||
|
||||
<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
|
||||
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="submit"
|
||||
sx={{
|
||||
borderRadius: "12px",
|
||||
px: 4,
|
||||
py: 1.5,
|
||||
bgcolor: `${props.step === 12 ? "green" : "#2563eb"}`,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{props.step === 12 ? "اتمام و ثبت نهايي" : "گام بعدی"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Form>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
166
ui/forms/register-center/InnerRegistrationCenterForm.tsx
Normal file
166
ui/forms/register-center/InnerRegistrationCenterForm.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
"use client";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Chip,
|
||||
Button,
|
||||
FormHelperText,
|
||||
} from "@mui/material";
|
||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||
import { Form, FormikProps } from "formik";
|
||||
import BusinessIcon from "@mui/icons-material/Business";
|
||||
import LocationOnIcon from "@mui/icons-material/LocationOn";
|
||||
import LocalHospitalIcon from "@mui/icons-material/LocalHospital";
|
||||
import { useGetAllCenters } from "@/hooks/center.hook";
|
||||
import {
|
||||
RegistrationCenterFormProps,
|
||||
RegistrationCenterFormValues,
|
||||
} from "./RegistrationCenterForm";
|
||||
import { CenterItem } from "@/core/types";
|
||||
|
||||
// تعریف اینترفیس برای تمیزی بیشتر
|
||||
interface InnerFormProps
|
||||
extends
|
||||
FormikProps<RegistrationCenterFormValues>,
|
||||
RegistrationCenterFormProps {}
|
||||
|
||||
export default function InnerRegistrationCenterForm(props: InnerFormProps) {
|
||||
const { data } = useGetAllCenters();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const handleBack = () => {
|
||||
props.update({ registrationCenter: props.values});
|
||||
props.setStep((prev) => Math.max(1, prev - 1));
|
||||
};
|
||||
|
||||
// منطق نمایش خطا
|
||||
const isSelectedCenterError =
|
||||
(props.touched.selectedCenter || props.submitCount > 0) &&
|
||||
!!props.errors.selectedCenter;
|
||||
|
||||
const handleCenterSelect = (center: CenterItem) => {
|
||||
props.setFieldValue("selectedCenter", center);
|
||||
props.setFieldTouched("selectedCenter", true, true); // فعال کردن حالت لمس شده
|
||||
};
|
||||
|
||||
const handleNext = async () => {
|
||||
// اگر مرحله اول است، فیلد را لمس کن تا اگر خالی بود خطا نشان دهد
|
||||
if (props.step === 1) {
|
||||
props.setFieldTouched("selectedCenter", true, true);
|
||||
}
|
||||
|
||||
// اعتبارسنجی دستی کل فرم
|
||||
const errors = await props.validateForm();
|
||||
|
||||
// اگر در گام فعلی خطایی وجود ندارد، برو مرحله بعد
|
||||
if (Object.keys(errors).length === 0) {
|
||||
if (props.step === 12) {
|
||||
props.submitForm(); // ثبت نهایی
|
||||
} else {
|
||||
props.setStep(props.step + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderCenterList = () => (
|
||||
<Box sx={{ width: "100%", gridColumn: "1 / -1" }}>
|
||||
<div className="w-full grid grid-cols-2 gap-4">
|
||||
{data?.data.map((center: CenterItem) => {
|
||||
const isSelected = props.values.selectedCenter?.id === center.id;
|
||||
return (
|
||||
<div className="col-span-1" key={center.id}>
|
||||
<Box
|
||||
onClick={() => handleCenterSelect(center)}
|
||||
sx={{
|
||||
p: 2.5,
|
||||
borderRadius: "18px",
|
||||
border: isSelected
|
||||
? "2px solid #2563eb"
|
||||
: isSelectedCenterError
|
||||
? "2px solid #d32f2f"
|
||||
: "1px solid #e2e8f0",
|
||||
backgroundColor: isSelected ? "#eff6ff" : "#fff",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.25s ease",
|
||||
"&:hover": { borderColor: "#2563eb" },
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "flex-start", gap: 2 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<BusinessIcon sx={{ color: "#2563eb", fontSize: 22 }} />
|
||||
<Typography sx={{ fontWeight: 800, color: "#0f172a" }}>
|
||||
{center.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography sx={{ color: "#64748b", fontSize: "0.92rem" }}>
|
||||
{center.address}
|
||||
</Typography>
|
||||
</Box>
|
||||
{isSelected && <CheckCircleIcon sx={{ color: "#2563eb" }} />}
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* نمایش پیام خطا به صورت تمیز زیر لیست */}
|
||||
{isSelectedCenterError && (
|
||||
<FormHelperText
|
||||
error
|
||||
sx={{ mt: 2, fontSize: "0.9rem", fontWeight: 600 }}
|
||||
>
|
||||
{props.errors.selectedCenter as string}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<div style={{ width: "100%", flexGrow: 1 }}>
|
||||
{/* رندر محتوای استپ ها */}
|
||||
{props.step === 1 ? (
|
||||
renderCenterList()
|
||||
) : (
|
||||
<Typography>محتوای مرحله {props.step}</Typography>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", mt: 5 }}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,327 +1,56 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Typography,
|
||||
Paper,
|
||||
Container,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Chip,
|
||||
} from "@mui/material";
|
||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
|
||||
import BusinessIcon from "@mui/icons-material/Business";
|
||||
import LocationOnIcon from "@mui/icons-material/LocationOn";
|
||||
import LocalHospitalIcon from "@mui/icons-material/LocalHospital";
|
||||
import { withFormik } from "formik";
|
||||
import * as yup from "yup";
|
||||
import InnerRegistrationCenterForm from "./InnerRegistrationCenterForm";
|
||||
import { CenterItem, IdentityFormValues } from "@/core/types";
|
||||
|
||||
const TOTAL_STEPS = 12;
|
||||
const STEP_LABELS = [
|
||||
"انتخاب مرکز",
|
||||
"موقعیت و آدرس",
|
||||
"وضعیت فوریت",
|
||||
"توضیحات تکمیلی",
|
||||
"ساعات کاری",
|
||||
"تصاویر مرکز",
|
||||
"تجهیزات موجود",
|
||||
"پرسنل",
|
||||
"بیمههای طرف قرارداد",
|
||||
"مجوزها",
|
||||
"شرایط پذیرش",
|
||||
"بررسی نهایی",
|
||||
];
|
||||
|
||||
type CenterItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
isUrgent: boolean;
|
||||
};
|
||||
|
||||
const centersMock: CenterItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "مرکز درمانی امید",
|
||||
address: "تهران، خیابان ولیعصر، بالاتر از پارک ساعی، پلاک ۱۲۳",
|
||||
isUrgent: true,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "کلینیک تخصصی مهر",
|
||||
address: "مشهد، بلوار وکیلآباد، بین وکیلآباد ۲۱ و ۲۳",
|
||||
isUrgent: false,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "بیمارستان شبانهروزی آتیه",
|
||||
address: "اصفهان، خیابان شریعتی، کوچه ۸، ساختمان آتیه",
|
||||
isUrgent: true,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "مرکز سلامت نوین",
|
||||
address: "شیراز، میدان مطهری، خیابان معدل، نبش کوچه ۶",
|
||||
isUrgent: false,
|
||||
},
|
||||
];
|
||||
|
||||
export default function CenterRegistrationForm() {
|
||||
const [activeStep, setActiveStep] = useState(1);
|
||||
const [maxStepReached, setMaxStepReached] = useState(1);
|
||||
|
||||
const [selectedCenterId, setSelectedCenterId] = useState<string | null>(null);
|
||||
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
const selectedCenter =
|
||||
centersMock.find((center) => center.id === selectedCenterId) || null;
|
||||
|
||||
const handleNext = () => {
|
||||
if (activeStep < TOTAL_STEPS) {
|
||||
const nextStep = activeStep + 1;
|
||||
setActiveStep(nextStep);
|
||||
if (nextStep > maxStepReached) setMaxStepReached(nextStep);
|
||||
export interface RegistrationCenterFormValues {
|
||||
selectedCenter: CenterItem | null;
|
||||
}
|
||||
|
||||
export interface WizardFormData {
|
||||
registrationCenter: {
|
||||
selectedCenter: CenterItem | null;
|
||||
};
|
||||
identity: IdentityFormValues;
|
||||
}
|
||||
|
||||
export interface RegistrationCenterFormProps {
|
||||
step: number;
|
||||
setStep: React.Dispatch<React.SetStateAction<number>>;
|
||||
data: WizardFormData;
|
||||
update: (patch: Partial<WizardFormData>) => void;
|
||||
}
|
||||
|
||||
const EMPTY_VALUES: RegistrationCenterFormValues = {
|
||||
selectedCenter: null,
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (activeStep > 1) setActiveStep((prev) => prev - 1);
|
||||
};
|
||||
const RegistrationCenterFormValidationSchema = yup.object({
|
||||
selectedCenter: yup
|
||||
.mixed<CenterItem>()
|
||||
.nullable()
|
||||
.required("لطفاً یک مرکز را انتخاب کنید"),
|
||||
});
|
||||
|
||||
const goToStep = (step: number) => {
|
||||
if (step <= maxStepReached) setActiveStep(step);
|
||||
};
|
||||
const RegistrationCenterForm = withFormik<
|
||||
RegistrationCenterFormProps,
|
||||
RegistrationCenterFormValues
|
||||
>({
|
||||
enableReinitialize: true,
|
||||
|
||||
const renderCenterList = () => {
|
||||
return (
|
||||
<>
|
||||
mapPropsToValues: (props) => ({
|
||||
selectedCenter: props.data.registrationCenter.selectedCenter ?? null,
|
||||
}),
|
||||
|
||||
<>
|
||||
{centersMock.map((center) => {
|
||||
const isSelected = selectedCenterId === center.id;
|
||||
validationSchema: RegistrationCenterFormValidationSchema,
|
||||
|
||||
return (
|
||||
<div className="col-span-1" key={center.id}>
|
||||
<Box
|
||||
onClick={() => setSelectedCenterId(center.id)}
|
||||
sx={{
|
||||
p: 2.5,
|
||||
borderRadius: "18px",
|
||||
border: isSelected
|
||||
? "2px solid #2563eb"
|
||||
: "1px solid #e2e8f0",
|
||||
backgroundColor: isSelected ? "#eff6ff" : "#fff",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.25s ease",
|
||||
boxShadow: isSelected
|
||||
? "0 10px 25px -15px rgba(37,99,235,0.45)"
|
||||
: "0 4px 12px rgba(15,23,42,0.04)",
|
||||
"&:hover": {
|
||||
transform: "translateY(-2px)",
|
||||
borderColor: "#2563eb",
|
||||
boxShadow: "0 12px 24px -16px rgba(37,99,235,0.35)",
|
||||
handleSubmit: (values, { props }) => {
|
||||
props.update({
|
||||
registrationCenter: values,
|
||||
});
|
||||
|
||||
props.setStep((prev) => prev + 1);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
gap: 2,
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<BusinessIcon sx={{ color: "#2563eb", fontSize: 22 }} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
color: "#0f172a",
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
{center.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
})(InnerRegistrationCenterForm);
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<LocationOnIcon
|
||||
sx={{ color: "#94a3b8", fontSize: 18, mt: "2px" }}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
color: "#64748b",
|
||||
fontSize: "0.92rem",
|
||||
lineHeight: 1.9,
|
||||
}}
|
||||
>
|
||||
{center.address}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Chip
|
||||
icon={<LocalHospitalIcon />}
|
||||
label={
|
||||
center.isUrgent
|
||||
? "استخدام فوری دارد"
|
||||
: "استخدام فوری ندارد"
|
||||
}
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
backgroundColor: center.isUrgent
|
||||
? "#fee2e2"
|
||||
: "#e2e8f0",
|
||||
color: center.isUrgent ? "#b91c1c" : "#475569",
|
||||
"& .MuiChip-icon": {
|
||||
color: center.isUrgent ? "#dc2626" : "#64748b",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{isSelected && (
|
||||
<CheckCircleIcon
|
||||
sx={{ color: "#2563eb", fontSize: 24 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStepContent = (step: number) => {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return renderCenterList();
|
||||
|
||||
case 2:
|
||||
return selectedCenter ? (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Typography sx={{ fontWeight: 800, color: "#0f172a", mb: 2 }}>
|
||||
مرکز انتخابشده
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
borderRadius: "20px",
|
||||
backgroundColor: "#fff",
|
||||
border: "1px solid #e2e8f0",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
fontSize: "1.1rem",
|
||||
color: "#2563eb",
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
{selectedCenter.name}
|
||||
</Typography>
|
||||
<Typography sx={{ color: "#64748b", mb: 2 }}>
|
||||
{selectedCenter.address}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={
|
||||
selectedCenter.isUrgent
|
||||
? "استخدام فوری دارد"
|
||||
: "استخدام فوری ندارد"
|
||||
}
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
backgroundColor: selectedCenter.isUrgent
|
||||
? "#dbeafe"
|
||||
: "#e2e8f0",
|
||||
color: selectedCenter.isUrgent ? "#1d4ed8" : "#475569",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography className="text-center text-[#94a3b8]">
|
||||
ابتدا از مرحله قبل یک مرکز را انتخاب کنید.
|
||||
</Typography>
|
||||
);
|
||||
|
||||
case 3:
|
||||
return selectedCenter ? (
|
||||
<Box sx={{ textAlign: "center" }}>
|
||||
<BusinessIcon
|
||||
sx={{
|
||||
fontSize: 60,
|
||||
color: selectedCenter.isUrgent ? "#ef4444" : "#2563eb",
|
||||
mb: 2,
|
||||
}}
|
||||
/>
|
||||
<Typography variant="h6" sx={{ mb: 1, fontWeight: 700 }}>
|
||||
وضعیت استخدام فوری
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{selectedCenter.isUrgent
|
||||
? "این مرکز دارای استخدام فوری است."
|
||||
: "این مرکز در حال حاضر استخدام فوری ندارد."}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={selectedCenter.isUrgent ? "فوری" : "عادی"}
|
||||
sx={{
|
||||
px: 1,
|
||||
fontWeight: 800,
|
||||
backgroundColor: selectedCenter.isUrgent
|
||||
? "#fee2e2"
|
||||
: "#e2e8f0",
|
||||
color: selectedCenter.isUrgent ? "#dc2626" : "#475569",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography className="text-center text-[#94a3b8]">
|
||||
ابتدا یک مرکز انتخاب کنید.
|
||||
</Typography>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<Typography className="text-center text-[#94a3b8]">
|
||||
محتوای مرحله <b>«{STEP_LABELS[step - 1]}»</b> <br />
|
||||
(در حال توسعه...)
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ width: isMobile ? "100%" : "100%", flexGrow: 1 }}>
|
||||
<div className="w-full grid grid-cols-2 gap-4">
|
||||
{renderStepContent(activeStep)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default RegistrationCenterForm;
|
||||
|
||||
Reference in New Issue
Block a user