# Permission Catalog API

> **Prefix:** `/api/v1/permission-catalog`

## چرا این اندپوینت هست

فهرستِ منابعِ قابل‌مجوزدهی تا پیش از این شش جا تکرار شده بود — دو Entity، سه فایل UI و
یک interface تایپ‌اسکریپت — و از هم واگرا شده بودند. نتیجه‌اش این بود که صفحهٔ تازه
به‌جای منبعِ خودش، مجوزِ نزدیک‌ترین صفحهٔ موجود را قرض می‌گرفت.

حالا `App\Shared\Security\PermissionCatalog` تنها جایی است که می‌گوید چه منابع و
اکشن‌هایی وجود دارند. این اندپوینت همان رجیستری را به پنل می‌دهد تا هر دو فرمِ
مجوز — منشی و پزشکِ عضو کلینیک — به‌جای فهرستِ هاردکد از آن رندر شوند.

**افزودن صفحهٔ تازه = یک ردیف در `PermissionCatalog::RESOURCES`.** فرانت تغییری لازم ندارد.

رجیستری فقط **ساختار** می‌دهد. مقدارِ پیش‌فرضِ هر نقش سیاست است و در Entity خودش
می‌ماند: `DoctorSecretary::DEFAULT_PERMISSIONS` و `ClinicDoctorPermission::DEFAULT_PERMISSIONS`.

---

## GET `/api/v1/permission-catalog`

فهرستِ کاملِ منابع و اکشن‌ها با برچسب فارسی.

**Permission:** `IS_AUTHENTICATED_FULLY`

پاسخ به کاربر بستگی ندارد و تا وقتی رجیستری عوض نشود ثابت است، پس کلاینت می‌تواند
بلندمدت cache کند (`staleTime: Infinity`). مقدارِ واقعیِ مجوزِ هر رابطه از
اندپوینت‌های خودِ منشی/پزشک می‌آید، نه از اینجا.

`resources` آرایه است نه object، تا ترتیبِ نمایش بخشی از قرارداد باشد؛ ترتیبِ
کلیدهای JSON قرارداد نیست.

### Response `200` (خروجی واقعی، بریده‌شده)

```json
{
  "success": true,
  "data": {
    "version": 1,
    "resources": [
      {
        "key": "appointments",
        "label": "مدیریت نوبت‌ها",
        "clinic_only": false,
        "actions": [
          { "key": "view", "label": "مشاهده نوبت‌ها" },
          { "key": "create", "label": "ایجاد نوبت" },
          { "key": "cancel", "label": "لغو نوبت" },
          { "key": "update_status", "label": "تغییر وضعیت نوبت" }
        ]
      },
      {
        "key": "clinic_doctors",
        "label": "مدیریت پزشکان کلینیک",
        "clinic_only": true,
        "actions": [
          { "key": "view", "label": "مشاهده پزشکان" },
          { "key": "create", "label": "افزودن پزشک" },
          { "key": "update", "label": "ویرایش پزشک" },
          { "key": "delete", "label": "حذف پزشک" }
        ]
      }
    ]
  }
}
```

| فیلد | نوع | توضیح |
| --- | --- | --- |
| `version` | int | نسخهٔ شِمای مجوزها. همان چیزی که در envelope ذخیره می‌شود |
| `resources[].key` | string | کلیدِ منبع — همان چیزی که در `permission` JSON می‌نشیند |
| `resources[].label` | string | برچسب فارسی برای نمایش |
| `resources[].clinic_only` | bool | فقط در محیطِ کلینیک معنا دارد؛ پزشکِ مستقل نباید ببیندش |
| `resources[].actions[].key` | string | `view` / `create` / `update` / `delete` / `cancel` / `update_status` |
| `resources[].actions[].label` | string | برچسب فارسیِ همان اکشن |

### منابعِ فعلی (۱۷)

`appointments` · `patients` · `treatment` · `payments` · `insurances` · `addresses` ·
`clinic_info` · `services` · `inventory` · `staff` · `tags` · `discounts` · `sms` ·
`appointment_settings` · `resources` · `clinic_doctors` · `subscription`

اکشن‌ها یکسان نیستند: `subscription` فقط `view/create` دارد، و
`clinic_info` / `appointment_settings` / `treatment` فقط `view/update`.

### Errors

| Status | شرایط |
| --- | --- |
| `401` | بدون توکن یا توکنِ نامعتبر |

---

## سازگاری با دادهٔ موجود

`getPermissions()` روی هر دو Entity، JSONِ ذخیره‌شده را روی پیش‌فرضِ نقش merge می‌کند:

- منبعی که در رجیستری هست و در JSONِ ردیف نیست (یعنی بعد از ساختِ آن ردیف اضافه شده)
  **پیش‌فرضِ نقش** را می‌گیرد، نه `false`. بدون این، هر منبع تازه برای همهٔ ردیف‌های
  موجود خاموش می‌ماند.
- مقدارِ صریحِ ذخیره‌شده هرگز بازنویسی نمی‌شود — حتی `false`.
- هیچ migration دادهٔ انبوهی لازم نیست؛ ستون `permission` از نوع `json` است و
  schema عوض نمی‌شود.

نوشتن‌ها از `PermissionCatalog::filterPatch()` عبور می‌کنند: منبع یا اکشنِ خارج از
رجیستری **بی‌صدا** کنار گذاشته می‌شود (نه `422`) — همان رفتاری که کلاینت‌های فعلی روی
آن حساب کرده‌اند. بقیهٔ کلیدهای همان درخواست اعمال می‌شوند.

مصرف‌کننده‌ها: [secretary.md](secretary.md) · [clinic.md](clinic.md)
