# Permissions Guide — Backend Model & Frontend Handling

Written for: frontend developers building the admin dashboard and the trainer app.

This explains **who can do what**, how permissions are stored, which endpoints manage them, and exactly how the frontend should read, gate and edit them. §8 is the complete list of admin permissions with the endpoint each one protects.

**Admin permissions are now enforced by the server.** A supervisor calling an endpoint their role does not include gets a 403. Frontend gating is for UX only; the server is the source of truth.

---

## 1. The mental model

There are two separate permission systems. Do not mix them.

| Actor | `user_type` | Where permissions come from | Managed by |
|---|---|---|---|
| Super admin | `super_admin` | None needed — bypasses every check | — |
| Supervisor / any admin-panel staff | `supervisor` | **A role** → role has many permissions | Admin: Roles screen |
| Trainer | `trainer` | **Permissions directly on the trainer** (no role) | Admin: Trainer create/edit screen |
| Learner | `user` | None | — |

- Admin permissions are **one row per admin API route**, and the permission identifier is the route's full name (`admin.courses.publish`, `admin.users.index`, …). A role is a bag of those rows.
- The server checks the request's route name against the role's permissions. No mapping is involved, so the catalog's `route_name` is exactly the string you gate on.
- Trainer permissions are **18 named capabilities** (`view_courses`, `manage_lessons`, …). Trainers can already do their normal work (view own courses, students, statistics, join sessions); a capability unlocks a *restricted* action.
- Both lists live in the same `permissions` table and are listed by the same endpoint, filtered by `type`.

---

## 2. The permission catalog

`GET /api/v1/admin/permissions`

| Query | Meaning |
|---|---|
| `filters[type]=admin` | Admin permissions (for the **Roles** screen) |
| `filters[type]=trainer` | Trainer capabilities (for the **Trainer** screen) |
| `search=courses.publish` | Matches route_name, resource, action |

Response item (not paginated, sorted by `resource` then `action`):

```json
{
  "id": 163,
  "name": null,
  "route_name": "admin.courses.publish",
  "resource": "courses",
  "action": "publish",
  "desc": null
}
```

**Frontend rules**

- `id` (number) is what you send back when saving a role or a trainer.
- Group the checkbox list by **`resource`** (one section per module), show `action` inside it.
- `name` is currently `null` and there is no translated title in the response. **Build the label yourself** from `resource` + `action` (e.g. i18n keys like `permissions.courses.publish`). Do not rely on `name`.
- `action` can be nested: `sessions.attendance.store`, `slots.index`, `bulk-delete`. Treat it as an opaque string.
- Standard actions on most resources: `index, show, store, update, destroy`. Extra actions exist for workflows: `publish, unpublish, approve, request-revision, toggle, bulk-delete, reply, meeting-link, …`.
- Some catalog rows have no matching endpoint (e.g. `store` on `reviews`). Harmless — granting them does nothing, but hide them (list in §8).

---

## 3. Admin side (roles → permissions)

### 3.1 What the logged-in admin receives

`POST /api/v1/admin/auth/login` → `data.permissions`:

| `user_type` | `permissions` value | What the frontend does |
|---|---|---|
| `super_admin` | `[]` (empty) | **Do not read the list. Allow everything** based on `user_type`. |
| `supervisor` (any role user) | array of `route_name` strings, e.g. `["admin.users.index", "admin.courses.publish", …]` | Show/hide UI by membership in this array |
| `user` | field is absent | n/a |

> ⚠️ Admin permissions are returned **only by login**. `GET /admin/profile` does not include them, so persist `data.permissions` (with `user_type`) in your auth store and refresh it by logging in again after a role change.

Helper to use everywhere:

```ts
const can = (user, routeName: string) =>
  user.user_type.value === 'super_admin' || user.permissions.includes(routeName);
```

Gate by the **exact route name of the action**:

| UI element | Check |
|---|---|
| "Courses" menu item | `can('admin.courses.index')` |
| "Publish" button | `can('admin.courses.publish')` |
| "Create user" button | `can('admin.users.create')` |
| "Delete selected" (bulk) | `can('admin.courses.bulk-delete')` |

> ⚠️ Route names are **not always `store`/`show`/`destroy`**. Users, trainers, feature-flags and partners use `create`, `view`, `delete`; book-orders update status via `update`; `POST /admin/notifications/send` is `admin.notifications.create`. Copy names from §8 — never guess.

