# پوشش و اعمالِ کاملِ مجوز پزشکِ عضو کلینیک (ClinicDoctorPermission) — همتای کار منشی

## زمینه

برای منشی (`DoctorSecretary.permission`) یک سیستم مجوزِ کامل ساختیم: ۱۵ منبع، enforcement در بک‌اند
(`SecretaryAccessChecker::denyUnlessGranted`)، گِیت سایدبار/Route با `usePermissions().can`، و گِیتِ
دکمه‌های CRUD در همهٔ صفحات. سیستمِ **موازی** برای «پزشکی که به کلینیک اضافه می‌شود» (پزشکِ مهمانِ
عضو کلینیک) `ClinicDoctorPermission` است — اما ناقص مانده و با کار منشی هم‌تراز نیست:

- فقط **۶ منبع** دارد: `appointments`, `appointment_settings`, `patients`, `payments`, `services`, `clinic_info`
  (`src/Clinic/Entity/ClinicDoctorPermission.php:21`) در برابر ۱۵ منبعِ منشی.
- enforcement فقط در **۶ نقطه** صدا زده می‌شود: `AppointmentAccessChecker`, `AppointmentSettingsController`,
  `DashboardController`, `PatientRecordScopeResolver`, `ClinicController`, `InsuranceController`.
- سایدبارِ پزشکِ مهمان فقط `appointments` و `patients` را نشان می‌دهد؛ بقیهٔ منابع (حتی `services`/
  `appointment_settings` که toggle دارند) در منو نیستند.
- چند toggle **مرده‌اند**: `services.update` هیچ‌جا enforce نمی‌شود، و بیمه با checkerِ منشی گِیت شده نه
  ClinicDoctorPermission.

هدف: `ClinicDoctorPermission` را به همان کاملیِ سیستم منشی برسانیم — برای **دو حالت** «پزشک مستقل»
(بدون context کلینیک، آزاد) و «پزشک عضو کلینیک» (`scope=clinic`، محدود به مجوزهای کلینیک).

> پیش از هر گرِپ/خواندن: `graphify query "..."`. بعد از هر تغییر کد (پس از commit): `graphify update .`.

## مشکل / هدف

سه شکاف، مطابق همان الگوی منشی:

1. **پوشش (Coverage):** هر صفحه/عملیاتی که پزشکِ عضو کلینیک به آن می‌رسد باید toggle مجوز داشته باشد و
   در فرم «مدیریت دسترسی‌های پزشک» نمایش داده شود. حداقل بررسی: `insurances`, `inventory`, `tags`,
   `staff`, `discounts`, `sms` — هرکدام که پزشک عضو باید کنترل شود.
2. **اعمال (Enforcement):** هر منبع باید در نقطهٔ درستِ بک‌اند با `ClinicDoctorPermissionChecker::can(
   $user, $clinic, $resource, $action)` گِیت شود (۴۰۳ اگر مجوز نبود)، نه فقط توگل ذخیره‌شود. توگل‌های
   مردهٔ فعلی (`services.update`، `payments.*`، …) واقعاً enforce شوند.
3. **پنل (Frontend):** سایدبارِ پزشکِ مهمان همهٔ منابعِ مجاز را با `can()` نشان دهد؛ Route‌ها با
   `blockClinicScope` + `permission` گِیت شوند؛ دکمه‌های CRUD طبق مجوز پنهان شوند (بخش زیادی از این با
   sweepِ منشی که role-agnostic بود از قبل کار می‌کند — فقط تأیید و تکمیل).

## فایل‌های مرتبط

