- 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.
14 KiB
رفع قفل ویرایش تخصص پزشک + یکپارچهسازی عنوان «دکتر» + ترمیم کامل داده خزنده
زمینه
پزشکان زیادی توسط خزندهٔ نظام پزشکی (clinicpro-crawler/) وارد شدهاند
(source='irimc', ~۲۳۴۰ رکورد). یک دور ترمیم قبلاً انجام شده و کامند
app:doctors:repair ساخته شده (درجه، حذف پیشوند «دکتر» از نام، افزودن تخصص والد،
نقش جانشین). اما مشکل اصلیِ گزارششدهٔ کاربر هنوز باقی است: در پروفایل ادمینِ یک پزشک
(/admin/doctors/1303ec61-6d5f-4ab5-a4ff-86dbafbd8974، ارتوپدی) امکان تغییر/حذف/افزودن
تخصص وجود ندارد — و این باگ UI است، نه داده.
علاوه بر آن دو کار سیستمی خواسته شده: (۱) سیاست واحد نمایش عنوان «دکتر» در کل محصول، (۲) یک بازبینی کامل روی همهٔ پزشکان خزندهای برای صحت داده و قابلویرایش بودن بینقص.
مشکل / هدف
سه محور مستقل:
-
باگ قفل تخصص (اولویت اصلی): کامپوننت
EditSpecialtyPickerدر مودال ویرایش پزشک عملاً تکانتخابی است. با انتخاب اولین تخصص، همهٔ گروههای تخصصی دیگرdisabledمیشوند و انتخاب هر تخصص کل آرایه را جایگزین میکند. نتیجه: کاربر حس میکند فیلد قفل است و نمیتواند تخصص دوم اضافه کند یا آسان تخصص را عوض کند. -
سیاست عنوان «دکتر»: باید یک قانون واحد در کل سیستم اعمال شود — عنوان «دکتر» هرگز در فیلد
nameذخیره نشود و فقط در لایهٔ نمایش افزوده شود. بررسی شود که همهٔ مسیرهای ذخیره این را رعایت کنند و همهٔ مسیرهای نمایش عنوان را یکجا اضافه کنند. -
ترمیم و صحت کامل داده خزنده: اطمینان از اینکه همهٔ پزشکان
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).