Files
clinicpro/.claude/prompt/admin-jalali-date-input.md

9.0 KiB
Raw Permalink Blame History

همه‌ی تقویم‌های پنل ادمین باید شمسی باشند (رفع تقویم میلادی PersianDateInput)

پروژه

clinicpro (React 19 admin SPA داخل Symfony — assets/admin/)

زمینه

کل محصول فارسی/RTL و تاریخ‌ها Jalali (شمسی) است. تقویم شمسی قبلاً نوشته شده: PersianCalendar.tsx (پیکر ماه/سال/روز شمسی، خروجی YYYY-MM-DD میلادی) و روکش آن PersianDatePicker.tsx. اما یک کامپوننت دیگر به‌نام PersianDateInput.tsx هنوز از <input type="date"> بومی مرورگر استفاده می‌کند که تقویم میلادی مرورگر را باز می‌کند. هرجای پنل که این کامپوننت استفاده شده، کاربر تقویم میلادی می‌بیند — خلاف قاعده‌ی محصول.

مشکل / هدف

هدف: هرجای پنل ادمین که از انتخاب تاریخ (تقویم) استفاده می‌شود، تقویم شمسی نمایش دهد.

تنها منبع میلادی در کل ادمین: assets/admin/components/ui/PersianDateInput.tsx خط ۵۶ (type="date"). با اصلاح همین یک فایل، همه‌ی call siteهای زیر یک‌جا شمسی می‌شوند (بدون تغییر در آن‌ها، چون امضای Props ثابت می‌ماند).

بررسی انجام‌شده:

  • PersianCalendar / PersianDatePicker قبلاً شمسی‌اند — نیازی به بازنویسی ندارند.
  • در کل assets/admin فقط یک type="date" وجود دارد (همین فایل). هیچ datetime-local / month / week بومی دیگری نیست.
  • تقویم ماهانه‌ی درون DoctorDetailPage.tsx و PersianDateInput محلیِ همان فایل از قبل با jalaali-js شمسی‌اند — دست نزن.

فایل‌های مرتبط

فایل نقش
assets/admin/components/ui/PersianDateInput.tsx تنها فایلی که تغییر می‌کند — حذف input بومی، استفاده از PersianCalendar
assets/admin/components/ui/PersianCalendar.tsx پیکر شمسی موجود (Props: value, onChange, onClose, enableYearPicker) — مرجع
assets/admin/components/ui/PersianDatePicker.tsx الگوی درستِ استفاده از PersianCalendar (کپی همین ساختار)
assets/admin/lib/utils.tsformatDate() نمایش شمسیِ مقدار انتخاب‌شده (از قبل استفاده می‌شود)

call siteهای فعلی PersianDateInput (نباید تغییر کنند — فقط برای اطمینان از سازگاری Props)

components/AppointmentActions.tsx:482,613   value/onChange
components/InsuranceModal.tsx:153,157        value/onChange/placeholder
components/NewAppointmentDrawer.tsx:257      value/onChange
components/PatientsFilterModal.tsx:84,86     value/onChange/placeholder
pages/AppointmentEditPage.tsx:150            value/onChange
pages/MyPaymentsPage.tsx:108,111             value/onChange/placeholder
pages/AppointmentCreatePage.tsx:254          value/onChange
pages/PatientDetailPage.tsx:373,510          value/onChange
pages/PatientRecordFormPage.tsx:132          value/onChange  ← تاریخ تولد (نیاز به enableYearPicker)
pages/DoctorDetailPage.tsx:1687,1914,1918    این‌ها به PersianDateInput محلیِ همان فایل وصل‌اند، نه فایل مشترک — دست نزن

وضعیت فعلی (کد مشکل‌دار)

assets/admin/components/ui/PersianDateInput.tsx — لایه‌ی متنی شمسی است ولی پیکر بومی میلادی:

{/* hidden native input — opens picker on click */}
<input
  ref={hiddenRef}
  type="date"                    // ← تقویم میلادی مرورگر
  value={value}
  min={min}
  max={max}
  onChange={e => onChange(e.target.value)}
  style={{ position: 'absolute', opacity: 0, pointerEvents: 'none', width: 1, height: 1, top: 0, left: 0 }}
  tabIndex={-1}
/>

Props فعلی: value, onChange, placeholder?, min?, max?, style?, className?. نکته: min/max در هیچ call siteی پاس داده نمی‌شوند (Prop مرده‌اند).

وظایف

۱. بازنویسی PersianDateInput.tsx روی پیکر شمسی

