# رفع قفل ویرایش تخصص پزشک + یکپارچه‌سازی عنوان «دکتر» + ترمیم کامل داده خزنده

## زمینه

پزشکان زیادی توسط خزندهٔ نظام پزشکی (`clinicpro-crawler/`) وارد شده‌اند
(`source='irimc'`, ~۲۳۴۰ رکورد). یک دور ترمیم قبلاً انجام شده و کامند
`app:doctors:repair` ساخته شده (درجه، حذف پیشوند «دکتر» از نام، افزودن تخصص والد،
نقش جانشین). اما مشکل اصلیِ گزارش‌شدهٔ کاربر هنوز باقی است: در پروفایل ادمینِ یک پزشک
(`/admin/doctors/1303ec61-6d5f-4ab5-a4ff-86dbafbd8974`، ارتوپدی) **امکان تغییر/حذف/افزودن
تخصص وجود ندارد** — و این باگ UI است، نه داده.

علاوه بر آن دو کار سیستمی خواسته شده: (۱) سیاست واحد نمایش عنوان «دکتر» در کل محصول،
(۲) یک بازبینی کامل روی همهٔ پزشکان خزنده‌ای برای صحت داده و قابل‌ویرایش بودن بی‌نقص.

## مشکل / هدف

سه محور مستقل:

1. **باگ قفل تخصص (اولویت اصلی):** کامپوننت `EditSpecialtyPicker` در مودال ویرایش پزشک
   عملاً **تک‌انتخابی** است. با انتخاب اولین تخصص، همهٔ گروه‌های تخصصی دیگر `disabled`
   می‌شوند و انتخاب هر تخصص کل آرایه را جایگزین می‌کند. نتیجه: کاربر حس می‌کند فیلد قفل
   است و نمی‌تواند تخصص دوم اضافه کند یا آسان تخصص را عوض کند.

2. **سیاست عنوان «دکتر»:** باید یک قانون واحد در کل سیستم اعمال شود — **عنوان «دکتر»
   هرگز در فیلد `name` ذخیره نشود و فقط در لایهٔ نمایش افزوده شود.** بررسی شود که همهٔ
   مسیرهای ذخیره این را رعایت کنند و همهٔ مسیرهای نمایش عنوان را یک‌جا اضافه کنند.

3. **ترمیم و صحت کامل داده خزنده:** اطمینان از اینکه همهٔ پزشکان `source='irimc'` داده
   درست دارند و بدون خطا قابل ویرایش‌اند.

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

| فایل | نقش |
|------|-----|
| `assets/admin/pages/DoctorDetailPage.tsx` | مودال ویرایش پزشک + `EditSpecialtyPicker` (باگ اصلی) |
| `src/Doctor/Controller/DoctorController.php` | `update()` (PATCH `/api/v1/doctor/{uuid}`) + `hydrateDoctor()` |
| `src/Doctor/Entity/Doctor.php` | `toListArray()` / `toDetailArray()` — `name` بدون عنوان برمی‌گردد |
| `src/Doctor/Command/RepairImportedDoctorsCommand.php` | کامند ترمیم (`app:doctors:repair`) — قبلاً ساخته شده |
| `src/Doctor/Service/Repair/` | گام‌های ترمیم (names, degrees, specialty-parents, surrogate-role) |
| `src/Shared/Util/PersianText.php` | `stripDoctorTitle()` — حذف پیشوند هنگام ذخیره |
| `assets/admin/lib/utils.ts` | محل مناسب برای helper نمایش عنوان (`displayDoctorName`) |
| `docs/api/doctor.md` / `docs/api/doctor-import.md` | مستندات |

## وضعیت فعلی

### باگ ۱ — `EditSpecialtyPicker` تک‌انتخابی (DoctorDetailPage.tsx:831-997)

```tsx
// خط ۹۲۳ — با انتخاب یک تخصص، بقیهٔ گروه‌ها disabled می‌شوند
const isDisabled = hasSelection && !isMarked;
// ...
<button disabled={isDisabled} style={{ opacity: isDisabled ? 0.35 : 1, cursor: isDisabled ? 'not-allowed' : ... }}>

// خط ۸۵۹-۸۶۶ — انتخاب فرزند: کل آرایه را جایگزین می‌کند
const selectChild = (child: SpecialtyOpt) => {
  const parentId = child.parent_id!;
  if (selected.includes(child.id)) { onChange([]); }
  else { onChange([parentId, child.id]); }   // فقط همین یک تخصص + والدش
};

// خط ۸۶۸-۸۷۵ — انتخاب ریشه هم کل آرایه را جایگزین می‌کند
const selectRoot = (root: SpecialtyOpt) => {
  if (selected.includes(root.id)) { onChange([]); }
  else { setActiveParentId(null); onChange([root.id]); }
};

// خط ۹۶۷ — حتی سرتیتر می‌گوید «یک مورد»
انتخاب تخصص — یک مورد
```

