# جایگزینی همه‌ی `<select>` بومی پنل ادمین با `SearchableSelect`

## پروژه

`clinicpro` (React 19 admin SPA — `assets/admin/`)

## زمینه

در سراسر پنل ادمین از `<select>` بومی HTML استفاده شده (استایل درون‌خطی تکراری،
بدون جست‌وجو، ظاهر ناهماهنگ با طراحی‌سیستم، بدون RTL/dark درست). طراحی‌سیستم یک
کامپوننت مشترک دارد: `assets/admin/components/ui/SearchableSelect.tsx` (روی `react-select`،
قابل جست‌وجو، RTL، هماهنگ با توکن‌های CSS و dark mode، منو portal با `zIndex 9999`).

**هدف:** همه‌ی `<select>`های بومی پنل با `SearchableSelect` جایگزین شوند تا ظاهر و رفتار
یکدست شود (مثل دسته‌بندی «افزودن کالا» در `AddItemModal.tsx` که از قبل `SearchableSelect` است).

## API کامپوننت `SearchableSelect` (مرجع — تغییرش نده)

```tsx
interface SelectOption { value: string | number; label: string }
interface Props {
  options: SelectOption[];
  value?: string | number | null;
  onChange?: (value: string | number | null) => void;   // مقدار خام، نه event
  placeholder?: string;          // پیش‌فرض «انتخاب کنید...»
  isLoading?: boolean;
  isDisabled?: boolean;
  isClearable?: boolean;
  noOptionsMessage?: string;     // پیش‌فرض «موردی یافت نشد»
  inputId?: string;
  height?: number;               // پیش‌فرض 42؛ برای هم‌ارتفاعی با فیلدهای 38px مقدار بده
}
```

نکات مهمِ تفاوت با `<select>`:
- `onChange` مقدار خام می‌دهد (`string|number|null`) نه `e.target.value`.
- `options` باید `{ value, label }` باشد — نه `<option>`.
- گزینه‌ی خالی (`<option value="">انتخاب...</option>`) حذف و به `placeholder` منتقل شود؛
  اگر خالی‌کردن مجاز است `isClearable` بده.
- `disabled` → `isDisabled`.

## فایل‌های دارای `<select>` بومی (۱۶ فایل، ~۳۵ مورد)

| فایل | تعداد | نکته |
|------|:----:|------|
| `pages/MyPatientsPage.tsx` | 4 | فیلترها |
| `pages/AppointmentEditPage.tsx` | 4 | |
| `pages/AppointmentCreatePage.tsx` | 4 | |
| `components/NewAppointmentDrawer.tsx` | 4 | بخش/زیربخش/سرویس (وابسته) |
| `components/AppointmentActions.tsx` | 4 | خطوط 850,869,961,976 |
| `pages/DoctorDetailPage.tsx` | 3 | |
| `pages/PatientRecordFormPage.tsx` | 2 | **RHF `register`** (gender:119، referral_source:135) |
| `components/AppointmentFiltersModal.tsx` | 2 | `sel` استایل مشترک؛ سرویس وابسته به بخش (`disabled`) |
| `pages/ReserveAppointmentsPage.tsx` | 1 | |
| `pages/RepresentationSettlementPage.tsx` | 1 | |
| `pages/MyPaymentsPage.tsx` | 1 | |
| `pages/InventoryPage.tsx` | 1 | فیلتر دسته |
| `pages/AppointmentsPage.tsx` | 1 | سرویس (خط 653) |
| `components/inventory/AddPackageModal.tsx` | 1 | |
| `components/dashboard/TauriDashboardView.tsx` | 1 | |
| `components/PatientsFilterModal.tsx` | 1 | |

## وضعیت فعلی — سه الگوی رایج

### الگوی A — controlled با `value` + `onChange` (بیشترین)
```tsx
// AppointmentsPage.tsx:653
<select aria-label="سرویس" value={value} onChange={e => onChange(e.target.value)} style={sel}>
  <option value="">سرویس مورد نظر را انتخاب کنید...</option>
  {options.map(s => <option key={s.uuid} value={s.uuid}>{s.name}</option>)}
</select>
```

### الگوی B — گزینه‌های وابسته / disabled
```tsx
// AppointmentFiltersModal.tsx:100
<select aria-label="سرویس" style={{ ...sel, margin: '6px 0 14px' }} value={f.itemUuid} disabled={!f.sectionUuid}
  onChange={...}>
  <option value="">انتخاب سرویس</option>
  {(itemsQ.data?.data ?? []).map(o => <option key={o.uuid} value={o.uuid}>{o.name}</option>)}
</select>
```

### الگوی C — React Hook Form با `register` (خاص — نیازمند Controller)
```tsx
// PatientRecordFormPage.tsx:119
<div className="field"><select {...form.register('gender')} style={{...}}>
  <option value="">انتخاب...</option>
  <option value="female">زن</option>
  <option value="male">مرد</option>
</select></div>
```

## وظایف

> **یک فایل در هر مرحله.** بعد از هر فایل: `tsc` سبز شود، بعد فایل بعدی. ترتیب: از فایل‌های کم‌مورد به پرمورد، یا هر ترتیبی، ولی هر فایل مستقل تست/تایپ‌چک شود.