<input type="date"> را حذف کن و همان الگوی PersianDatePicker.tsx را به کار ببر: state باز/بسته + رندر شرطی <PersianCalendar>. لایه‌ی متنی visible و formatDate(value) و دکمه‌ی پاک‌کردن (X) و امضای Props فعلی را حفظ کن تا هیچ call siteی نشکند.

import { useState } from 'react';
import { CalendarDaysIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { formatDate } from '../../lib/utils';
import PersianCalendar from './PersianCalendar';

interface Props {
  value: string;                 // YYYY-MM-DD میلادی
  onChange: (v: string) => void;
  placeholder?: string;
  enableYearPicker?: boolean;    // برای تاریخ تولد
  style?: React.CSSProperties;
  className?: string;
}

export default function PersianDateInput({ value, onChange, placeholder = 'انتخاب تاریخ', enableYearPicker = false, style, className }: Props) {
  const [open, setOpen] = useState(false);
  return (
    <div style={{ position: 'relative', display: 'inline-block', ...style }} className={className}>
      <div onClick={() => setOpen(o => !o)} style={{ /* همان استایل visible فعلی: height 36, border/surface, icon, placeholder color */ }}>
        <CalendarDaysIcon /* … */ />
        <span style={{ flex: 1 }}>{value ? formatDate(value) : placeholder}</span>
        {value && <span onClick={e => { e.stopPropagation(); onChange(''); }}><XMarkIcon /* … */ /></span>}
      </div>
      {open && (
        <PersianCalendar
          value={value}
          onChange={v => { onChange(v); setOpen(false); }}
          onClose={() => setOpen(false)}
          enableYearPicker={enableYearPicker}
        />
      )}
    </div>
  );
}
  • min/max را حذف کن (استفاده‌ای ندارند). اگر خواستی امن‌تر باشی، نگه‌شان دار ولی بدون اثر — ترجیح: حذف، مطابق قاعده‌ی «کد مرده ننویس».
  • استایل لایه‌ی visible دقیقاً همان مقادیر فعلی فایل بماند (height 36، --border، --surface، minWidth 148، fontSize 13، رنگ placeholder با --text-3).

۲. فعال‌کردن انتخاب سال برای تاریخ تولد

در pages/PatientRecordFormPage.tsx:132 (فیلد birth_date) پراپ enableYearPicker را بده تا کاربر بتواند سریع سال تولد را انتخاب کند:

<PersianDateInput value={form.watch('birth_date') ?? ''} onChange={(v) => form.setValue('birth_date', v)} enableYearPicker />

(الگوی مشابه از قبل در PatientRecordInfoForm.tsx:133 با PersianDatePicker … enableYearPicker هست.)

۳. رفع هم‌پوشانی SOLID (اختیاری ولی توصیه‌شده)

بعد از این تغییر، PersianDateInput و PersianDatePicker تقریباً یکی می‌شوند (هر دو = لایه‌ی متنی + PersianCalendar). برای پرهیز از دوگانگی:

  • گزینه‌ی ساده: PersianDateInput را یک روکش نازک روی PersianDatePicker کن (return <PersianDatePicker {...props} />)، یا
  • در همین تسک فقط رفتار را یکی کن و در کامنت بالای فایل اشاره کن که این دو باید در آینده ادغام شوند. حذف کامل یکی از آن‌ها → نیازمند به‌روزرسانی همه‌ی importها است؛ اگر انجامش می‌دهی، همه‌ی call siteها را هم اصلاح کن و tscرا سبز نگه دار.

نکات مهم

  • بعد از تغییر، حتماً تایپ‌چک: ddev exec npx tsc --noEmit --project tsconfig.json باید سبز شود.
  • خروجی PersianCalendar.onChange همان YYYY-MM-DD میلادی است؛ قرارداد داده‌ی ارسالی به API تغییر نمی‌کند — فقط UI تقویم شمسی می‌شود. رفتار submit/فیلترها نباید عوض شود.
  • DoctorDetailPage.tsx یک PersianDateInput محلیِ درون‌فایل دارد (تعریف حدود خط ۱۷۷) که با jalaali-js از قبل شمسی است و پراپ minDate دارد؛ به فایل مشترک ربطی ندارد — دست نزن.
  • تست‌ها: اگر تستی برای PersianDateInput هست، آپدیت کن؛ در غیر این صورت یک تست کوتاه Vitest اضافه کن که کلیک روی input، پیکر شمسی را باز می‌کند و انتخاب روز، onChange با YYYY-MM-DD را صدا می‌زند (حالت موفق + پاک‌کردن مقدار).
  • تغییرِ فقط-UI است؛ backend و docs/api/* نیاز به تغییر ندارند.