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

14 KiB
Raw Permalink Blame History

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

زمینه

پزشکان زیادی توسط خزندهٔ نظام پزشکی (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)

// خط ۹۲۳ — با انتخاب یک تخصص، بقیهٔ گروه‌ها 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 است

// 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 کن و والد را حفظ کن:
    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 بساز:
    /** عنوان «دکتر» فقط در نمایش؛ هرگز در دیتابیس ذخیره نمی‌شود. */
    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).