**Wildcards are not used.** Permissions are exact route names. The server would accept a stored `admin.courses.*` value, but the UI never creates one, so `includes()` is enough.

Permissions are cached server-side (1 hour) and the cache is invalidated globally whenever a role is created, updated or deleted, so a role edit applies to the affected admins on their next request. Their *client-side* copy only updates on the next login.

### 3.2 Default supervisor role

Seeded automatically. The supervisor gets **every admin permission except** access-control/config resources:

`roles`, `supervisors`, `permissions`, `settings`, `feature-flags`, `audit-logs`

So a fresh supervisor will not see Roles / Supervisors / Settings / Feature flags / Audit logs. Super admin can change this from the Roles screen.

It does include the account self-service permissions (`admin.profile.view`, `admin.profile.update`, `admin.profile.change-password`) and `admin.notifications.index`.

### 3.3 Roles CRUD

`/api/v1/admin/roles` (list, show, create, update, delete, `DELETE roles/bulk-delete`).

Create / update body:

```json
{
  "slug": "content-manager",
  "is_active": true,
  "parent_id": 2,
  "en": { "name": "Content Manager" },
  "ar": { "name": "مدير المحتوى" },
  "permissions": [163, 162, 40]
}
```

- `permissions` = array of **permission ids** from the catalog (`type=admin`).
- **Sync semantics:** the array **replaces** the role's permissions. Key omitted = untouched. `[]` = remove all.
- `slug`, `en.name`, `ar.name` are required on create, optional on update.
- `parent_id` (nullable) makes the role inherit its parent's permissions.
- Role response:

```json
{
  "id": "<role uuid>",
  "name": "Supervisor",
  "slug": "supervisor",
  "parent_id": 1,
  "level": 1,
  "is_active": true,
  "permissions": [ { "id": 40, "name": null, "slug": null, "action": "index", "resource": "users" } ],
  "created_at": "…"
}
```

  Use `permissions[].id` to pre-tick checkboxes when editing. Ignore `name`/`slug` inside each permission (null). Note: role `id` is a **uuid**, `parent_id` is a **numeric** id.
- Show/list load the `permissions` array; show also returns `users_count`.

### 3.4 Supervisors (assigning a role to a person)

`/api/v1/admin/supervisors` body: `full_name, email, phone?, password (create), is_active?, role_id, image?`.
Send `role_id` exactly as the role identifier returned in a supervisor's `role.id`. **Verify once against the API** whether the role dropdown should send the uuid or numeric id — the roles list returns a uuid `id` while `role_id` is validated against `roles.id`.

Supervisor details also return the resolved `permissions` of their role.

---

## 4. Trainer side (direct permissions)

### 4.1 The 18 capabilities (mapped to the trainer dashboard design)

The admin controls, per trainer, **what is shown** (`view_*`) and **what can be done**. The catalog (`GET /admin/permissions?filters[type]=trainer`) groups them with `resource`, which matches the dashboard sections — use it for the checkbox groups:

| `resource` (group) | Section |
|---|---|
| `trainer-courses` | الكورسات |
| `trainer-programs` | البرامج التدريبية |
| `trainer-shared` | Courses **and** programs (materials, ratings tab, timer, attendance) |
| `trainer-students` | الطلاب |
| `trainer-consultations` | الاستشارات |
| `trainer-account` | الرئيسية statistics + الملف الشخصي |

**Show (visibility)** — without it the endpoint returns 403 and the frontend should hide the menu item / tab:

| Code (`action`) | Design element it shows | Endpoints |
|---|---|---|
| `view_courses` | Sidebar "الكورسات", courses list + stat tiles, course details, المحتوى التعليمي tab, sessions | `GET /trainer/courses`, `/courses/stats`, `/courses/{c}`, `/courses/{c}/lessons`, `/courses/{c}/materials`, `/courses/{c}/sessions`, `/sessions/{s}`, `/lessons/{l}/questions` |
| `view_programs` | Sidebar "البرامج التدريبية", programs list + tiles, نظرة عامة / جدول الجلسات / المواد والمرفقات tabs | `GET /trainer/programs`, `/programs/stats`, `/programs/{p}`, `/programs/{p}/sessions`, `/programs/{p}/materials` |
| `view_consultations` | Sidebar "الاستشارات", list, details (client name/phone/email, join button), and the offerings list | `GET /trainer/consultations…`, `POST …/join`, `…/finish`, `PUT …/notes`, `…/meeting-link`, `GET /trainer/consultant-offerings` |
| `view_trainees` | "المتدربون" tab of a course/program and the الطلاب → المتدربون tab (+ export) | `GET /trainer/students`, `/students/export`, `/courses/{c}/students`, `/programs/{p}/trainees` (and the `/trainees…` aliases) |
| `view_attendance_progress` | الطلاب → الحضور والتقدم tab | `GET /trainer/students/attendance-progress` |
| `view_exam_results` | الطلاب → الاختبارات والنتائج tab | `GET /trainer/students/quizzes`, `/students/quizzes/{quiz}` |
| `view_ratings` | نتائج التقييم tab of a course / program | `GET /trainer/courses/{c}/ratings`, `GET /trainer/programs/{p}/ratings` |
| `view_statistics` | الرئيسية dashboard statistics | `GET /trainer/statistics` |