### ۱. تبدیل الگوی A (controlled)

هر `<select value onChange>` را با این جایگزین کن:

```tsx
<SearchableSelect
  options={options.map(s => ({ value: s.uuid, label: s.name }))}
  value={value || null}
  onChange={v => onChange(v ? String(v) : '')}   // اگر state رشته است String() بزن
  placeholder="سرویس مورد نظر را انتخاب کنید..."  // همان متن option خالی
  isClearable                                     // اگر خالی‌کردن مجاز بود
  height={38}                                     // برای هم‌ارتفاعی با فیلدهای فعلی
/>
```

- import در بالای فایل: `import SearchableSelect from '../ui/SearchableSelect';`
  (عمق مسیر را بر اساس محل فایل تنظیم کن: از `pages/` → `'../components/ui/SearchableSelect'`).
- استایل درون‌خطی `sel`/`style` روی select حذف شود (کامپوننت خودش استایل دارد).
  اگر `margin` بیرونی لازم بود، در یک `<div style={{ margin }}>` دور کامپوننت بگذار.
- `aria-label` را حفظ کن: چون react-select خودش input دارد، برای دسترس‌پذیری از
  `inputId` + یک `<label htmlFor>` مخفی یا `aria-label` روی wrapper استفاده کن (اختیاری ولی بهتر).

### ۲. تبدیل الگوی B (وابسته/disabled)

مثل A ولی `isDisabled` را از شرط قبلی بده و در صورت لود‌شدن آسنکرون `isLoading` را از
`query.isLoading` بده:

```tsx
<SearchableSelect
  options={(itemsQ.data?.data ?? []).map(o => ({ value: o.uuid, label: o.name }))}
  value={f.itemUuid || null}
  onChange={v => setF({ ...f, itemUuid: v ? String(v) : '' })}
  placeholder="انتخاب سرویس"
  isDisabled={!f.sectionUuid}
  isLoading={itemsQ.isLoading}
  isClearable
  height={38}
/>
```

### ۳. تبدیل الگوی C (React Hook Form)

`register` روی `SearchableSelect` کار نمی‌کند (input بومی نیست). دو راه — راه ساده `watch`+`setValue`:

```tsx
<SearchableSelect
  options={[{ value: 'female', label: 'زن' }, { value: 'male', label: 'مرد' }]}
  value={form.watch('gender') || null}
  onChange={v => form.setValue('gender', v ? String(v) : '', { shouldValidate: true, shouldDirty: true })}
  placeholder="انتخاب..."
  isClearable
  height={38}
/>
```

- برای فیلدهای الزامیِ Zod، `shouldValidate: true` را نگه دار تا خطاها به‌روز شوند.
- اگر فیلد قبلاً از `REFERRAL_OPTIONS` می‌ساخت: `REFERRAL_OPTIONS.map(o => ({ value: o, label: o }))`.

### ۴. حذف کد مرده

بعد از تبدیل، هر متغیر استایل مشترکِ بلااستفاده (`const sel = {...}`) و importهای بی‌استفاده
را حذف کن (`tsc`/eslint نشان می‌دهد).

## نکات مهم

- **رفتار داده تغییر نکند:** مقداری که به state/RHF/API می‌رود همان `uuid`/کلید قبلی بماند
  (فقط `''` ↔ `null` را مدیریت کن — `SearchableSelect` هنگام خالی `null` می‌دهد، اگر state
  انتظار رشته دارد به `''` تبدیل کن).
- **ارتفاع:** فیلدهای فعلی معمولاً `height: 38px`‌اند؛ `height={38}` بده تا ردیف‌ها جابه‌جا نشوند.
  پیش‌فرض کامپوننت 42 است.
- **گزینه‌های وابسته (بخش→زیربخش→سرویس در NewAppointmentDrawer / AppointmentActions):**
  با تغییر والد، مقدار فرزند ریست شود (همان منطق فعلی onChange والد را حفظ کن).
- **منوی داخل Modal/Drawer:** `SearchableSelect` منو را با `menuPortal`+`zIndex 9999` به `body`
  می‌برد؛ مشکل بریدگی/overflow نخواهد داشت — نیازی به تنظیم اضافه نیست.
- **تست:** بعد از هر فایل `ddev exec npx tsc --noEmit --project tsconfig.json`؛ در پایان
  `npx vitest run` (روی host؛ داخل ddev esbuild پلتفرم mismatch دارد). تست‌های موجودِ
  فایل‌هایی که select داشتند (مثل `AppointmentFiltersModal.test.tsx`، `PatientRecordInfoForm.test.tsx`)
  را اجرا کن؛ اگر با `getByRole('combobox')`/`selectOptions` بودند، به تعامل react-select
  (کلیک + انتخاب گزینه با متن) به‌روز کن.
- **بدون کتابخانه جدید:** `react-select` از قبل نصب است. RTL و dark از خود کامپوننت می‌آید.
- تغییر فقط-UI است؛ backend و `docs/api/*` تغییری ندارد.
- **این یک قاعده‌ی دائمی است:** از این پس هیچ `<select>` بومیِ جدیدی در پنل ادمین نساز؛
  همیشه `SearchableSelect`.
