# افزودن فیلد «تاریخ شروع فعالیت» (سال تجربه) در پنل ادمین با تقویم شمسی ## پروژه `clinicpro` (Admin SPA + کمی backend/docs). سایت عمومی `nobat724_front` تغییری لازم ندارد (توضیح در «نکات مهم»). ## زمینه هر پزشک باید «سال تجربه» داشته باشد. این عدد در backend از فیلد `activity_time` (تاریخ شروع کار، به‌صورت Unix timestamp) محاسبه می‌شود و همه‌چیزِ backend و سایت عمومی از قبل آماده است: - `Doctor::$activityTime` (ستون `activity_time`, `integer`, nullable) + `getExperience()` که سال تجربه را حساب می‌کند. - `toDetailArray()` هم `experience` (عدد سال) و هم `activity_time` (رشتهٔ timestamp) را برمی‌گرداند. - endpointهای `POST /api/v1/doctor` و `PATCH /api/v1/doctor/{uuid}` کلید `activity_time` را در بدنه می‌پذیرند (`hydrate...` → `setActivityTime((int) $data['activity_time'])`). - سایت عمومی در `nobat724_front/components/doctor/detailDoctor/Title.js` از قبل `{doctor?.experience} سال تجربه` را نشان می‌دهد. مشکل این است که **پنل ادمین (Admin SPA) هیچ فیلدی برای ست‌کردن این تاریخ ندارد**، پس `activity_time` همیشه `null` می‌ماند، `getExperience()` صفر برمی‌گرداند و در صفحهٔ عمومی «۰ سال تجربه» نمایش داده می‌شود (مثال: `/doctor/fbc11068-dd01-4c0c-9dab-ab86c38061ca`). ## مشکل / هدف در فرم‌های پزشک در Admin SPA یک فیلد «تاریخ شروع فعالیت» با **تقویم شمسی** اضافه شود که ترتیب انتخاب آن **سال → ماه → روز** باشد. این کامپوننت از قبل در پروژه وجود دارد: `PersianDatePicker` با prop `enableYearPicker`. مقدار انتخاب‌شده باید به Unix timestamp (ثانیه) تبدیل و در کلید `activity_time` به API ارسال شود؛ و هنگام ویرایش، `activity_time` موجود باید به تاریخ برای نمایش در picker تبدیل شود. ## فایل‌های مرتبط | فایل | نقش | |------|-----| | `clinicpro/assets/admin/pages/DoctorDetailPage.tsx` | صفحهٔ اصلی ویرایش پزشک (هم ادمین هم پروفایل خود پزشک via `DoctorProfilePage`) — فرم ویرایش اینجاست | | `clinicpro/assets/admin/pages/DoctorFormPage.tsx` | فرم «افزودن پزشک جدید» (POST) | | `clinicpro/assets/admin/components/ui/PersianDatePicker.tsx` | تقویم شمسی موجود؛ prop `enableYearPicker` = ترتیب سال→ماه→روز | | `clinicpro/assets/admin/lib/utils.ts` | `toDate()`، `toGregorianDate()`، `formatDate()` برای تبدیل تاریخ | | `clinicpro/src/Doctor/Controller/DoctorController.php` | annotationهای `OA\Property` بدنهٔ create/update (فاقد `activity_time`) | | `clinicpro/docs/api/doctor.md` | مستندات request body (فاقد `activity_time`) | ## وضعیت فعلی ### backend (آماده — فقط annotation/doc ناقص) ```php // src/Doctor/Entity/Doctor.php public function getExperience(): int { if ($this->activityTime === null) { return 0; } return max(0, (int)((time() - $this->activityTime) / (365.25 * 24 * 3600))); } // src/Doctor/Controller/DoctorController.php (hydrate مشترک create/update) if (array_key_exists('activity_time', $data)) $doctor->setActivityTime((int) $data['activity_time']); ``` ### کامپوننت تقویم (قرارداد ورودی/خروجی) ```tsx // PersianDatePicker.tsx interface Props { value: string; // YYYY-MM-DD میلادی onChange: (v: string) => void; placeholder?: string; height?: number; minWidth?: number; enableYearPicker?: boolean; // سال→ماه→روز } ``` مقدار picker یک رشتهٔ **میلادی `YYYY-MM-DD`** است (نمایش داخلی‌اش شمسی است). نمونهٔ استفادهٔ موجود در `RepresentationProfilePage.tsx`: ```tsx ``` ### فرم ویرایش پزشک — `DoctorDetailPage.tsx` (وضعیت فعلی، بدون فیلد تاریخ) ```tsx // interface (خط ~44) experience: number; activity_time: string | null; medical_system_code: string | null; // zod schema (خط ~2191) const editSchema = z.object({ name: z.string().min(2, 'نام حداقل ۲ کاراکتر'), gender: z.enum(['man', 'woman']).optional().or(z.literal('')), degree: z.string().optional().or(z.literal('')), medical_system_code: z.string().max(30).optional().or(z.literal('')), // ... activity_time نیست }); // reset مقادیر هنگام لود (خط ~2383) reset({ name: doctor.name, gender: (doctor.gender as any) ?? '', degree: doctor.degree ?? '', medical_system_code: doctor.medical_system_code ?? '', // ... activity_time نیست }); // ارسال PATCH (خط ~2415) mutationFn: (body: EditForm) => api.patch>(`/api/v1/doctor/${uuid}`, { title: body.name, gender: editGender || undefined, degree: body.degree || undefined, medical_system_code: body.medical_system_code || undefined, info: body.info || undefined, // ... activity_time نیست }), // JSX فرم — بعد از grid «درجه تحصیلی / کد نظام پزشکی» (خط ~2878) ``` ### فرم ساخت پزشک — `DoctorFormPage.tsx` (خط ~282) ```tsx mutationFn: (values: FormValues) => api.post>(createDoctorEndpoint, { // ... gender: gender || undefined, degree: values.degree || undefined, medical_system_code: values.medical_system_code || undefined, info: values.info || undefined, // ... activity_time نیست }), ``` ## وظایف ### ۱. افزودن فیلد تاریخ به فرم ویرایش `DoctorDetailPage.tsx` (اصلی) state جدا برای تاریخ نگه‌دار (مثل الگوی `editGender`) تا نیازی به دست‌زدن به zod نباشد: ```tsx const [editActivityDate, setEditActivityDate] = useState(''); // YYYY-MM-DD میلادی یا '' ``` هنگام لود در `useEffect`/`reset` مقدار اولیه را از `activity_time` (ثانیه) بساز: ```tsx setEditActivityDate( doctor.activity_time ? toGregorianDate(toDate(Number(doctor.activity_time))!) : '' ); ``` (از `../lib/utils` → `toDate`, `toGregorianDate` را import کن اگر نیستند.) در JSX، بعد از grid «درجه تحصیلی / کد نظام پزشکی» (خط ~۲۸۷۸) یک `EditField` اضافه کن: ```tsx
``` در `updateMut` کلید `activity_time` را اضافه کن (تبدیل به ثانیه؛ خالی → `null` تا پاک شود): ```tsx activity_time: editActivityDate ? Math.floor(new Date(`${editActivityDate}T12:00:00`).getTime() / 1000) : null, ``` > نکته: backend فقط وقتی کلید `activity_time` در بدنه **باشد** آن را ست می‌کند؛ ارسال `null` باعث `setActivityTime((int) null) = 0` می‌شود. اگر می‌خواهی «پاک‌کردن» واقعی (بازگشت به null) پشتیبانی شود، ببین وظیفهٔ ۳ (backend باید `null` را جدا از عدد مدیریت کند). در غیر این صورت وقتی خالی است اصلاً کلید را نفرست: > ```tsx > ...(editActivityDate ? { activity_time: Math.floor(new Date(`${editActivityDate}T12:00:00`).getTime()/1000) } : {}), > ``` > رویکرد دوم (نفرستادن هنگام خالی) ساده‌تر و بدون تغییر backend است — همان را استفاده کن مگر پاک‌کردن لازم باشد. همچنین (اختیاری ولی مفید): در بخش نمایش، کارت `experience` از قبل هست (`InfoCard ... label="سابقه (سال)"`); می‌توانی زیرش تاریخ شروع را با `formatDate(doctor.activity_time)` نشان دهی. ### ۲. افزودن همان فیلد به فرم ساخت `DoctorFormPage.tsx` - `const [activityDate, setActivityDate] = useState('')` - در JSX کنار سایر فیلدهای «اطلاعات حرفه‌ای» یک `PersianDatePicker ... enableYearPicker` با همان الگو. - در بدنهٔ `api.post(createDoctorEndpoint, {...})` هنگام مقدار داشتن، `activity_time` را به ثانیه اضافه کن (همان تبدیل وظیفهٔ ۱). ### ۳. (فقط اگر «پاک‌کردن» لازم است) اصلاح hydrate در backend اگر تصمیم گرفتی ارسال `null` را پشتیبانی کنی، در `DoctorController` جایی که `activity_time` hydrate می‌شود مقدار `null` را جدا مدیریت کن تا به `0` تبدیل نشود: ```php if (array_key_exists('activity_time', $data)) { $doctor->setActivityTime($data['activity_time'] !== null ? (int) $data['activity_time'] : null); } ``` اگر رویکرد «نفرستادن هنگام خالی» را انتخاب کردی، این وظیفه لازم نیست. ### ۴. به‌روزرسانی annotation و مستندات API - در `DoctorController` به بدنهٔ **هر دو** endpoint (create ~خط ۶۳ و update ~خط ۲۷۱) این property را اضافه کن: ```php new OA\Property(property: 'activity_time', type: 'integer', nullable: true, description: 'Unix timestamp (ثانیه) تاریخ شروع فعالیت؛ مبنای محاسبهٔ سال تجربه'), ``` - در `clinicpro/docs/api/doctor.md`: به جدول request body هر دو endpoint (POST و PATCH) ردیف `activity_time` (integer, Unix seconds, nullable) را اضافه کن و توضیح بده که `experience` در پاسخ از همین فیلد محاسبه می‌شود. ## نکات مهم - **تقویم موجود را استفاده کن، جدید نساز:** `PersianDatePicker` با `enableYearPicker` دقیقاً ترتیب سال→ماه→روز را می‌دهد (همان که کاربر گفت «قبلاً این تقویم را نوشتی»). از `PersianDateInput` (که native `type=date` میلادی است) استفاده **نکن**. - **قرارداد مقدار picker میلادی است** (`YYYY-MM-DD`)؛ نمایش داخلی خودش شمسی است. تبدیل به/از Unix timestamp سمت فرم انجام می‌شود. - **واحد timestamp ثانیه است** (نه میلی‌ثانیه) — `getExperience()` با `time()` (ثانیه) کار می‌کند. حتماً `/1000` بزن. - برای ثبات در برابر timezone از `T12:00:00` (ظهر) هنگام ساخت `Date` استفاده کن تا با تبدیل شمسی یک‌روز-جابه‌جایی رخ ندهد (الگوی موجود در `utils.toDate`). - `DoctorProfilePage` صرفاً `` است؛ با اصلاح `DoctorDetailPage` هر دو مسیر (ادمین و پروفایل خود پزشک) پوشش داده می‌شوند. - **سایت عمومی تغییری لازم ندارد:** `Title.js` از قبل `doctor.experience` را رندر می‌کند؛ به‌محض ست‌شدن `activity_time`، مقدار درست نمایش داده می‌شود. (اختیاری: اگر خواستی وقتی `experience === 0` عبارت «۰ سال تجربه» نمایش داده نشود، این یک تغییر کوچک نمایشی در `nobat724_front` است، خارج از این پرامپت.) - بعد از تغییر TSX: `ddev exec npx tsc --noEmit --project tsconfig.json` و `ddev exec yarn dev` برای build. - بعد از تغییر annotation کنترلر: `ddev exec php bin/console cache:clear` و طبق قانون پروژه `docs/api/doctor.md` را در همین session به‌روز کن.