Files
clinicpro/.claude/prompt/doctor-experience-activity-date.md
T

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/utilstoDate, 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 (که native type=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 به‌روز کن.