# Secretary Permissions — Full Coverage & Panel Enforcement (Phase 2 / completion)

## پروژه

`clinicpro` (backend Symfony + پنل ادمین React). تک‌پروژه، cross-repo نیست.

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

## زمینه — چه چیزی از قبل انجام شده (تأییدشده در کد)

فاز اول ([secretary-permissions-coverage-and-panel-enforcement.md](secretary-permissions-coverage-and-panel-enforcement.md)) بخش زیادی را ساخته است. **دوباره نساز، فقط شکاف‌ها را کامل کن.**

آنچه **قبلاً هست**:

- **مدل مجوز:** `DoctorSecretary::DEFAULT_PERMISSIONS['resources']` با ۸ منبع:
  `appointments, patients, payments, insurances, addresses, clinic_info, inventory, tags`
  (`src/Secretary/Entity/DoctorSecretary.php:20`).
- **Enforcement مشترک:** `src/Secretary/Security/SecretaryAccessChecker.php` — نقطهٔ واحد
  (`can()`, `canOrNonSecretary()`, `denyUnlessGranted()`). صدا زده می‌شود در:
  `PaymentController`, `PatientController`, `InventoryController`, `TenantTagController`,
  `InsuranceController`, `PaymentMethodController`, و `AppointmentAccessChecker`.
- **اسکوپ per-doctor نوبت‌ها:** `MyAppointmentsController::resolveSecretaryFilter()` (L316) لیست را
  به `doctorIds` مجاز (ردیف‌های `DoctorSecretary`) محدود می‌کند؛ `AppointmentRepository::findByUserAndDoctorIds()`
  و `PatientRecordScopeResolver::forSecretary()` (L87) هم بر همان مجموعه پزشکان فیلتر می‌کنند.
- **Frontend gating primitive:** `assets/admin/hooks/usePermissions.ts` → `can(resource, action)`
  (خالی‌بودن `permissions` = محیط شخصی، آزاد). Sidebar از آن استفاده می‌کند.
- **UI مجوزها:** `assets/admin/pages/MySecretariesPage.tsx` → `PERMISSION_SECTIONS` + `EMPTY_PERMISSIONS`
  همان ۸ منبع را نمایش می‌دهد. `SecretaryPermissions` type در `assets/admin/types/index.ts:458`.

## مشکل / هدف — شکاف‌های باقی‌مانده

سه شکاف مشخص، مطابق پلن ۱۰-پرامپتی کاربر:

1. **منابع مجوزِ جانداده‌شده (Coverage gap).** این صفحات/ماژول‌ها منشی به آن‌ها دسترسی دارد یا در
   منوی تنظیمات دیده می‌شوند ولی **هیچ toggle مجوز ندارند** و enforce نمی‌شوند:
   `services` (سرویس‌ها)، `discounts` (تخفیف‌ها)، `sms` (پیامک‌ها)، `staff` (پرسنل)،
   `clinic_doctors` (مدیریت پزشکان کلینیک — فقط حالت کلینیک)، `appointment_settings` (تنظیمات نوبت‌دهی)،
   `subscription` (خرید اشتراک/پرداخت‌ها).
2. **اعمال ناقص در Frontend (Panel gap).** `usePermissions().can` فقط برای
   `appointments/patients/payments/insurances` در Sidebar استفاده شده
   (`assets/admin/components/layout/Sidebar.tsx:365-388`). `inventory`, `tags` و منابع جدید
   نه در Sidebar گِیت می‌شوند، نه Route آن‌ها در `App.tsx` گارد دارد، نه دکمه‌های CRUD صفحه پنهان می‌شوند.
3. **حساب کاربری (Account) نباید هیچ مجوزی داشته باشد** — باید همیشه برای منشی باز باشد؛ مطمئن شو
   هیچ گارد اشتباهی رویش نیست (Prompt 3).

هدف نهایی: مالک (پزشک مستقل یا کلینیک) دقیقاً تعیین کند هر منشی به کدام صفحه/عملیات دسترسی دارد،
و منشی فقط همان‌ها را در Sidebar/Route/دکمه‌ها ببیند و API هم خارج از مجوز ۴۰۳ بدهد. تست برای **دو حالت:
پزشک مستقل** و **کلینیک چند-پزشکه**.

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

