feat(date-input): refactor PersianDateInput to use PersianDatePicker for Jalali calendar support

This commit is contained in:
hamed
2026-07-16 08:26:39 +03:30
parent 2f060bd5be
commit 7b8a5c2775
4 changed files with 210 additions and 63 deletions
+156
View File
@@ -0,0 +1,156 @@
# همه‌ی تقویم‌های پنل ادمین باید شمسی باشند (رفع تقویم میلادی 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.ts``formatDate()` | نمایش شمسیِ مقدار انتخاب‌شده (از قبل استفاده می‌شود) |
### 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` — لایه‌ی متنی شمسی است ولی پیکر بومی میلادی:
```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ی نشکند.
```tsx
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` را بده
تا کاربر بتواند سریع سال تولد را انتخاب کند:
```tsx
<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/*` نیاز به تغییر ندارند.