12 KiB
افزودن فیلد «تاریخ شروع فعالیت» (سال تجربه) در پنل ادمین با تقویم شمسی
پروژه
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 ناقص)
// 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']);
کامپوننت تقویم (قرارداد ورودی/خروجی)
// 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:
<PersianDatePicker value={birthDate} onChange={setBirthDate} placeholder="تاریخ تولد" enableYearPicker />
فرم ویرایش پزشک — DoctorDetailPage.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<ApiResponse<any>>(`/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)
mutationFn: (values: FormValues) =>
api.post<ApiResponse<{ uuid: string }>>(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 نباشد:
const [editActivityDate, setEditActivityDate] = useState(''); // YYYY-MM-DD میلادی یا ''
هنگام لود در useEffect/reset مقدار اولیه را از activity_time (ثانیه) بساز:
setEditActivityDate(
doctor.activity_time ? toGregorianDate(toDate(Number(doctor.activity_time))!) : ''
);
(از ../lib/utils → toDate, toGregorianDate را import کن اگر نیستند.)
در JSX، بعد از grid «درجه تحصیلی / کد نظام پزشکی» (خط ~۲۸۷۸) یک EditField اضافه کن:
<div style={{ marginTop: 14 }}>
<EditField label="تاریخ شروع فعالیت">
<PersianDatePicker
value={editActivityDate}
onChange={setEditActivityDate}
placeholder="انتخاب تاریخ"
enableYearPicker
minWidth={200}
/>
</EditField>
</div>
در updateMut کلید activity_time را اضافه کن (تبدیل به ثانیه؛ خالی → null تا پاک شود):
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را جدا از عدد مدیریت کند). در غیر این صورت وقتی خالی است اصلاً کلید را نفرست:...(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 تبدیل نشود:
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 را اضافه کن: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(که nativetype=dateمیلادی است) استفاده نکن. - قرارداد مقدار picker میلادی است (
YYYY-MM-DD)؛ نمایش داخلی خودش شمسی است. تبدیل به/از Unix timestamp سمت فرم انجام میشود. - واحد timestamp ثانیه است (نه میلیثانیه) —
getExperience()باtime()(ثانیه) کار میکند. حتماً/1000بزن. - برای ثبات در برابر timezone از
T12:00:00(ظهر) هنگام ساختDateاستفاده کن تا با تبدیل شمسی یکروز-جابهجایی رخ ندهد (الگوی موجود درutils.toDate). DoctorProfilePageصرفاً<DoctorDetailPage isOwnProfile />است؛ با اصلاح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 بهروز کن.