**Use (actions):**

| Code (`action`) | Design element it enables | Endpoints |
|---|---|---|
| `use_training_timer` | عداد التدريب widget (start / stop / reset) on courses and programs | `GET/POST /trainer/courses/{c}/timer…`, `/programs/{p}/timer…` |
| `manage_lessons` | إضافة محاضرة, رفع فيديو, edit, drag-reorder, delete video lectures | `POST …/lessons/video`, `PUT /lessons/video/{l}`, `POST …/lessons/reorder`, `DELETE /lessons/{l}` |
| `manage_attachments` | رفع ملف / حذف on course and program materials | `POST/PUT/DELETE /trainer/materials…`, `/trainer/programs/{p}/materials…` |
| `manage_exam_questions` | Preparing questions (add/edit/delete/reorder) | `/trainer/lessons/{l}/questions…`, `/trainer/questions/{q}` |
| `edit_exam_settings` | Creating an exam and its settings / certificate link | `POST …/lessons/quiz`, `PUT /lessons/quiz/{l}`, deleting a quiz |
| `publish_content` | "إرسال للمراجعة" button (إجراءات المحتوى card) | `POST /trainer/courses/{c}/submit-for-review` |
| `manual_attendance` | Marking onsite attendance (and viewing a program session's roster) | `POST /trainer/courses/{c}/sessions/{s}/attendance`, `GET`/`POST /trainer/programs/{p}/sessions/{s}/attendance` |
| `edit_pricing` | Editing consultation price | `PUT /trainer/consultant-offerings/{o}` |
| `manage_consultation_slots` | Adding new consultation slots | `POST /trainer/consultant-offerings/{o}/slots` |
| `edit_profile` | Editing personal data (phone, email, bio, photo) | `PUT /trainer/profile`, `POST /trainer/profile/change-phone` |

**How the frontend uses it**

- Read the trainer's `permissions` array from login / `GET /trainer/profile` (§4.3). Show a sidebar item / tab / button only when the matching code is present, e.g. `الكورسات` needs `view_courses`; the ratings tab needs `view_ratings`; the timer card needs `use_training_timer`.
- Action buttons need both the section and the action, e.g. "إضافة محاضرة" = `view_courses` + `manage_lessons`; "رفع ملف" = `view_programs` (or `view_courses`) + `manage_attachments`.
- Not gated (always available): notifications, profile view, change password, static pages (الشروط والأحكام, سياسة الخصوصية, تواصل معنا), joining live sessions, logout.

**New endpoints added for the design**

- `GET /trainer/courses/{course}/ratings` and `GET /trainer/programs/{program}/ratings` — نتائج التقييم tab. Response: `average` (e.g. 4.0, null if none), `total_reviews`, `enrolled_count`, `satisfaction_percent` (share of 4–5★), `comments_count`, `distribution` (5→1 with `count` and `percent`), and `reviews` (`data[]` of `user_name, rating, comment, created_at` + `meta` paging; `?per_page=`, max 50). Hidden reviews are excluded. Render the label "جيد جداً" etc. client-side from `average`.
- `GET /trainer/programs/{program}/sessions` — جدول الجلسات tab (each session includes `attended_count`).
- `GET /trainer/programs/{program}/materials` — المواد والمرفقات tab.

Notes:
- Capabilities are independent — e.g. `manage_lessons` does **not** allow creating a quiz (needs `edit_exam_settings`) or uploading attachments (needs `manage_attachments`).
- **Not implemented because the BRD gives the trainer no endpoint for it:** editing a course/program's price, category, certificate terms or availability; making an exam available; editing the consultation type. These stay admin-only.
- **Never grantable:** publishing / approving a course — admin only. A trainer only *submits for review*.
- One legacy catalog row still exists and is **not enforced**: `trainer.feature.route`. Hide it from the UI. There are no `teacher.*` permissions.

Ownership always applies on top: a trainer with `manage_lessons` still gets 403 on a course that isn't assigned to them.

### 4.2 Admin assigns capabilities

`POST /api/v1/admin/trainers` and `PUT /api/v1/admin/trainers/{id}` accept:

```json
{ "permission_ids": [201, 202, 205] }
```

- Send the **catalog `id`s** (`filters[type]=trainer`). Backend stores the capability code.
- **Replace semantics:** the array becomes the trainer's full set. Key **omitted** = no change. `[]` = clear all. (If none of the sent ids resolve, nothing is changed.)
- Always send the **complete** selected set, not a delta.

Reading them back — `GET /api/v1/admin/trainers/{id}`:

```json
"permissions": [ { "permission_code": "manage_lessons" }, { "permission_code": "edit_pricing" } ]
```

To tick checkboxes, match each `permission_code` against the catalog row's **`action`** (not `id`, not `route_name`). Example: code `manage_lessons` ↔ catalog row with `action: "manage_lessons"`.

Seeded trainers (fresh `migrate:fresh --seed`): Ahmed has all 18; Sara has the views for courses/programs/consultations/trainees/attendance/ratings/statistics plus timer, lessons, attachments, exam questions, attendance, slots and profile; Khalid has only `view_courses`, `view_programs`, `view_trainees` and `manage_attachments`.

### 4.3 What the trainer app receives

Trainers log in through the shared login endpoint (`POST /api/v1/user/auth/login`). The response and the profile endpoint (`GET /api/v1/trainer/profile`) both include their capability codes:

```json
"data": {
  "user_type": { "value": "trainer", "…": "…" },
  "permissions": ["manage_lessons", "edit_pricing"]
}
```

- `permissions` is an array of the codes from §4.1 (empty `[]` if the admin granted none). It is **only present for trainers** — learners don't get the field, and admins get route names (§3.1).
- Gate the restricted buttons on it:

```ts
const canTrainer = (user, code: string) => user.permissions?.includes(code) ?? false;
// canTrainer(user, 'manual_attendance') → show "Mark attendance"
```

- Refresh it from `GET /trainer/profile` on app start and after any admin-side change you expect (an admin editing the trainer takes effect on the next request; the trainer's cached login payload will be stale until refreshed).
- Still handle the 403 (§5) — the server is the source of truth.

---

## 5. Error handling

### Trainer capability denied → HTTP 403

```json
{
  "status": "error",
  "message": "You are not permitted to edit your consultation pricing.",
  "data": { "error_code": "PERMISSION_DENIED", "correlation_id": "…" }
}
```

- Branch on `data.error_code === "PERMISSION_DENIED"`; show `message` (already localized by `Accept-Language`).
- Same 403 + code is returned when the resource isn't yours ("This course is not assigned to you."). Both mean "not allowed" — show the message, don't retry.
- Don't log the user out on 403 (that's for 401).

### Admin permission denied → HTTP 403

```json
{ "status": "fail", "message": "You do not have permission: admin.courses.publish", "data": null }
```
plus response header `X-Missing-Permission: admin.courses.publish`. Treat as "hide/disable and show message". The header holds the exact permission the role lacks, which makes it easy to log or display.

### Other statuses
- 401 unauthenticated → login.
- 422 validation → field errors (e.g. `permissions.0` invalid id).
- 404 → route/model not found.

---

## 6. Important caveats (read before shipping)

1. **Admin permissions are enforced.** Every route in §8 returns 403 unless the admin's role includes its `route_name` (super admin always passes). Trainer capabilities (§4) are enforced too.
2. **Roles need supporting permissions, not just the feature permission.** Each endpoint is checked independently, so a screen that loads several endpoints needs all of them. Examples:
   - The Roles form loads the catalog: needs `admin.permissions.index`.
   - A form that fills a dropdown from another module (categories, countries, trainers…) needs that module's `…index`.
   - The profile page needs `admin.profile.view`; the notification list needs `admin.notifications.index`.

   Keep the role editor forgiving: when a permission is ticked, offer to tick its usual companions, and handle a 403 on a dropdown with an empty state, not a broken page. Read `X-Missing-Permission` to see which one is missing.
3. **Not gated by RBAC** (any active admin or supervisor passes): `POST /admin/auth/login`, `POST /admin/auth/logout` and `GET /admin/notifications/stream`.
4. `name` on catalog rows is `null` — build labels client-side (§2).
5. Trainer permission data is per trainer profile; changing it takes effect on the next request (no cache).
6. Admin routes also require `user_type` of `super_admin` or `supervisor`. Trainers and learners get 403 on every `/admin/*` route regardless of permissions, and trainer routes likewise require `user_type` of `trainer`.

---

## 7. Quick reference — endpoints

| Purpose | Endpoint |
|---|---|
| Permission catalog | `GET /admin/permissions?filters[type]=admin\|trainer&search=` |
| Roles | `GET/POST /admin/roles`, `GET/PUT/DELETE /admin/roles/{uuid}`, `DELETE /admin/roles/bulk-delete` |
| Supervisors | `GET/POST /admin/supervisors`, `GET/PUT/DELETE /admin/supervisors/{id}` |
| Trainers + capabilities | `GET/POST /admin/trainers`, `GET/PUT/DELETE /admin/trainers/{id}` (`permission_ids`) |
| Admin's own permissions | `POST /admin/auth/login` → `data.permissions` |

All paths are prefixed with `/api/v1`.

---

## 8. Full admin permission catalog

One row per permission. **Gate a control by the `route_name` of the endpoint it calls.** Endpoints are shown without the `/api/v1` prefix. Where a permission covers both `PUT` and `PATCH`, one permission covers both.

### `audit-logs` — _not in default supervisor role_

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.audit-logs.index` | GET | `/admin/audit-logs` |

### `book-orders`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.book-orders.index` | GET | `/admin/book-orders` |
| `admin.book-orders.show` | GET | `/admin/book-orders/{order}` |
| `admin.book-orders.update` | PUT | `/admin/book-orders/{order}/status` |

### `books`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.books.index` | GET | `/admin/books` |
| `admin.books.store` | POST | `/admin/books` |
| `admin.books.bulk-delete` | DELETE | `/admin/books/bulk-delete` |
| `admin.books.show` | GET | `/admin/books/{book}` |
| `admin.books.update` | PUT / PATCH | `/admin/books/{book}` |
| `admin.books.destroy` | DELETE | `/admin/books/{book}` |
| `admin.books.file` | GET | `/admin/books/{book}/file` |

### `categories`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.categories.index` | GET | `/admin/categories` |
| `admin.categories.store` | POST | `/admin/categories` |
| `admin.categories.bulk-delete` | DELETE | `/admin/categories/bulk-delete` |
| `admin.categories.show` | GET | `/admin/categories/{category}` |
| `admin.categories.update` | PUT / PATCH | `/admin/categories/{category}` |
| `admin.categories.destroy` | DELETE | `/admin/categories/{category}` |

### `consultation-bookings`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.consultation-bookings.index` | GET | `/admin/consultation-bookings` |
| `admin.consultation-bookings.show` | GET | `/admin/consultation-bookings/{consultation_booking}` |
| `admin.consultation-bookings.meeting-link` | PUT | `/admin/consultation-bookings/{consultation_booking}/meeting-link` |

### `consultation-types`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.consultation-types.index` | GET | `/admin/consultation-types` |
| `admin.consultation-types.store` | POST | `/admin/consultation-types` |
| `admin.consultation-types.show` | GET | `/admin/consultation-types/{consultation_type}` |
| `admin.consultation-types.update` | PUT / PATCH | `/admin/consultation-types/{consultation_type}` |
| `admin.consultation-types.destroy` | DELETE | `/admin/consultation-types/{consultation_type}` |

### `contact-messages`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.contact-messages.index` | GET | `/admin/contact-messages` |
| `admin.contact-messages.show` | GET | `/admin/contact-messages/{id}` |
| `admin.contact-messages.destroy` | DELETE | `/admin/contact-messages/{id}` |
| `admin.contact-messages.reply` | POST | `/admin/contact-messages/{id}/reply` |

### `countries`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.countries.index` | GET | `/admin/countries` |
| `admin.countries.store` | POST | `/admin/countries` |
| `admin.countries.bulk-delete` | DELETE | `/admin/countries/bulk-delete` |
| `admin.countries.show` | GET | `/admin/countries/{country}` |
| `admin.countries.update` | PUT / PATCH | `/admin/countries/{country}` |
| `admin.countries.destroy` | DELETE | `/admin/countries/{country}` |

### `courses`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.courses.index` | GET | `/admin/courses` |
| `admin.courses.bulk-delete` | DELETE | `/admin/courses/bulk-delete` |
| `admin.courses.live.store` | POST | `/admin/courses/live` |
| `admin.courses.live.update` | PUT | `/admin/courses/live/{course}` |
| `admin.courses.recorded.store` | POST | `/admin/courses/recorded` |
| `admin.courses.recorded.update` | PUT | `/admin/courses/recorded/{course}` |
| `admin.courses.show` | GET | `/admin/courses/{course}` |
| `admin.courses.destroy` | DELETE | `/admin/courses/{course}` |
| `admin.courses.approve` | POST | `/admin/courses/{course}/approve` |
| `admin.courses.completion-summary` | GET | `/admin/courses/{course}/completion-summary` |
| `admin.courses.lessons.index` | GET | `/admin/courses/{course}/lessons` |
| `admin.courses.publish` | POST | `/admin/courses/{course}/publish` |
| `admin.courses.publish-check` | GET | `/admin/courses/{course}/publish-check` |
| `admin.courses.request-revision` | POST | `/admin/courses/{course}/request-revision` |
| `admin.courses.sessions.index` | GET | `/admin/courses/{course}/sessions` |
| `admin.courses.sessions.store` | POST | `/admin/courses/{course}/sessions` |
| `admin.courses.sessions.reorder` | PATCH | `/admin/courses/{course}/sessions/reorder` |
| `admin.courses.unpublish` | POST | `/admin/courses/{course}/unpublish` |

### `dashboard`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.dashboard.index` | GET | `/admin/dashboard` |

### `enrolled-users`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.enrolled-users.index` | GET | `/admin/enrolled-users` |

### `enrollments`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.enrollments.users` | GET | `/admin/enrollments/users` |

### `faqs`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.faqs.index` | GET | `/admin/faqs` |
| `admin.faqs.store` | POST | `/admin/faqs` |
| `admin.faqs.bulk-delete` | DELETE | `/admin/faqs/bulk-delete` |
| `admin.faqs.show` | GET | `/admin/faqs/{faq}` |
| `admin.faqs.update` | PUT / PATCH | `/admin/faqs/{faq}` |
| `admin.faqs.destroy` | DELETE | `/admin/faqs/{faq}` |

### `feature-flags` — _not in default supervisor role_

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.feature-flags.index` | GET | `/admin/feature-flags` |
| `admin.feature-flags.create` | POST | `/admin/feature-flags` |
| `admin.feature-flags.bulk-delete` | DELETE | `/admin/feature-flags/bulk-delete` |
| `admin.feature-flags.update` | PUT | `/admin/feature-flags/{id}` |
| `admin.feature-flags.delete` | DELETE | `/admin/feature-flags/{id}` |
| `admin.feature-flags.toggle` | POST | `/admin/feature-flags/{id}/toggle` |

### `governorates`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.governorates.index` | GET | `/admin/governorates` |
| `admin.governorates.store` | POST | `/admin/governorates` |
| `admin.governorates.bulk-delete` | DELETE | `/admin/governorates/bulk-delete` |
| `admin.governorates.show` | GET | `/admin/governorates/{governorate}` |
| `admin.governorates.update` | PUT / PATCH | `/admin/governorates/{governorate}` |
| `admin.governorates.destroy` | DELETE | `/admin/governorates/{governorate}` |

### `lessons`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.lessons.show` | GET | `/admin/lessons/{lesson}` |

### `notifications`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.notifications.index` | GET | `/admin/notifications` |
| `admin.notifications.create` | POST | `/admin/notifications/send` |

### `pages`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.pages.index` | GET | `/admin/pages` |
| `admin.pages.store` | POST | `/admin/pages` |
| `admin.pages.bulk-delete` | DELETE | `/admin/pages/bulk-delete` |
| `admin.pages.show` | GET | `/admin/pages/{page}` |
| `admin.pages.update` | PUT / PATCH | `/admin/pages/{page}` |
| `admin.pages.destroy` | DELETE | `/admin/pages/{page}` |

### `partners`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.partners.index` | GET | `/admin/partners` |
| `admin.partners.create` | POST | `/admin/partners` |
| `admin.partners.bulk-delete` | DELETE | `/admin/partners/bulk-delete` |
| `admin.partners.show` | GET | `/admin/partners/{id}` |
| `admin.partners.update` | PUT | `/admin/partners/{id}` |
| `admin.partners.destroy` | DELETE | `/admin/partners/{id}` |

### `permissions` — _not in default supervisor role_

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.permissions.index` | GET | `/admin/permissions` |

### `profile`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.profile.view` | GET | `/admin/profile` |
| `admin.profile.update` | PUT | `/admin/profile` |
| `admin.profile.change-password` | POST | `/admin/profile/change-password` |

### `programs`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.programs.index` | GET | `/admin/programs` |
| `admin.programs.store` | POST | `/admin/programs` |
| `admin.programs.bulk-delete` | DELETE | `/admin/programs/bulk-delete` |
| `admin.programs.sessions.live` | GET | `/admin/programs/sessions/live` |
| `admin.programs.show` | GET | `/admin/programs/{program}` |
| `admin.programs.update` | PUT / PATCH | `/admin/programs/{program}` |
| `admin.programs.destroy` | DELETE | `/admin/programs/{program}` |
| `admin.programs.certificates.index` | GET | `/admin/programs/{program}/certificates` |
| `admin.programs.certificates.store` | POST | `/admin/programs/{program}/certificates` |
| `admin.programs.content-status` | POST | `/admin/programs/{program}/content-status` |
| `admin.programs.sessions.index` | GET | `/admin/programs/{program}/sessions` |
| `admin.programs.sessions.store` | POST | `/admin/programs/{program}/sessions` |
| `admin.programs.sessions.attendance-summary` | GET | `/admin/programs/{program}/sessions/attendance-summary` |
| `admin.programs.sessions.bulk-store` | POST | `/admin/programs/{program}/sessions/bulk` |
| `admin.programs.sessions.show` | GET | `/admin/programs/{program}/sessions/{session}` |
| `admin.programs.sessions.update` | PUT / PATCH | `/admin/programs/{program}/sessions/{session}` |
| `admin.programs.sessions.destroy` | DELETE | `/admin/programs/{program}/sessions/{session}` |
| `admin.programs.sessions.attendance` | GET | `/admin/programs/{program}/sessions/{session}/attendance` |
| `admin.programs.sessions.attendance.store` | POST | `/admin/programs/{program}/sessions/{session}/attendance` |

### `regions`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.regions.index` | GET | `/admin/regions` |
| `admin.regions.store` | POST | `/admin/regions` |
| `admin.regions.bulk-delete` | DELETE | `/admin/regions/bulk-delete` |
| `admin.regions.show` | GET | `/admin/regions/{region}` |
| `admin.regions.update` | PUT / PATCH | `/admin/regions/{region}` |
| `admin.regions.destroy` | DELETE | `/admin/regions/{region}` |

### `reports`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.reports.courses` | GET | `/admin/reports/courses` |
| `admin.reports.orders` | GET | `/admin/reports/orders` |
| `admin.reports.programs` | GET | `/admin/reports/programs` |
| `admin.reports.revenue` | GET | `/admin/reports/revenue` |
| `admin.reports.transactions` | GET | `/admin/reports/revenue/transactions` |

### `reviews`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.reviews.index` | GET | `/admin/reviews` |
| `admin.reviews.show` | GET | `/admin/reviews/{review}` |
| `admin.reviews.update` | PUT | `/admin/reviews/{review}/status` |

### `roles` — _not in default supervisor role_

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.roles.index` | GET | `/admin/roles` |
| `admin.roles.store` | POST | `/admin/roles` |
| `admin.roles.bulk-delete` | DELETE | `/admin/roles/bulk-delete` |
| `admin.roles.show` | GET | `/admin/roles/{role}` |
| `admin.roles.update` | PUT / PATCH | `/admin/roles/{role}` |
| `admin.roles.destroy` | DELETE | `/admin/roles/{role}` |

### `sessions`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.sessions.update` | PUT | `/admin/sessions/{session}` |
| `admin.sessions.destroy` | DELETE | `/admin/sessions/{session}` |
| `admin.sessions.unschedule` | POST | `/admin/sessions/{session}/unschedule` |

### `settings` — _not in default supervisor role_

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.settings.index` | GET | `/admin/settings` |
| `admin.settings.update` | PUT | `/admin/settings` |

### `sliders`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.sliders.index` | GET | `/admin/sliders` |
| `admin.sliders.store` | POST | `/admin/sliders` |
| `admin.sliders.bulk-delete` | DELETE | `/admin/sliders/bulk-delete` |
| `admin.sliders.show` | GET | `/admin/sliders/{slider}` |
| `admin.sliders.update` | PUT / PATCH | `/admin/sliders/{slider}` |
| `admin.sliders.destroy` | DELETE | `/admin/sliders/{slider}` |

### `supervisors` — _not in default supervisor role_

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.supervisors.index` | GET | `/admin/supervisors` |
| `admin.supervisors.store` | POST | `/admin/supervisors` |
| `admin.supervisors.bulk-delete` | DELETE | `/admin/supervisors/bulk-delete` |
| `admin.supervisors.show` | GET | `/admin/supervisors/{supervisor}` |
| `admin.supervisors.update` | PUT / PATCH | `/admin/supervisors/{supervisor}` |
| `admin.supervisors.destroy` | DELETE | `/admin/supervisors/{supervisor}` |

### `trainer-consultants`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.trainer-consultants.index` | GET | `/admin/trainer-consultants` |
| `admin.trainer-consultants.store` | POST | `/admin/trainer-consultants` |
| `admin.trainer-consultants.show` | GET | `/admin/trainer-consultants/{trainer_consultant}` |
| `admin.trainer-consultants.update` | PUT / PATCH | `/admin/trainer-consultants/{trainer_consultant}` |
| `admin.trainer-consultants.destroy` | DELETE | `/admin/trainer-consultants/{trainer_consultant}` |
| `admin.trainer-consultants.slots.index` | GET | `/admin/trainer-consultants/{trainer_consultant}/slots` |
| `admin.trainer-consultants.slots.store` | POST | `/admin/trainer-consultants/{trainer_consultant}/slots` |
| `admin.trainer-consultants.slots.destroy` | DELETE | `/admin/trainer-consultants/{trainer_consultant}/slots/{slot}` |

### `trainers`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.trainers.index` | GET | `/admin/trainers` |
| `admin.trainers.create` | POST | `/admin/trainers` |
| `admin.trainers.view` | GET | `/admin/trainers/{id}` |
| `admin.trainers.update` | PUT | `/admin/trainers/{id}` |
| `admin.trainers.delete` | DELETE | `/admin/trainers/{id}` |
| `admin.trainers.toggle` | POST | `/admin/trainers/{id}/toggle-active` |

### `users`

| Permission (`route_name`) | Method | Endpoint |
|---|---|---|
| `admin.users.index` | GET | `/admin/users` |
| `admin.users.create` | POST | `/admin/users` |
| `admin.users.view` | GET | `/admin/users/{id}` |
| `admin.users.update` | PUT | `/admin/users/{id}` |
| `admin.users.delete` | DELETE | `/admin/users/{id}` |
| `admin.users.toggle` | POST | `/admin/users/{id}/toggle-active` |

### Catalog rows with no endpoint (hide these)

These exist in `GET /admin/permissions?filters[type]=admin` but no route checks them, so granting them does nothing. Hide any row whose `route_name` is not listed above.

`admin.audit-logs.destroy`, `admin.audit-logs.show`, `admin.audit-logs.store`, `admin.audit-logs.update`, `admin.book-orders.destroy`, `admin.book-orders.store`, `admin.consultation-bookings.destroy`, `admin.consultation-bookings.store`, `admin.consultation-bookings.update`, `admin.contact-messages.store`, `admin.contact-messages.update`, `admin.courses.store`, `admin.courses.update`, `admin.dashboard.destroy`, `admin.dashboard.show`, `admin.dashboard.store`, `admin.dashboard.update`, `admin.enrolled-users.destroy`, `admin.enrolled-users.show`, `admin.enrolled-users.store`, `admin.enrolled-users.update`, `admin.feature-flags.destroy`, `admin.feature-flags.show`, `admin.feature-flags.store`, `admin.notifications.destroy`, `admin.notifications.show`, `admin.notifications.store`, `admin.notifications.update`, `admin.partners.delete`, `admin.partners.store`, `admin.reviews.destroy`, `admin.reviews.store`, `admin.settings.destroy`, `admin.settings.show`, `admin.settings.store`, `admin.shipping-zones.destroy`, `admin.shipping-zones.index`, `admin.shipping-zones.show`, `admin.shipping-zones.store`, `admin.shipping-zones.update`, `admin.trainers.destroy`, `admin.trainers.show`, `admin.trainers.store`, `admin.users.destroy`, `admin.users.show`, `admin.users.store`