| فایل | نقش | کار لازم |
|------|-----|----------|
| `src/Secretary/Entity/DoctorSecretary.php` | `DEFAULT_PERMISSIONS` | افزودن منابع جدید |
| `src/Secretary/Security/SecretaryAccessChecker.php` | نقطهٔ واحد enforcement | بدون تغییر ساختار؛ فقط صدا زدن در کنترلرهای جدید |
| `src/DoctorService/Controller/DoctorServiceController.php` · `src/ClinicService/Controller/ClinicServiceController.php` | سرویس‌ها | گارد `services` |
| `src/Discount/Controller/DiscountController.php` | تخفیف‌ها | گارد `discounts` |
| `src/Sms/Controller/{SmsController,SmsMessageController,SmsWalletController}.php` | پیامک‌ها | گارد `sms` |
| `src/Staff/Controller/StaffController.php` | پرسنل | گارد `staff` |
| `src/Clinic/Controller/ClinicController.php` (بخش پزشکان کلینیک) | مدیریت پزشکان کلینیک | گارد `clinic_doctors` — فقط clinic |
| `src/Appointment/Controller/AppointmentSettingsController.php` | تنظیمات نوبت‌دهی | گارد `appointment_settings` |
| `src/Subscription/Controller/SubscriptionController.php` | اشتراک | گارد `subscription` (یا حذف `ROLE_SECRETARY` اگر نباید ببیند) |
| `assets/admin/pages/MySecretariesPage.tsx` | فرم مجوز | افزودن `PERMISSION_SECTIONS` + `EMPTY_PERMISSIONS` منابع جدید |
| `assets/admin/types/index.ts` (`SecretaryPermissions` L458) | type | افزودن کلیدهای جدید |
| `assets/admin/hooks/usePermissions.ts` | `can()` | بدون تغییر — استفاده گسترده‌تر |
| `assets/admin/components/layout/Sidebar.tsx` (بخش `primaryRole === "secretary"` L361) | منوی منشی | گِیت هر آیتم با `can(resource,'view')` |
| `assets/admin/App.tsx` | جدول Route | گارد Route هر صفحهٔ منشی |
| صفحات پنل هر منبع (Services/Discounts/Sms/Staff/…Page.tsx) | دکمه‌های CRUD | پنهان‌کردن دکمه create/edit/delete با `can()` |

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

منبع حقیقت مجوز:

```php
// src/Secretary/Entity/DoctorSecretary.php:20
public const DEFAULT_PERMISSIONS = [
    'version'   => 1,
    'resources' => [
        'appointments' => ['view' => true,  'create' => true,  'cancel' => false, 'update_status' => true],
        'patients'     => ['view' => true,  'create' => false, 'update' => false, 'delete' => false],
        'payments'     => ['view' => true,  'create' => false, 'update' => false, 'delete' => false],
        'insurances'   => ['view' => true,  'create' => false, 'update' => false, 'delete' => false],
        'addresses'    => ['view' => true,  'create' => false, 'update' => false, 'delete' => false],
        'clinic_info'  => ['view' => true,  'update' => false],
        'inventory'    => ['view' => false, 'create' => false, 'update' => false, 'delete' => false],
        'tags'         => ['view' => false, 'create' => false, 'update' => false, 'delete' => false],
        // ← منابع جدید اینجا اضافه می‌شوند
    ],
];
```

نقطهٔ واحد enforcement (از این الگو در کنترلرهای جدید استفاده کن — چیز جدید نساز):

```php
// src/Secretary/Security/SecretaryAccessChecker.php:73
public function denyUnlessGranted(User $user, string $resource, string $action): void
{
    if (!$this->canOrNonSecretary($user, $resource, $action)) {
        // → ۴۰۳ استاندارد
    }
}
```

Frontend فقط ۴ منبع را در Sidebar گِیت کرده:

```tsx
// assets/admin/components/layout/Sidebar.tsx (primaryRole === "secretary")
if (can("appointments", "view")) { ... }
if (can("patients", "view"))     { ... }
if (can("payments", "view"))     { ... }
if (can("insurances", "view"))   { ... }
// ← inventory / tags / services / discounts / sms / staff / clinic_doctors / appointment_settings گِیت نشده‌اند
```

## وظایف

> قانون هر منبع: **همزمان در هر ۳ جا** اضافه شود وگرنه merge/نمایش می‌شکند —
> (۱) `DoctorSecretary::DEFAULT_PERMISSIONS`، (۲) `MySecretariesPage.tsx` (`EMPTY_PERMISSIONS` + `PERMISSION_SECTIONS`)،
> (۳) `SecretaryPermissions` type. سپس backend enforce + frontend gate.
> بعد از هر تغییر endpoint: `docs/api/*` همان session به‌روز شود. بدون تست (موفق+۴۰۳+مرزی) هیچ تسکی تمام نیست.

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

1. `grep -rln ROLE_SECRETARY src/` را با فهرست صفحاتی که منشی در Sidebar/پنل می‌بیند تطبیق بده.
   جدول بساز: **منبع | کنترلر(ها) | toggle دارد؟ | backend enforce؟ | sidebar gate؟ | route guard؟ | CRUD gate؟**.
