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.
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
# رفع قفل ویرایش تخصص پزشک + یکپارچهسازی عنوان «دکتر» + ترمیم کامل داده خزنده
|
||||
|
||||
## زمینه
|
||||
|
||||
پزشکان زیادی توسط خزندهٔ نظام پزشکی (`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).
|
||||
Reference in New Issue
Block a user