| فایل | نقش | کار |
|------|-----|-----|
| `src/Clinic/Entity/ClinicDoctorPermission.php` | `DEFAULT_PERMISSIONS` + `mergePermissions` (valid-resource whitelist) | افزودن منابع جدید |
| `src/Clinic/Security/ClinicDoctorPermissionChecker.php` | checker (`can($user,$clinic,$resource,$action)`) | نقطهٔ واحد enforcement پزشک عضو |
| `src/ClinicService/Controller/ClinicServiceController.php` | خدمات — **هیچ چک ClinicDoctorPermission ندارد** | گارد `services` برای پزشک عضو |
| `src/Insurance/Controller/InsuranceController.php` | نوشتن‌ها با `secretaryAccess` گِیت شده، نه پزشک عضو | گارد `insurances` برای پزشک عضو |
| `src/Inventory/Controller/InventoryController.php` · `src/Tag/Controller/TenantTagController.php` · `src/Staff/Controller/StaffController.php` · `src/Discount/Controller/DiscountController.php` · `src/Sms/Controller/SmsWalletController.php` | فقط `secretaryAccess` دارند | گارد پزشک عضو اگر باید کنترل شود |
| `src/Patient/Controller/PatientController.php` · `PatientRecordScopeResolver.php` | scope + نوشتن‌ها | تأیید گارد `patients` پزشک عضو |
| `assets/admin/components/ui/DoctorPermissionsModal.tsx` | `RESOURCE_LABELS` (۶ منبع) | افزودن منابع جدید |
| `assets/admin/components/layout/Sidebar.tsx` (بلوک `primaryRole === "doctor" && scope === "clinic"`, L71) | منوی پزشک مهمان — فقط appointments/patients | افزودن بقیهٔ منابع با `can()` |
| `assets/admin/App.tsx` | Route‌ها با `blockClinicScope` + `permission` | تأیید/تکمیل گِیت پزشک مهمان |
| `src/Auth/Controller/AuthController.php` (`contextPermissions`, L~778؛ `buildAvailableContexts`) | تزریق permissions پزشک مهمان به context | بدون تغییر ساختار |
| `docs/api/clinic.md` | مستندات مجوز پزشک کلینیک | به‌روزرسانی |

## وضعیت فعلی (کد واقعی)

منبع حقیقتِ مجوز پزشک عضو:

```php
// src/Clinic/Entity/ClinicDoctorPermission.php:21
public const DEFAULT_PERMISSIONS = [
    'version'   => 1,
    'resources' => [
        'appointments'         => ['view' => true,  'create' => true,  'cancel' => true,  'update_status' => true],
        'appointment_settings' => ['view' => true,  'update' => true],
        'patients'             => ['view' => true,  'create' => true,  'update' => true,  'delete' => false],
        'payments'             => ['view' => true,  'create' => false, 'update' => false, 'delete' => false],
        'services'             => ['view' => true,  'update' => false],
        'clinic_info'          => ['view' => true,  'update' => false],
    ],
];

// mergePermissions فقط منابعِ داخل DEFAULT_PERMISSIONS را می‌پذیرد (منبع ناشناس رد می‌شود):
// L98: if (!is_array($actions) || !isset(self::DEFAULT_PERMISSIONS['resources'][$resource])) { continue; }
// can(): L88: return (bool) ($this->permissions['resources'][$resource][$action] ?? false);
```

سایدبارِ پزشک مهمان فقط ۲ منبع را گِیت می‌کند:

```tsx
// assets/admin/components/layout/Sidebar.tsx — بلوک doctor + scope=clinic (L71)
if (primaryRole === "doctor" && scope === "clinic") {
    const items = [{ to: "/admin/dashboard", ... }];
    if (can("appointments", "view")) { items.push({ to: "/admin/appointments", ... }); }
    if (can("patients", "view"))     { items.push({ to: "/admin/patients", ... }); }
    return [{ label: "عمومی", items }];   // ← services / appointment_settings / payments / … نیستند
}
```

توگل مردهٔ services (پزشک عضو با `services.update=false` هم می‌تواند ویرایش کند):

```php
// src/ClinicService/Controller/ClinicServiceController.php
// هیچ ClinicDoctorPermissionChecker صدا زده نمی‌شود؛ فقط SecretaryAccessChecker (مخصوص منشی).
// resolveEntity برای پزشکِ عضو، کلینیک را برمی‌گرداند و بدون هیچ گِیتی اجازهٔ نوشتن می‌دهد.
```

بیمه با checkerِ اشتباه:

```php
// src/Insurance/Controller/InsuranceController.php:307
$this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'update'); // ← فقط منشی؛
// برای پزشک عضو canOrNonSecretary → true → گِیت نمی‌شود. باید permChecker->can($user,$clinic,'insurances',$action) هم باشد.
// (خط ۷۶ همین کنترلر از permChecker با منبع 'services' استفاده می‌کند — الگوی درست همان‌جاست.)
```

## وظایف