2. برای هر ماژول تصمیم بگیر: **الف)** منشی می‌تواند دسترسی داشته باشد → toggle اضافه کن؛
   **ب)** هرگز نباید ببیند → `ROLE_SECRETARY` را از کنترلر بردار و در پرامپت دلیل را بنویس. (مثلاً `subscription`
   احتمالاً باید فقط برای owner باشد؛ اگر منشی نباید بخرد، به‌جای toggle نقش را حذف کن.)

### ۱. افزودن منابع مجوز جدید (Prompt 2 + 4 + 9)

برای هر منبعِ زیر که در ممیزی «باید toggle داشته باشد»، در هر ۳ جا اضافه کن:

```php
// DoctorSecretary::DEFAULT_PERMISSIONS['resources'] — همه پیش‌فرض false (اصل least-privilege)
'services'             => ['view' => false, 'create' => false, 'update' => false, 'delete' => false],
'discounts'            => ['view' => false, 'create' => false, 'update' => false, 'delete' => false],
'sms'                  => ['view' => false, 'create' => false, 'update' => false, 'delete' => false],
'staff'                => ['view' => false, 'create' => false, 'update' => false, 'delete' => false],
'appointment_settings' => ['view' => false, 'update' => false],
'clinic_doctors'       => ['view' => false, 'create' => false, 'update' => false, 'delete' => false], // فقط clinic
```

- `mergePermissions` عمیق merge می‌کند؛ منشی‌های موجود نمی‌شکنند. اما یک data migration اختیاری برای
  backfill کلیدهای جدید روی ردیف‌های قدیمی در نظر بگیر (یا در `getPermissions()` با `DEFAULT_PERMISSIONS` ادغام کن).
- `clinic_doctors` فقط در حالت `OWNER_CLINIC` معنا دارد: در `MySecretariesPage.tsx` این section را
  فقط وقتی نمایش بده که owner کلینیک است (پزشک مستقل نباید این toggle را ببیند — Prompt 9).

### ۲. Enforcement در Backend (Prompt 6)

در هر کنترلر جدید، **در ابتدای هر اکشن** با نقطهٔ واحد گارد بگذار (چیز جدید نساز):

```php
$this->secretaryAccess->denyUnlessGranted($user, 'services', 'view');    // GET
$this->secretaryAccess->denyUnlessGranted($user, 'services', 'create');  // POST
$this->secretaryAccess->denyUnlessGranted($user, 'services', 'update');  // PUT/PATCH
$this->secretaryAccess->denyUnlessGranted($user, 'services', 'delete');  // DELETE
```

- `canOrNonSecretary` تضمین می‌کند برای owner/پزشک/ادمین رفتار تغییر نکند (فقط منشی محدود شود).
- نبود مجوز → همان ۴۰۳ استاندارد `AccessChecker` (نه ۲۰۰ با لیست خالی، نه ۵۰۰).
- کنترلرهای: `DoctorServiceController`/`ClinicServiceController` (`services`)، `DiscountController` (`discounts`)،
  `Sms*Controller` (`sms`)، `StaffController` (`staff`)، `AppointmentSettingsController` (`appointment_settings`)،
  بخش پزشکانِ `ClinicController` (`clinic_doctors`).

### ۳. Enforcement در Frontend — Sidebar + Route + CRUD (Prompt 5 + 7)

1. **Sidebar** (`Sidebar.tsx`، بخش `secretary`): هر آیتم منو را با `can(resource,'view')` بپیچ —
   شامل `inventory`, `tags` و همهٔ منابع جدید. منشی نباید هیچ منوی بدون‌مجوز ببیند و نباید هیچ منوی اضافی بماند.
2. **Route guard** (`App.tsx`): هر Route صفحهٔ منشی را گارد کن؛ ورود مستقیم با URL بدون مجوز →
   redirect یا صفحهٔ «دسترسی ندارید» فارسی استاندارد (نه صفحهٔ سفید). اگر helper مشترکی نیست،
   یک `<RequirePermission resource action>` سبک بساز که از `usePermissions().can` استفاده کند
   (از `FeatureGate` موجود الگو بگیر).
3. **CRUD/Action buttons** در هر صفحه: دکمه‌های create/edit/delete/import/export و
   FloatingAction/ContextMenu/QuickAction را با `can(resource, action)` شرطی کن. نبودِ مجوز = پنهان، نه disable.
4. Breadcrumb صفحهٔ بدون‌مجوز هم نباید ساخته شود.

### ۴. حساب کاربری بدون مجوز (Prompt 3)

- صفحهٔ Account/پروفایل کاربری باید برای منشی همیشه باز باشد. بررسی کن هیچ `ROLE_*` سخت‌گیرانه یا
  `denyUnlessGranted` اشتباه رویش نیست و هیچ toggle برایش تعریف نشده. اگر endpoint اکانت
  `IS_AUTHENTICATED_FULLY` دارد کافی است — دست نزن.

