Files
clinicpro/.claude/prompt/fix-doctor-specialty-edit-and-title-policy.md
hamed 74577c2ff6 feat: unify doctor title handling and enhance specialty selection
- Implemented a helper function `displayDoctorName` to prepend "دکتر" to doctor names for consistent display across the application.
- Updated various components (InviteDoctorModal, DashboardPage, DoctorDetailPage, DoctorsPage, etc.) to utilize the new helper for rendering doctor names.
- Modified the DoctorFormPage to automatically add the "دکتر" title in the UI without requiring user input.
- Fixed the EditSpecialtyPicker component to allow multiple specialty selections, resolving a UI bug where only one specialty could be selected at a time.
- Ensured that the backend strips the "دکتر" title from the name during pre-registration and doctor creation processes.
- Added tests for the new functionality, including checks for title handling and specialty selection logic.
- Updated API documentation to reflect changes in name handling and display logic.
2026-07-19 19:57:03 +03:30

202 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# رفع قفل ویرایش تخصص پزشک + یکپارچه‌سازی عنوان «دکتر» + ترمیم کامل داده خزنده
## زمینه
پزشکان زیادی توسط خزندهٔ نظام پزشکی (`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).