نکته: در همان فایل یک پیکر **چند‌انتخابی درست** به‌نام `HierarchicalSpecialtyPicker`
(خط ۳۴۷-۴۵۱) وجود دارد که با checkbox و `toggleSelect` کار می‌کند و
`onChange(selected.includes(id) ? selected.filter(...) : [...selected, id])` دارد —
یعنی الگوی درست قبلاً در همین فایل هست، فقط مودال از پیکر اشتباه استفاده می‌کند.

### Backend سالم است — باگ فقط UI است

```php
// DoctorController::update() خط ۳۴۹ — ادمین اجازهٔ کامل دارد
if ($doctor->getUser()->getId() !== $user->getId() && !$user->hasRole('ROLE_ADMIN')) {
    return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
}
// hydrateDoctor خط ۷۹۳ — آرایهٔ چند‌تایی تخصص را کامل می‌پذیرد و با والدها گسترش می‌دهد
if (array_key_exists('specialties', $data) && is_array($data['specialties'])) {
    foreach ($this->specialtyRepo->expandWithAncestors(array_map('intval', $data['specialties'])) as $id) { ... }
}
```

PATCH با آرایهٔ چند تخصص از قبل کار می‌کند؛ **هیچ تغییری در backend برای رفع باگ لازم نیست.**

### سیاست عنوان — وضعیت فعلی

`Doctor::toListArray()` / `toDetailArray()` مقدار `name` را **بدون** «دکتر» برمی‌گردانند
(خط ۵۵۵ و ۵۸۴). ذخیره هم با `PersianText::stripDoctorTitle()` عنوان را حذف می‌کند
(`DoctorController::update()` خط ۳۵۴، و `DoctorImportService`). یعنی **سیاست درست
همین است**: name خام، «دکتر» فقط در نمایش. اما در فرانت‌اند این نمایش یک‌جا/سازگار نیست
(مثلاً placeholder فیلد نام `"دکتر ..."` در خط ۱۷۲۳ کاربر را گمراه می‌کند که انگار باید
«دکتر» تایپ کند).

## وظایف

### ۱. رفع باگ قفل تخصص (اصلی)

مودال ویرایش (خط ۱۸۲۴) به‌جای `EditSpecialtyPicker` تک‌انتخابی، از منطق چند‌انتخابی
استفاده کند. دو گزینه — **گزینهٔ الف ترجیح داده می‌شود**:

**الف) `EditSpecialtyPicker` را چند‌انتخابی کن** (تغییر کمتر، حفظ ظاهر دو‌ستونی master-detail):
- خط ۹۲۳: `isDisabled` را حذف کن — هیچ گروهی نباید غیرفعال شود.
- `selectChild`: به‌جای جایگزینی، toggle کن و والد را حفظ کن:
  ```tsx
  const selectChild = (child: SpecialtyOpt) => {
    const parentId = child.parent_id!;
    if (selected.includes(child.id)) {
      // حذف فرزند؛ والد را هم اگر فرزند دیگری از او انتخاب نیست حذف کن
      const siblings = childMap[parentId] ?? [];
      const otherSelected = siblings.some(k => k.id !== child.id && selected.includes(k.id));
      onChange(selected.filter(id => id !== child.id && (otherSelected || id !== parentId)));
    } else {
      onChange([...new Set([...selected, parentId, child.id])]);
    }
  };
  ```
- `selectRoot`: toggle به‌جای جایگزینی — `onChange(selected.includes(root.id) ? selected.filter(id => id !== root.id) : [...selected, root.id])`.
- `removeEntry(childId)`: فقط همان chip را حذف کند نه کل انتخاب‌ها (الان `onChange([])`).
- سرتیتر خط ۹۶۷ از «یک مورد» به «انتخاب تخصص» تغییر کند.
- `chips` باید همهٔ تخصص‌های انتخاب‌شده را نشان دهد (منطق فعلی چند‌تایی را پشتیبانی می‌کند، فقط با toggleِ درست تغذیه شود).

**ب) جایگزینی با `HierarchicalSpecialtyPicker`** (اگر master-detail ارزش نگه‌داری ندارد):
همان کامپوننت خط ۳۴۷ که از قبل چند‌انتخابی است را در مودال استفاده کن و
`EditSpecialtyPicker` را حذف کن.

**پس از هر دو:** `setValue('specialties', ids, { shouldDirty: true })` تا فرم dirty شود.

بعد از رفع، دقیقاً همان پزشک تست شود: افزودن تخصص دوم، حذف تخصص، تعویض تخصص — و ذخیره.

### ۲. یکپارچه‌سازی سیاست عنوان «دکتر»

سیاست نهایی (تثبیت وضع موجود، نه تغییر مدل داده):
**«دکتر» هرگز در `name` ذخیره نمی‌شود؛ فقط لایهٔ نمایش آن را می‌افزاید.**

- یک helper واحد در `assets/admin/lib/utils.ts` بساز:
  ```ts
  /** عنوان «دکتر» فقط در نمایش؛ هرگز در دیتابیس ذخیره نمی‌شود. */
  export const displayDoctorName = (name?: string | null): string => {
    const n = (name ?? '').trim();
    if (!n) return '';
    return n.startsWith('دکتر') ? n : `دکتر ${n}`;
  };
  ```