> هر منبع همزمان در سه‌گانه اضافه شود وگرنه merge/نمایش می‌شکند:
> (۱) `ClinicDoctorPermission::DEFAULT_PERMISSIONS['resources']`،
> (۲) `DoctorPermissionsModal.tsx::RESOURCE_LABELS`،
> (۳) هر جای دیگری که منابع را فهرست می‌کند (contextPermissions خودکار از entity می‌خواند، دستی نیست).
> بعد از تغییر endpoint: `docs/api/*` همان session. بدون تست (موفق+۴۰۳+مرزی) هیچ تسکی تمام نیست.

### ۰. ممیزی و تصمیم (اول این)

1. برای هر منبعی که سیستم منشی دارد ولی ClinicDoctorPermission ندارد
   (`insurances, inventory, tags, staff, discounts, sms`)، مشخص کن آیا پزشکِ عضو کلینیک به آن صفحه
   دسترسی دارد (از سایدبار/Route/endpoint). جدول بساز:
   **منبع | پزشک عضو می‌رسد؟ | toggle در ClinicDoctorPermission؟ | enforce با permChecker؟ | sidebar؟ | Route؟**.
2. تصمیم بگیر کدام‌ها باید toggle بگیرند (مثلاً بیمه/انبار/تگ/خدمات منطقی‌اند؛ خرید اشتراک و مدیریت
   پزشکان کلینیک ذاتاً مالک‌اند و برای پزشکِ عضو نباید باشند). دلیل هر تصمیم را در پرامپت بنویس.

### ۱. افزودن منابع مجوز (Coverage)

برای هر منبعِ تصمیم‌گرفته‌شده، در `DEFAULT_PERMISSIONS['resources']` و `RESOURCE_LABELS` اضافه کن.
پیش‌فرضِ منطقی برای پزشکِ عضو: `view=true` و نوشتن‌ها `false` (مثل الگوی فعلی `services`/`clinic_info`).

```php
// نمونه افزودن به ClinicDoctorPermission::DEFAULT_PERMISSIONS
'insurances' => ['view' => true, 'create' => false, 'update' => false, 'delete' => false],
'inventory'  => ['view' => true, 'create' => false, 'update' => false, 'delete' => false],
'tags'       => ['view' => true, 'create' => false, 'update' => false, 'delete' => false],
// ... طبق تصمیم وظیفهٔ ۰
```

> چون `mergePermissions` whitelist دارد، منابعِ جدید حتماً باید در `DEFAULT_PERMISSIONS` باشند تا از فرم
> ذخیره شوند. `contextPermissions` خودکار از entity می‌خواند؛ نیازی به تغییر دستی نیست.

### ۲. اعمال در بک‌اند (Enforcement)

الگو را از `InsuranceController.php:76` بگیر (`$this->permChecker->can($user, $clinic, $resource, $action)`).
برای مسیرهای چند-نقشه، فقط پزشکِ عضو کلینیک را محدود کن (سایر نقش‌ها بدون تغییر). نکات:

- **services:** در `ClinicServiceController` برای هر اکشن، اگر کاربر پزشکِ عضو کلینیک است، با
  `permChecker->can($user, $clinic, 'services', $action)` گِیت کن (view/update). کلینیک را از محیطِ فعال
  حل کن (همان `EntityContextResolver` که الان استفاده می‌شود کلینیک را می‌دهد؛ فقط چکِ مجوز اضافه کن).
- **insurances:** در `InsuranceController` نوشتن‌ها، علاوه‌بر `secretaryAccess`، `permChecker->can(...,'insurances',$action)`
  را هم برای پزشکِ عضو اعمال کن (اگر `insurances` toggle گرفت).
- **payments / patients:** تأیید کن نوشتن‌های پزشکِ عضو با `permChecker` گِیت می‌شوند (patients scope از
  `PatientRecordScopeResolver` می‌آید؛ نوشتن‌ها گارد جدا لازم دارند).
- **inventory/tags/staff/discounts/sms:** اگر toggle گرفتند، در همان کنترلرها برای پزشکِ عضو گارد بگذار
  (این کنترلرها الان فقط `secretaryAccess` دارند).
- نبودِ مجوز → `403 ERR_FORBIDDEN_001` استاندارد (مثل `SecretaryAccessChecker`).

> **SOLID:** اگر منطقِ «آیا این کاربر پزشکِ عضوِ همین کلینیک است و مجوز دارد؟» در چند کنترلر تکرار شد،
> یک متدِ کمکی روی `ClinicDoctorPermissionChecker` بساز (قرینهٔ `SecretaryAccessChecker::denyUnlessGranted`)
> تا تکرار نشود. الگوی موجود منشی را دنبال کن.

