From 6ab7ed38b8d0cfee8b3879806b3dc3426b32e7c3 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Fri, 17 Jul 2026 21:56:27 +0330 Subject: [PATCH] feat: refactor clinic management into a dedicated settings tab - Removed MyClinicPage and redirected its functionality to a new ClinicDoctorsPage. - Created ClinicDoctorsManager component for managing doctors and invitations within the settings layout. - Updated backend permissions to allow clinic owners to detach doctors, alongside admins. - Adjusted API documentation to reflect new permission structure. - Updated tests to cover new functionality and permissions. - Modified sidebar and settings menu to reflect the new structure and role-based visibility. --- .claude/prompt/settings-clinic-doctors-tab.md | 206 +++++++++++++ assets/admin/App.tsx | 8 +- .../components/ClinicDoctorsManager.test.tsx | 48 +++ .../admin/components/ClinicDoctorsManager.tsx | 284 ++++++++++++++++++ .../layout/PurchaseSubscriptionSidebar.tsx | 35 ++- .../components/layout/SettingsLayout.test.tsx | 17 +- .../components/layout/SettingsLayout.tsx | 2 +- assets/admin/components/layout/Sidebar.tsx | 2 +- assets/admin/pages/ClinicDetailPage.tsx | 259 +--------------- assets/admin/pages/ClinicDoctorsPage.tsx | 53 ++++ assets/admin/pages/MyClinicPage.tsx | 34 --- assets/admin/pages/SettingsMenuPage.test.tsx | 2 +- docs/api/clinic.md | 3 +- src/Clinic/Controller/ClinicController.php | 18 +- tests/Clinic/DetachDoctorPermissionTest.php | 78 +++++ 15 files changed, 731 insertions(+), 318 deletions(-) create mode 100644 .claude/prompt/settings-clinic-doctors-tab.md create mode 100644 assets/admin/components/ClinicDoctorsManager.test.tsx create mode 100644 assets/admin/components/ClinicDoctorsManager.tsx create mode 100644 assets/admin/pages/ClinicDoctorsPage.tsx delete mode 100644 assets/admin/pages/MyClinicPage.tsx create mode 100644 tests/Clinic/DetachDoctorPermissionTest.php diff --git a/.claude/prompt/settings-clinic-doctors-tab.md b/.claude/prompt/settings-clinic-doctors-tab.md new file mode 100644 index 00000000..d18f312b --- /dev/null +++ b/.claude/prompt/settings-clinic-doctors-tab.md @@ -0,0 +1,206 @@ +# انتقال مدیریت کلینیک و پزشکان کلینیک به یک تب مجزا در تنظیمات (نقش‌محور) + +## پروژه + +`clinicpro` (پنل ادمین React + یک اصلاح کوچک permission در Backend Symfony — همان ریپو، cross-repo نیست). + +## زمینه + +کاربری که به‌عنوان **مدیر/مالک کلینیک** ثبت‌نام می‌کند `primaryRole === 'clinic'` می‌گیرد (برچسب «مالک کلینیک»). امروز در منوی تنظیمات یک تب به نام **«مدیریت مطب»** وجود دارد (`key: 'clinic'`) که به `/admin/my-clinic` می‌رود؛ آن صفحه بلافاصله به `/admin/clinics/{dbUuid}` = `ClinicDetailPage` **ری‌دایرکت** می‌کند. یعنی با کلیک روی تب تنظیمات، کاربر از پوسته‌ی تنظیمات (`SettingsLayout`) خارج می‌شود و به یک صفحه‌ی جنریک ادمین (همان صفحه‌ای که مدیرکل برای هر کلینیک می‌بیند) پرتاب می‌شود. این صفحه هم مدیریت اطلاعات کلینیک و هم مدیریت پزشکانِ کلینیک (لیست/دعوت/تعلیق/حذف دعوتنامه/جداسازی پزشک) را در خود دارد. + +دو مشکل: +1. مدیریت پزشکانِ کلینیک به‌جای اینکه یک تب مستقل و تمیز داخل تنظیمات باشد، داخل یک صفحه‌ی بزرگ ادمین قاطی شده و کاربر را از تنظیمات بیرون می‌برد. +2. تب «مدیریت مطب» در **sidebar دسکتاپِ تنظیمات** (`PurchaseSubscriptionSidebar`) اصلاً نقش‌محور نیست و برای همه (از جمله پزشک مهمان) نمایش داده می‌شود — این در `SettingsLayout.test.tsx` هم به‌عنوان رفتار فعلی ثبت شده. + +## هدف + +1. یک **تب مجزا** در تنظیمات به نام **«پزشکان کلینیک»** ساخته شود که کل مدیریت کلینیک و پزشکانِ کلینیک را **داخل پوسته‌ی تنظیمات** (`SettingsLayout`) در دسترس بگذارد. +2. تب فعلی **«مدیریت مطب»** به‌طور کامل از هر دو منوی تنظیمات حذف شود (موبایل: `SETTINGS_MENU`؛ دسکتاپ: `PurchaseSubscriptionSidebar`). +3. تمام امکانات مدیریت کلینیک + پزشکانِ کلینیک از همان تب جدید در دسترس باشد. +4. کنترل دسترسی نقش‌محور: + * **مدیر کلینیک** (`primaryRole === 'clinic'`): افزودن، ویرایش، حذف/جداسازی و مدیریت کامل پزشکانِ کلینیک. + * **پزشک** (`primaryRole === 'doctor'`): این تب مدیریتی را **اصلاً نبیند** و به تنظیمات مدیریتی کلینیک دسترسی نداشته باشد؛ فقط بخش‌های مربوط به خودش (پروفایل/نوبت‌دهی/دعوت‌نامه‌های دریافتی خودش که جای دیگری است). + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `assets/admin/components/layout/SettingsLayout.tsx` | منبع حقیقت `SETTINGS_MENU` (منوی موبایل) + `menuForRole()` + پوسته‌ی تنظیمات | +| `assets/admin/components/layout/PurchaseSubscriptionSidebar.tsx` | sidebar دسکتاپِ تنظیمات؛ `NAV_ITEMS` مستقل و **بدون نقش‌گِیت** | +| `assets/admin/pages/MyClinicPage.tsx` | تب فعلی «مدیریت مطب» → فقط ری‌دایرکت به `ClinicDetailPage` | +| `assets/admin/pages/ClinicDetailPage.tsx` | صفحه‌ی جنریک ادمین؛ بلوک «پزشکان + دعوتنامه‌ها» (خطوط ~۴۷۶–۹۹۰) منبع کد قابل‌استخراج | +| `assets/admin/App.tsx` | جدول route؛ `RoleRoute` (خط ۱۱۷) و route `my-clinic` (خط ۱۹۱) | +| `assets/admin/stores/authStore.ts` | `primaryRole`, `dbUuid`, `context` (`ContextItem.scope`) | +| `assets/admin/components/layout/SettingsLayout.test.tsx` | تست رفتار فعلی نمایش «مدیریت مطب» | +| `assets/admin/pages/SettingsMenuPage.test.tsx` | تست منوی موبایل | +| `assets/admin/components/layout/Sidebar.tsx` (خط ~۲۰۶) | لینک `/admin/my-clinic` در نویگیشن اصلی | +| `src/Clinic/Controller/ClinicController.php` (خط ۳۵۵–۳۵۶) | endpoint جداسازی پزشک — **`ROLE_ADMIN` only** | +| `src/ClinicInvitation/Controller/ClinicInvitationController.php` | endpointهای دعوت/لیست/تعلیق/حذف دعوتنامه (`IS_AUTHENTICATED_FULLY`) | + +## وضعیت فعلی + +### منوی تنظیمات موبایل — `SettingsLayout.tsx` + +```tsx +export const SETTINGS_MENU: SettingsMenuItem[] = [ + { key: 'subscription', label: 'خرید اشتراک', icon: CreditCardIcon, to: '/admin/subscription' }, + { key: 'doctor', label: 'مدیریت پزشک', icon: UserIcon, to: '/admin/profile', roles: ['doctor'] }, + { key: 'appointment', label: 'مدیریت نوبت دهی', icon: CalendarDaysIcon, to: '/admin/appointment-settings', roles: ['doctor'] }, + { key: 'clinic', label: 'مدیریت مطب', icon: BuildingOffice2Icon, to: '/admin/my-clinic', roles: ['clinic'] }, + // ... +]; +export function menuForRole(role: string | null | undefined): SettingsMenuItem[] { + return SETTINGS_MENU.filter((i) => !i.roles || (role != null && i.roles.includes(role))); +} +``` + +### sidebar دسکتاپ — `PurchaseSubscriptionSidebar.tsx` (بدون نقش‌گِیت!) + +```tsx +const NAV_ITEMS: NavItem[] = [ + // ... + { key: 'clinic', label: 'مدیریت مطب', to: '/admin/my-clinic' }, // برای همه‌ی نقش‌ها دیده می‌شود + // ... +]; +export default function PurchaseSubscriptionSidebar({ active }: { active: string }) { /* هیچ نقشی نمی‌گیرد */ } +``` + +### تب فعلی — `MyClinicPage.tsx` (فقط ری‌دایرکت، از تنظیمات خارج می‌شود) + +```tsx +function MyClinicPageContent() { + const { dbUuid, fetchMe } = useAuthStore(); + const navigate = useNavigate(); + useEffect(() => { + if (dbUuid) navigate(`/admin/clinics/${dbUuid}`, { replace: true }); // ← به ClinicDetailPage می‌پرد + // ... + }, [dbUuid, fetchMe, navigate]); + // ... +} +``` + +### route فعلی — `App.tsx:191` + +```tsx +} /> +``` + +### endpointهای موجود مدیریت پزشکانِ کلینیک (از `ClinicDetailPage.tsx`) + +``` +GET /api/v1/clinic/doctor-list/{uuid} لیست پزشکان کلینیک +GET /api/v1/admin/clinic/{uuid}/invitations?limit=50 لیست دعوتنامه‌ها +POST /api/v1/admin/clinic/{uuid}/invite-doctor دعوت پزشک (InviteDoctorModal) +POST /api/v1/admin/clinic/invitation/{invUuid}/resend ارسال مجدد +PATCH /api/v1/admin/clinic/invitation/{invUuid}/status تعلیق/فعال +DELETE /api/v1/admin/clinic/invitation/{invUuid} حذف دعوتنامه +DELETE /api/v1/admin/clinic/{uuid}/doctor/{doctorUuid} جداسازی پزشک ← ROLE_ADMIN only ⚠️ +``` + +### مشکل permission در Backend — `ClinicController.php:355` + +```php +#[Route('/api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}', methods: ['DELETE'])] +#[IsGranted('ROLE_ADMIN')] // ← مالک کلینیک نمی‌تواند پزشک را جدا کند +public function detachDoctor(...) { ... } +``` + +## وظایف + +### ۱. Backend — اجازه‌ی جداسازی پزشک به مالک کلینیک + +هدف task شماره ۴ این است که مدیر کلینیک بتواند پزشک را حذف/جدا کند، ولی endpoint جداسازی الان `ROLE_ADMIN` است. + +- در `src/Clinic/Controller/ClinicController.php` متد `detachDoctor` (خط ۳۵۵): گارد `#[IsGranted('ROLE_ADMIN')]` را به `#[IsGranted('IS_AUTHENTICATED_FULLY')]` تغییر بده و **داخل متد/سرویس یک بررسی مالکیت** اضافه کن: کاربر فعلی یا ادمین باشد یا مالک همان کلینیک (`clinicUuid`). اگر نه → `throw new AppException(ErrorCodes::ERR_FORBIDDEN, null, 403)`. +- **اول بگرد**: احتمالاً همین الگوی بررسی مالکیت در `invite-doctor`/`invitations` (که `IS_AUTHENTICATED_FULLY` هستند) در سرویس `ClinicInvitation` وجود دارد — همان helper را دوباره استفاده کن، کد جدید ننویس. +- endpointهای دعوتنامه را هم بررسی کن که مالک کلینیک (نه فقط ادمین) بتواند صدایشان بزند؛ اگر بررسی مالکیت ندارند، همان helper را اضافه کن. +- پس از تغییر، فایل `docs/api/clinic.md` (و در صورت لزوم مستندِ ClinicInvitation) را در همین session به‌روزرسانی کن (Standing Rule). +- تست: PHPUnit برای سه حالت — مالک کلینیک (موفق)، پزشک/کاربر غیرمالک (۴۰۳)، ادمین (موفق). + +> اگر بررسی مالکیت روی این endpointها از قبل به‌شکل کامل وجود دارد، فقط گارد `ROLE_ADMIN` را شل کن و دلیلش را در توضیح PR/commit بنویس. + +### ۲. استخراج بلوک مدیریت پزشکان به یک کامپوننت مشترک (SOLID) + +`ClinicDetailPage.tsx` بلوک «پزشکان + دعوتنامه‌ها» را در خطوط ~۸۴۷–۹۹۰ دارد (tab پزشکان/دعوتنامه‌ها، دعوت، ارسال مجدد، تعلیق، حذف دعوتنامه، جداسازی پزشک، `InviteDoctorModal`، `ConfirmDialog` جداسازی). این منطق نباید کپی شود. + +- یک کامپوننت جدید بساز: `assets/admin/components/ClinicDoctorsManager.tsx` با prop `clinicUuid: string` و `readOnly?: boolean`. +- تمام state/queryها/mutationهای مربوط به `doctorsQ`, `invitationsQ`, `resendInvMut`, `changeInvStatusMut`, `deleteInvMut`, `detachDoctorMut`, `inviteOpen`, `detachDoctorConfirm` را به این کامپوننت منتقل کن (از `ClinicDetailPage` بردار). +- `ClinicDetailPage.tsx` را ریفکتور کن تا همین کامپوننت مشترک را با `clinicUuid={uuid}` رندر کند (رفتار صفحه‌ی ادمین نباید تغییر کند). +- endpointها و envelopeها دقیقاً همان‌های فعلی (`data?.data ?? raw`). + +### ۳. صفحه‌ی تنظیماتِ تب جدید — `ClinicDoctorsPage.tsx` + +- فایل جدید: `assets/admin/pages/ClinicDoctorsPage.tsx`. +- `dbUuid` مالک کلینیک را از `useAuthStore` بگیر (اگر خالی بود `fetchMe()` مثل `MyClinicPage`). سپس **داخل** `SettingsLayout` رندر کن — نه ری‌دایرکت: + +```tsx +export default function ClinicDoctorsPage() { + const { dbUuid, fetchMe } = useAuthStore(); + useEffect(() => { if (!dbUuid) fetchMe(); }, [dbUuid, fetchMe]); + return ( + + {dbUuid + ? + :
+