- همهٔ جاهایی که نام پزشک با «دکتر …» رندر می‌شود (DoctorDetailPage هدر، DoctorsPage لیست،
  کارت‌ها، …) از این helper استفاده کنند — نه الحاق دستی `\`دکتر ${name}\``. مکان‌های فعلی
  را با grep `دکتر ` در `assets/admin/` پیدا و یکسان کن.
- placeholder فیلد نام در مودال (خط ۱۷۲۳) از `"دکتر ..."` به `"مثلاً: حامد حسینی"` تغییر کند
  تا کاربر عنوان تایپ نکند. زیر فیلد یک hint: «عنوان «دکتر» خودکار نمایش داده می‌شود».
- سمت ذخیره: مطمئن شو **همهٔ** مسیرهای ساخت/ویرایش پزشک `stripDoctorTitle` را صدا می‌زنند
  (admin create در `AdminApiController`, `DoctorController::update`, `DoctorImportService`,
  claim, pre-registration). اگر مسیری این کار را نمی‌کند، اضافه کن.
- cross-repo (خارج از این تسک، فقط یادداشت در گزارش): سایت عمومی `nobat724_front` هم نام را
  از همین API می‌گیرد؛ چون API نام خام می‌دهد، آن‌جا هم باید در نمایش «دکتر» بگذارد. اگر لازم
  شد پرامپت جدا برای nobat724 نوشته شود.

### ۳. ترمیم و بازبینی کامل داده خزنده

کامند `app:doctors:repair` از قبل هست. این وظیفه = اجرای کامل + گزارش صحت:

- `ddev exec php bin/console app:doctors:repair --dry-run` → بررسی همهٔ گام‌ها.
- سپس بدون `--dry-run` اعمال کن (شامل `specialty-parents` که ~۱۵۹ تغییر معلق دارد).
- کوئری‌های صحت‌سنجی روی `source='irimc'` و گزارش بده:
  - پزشکان بدون هیچ تخصص (`doctor_specialties` خالی) — چند مورد؟ علت؟
  - `degree` نامعتبر (خارج از `Doctor::DEGREES`) یا NULL که `info` قابل نگاشت دارد.
  - نام‌های هنوز دارای پیشوند «دکتر».
  - رکوردهای تکراری: همان `medical_system_code` با بیش از یک ردیف — گزارش کن (ادغام فقط با
    تأیید، چون idempotency خزنده روی `(source, medical_system_code)` است).
  - آدرس/شهر/استان ناقص.
- اگر نقص جدیدی پیدا شد که گام موجود پوشش نمی‌دهد، **یک گام جدید `DoctorRepairStep`** در
  `src/Doctor/Service/Repair/` اضافه کن (خودکار کشف می‌شود؛ کامند نیازی به تغییر ندارد) و
  برایش تست بنویس.
- تأیید قابل‌ویرایش بودن: با `driver.mjs` یا دستی، چند پزشک irimc را در مودال ادمین باز کن،
  تخصص/نام/درجه را تغییر بده و ذخیرهٔ موفق را تأیید کن.

## نکات مهم

- **باگ اصلی صرفاً فرانت‌اند است.** backend و PATCH از قبل چند‌تخصص را می‌پذیرند؛ برای رفع
  قفل هیچ migration و تغییر controller لازم نیست. وقت را صرف backend نکن.
- **تخصص‌ها درختی‌اند:** `expandWithAncestors` سمت سرور والدها را اضافه می‌کند، پس UI می‌تواند
  فقط برگ‌ها را بفرستد؛ ولی نمایش chip باید والد+فرزند را نشان دهد (منطق فعلی `chips` درست است).
- **سیاست عنوان = تثبیت وضع موجود.** مدل داده را عوض نکن؛ فقط نمایش را یک‌جا و ذخیره را
  همه‌جا `stripDoctorTitle` کن. از تغییری که باعث «دکتر دکتر» یا نام بدون عنوان شود بپرهیز.
- **گام‌های repair باید idempotent بمانند** — اجرای دوم صفر تغییر. تست موجود
  `RepairImportedDoctorsCommandTest` این را قفل کرده؛ گام جدید هم باید همین را رعایت کند.
- **قوانین پروژه:** SPA فقط از `SearchableSelect` (نه `<select>` خام)؛ TanStack Query برای
  fetch؛ RHF + Zod برای فرم؛ رشته‌های UI فارسی. بعد از تغییر API، `docs/api/doctor.md`
  به‌روز شود. هر تغییر با تست (موفق + خطا + مرزی) و اجرای موفق `ddev exec php bin/phpunit`.
- **دیتابیس فعلی داده واقعی production است** (import شده)؛ قبل از اجرای repair بدون dry-run،
  از بکاپ `db/` مطمئن شو.

## خروجی نهایی (گزارش خواسته‌شده)

در پایان گزارش کامل بده: (۱) مشکلات شناسایی‌شده، (۲) root cause هرکدام، (۳) فایل‌های
تغییرکرده، (۴) اصلاحات، (۵) تست‌ها و نتیجه، (۶) موارد باقی‌مانده/نیازمند تصمیم (مثل ادغام
رکوردهای تکراری یا پرامپت جداگانهٔ nobat724).