### ۳. اعمال در پنل (Frontend)

1. **Sidebar** (`Sidebar.tsx`، بلوک `doctor && scope=clinic`, L71): برای هر منبعِ مجاز آیتم منو اضافه کن،
   هرکدام با `can(resource,'view')` — services، تنظیمات نوبت‌دهی، پرداخت‌ها، بیمه، انبار، تگ، … دقیقاً
   مثل بلوکِ `secretary`. منوی مطبِ شخصیِ پزشکِ مستقل (`primaryRole==='doctor'` بدون scope کلینیک) نباید
   تغییر کند — آنجا مالک است و آزاد.
2. **Route** (`App.tsx`): مطمئن شو هر صفحهٔ پزشکِ عضو با `blockClinicScope permission={[resource,'view']}`
   گِیت شده (این الگو از قبل برای پزشکِ مهمان طراحی شده؛ فقط منابعِ جدید را پوشش بده).
3. **CRUD buttons:** از قبل با `usePermissions().can` گِیت شده‌اند (sweepِ منشی role-agnostic بود، پس
   برای پزشکِ عضو هم کار می‌کند). فقط تأیید کن صفحاتِ منابعِ جدید هم پوشش دارند.

### ۴. تست‌ها و ماتریس نهایی

- بک‌اند (`ddev exec php bin/phpunit`): برای هر منبع، پزشکِ عضوِ مجاز (۲۰۰) و غیرمجاز (۴۰۳)؛ و تأیید
  اینکه **پزشک مستقل** (بدون context کلینیک) هیچ محدودیتی نمی‌گیرد. یک `ClinicDoctorPermissionEnforcementTest`
  قرینهٔ `SecretaryResourceEnforcementTest` بساز.
- فرانت‌اند (`yarn test` روی host با `npx vitest`): سایدبارِ پزشکِ مهمان با مجوزهای مختلف فقط آیتم‌های
  مجاز را رندر کند.
- جدول نهایی مثل کار منشی:

```
Resource              | Toggle | Sidebar | Route | API(403) | CRUD | Tested
appointments          |   ✓    |    ✓    |   ✓   |    ✓     |  ✓   |  PASS
appointment_settings  |   ✓    |    +    |   ✓   |    ✓     |  ✓   |   ?
patients              |   ✓    |    ✓    |   ✓   |    ?     |  ✓   |   ?
payments              |   ✓    |    +    |   +   |    +     |  ✓   |   ?
services              |   ✓    |    +    |   +   |    +     |  ✓   |   ?
clinic_info           |   ✓    |    +    |   +   |    +     |  ✓   |   ?
insurances            |   +    |    +    |   +   |    +     |  ✓   |   ?
inventory/tags/…      |   +    |    +    |   +   |    +     |  ✓   |   ?
```
(`+` = این پرامپت باید بسازد/تکمیل کند.)

## نکات مهم

- **منبع حقیقتِ پزشکِ عضو = `ClinicDoctorPermission.permissions`** (نه `DoctorSecretary` که مالِ منشی است).
  هر دو سیستم جدا هستند و نباید قاطی شوند.
- **پزشکِ مستقل مطلقاً محدود نشود:** `usePermissions().can` بدون context آزاد است؛ در بک‌اند هم گِیت فقط
  وقتی اعمال شود که کاربر پزشکِ عضوِ همان کلینیک باشد (`context.scope==='clinic'` / کلینیک از محیطِ فعال).
- `mergePermissions` whitelist دارد → منبع جدید حتماً در `DEFAULT_PERMISSIONS`.
- الگوی enforcement را از `InsuranceController::76` و کارِ منشی (`SecretaryAccessChecker`) بگیر؛ SOLID،
  بدون تکرار.
- تاریخ‌ها Unix timestamp؛ پاسخ‌ها `$this->success()/$this->error()`؛ لیست‌ها array-hydration.
- رشته‌های UI فارسی، RTL، شمسی. کد/کامیت/مستندات انگلیسی؛ گفت‌وگو فارسی. اول spec انگلیسی، تأیید فارسی، بعد پیاده‌سازی.
- بعد از تغییر endpoint‌ها: `docs/api/clinic.md` (+ هر domain متأثر) به‌روز شود.