در حال بارگذاری اطلاعات کلینیک...

+
} +
+ ); +} +``` + +- اگر می‌خواهی ویرایش اطلاعات کلینیک (نام/تلفن/تخصص‌ها/بیمه/گالری/آدرس) هم زیر همین تب باشد (task: «تمام امکانات مدیریت کلینیک»)، یک دکمه/لینک «ویرایش اطلاعات کلینیک» به همان `ClinicDetailPage` بگذار یا آن بلوک‌ها را هم به کامپوننت مشترک اضافه کن. **پیشنهاد:** برای این iteration فقط مدیریت پزشکان + دعوت را داخل تب بیاور و ویرایش اطلاعات کلینیک را با یک لینک به صفحه‌ی موجود نگه‌دار تا صفحه‌ی تنظیمات سبک بماند؛ اگر کاربر مدیریت کامل خواست، در وظیفه‌ی جدا انجام شود. + +### ۴. route جدید + حذف route قدیمی — `App.tsx` + +- route جدید (به‌جای/کنار `my-clinic`) با گارد نقش: + +```tsx + + + + } +/> +``` + +- `RoleRoute` قبلاً `roles=['clinic']` را چک می‌کند و با `blockClinicScope` پزشکِ مهمان در scope کلینیک را هم رد می‌کند — همین برای task شماره ۴ کافی است (پزشک به تب مدیریتی نمی‌رسد). +- route قدیمی `my-clinic` و `MyClinicPage` را حذف کن؛ اگر لینک قدیمی ممکن است جایی باز شود، یک ری‌دایرکت از `my-clinic` به `settings/clinic-doctors` بگذار. + +### ۵. به‌روزرسانی هر دو منوی تنظیمات + +- در `SettingsLayout.tsx` آیتم `{ key:'clinic', label:'مدیریت مطب', ... }` را حذف و جایگزین کن با: + +```tsx +{ key: 'clinic-doctors', label: 'پزشکان کلینیک', icon: BuildingOffice2Icon, to: '/admin/settings/clinic-doctors', roles: ['clinic'] }, +``` + +- در `PurchaseSubscriptionSidebar.tsx`: + - آیتم `{ key:'clinic', label:'مدیریت مطب', to:'/admin/my-clinic' }` را حذف و با `{ key:'clinic-doctors', label:'پزشکان کلینیک', to:'/admin/settings/clinic-doctors' }` جایگزین کن. + - این sidebar را **نقش‌محور** کن: `primaryRole` را از `useAuthStore` بگیر و `NAV_ITEMS` را با همان منطق `menuForRole` فیلتر کن (به `NavItem` فیلد اختیاری `roles?: string[]` اضافه کن و به آیتم `clinic-doctors` بده `roles: ['clinic']`، به آیتم `doctor`/`appointment` هم `roles:['doctor']` مطابق `SETTINGS_MENU`). این باعث می‌شود پزشک تب «پزشکان کلینیک» را در دسکتاپ هم نبیند (رفع باگ فعلی). + +### ۶. اصلاح لینک نویگیشن اصلی — `Sidebar.tsx` + +- خط ~۲۰۶ که `/admin/my-clinic` می‌سازد را به `/admin/settings/clinic-doctors` تغییر بده (فقط برای نقش `clinic`). منطق نقش همان‌جا را حفظ کن. + +### ۷. تست‌ها + +- `SettingsLayout.test.tsx`: تست فعلی که انتظار دارد «مدیریت مطب» دیده شود را به‌روز کن — حالا: + * برای `role='clinic'` باید «پزشکان کلینیک» دیده شود و «مدیریت مطب» **نباشد**. + * برای `role='doctor'` باید «پزشکان کلینیک» **دیده نشود** (چون دسکتاپ حالا نقش‌محور است — کامنت قدیمیِ «desktop sidebar is not role-gated» را هم اصلاح کن). +- `SettingsMenuPage.test.tsx`: `menuForRole('doctor')` نباید `clinic-doctors` بدهد؛ `menuForRole('clinic')` باید بدهد. +- تست جدید برای `ClinicDoctorsManager` (رندر لیست پزشکان از mock، نمایش دکمه‌های مدیریت وقتی `readOnly` نیست). +- `yarn test` و `npx tsc --noEmit` باید سبز شوند. + +## نکات مهم + +- **نقش‌ها:** `admin` (مدیرکل) · `clinic` (مالک/مدیر کلینیک — برچسب «مالک کلینیک») · `doctor` · `secretary` · `representation` · `user`. «مدیر کلینیک» در این سیستم = `primaryRole === 'clinic'`. پزشکِ مهمانِ دعوت‌شده = `doctor` با `context.scope === 'clinic'` که با `blockClinicScope` در `RoleRoute` رد می‌شود. +- **envelope:** لیست پزشکان و دعوتنامه‌ها با `data?.data ?? raw` استخراج می‌شوند (double-nest احتمالی). دقیقاً از الگوی فعلی `ClinicDetailPage` کپی کن، تغییر نده. +- **تاریخ‌ها:** timestampهای Unix (مثل `expires_at`)؛ انقضا با `Date.now()/1000 > inv.expires_at` سنجیده می‌شود — همین را نگه‌دار. +- **SearchableSelect:** طبق قانون پروژه هر جا `select` لازم شد از `SearchableSelect` استفاده کن، نه `