### ۵. اسکوپ per-doctor نوبت‌ها — تکمیل پوشش عملیات (Prompt 8)

اسکوپ لیست از قبل هست (`resolveSecretaryFilter`)؛ فقط مطمئن شو **همهٔ** عملیات نوبت هم به مجموعهٔ
پزشکان مجاز محدودند، نه فقط GET لیست:

- create / update / cancel / update_status / قطعی‌کردن / پرداخت / فاکتور / تغییر وضعیت / مشاهدهٔ پروندهٔ بیمار.
- در هر اکشن، پیش از عمل چک کن `doctor` هدف در `allowed doctorIds` منشی باشد (از `DoctorSecretaryRepository`)؛
  در غیر این صورت ۴۰۳ — حتی اگر `appointments.<action>=true`. (مجوز action و اسکوپ doctor **هر دو** لازم‌اند.)
- تست هر دو سناریو: پزشک مستقل (فقط نوبت‌های خودش) و کلینیک با منشیِ محدود به زیرمجموعه‌ای از پزشکان.

### ۶. تست نهایی و ماتریس (Prompt 10)

- Backend (`ddev exec php bin/phpunit`): برای هر منبع تازه، تست منشی مجاز (۲۰۰) و غیرمجاز (۴۰۳)،
  و برای نوبت‌ها تست دسترسی به پزشک خارج از اسکوپ (۴۰۳).
- Frontend (`yarn test`): Sidebar با permissionهای مختلف فقط آیتم‌های مجاز را رندر کند؛
  دکمه‌های CRUD بدون مجوز رندر نشوند.
- دستی با منشیِ واقعی (`clinicpro-QA accounts`) در دو حالت پزشک مستقل و کلینیک.
- جدول نهایی را در خروجی بده و فقط وقتی «تمام» اعلام کن که همه PASS باشند:

```
Resource              | Toggle | Sidebar | Route | API(403) | CRUD | Tested
appointments          |   ✓    |    ✓    |   ✓   |    ✓     |  ✓   |  PASS
patients              |   ✓    |    ✓    |   ✓   |    ✓     |  ✓   |  PASS
payments              |   ✓    |    ✓    |   ✓   |    ✓     |  ✓   |  PASS
insurances            |   ✓    |    ✓    |   ✓   |    ✓     |  ✓   |  PASS
addresses             |   ✓    |    …    |   …   |    ✓     |  …   |   ?
clinic_info           |   ✓    |    …    |   …   |    ✓     |  …   |   ?
inventory             |   ✓    |    ?    |   ?   |    ✓     |  ?   |   ?
tags                  |   ✓    |    ?    |   ?   |    ✓     |  ?   |   ?
services              |   +    |    +    |   +   |    +     |  +   |   ?
discounts             |   +    |    +    |   +   |    +     |  +   |   ?
sms                   |   +    |    +    |   +   |    +     |  +   |   ?
staff                 |   +    |    +    |   +   |    +     |  +   |   ?
appointment_settings  |   +    |    +    |   +   |    +     |  +   |   ?
clinic_doctors(clinic)|   +    |    +    |   +   |    +     |  +   |   ?
```
(`+` = این پرامپت باید بسازد، `…`/`?` = ممیزی وظیفهٔ ۰ وضعیت واقعی را پر کند.)

## نکات مهم

- **منبع حقیقت = `DoctorSecretary.permissions` JSON.** enforcement جدید همین را بخواند، نه
  `ClinicDoctorPermissionChecker` (که مال پزشکِ عضو کلینیک است، نه منشی).
- نقطهٔ واحد enforcement از قبل هست: `SecretaryAccessChecker`. **کنترلر/checker موازی نساز** — فقط `denyUnlessGranted` صدا بزن (اصل ۲ CLAUDE.md: اول موجود را استفاده کن).
- هر منبع همزمان در سه‌گانهٔ Entity default / UI section / TS type اضافه شود.
- `clinic_doctors` فقط حالت کلینیک؛ برای پزشک مستقل نه toggle نه منو.
- least-privilege: منابع جدید پیش‌فرض همه `false`.
- تاریخ‌ها Unix timestamp؛ پاسخ‌ها `$this->success()/$this->error()`؛ لیست‌ها array-hydration.
- رشته‌های UI فارسی، RTL، شمسی. کد/کامیت/مستندات انگلیسی؛ گفت‌وگو فارسی. اول spec انگلیسی، تأیید فارسی، بعد پیاده‌سازی.
- بعد از تغییر endpointها، این فایل‌های `docs/api/` به‌روز: `secretary.md`, `service.md`/`clinic-service.md`, `discount.md`, `sms.md`, `staff.md`, `clinic.md`, `appointment.md`, `subscription.md`.
