---
feature: Admin users, roles, activity log
slug: access
surface: admin
consumer: vue
version: 0.3.0
status: in-progress
brd_version: "Morvia Project Guide"
brd_ids: [ADM-049, ADM-050]
business_rules: [BR-ACC-01, BR-ACC-06, BR-LOY-05]
decisions: [TD-14, TD-28, TD-29]
schema_tables: [users, roles, role_translations, permissions, permission_translations, permission_role, audit_logs, media]
screens: []
related_docs: [auth.md]
last_updated: 2026-11-15
---

# Admin users, roles and activity log — Vue Dashboard Integration Guide

> **Base URL:** `{HOST}/api/v1/admin` · Consumer: Vue · [Shared conventions](../API_CONVENTIONS.md). This describes the existing base, not completed BRD functionality.

## Quick reference

| | |
|---|---|
| Version | 0.3.0 (2026-11-15) — permission `grid`, `delete` / `suspend` actions, roles in use protected, suspension through edit; review the remaining limitations in §13. |
| Status | in-progress; ADM-049, ADM-050; TD-14. |
| Auth | JWT bearer; active user and role; route name and resolved permission in §2. |

**Endpoints** (paths relative to base URL)

| Method | Path | Route name | Purpose |
|---|---|---|---|
| GET | `/users` | `admin.users.index` | Users list |
| GET | `/users/{id}` | `admin.users.view` | User details |
| POST | `/users/{id}/toggle-active` | `admin.users.toggle` | Toggle active |
| GET | `/supervisors` | `admin.supervisors.index` | Supervisors list |
| POST | `/supervisors` | `admin.supervisors.store` | Create supervisor |
| GET | `/supervisors/{supervisor}` | `admin.supervisors.show` | Supervisor details |
| PUT/PATCH | `/supervisors/{supervisor}` | `admin.supervisors.update` | Update supervisor |
| DELETE | `/supervisors/{supervisor}` | `admin.supervisors.destroy` | Delete supervisor |
| GET | `/roles` | `admin.roles.index` | Roles list |
| POST | `/roles` | `admin.roles.store` | Create role |
| GET | `/roles/{role}` | `admin.roles.show` | Role details |
| PUT/PATCH | `/roles/{role}` | `admin.roles.update` | Update role |
| DELETE | `/roles/{role}` | `admin.roles.destroy` | Delete role |
| GET | `/permissions` | `admin.permissions.index` | Permission choices |
| GET | `/audit-logs` | `admin.audit-logs.index` | Audit trail |
| GET | `/profile` | `admin.profile.view` | Current profile |
| PUT | `/profile` | `admin.profile.update` | Update current profile |
| POST | `/profile/change-password` | `admin.profile.change-password` | Change password |

**Statuses:** active is a boolean; user type is a label string, not an enum object (§7).
**Errors:** 401 authentication, 403 inactive/permission, 404 missing, 422 validation; no endpoint-specific error codes established (§6).
**Remaining gaps:** TD-14 violations and incomplete BRD activity/suspension behavior (§13).

---

## 1. Business summary

The base provides user lookup and activation, supervisor and role management, permission choices, an audit list and the current admin's profile. Supervisors are users with `user_type=supervisor`; the general users list covers all user types. The BRD requires permission-scoped admin actions and a complete sensitive-action trail; the present routes do not establish all of those requirements (§13).

## 2. Pages and permissions

Send `Accept: application/json`, `Authorization: Bearer {JWT}`, preferably `Accept-Language: en` or `ar`; JSON writes use `Content-Type: application/json`. `X-Correlation-ID` is optional. Paths and route names are in Quick reference. Use UUID path identifiers; these services resolve records by `uuid`.

A named admin route resolves to `module.action`. `index`, `show`, and `view` map to `view`; `store`, `create`, and `send` to `add`; `update`, `edit`, `toggle`, `reply`, and `change-password` to `edit`; `destroy` and `delete` to `delete` (0.3.0: split out of `edit`); `suspend` and `reactivate` to `suspend`. `approve`, `reject`, `cancel`, `settings`, and `reports` retain their action names. For example, `admin.users.index` requires `users.view` and `admin.users.toggle` requires `users.edit`. Unknown route actions fail closed.

RBAC is enforced in every environment. The `super_admin` account bypasses permission checks and the locked `super_admin` role is synchronized with all permissions by `PermissionSeeder`. A `supervisor` has only its assigned role's explicit permissions; no role means no grants. Own profile and authentication logout/refresh are self-service. No parent roles or wildcard inheritance exist.

Supervisors cannot see the super-admin account or role in user, supervisor, and role listings or details; writes targeting them return 404. The protected role cannot be changed, deleted, or assigned through the API. A super admin can see them. `GET /permissions` returns groups with `module`, a `permissions` array (each entry `uuid`, `action`, bilingual `label`) and, since 0.3.0, a `grid` object keyed by the fixed columns `view, add, edit, delete, approve, reject, cancel, suspend, settings, reports` whose value is that permission's `uuid` or `null` when the module has no such action — render one checkbox row per module and send the ticked uuids in `permissions[]` on role create / edit. A role assigned to admin users cannot be deleted (422, reassign first); suspending an admin through `PUT /supervisors/{id}` with `is_active=false` stamps `suspended_at` and locks them out on their next request (403); an admin cannot suspend themselves. The four starter roles are system manager, courses and content, finance, and operations and support; only currently registered modules receive grants.

Captured response: `GET /countries` by an active supervisor lacking `countries.view`, PHPUnit SQLite in-memory on 2026-11-15, HTTP 403:

```json
{"status":"fail","message":"You do not have permission: countries.view","data":null}
```

## 3. List / table

All list endpoints are GET. `page` selects the page. Behavior differs by endpoint; do not apply shared query parameters indiscriminately.

| List | Search | Filters | Sort and pagination |
|---|---|---|---|
| `/users` | `search`: full_name, email, phone | Top-level `is_active`, `user_type`, `from`, `to` (created date inclusive); not `filters[...]`. Send `is_active=0` or `1`: PHP casts a nonempty string such as `false` to true. | Always latest `created_at`; ignores `sort` and `paginate`. `per_page` default 15, clamp 1–100. |
| `/supervisors` | `search`: role_id, is_active, full_name, email, phone, created_at | `filters[role_id]`, `[is_active]`, `[full_name]`, `[email]`, `[phone]`, `[created_at]` exact equality. `role_id` compares the internal numeric FK, not a UUID; do not expose this filter as UUID-compatible. | `sort[field]=asc|desc` or `sort=field&direction=asc|desc`; default latest. Per page default 15, max 100, `limit` alias; `paginate=false` or `0` returns at most 500 rows. |
| `/roles` | `search`: is_active, translated name, created_at | `filters[is_active]`, `[name]`, `[created_at]` are accepted by generic code; `name` is translated and the generic equality filter targets the base table, so it may fail. | Same generic sort/pagination as supervisors; translated `name` sorting is followed by an additional raw `name` ordering and may fail. |
| `/permissions` | Ignored | Ignored | All entries ordered resource then action; no pagination or sorting parameters. |
| `/audit-logs` | Ignored | Top-level `model` (exact fully qualified auditable class), `actor_id` (integer internal user ID), `action`, `from`, `to` (inclusive created dates); not `filters[...]`. | Latest created first; always paginated; `per_page` default 25, clamp 1–100. `sort` and `paginate` ignored. |

Generic CRUD sort pipeline initially allows supervisor searchable fields or role searchable fields, but the service then applies the supplied sort again without that allowlist; use real base-table columns only, and do not send arbitrary fields. Pagination response for supervisors/roles uses `data.users` / `data.roles` plus `data.links` and `data.meta`; users uses a resource collection with pagination; audit uses the raw Laravel paginator (`data.data`, `current_page`, `first_page_url`, `from`, `last_page`, `last_page_url`, `links`, `next_page_url`, `path`, `per_page`, `prev_page_url`, `to`, `total`). In the captured isolated run, users rows were under `data.data`. Unpaginated CRUD `data` is an array.

The following are captured responses from the seeded application environment.

**GET `/users`**

```json
{
  "status": "success",
  "message": "Users retrieved",
  "data": {
    "data": [
      {
        "id": "a33b175d-51a7-4e1e-9512-5b60496ac5a1",
        "full_name": "Supervisor",
        "email": "supervisor@info.com",
        "phone_code": "966",
        "phone": "500000002",
        "image": "https://ui-avatars.com/api/?name=U&background=dbeafe&color=1d4ed8&size=200",
        "user_type": "Supervisor",
        "is_active": true,
        "is_ban": false,
        "is_banned": false,
        "created_at": "2026-11-15 11:25:03"
      }
    ],
    "links": {
      "first": "http://localhost/api/v1/admin/users?page=1",
      "last": "http://localhost/api/v1/admin/users?page=1",
      "prev": null,
      "next": null
    },
    "meta": {
      "current_page": 1,
      "from": 1,
      "last_page": 1,
      "links": [
        {"url": null, "label": "&laquo; Previous", "page": null, "active": false},
        {"url": "http://localhost/api/v1/admin/users?page=1", "label": "1", "page": 1, "active": true},
        {"url": null, "label": "Next &raquo;", "page": null, "active": false}
      ],
      "path": "http://localhost/api/v1/admin/users",
      "per_page": 15,
      "to": 1,
      "total": 1
    }
  }
}
```

**GET `/supervisors`**

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "users": [
      {
        "id": "a33b175c-f92f-4a71-b8e0-05fc5c2d73c6",
        "full_name": "Admin",
        "email": "admin@info.com",
        "phone_code": "966",
        "phone": "500000001",
        "image": "https://ui-avatars.com/api/?name=U&background=dbeafe&color=1d4ed8&size=200",
        "user_type": "Super Admin",
        "is_active": true,
        "role": {
          "id": "a33b175b-9538-4181-96e6-18347c3ccc64",
          "name": "المشرف العام",
          "slug": "super_admin"
        },
        "last_login_at": "2026-11-15 11:39:17",
        "created_at": "2026-11-15 11:25:03"
      },
      {
        "id": "a33b175d-51a7-4e1e-9512-5b60496ac5a1",
        "full_name": "Supervisor",
        "email": "supervisor@info.com",
        "phone_code": "966",
        "phone": "500000002",
        "image": "https://ui-avatars.com/api/?name=U&background=dbeafe&color=1d4ed8&size=200",
        "user_type": "Supervisor",
        "is_active": true,
        "role": {
          "id": "a33b175b-e523-4803-bce6-ddc6dd686d4e",
          "name": "المشرف",
          "slug": "supervisor"
        },
        "last_login_at": null,
        "created_at": "2026-11-15 11:25:03"
      }
    ],
    "links": {
      "first": "http://localhost/api/v1/admin/supervisors?page=1",
      "last": "http://localhost/api/v1/admin/supervisors?page=1",
      "prev": null,
      "next": null
    },
    "meta": {
      "current_page": 1,
      "from": 1,
      "last_page": 1,
      "links": [
        {"url": null, "label": "&laquo; Previous", "page": null, "active": false},
        {"url": "http://localhost/api/v1/admin/supervisors?page=1", "label": "1", "page": 1, "active": true},
        {"url": null, "label": "Next &raquo;", "page": null, "active": false}
      ],
      "path": "http://localhost/api/v1/admin/supervisors",
      "per_page": 15,
      "to": 2,
      "total": 2
    }
  }
}
```

**GET `/roles`**

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "roles": [
      {
        "id": "a33b175b-e523-4803-bce6-ddc6dd686d4e",
        "name": "المشرف",
        "slug": "supervisor",
        "is_active": true,
        "permissions": [
          {
            "id": "a33b175a-c502-4ae1-bf51-4e2f1b22c660",
            "name": "categories.view",
            "slug": null,
            "action": "view",
            "resource": "categories"
          }
        ],
        "created_at": "2026-11-15 11:25:02"
      }
    ],
    "links": {
      "first": "http://localhost/api/v1/admin/roles?page=1",
      "last": "http://localhost/api/v1/admin/roles?page=1",
      "prev": null,
      "next": null
    },
    "meta": {
      "current_page": 1,
      "from": 1,
      "last_page": 1,
      "links": [
        {"url": null, "label": "&laquo; Previous", "page": null, "active": false},
        {"url": "http://localhost/api/v1/admin/roles?page=1", "label": "1", "page": 1, "active": true},
        {"url": null, "label": "Next &raquo;", "page": null, "active": false}
      ],
      "path": "http://localhost/api/v1/admin/roles",
      "per_page": 15,
      "to": 1,
      "total": 6
    }
  }
}
```

**GET `/permissions`**

```json
{
  "status": "success",
  "message": "Permissions retrieved",
  "data": [
    {
      "module": "categories",
      "grid": {
        "view": "a33b175a-c502-4ae1-bf51-4e2f1b22c660",
        "add": "a33b175a-ca31-45ae-ace2-4397a6bab82a",
        "edit": "a33b175a-cdea-4116-b00d-d983797377e8",
        "delete": "a33b175a-d1ea-457a-9345-cbe60df0ba4c",
        "approve": null,
        "reject": null,
        "cancel": null,
        "suspend": null,
        "settings": null,
        "reports": null
      },
      "permissions": [
        {
          "uuid": "a33b175a-c502-4ae1-bf51-4e2f1b22c660",
          "action": "view",
          "label": {"ar": "عرض categories", "en": "View categories"}
        },
        {
          "uuid": "a33b175a-ca31-45ae-ace2-4397a6bab82a",
          "action": "add",
          "label": {"ar": "إضافة categories", "en": "Add categories"}
        }
      ]
    }
  ]
}
```

**GET `/audit-logs`**

```json
{"status":"success","message":"Audit logs retrieved","data":{"current_page":1,"data":[],"first_page_url":"http:\/\/localhost\/api\/v1\/admin\/audit-logs?page=1","from":null,"last_page":1,"last_page_url":"http:\/\/localhost\/api\/v1\/admin\/audit-logs?page=1","links":[{"url":null,"label":"&laquo; Previous","page":null,"active":false},{"url":"http:\/\/localhost\/api\/v1\/admin\/audit-logs?page=1","label":"1","page":1,"active":true},{"url":null,"label":"Next &raquo;","page":null,"active":false}],"next_page_url":null,"path":"http:\/\/localhost\/api\/v1\/admin\/audit-logs","per_page":25,"prev_page_url":null,"to":null,"total":0}}
```

## 4. Details view

GET `/users/{id}`, `/supervisors/{supervisor}`, `/roles/{role}`, or `/profile`. Create returns 201; update/show returns 200. CRUD create/update returns the details resource; delete returns 200 with `data: null`. Permission list returns an array directly in `data`. Successful responses use `{status, message, data}`.

| Resource | Fields in `data` | Notes |
|---|---|---|
| User list | `id`, `full_name`, `email`, `phone_code`, `phone`, `image`, `user_type`, `is_active`, `is_ban`, `is_banned`, `created_at` | UUID `id`; nullable contact fields and dates. `roles` is conditional on a `roles` relation being loaded, but controller loads singular `role`, so it is normally absent. |
| User details | All user list fields plus `gender`, `birth_date`, `locale`, `allow_notifications`, `email_verified_at`, `last_login_at`, `updated_at` | Date-only birth date; other dates `YYYY-MM-DD HH:mm:ss` or null. |
| Supervisor list | `id`, `full_name`, `email`, `phone_code`, `phone`, `image`, `user_type`, `is_active`, `role`, `last_login_at`, `created_at` | `role` is null or `{id,name,slug}`, with UUID id. |
| Supervisor details | Supervisor list fields plus `permissions`, `updated_at` | `permissions` null or array of permission objects from the assigned role; conditional on role being loaded. |
| Role list | `id`, `name`, `slug`, `is_active`, `permissions`, `created_at` | `id` is a UUID. Permission object: `id`, `name`, `slug`, `action`, `resource`. |
| Role details/create/update | Role list fields plus `updated_at`, `en.name`, `ar.name`; `users_count` only when `users` relation is loaded | Controller does not load `users`, so do not depend on `users_count`. Locale name may be null. |
| Permissions | `module`, `permissions` | Each permission contains `uuid`, `action`, `label.ar`, `label.en`. |
| Audit row | `uuid`, `auditable_type`, `auditable_id`, `action`, `old_values`, `new_values`, `actor_id`, `ip_address`, `user_agent`, `correlation_id`, `created_at`, `actor` | Raw model, not resource: integer `auditable_id`/`actor_id` leak; actor has `full_name`, `email` but its internal `id` is hidden and UUID was not selected. JSON old/new values can contain other internal identifiers. No dedicated reason field. |
| Profile | `id`, `role`, `name`, `email`, `phone_code`, `phone`, `country`, `image`, `gender`, `birth_date`, `user_type`, `is_active`, `is_verified`, `is_banned`, `is_suspended`, `settings`, `location` | `id` is currently numeric; `role` is null or `{id,name}` with numeric id; `country` null or `{id,name,code,flag,phone_length,phone_starting_number}` with numeric id. `settings` has `locale`, `allow_notifications`; `location` has null `lat`, `lng`. Birth date uses model serialization. |

**Actual capture:** GET `/profile`, authenticated Super Admin, HTTP 200:

```json
{
  "status": "success",
  "message": "Profile retrieved successfully",
  "data": {
    "id": 1,
    "role": {
      "id": 1,
      "name": "المشرف العام"
    },
    "name": "Admin",
    "email": "admin@info.com",
    "phone_code": "966",
    "phone": "500000001",
    "country": {
      "id": 1,
      "name": "المملكة العربية السعودية",
      "code": "966",
      "flag": "https://ui-avatars.com/api/?name=🏳&background=e5e7eb&color=6b7280&size=128",
      "phone_length": 9,
      "phone_starting_number": "5"
    },
    "image": "https://ui-avatars.com/api/?name=U&background=dbeafe&color=1d4ed8&size=200",
    "gender": null,
    "birth_date": null,
    "user_type": "Super Admin",
    "is_active": true,
    "is_verified": true,
    "is_banned": false,
    "is_suspended": false,
    "settings": {
      "locale": "ar",
      "allow_notifications": true
    },
    "location": {
      "lat": null,
      "lng": null
    }
  }
}
```

## 5. Forms (create / edit)

Supervisor POST `/supervisors`, PUT/PATCH `/supervisors/{uuid}`. A field marked required on create is `sometimes` on update; supplied values must still satisfy their rules. These are FormRequest rules, not hypothetical UI constraints.

**Request payload (`POST /supervisors`):**

```json
{
  "full_name": "سارة الشمري",
  "email": "sara.supervisor@example.com",
  "phone": "500000021",
  "password": "Password123!",
  "role_id": "a33b175b-e523-4803-bce6-ddc6dd686d4e",
  "is_active": true
}
```

| Field | Input type | Required on create | Validation (mirror in zod/yup) | Notes |
|---|---|---|---|---|
| `full_name` | text | yes | string, max 255 | |
| `email` | email | yes | email, max 255, unique users.email ignoring current user | |
| `phone` | text | no | nullable, string, max 20, unique users.phone ignoring current user | |
| `password` | password | yes | nullable string, min 8; required if creating | optional on edit; a super admin can set a new password for recovery; omitted/empty does not replace it; existing tokens are invalidated |
| `is_active` | boolean | no | sometimes boolean | |
| `role_id` | select | no | nullable UUID, exists roles.uuid except `super_admin` | role assignment; UUID, not numeric ID |
| `avatar` | media hash | no | nullable string, `MediaExists` image for current model | media hash from an existing stored image; it must be unattached or attached to this record, and its file must exist |

Role POST `/roles`, PUT/PATCH `/roles/{uuid}`:

**Request payload (`POST /roles`):**

```json
{
  "slug": "marketing_officer",
  "is_active": true,
  "ar": {
    "name": "مسؤول التسويق"
  },
  "en": {
    "name": "Marketing Officer"
  },
  "permissions": [
    "a33b175a-c502-4ae1-bf51-4e2f1b22c660"
  ]
}
```

| Field | Input type | Required on create | Validation (mirror in zod/yup) | Notes |
|---|---|---|---|---|
| `slug` | text | yes | string, max 50, unique roles.slug ignoring current role | |
| `en.name`, `ar.name` | text | yes | each string, max 255 | each sometimes on update |
| `is_active` | boolean | no | sometimes boolean | |
| `permissions` | multi-select | no | nullable array; each element UUID, exists permissions.uuid | when present, sync replaces all assignments; empty array clears |

Profile PUT `/profile`, all optional:

| Field | Input type | Required on create | Validation (mirror in zod/yup) | Notes |
|---|---|---|---|---|
| `full_name` | text | N/A | sometimes string max 100 | response calls it `name` |
| `email` | email | N/A | sometimes email, unique users.email ignoring self | no max rule here |
| `phone` | text | N/A | sometimes string max 20 | no unique rule here |
| `phone_code` | text | N/A | nullable string max 5 | |
| `gender` | select | N/A | sometimes `male` or `female` | not nullable if supplied |
| `birth_date` | date | N/A | nullable date | |
| `locale` | text | N/A | nullable string max 10 | no locale whitelist |
| `avatar` | media hash | N/A | nullable string, `MediaExists` image for current user | |

No response examples for these writes were captured; §13 records the verification gap.

## 6. Actions (non-CRUD)

### 6.1 Toggle active

POST `/users/{id}/toggle-active` · Permission `admin.users.toggle` → `users.edit`. No request fields: the server flips the current value, so an automatic retry may undo the first request. Success 200 `data` contains UUID `id`, `full_name`, boolean `is_active`. For admin accounts, suspending stores `suspended_at`; reactivation clears it. There is no reason field or explicit suspend/unsuspend command. Login and protected requests are blocked while inactive; reactivation restores an otherwise valid token.

### 6.2 Change password

POST `/profile/change-password` · Self-service; no role permission required.

| Field | Type | Required | Validation | Description |
|---|---|---|---|---|
| `current_password` | string | yes | required string; must match current hash | Wrong current password throws validation exception. |
| `password` | string | yes | required string min 8 confirmed | New password. |
| `password_confirmation` | string | yes for confirmation | must match `password` | Laravel `confirmed` companion. |

Success 200 has a message and null data. Error handling for all routes:

| HTTP | `error_code` | When | Dashboard behavior |
|---|---|---|---|
| 401 | none | Missing/invalid authentication | Return to login. |
| 403 | none | Inactive user/role or missing permission | Show message and disable action. RBAC raises `X-Missing-Permission`, but exception rendering may not preserve that header. |
| 404 | none | Missing UUID or route | Show not found; refresh list. |
| 405 | none | Wrong method | Treat as integration error. |
| 422 | none | FormRequest validation or wrong current password | Display `message`; FormRequest returns only first message, not field error map. |
| 429 / 500 | none guaranteed | Throttling / unexpected failure | Show message, report correlation ID; do not assume a business error code. |

No successful action response was captured (§13).

## 7. Statuses and enums

The table lists request values and the actual response label for `user_type`; API responses currently emit the **label string**, not `{value,label}`. For UI state use the local mapping below, not an assumed response object. Most labels are generated from enum case names; the client label is localized.

| Enum | `value` | Label (en) | Badge color | Actions allowed |
|---|---|---|---|---|
| UserType | `client` | Trainee | UI choice | User lookup/toggle with permission |
| UserType | `trainer` | Trainer | UI choice | User lookup/toggle with permission |
| UserType | `center` | Center | UI choice | User lookup/toggle with permission |
| UserType | `provider` | Provider | UI choice | User lookup/toggle with permission |
| UserType | `super_admin` | Super Admin | UI choice | Full access |
| UserType | `supervisor` | Supervisor | UI choice | Assigned role grants |
| is_active | `true` | Active | UI choice | Toggle to inactive; protected requests allowed if role active |
| is_active | `false` | Inactive | UI choice | Toggle to active; protected requests denied |
| gender | `male` / `female` | Male / Female | N/A | Profile input; response is raw value, not an enum object |

`is_ban` and `is_banned` are inverse `is_active` in user resources; profile `is_suspended` instead means an inactive role, not a separate account suspension state. No `can_*` flags are returned.

## Data model

| Table | Read / write | Notes |
|---|---|---|
| `users` | read/write | General users and supervisor records; supervisor scoped to admin type. |
| `roles`, `role_translations` | read/write | Localized names; no parent. |
| `permissions`, `permission_translations`, `permission_role` | read / role assignment write | Permission catalog and role pivot. |
| `audit_logs` | read | Generic audit pipeline writes asynchronously; raw list has internal IDs. |
| `media` | read / association | Avatar references. |

## 8. TypeScript types

These types describe current source behavior; nullable and conditional fields must be handled defensively. The profile and audit numeric IDs are known violations of TD-14, not the desired future contract.

```ts
export type UUID = string;
export type Envelope<T> = { status: 'success' | 'fail' | 'error'; message: string; data: T };
export type PermissionGroup = { module: string; permissions: { uuid: UUID; action: string; label: { ar: string | null; en: string | null } }[] };
export type Permission = { id: UUID; name: string | null; slug: string | null; action: string | null; resource: string | null };
export type RoleSummary = { id: UUID; name: string | null; slug: string };
export type UserTypeInput = 'client' | 'trainer' | 'center' | 'provider' | 'super_admin' | 'supervisor';
export type UserTypeLabel = 'Trainee' | 'Trainer' | 'Center' | 'Provider' | 'Super Admin' | 'Supervisor';
export interface UserRow { id: UUID; full_name: string; email: string | null; phone_code: string | null; phone: string | null; image: string | null; user_type: UserTypeLabel | null; is_active: boolean; is_ban: boolean; is_banned: boolean; roles?: RoleSummary[]; created_at: string | null }
export interface UserDetails extends UserRow { gender: string | null; birth_date: string | null; locale: string | null; allow_notifications: boolean; email_verified_at: string | null; last_login_at: string | null; updated_at: string | null }
export interface Supervisor { id: UUID; full_name: string; email: string | null; phone_code: string | null; phone: string | null; image: string | null; user_type: UserTypeLabel | null; is_active: boolean; role?: RoleSummary | null; last_login_at: string | null; created_at: string | null }
export interface SupervisorDetails extends Supervisor { permissions?: Permission[] | null; updated_at: string | null }
export interface Role { id: UUID; name: string | null; slug: string; is_active: boolean; permissions?: Permission[]; created_at: string | null }
export interface RoleDetails extends Role { updated_at: string | null; users_count?: number; en: { name: string | null }; ar: { name: string | null } }
export interface Profile { id: number; role: { id: number; name: string | null } | null; name: string; email: string | null; phone_code: string | null; phone: string | null; country: { id: number; name: string | null; code: string; flag: string | null; phone_length: number | null; phone_starting_number: string | null } | null; image: string | null; gender: string | null; birth_date: string | null; user_type: UserTypeLabel | null; is_active: boolean; is_verified: boolean; is_banned: boolean; is_suspended: boolean; settings: { locale: string; allow_notifications: boolean }; location: { lat: null; lng: null } }
export interface AuditRow { uuid: UUID; auditable_type: string; auditable_id: number; action: string; old_values: Record<string, unknown> | null; new_values: Record<string, unknown> | null; actor_id: number | null; ip_address: string | null; user_agent: string | null; correlation_id: string | null; created_at: string | null; actor: { full_name: string; email: string | null } | null }
export interface ResourcePage<T> { links: Record<string, string | null>; meta: { current_page: number; from: number | null; last_page: number; path: string; per_page: number; to: number | null; total: number; [key: string]: unknown }; [key: string]: unknown }
export interface AuditPage { data: AuditRow[]; current_page: number; first_page_url: string; from: number | null; last_page: number; last_page_url: string; links: unknown[]; next_page_url: string | null; path: string; per_page: number; prev_page_url: string | null; to: number | null; total: number }
export interface SupervisorInput { full_name?: string; email?: string; phone?: string | null; password?: string | null; is_active?: boolean; role_id?: UUID; avatar?: string | null }
export interface RoleInput { slug?: string; is_active?: boolean; permissions?: UUID[] | null; en?: { name: string }; ar?: { name: string } }
export interface ProfileInput { full_name?: string; email?: string; phone?: string; phone_code?: string | null; gender?: 'male' | 'female'; birth_date?: string | null; locale?: string | null; avatar?: string | null }
export interface ChangePasswordInput { current_password: string; password: string; password_confirmation: string }
```

## 9. Business rules enforced by the backend

| Rule | What the backend does | What the admin sees |
|---|---|---|
| BR-ACC-01 (partial) | Supervisor creation forces `user_type=supervisor`; role is optional. | Supervisor appears in admin-only list. |
| BR-ACC-06 (partial) | Protected routes check active status; RBAC resolves explicit role grants in every environment. Generic CRUD save/delete schedules audit work. | A missing grant or inactive account/role is refused; audit completeness is not guaranteed. |
| Base validation | UUID role and permission references must exist; role permission array replaces assignments when provided. | Invalid form returns first validation message. |
| Base self-service | Password change verifies old password; profile update uses authenticated user. | Wrong password returns 422. |

## 10. Notifications triggered

| Event | Who is notified | Channel |
|---|---|---|
| N/A | No notification is explicitly dispatched by the inspected access controllers/services. | N/A; model observers or infrastructure may have separate side effects. |

## 11. Integration checklist

- [ ] Obtain an admin JWT; send the headers in §2 and gate every action with its resolved permission.
- [ ] Use UUIDs for route identifiers and role/permission form inputs; do not assume profile/audit currently comply with TD-14.
- [ ] Implement each list's actual query parameters, page shape and limits from §3; avoid unsupported translated name filtering/sorting.
- [ ] Implement the precise forms, nullable fields and confirmation validation from §5–6.
- [ ] Render nullable/conditional resource fields, raw user type labels and audit JSON safely.
- [ ] Do not retry toggle blindly; refresh after any uncertain result.
- [ ] Handle 401/403/404/422/429/500 and localized `message`; test Arabic and English.
- [ ] Capture real list, detail, write and error responses and verify acceptance after backend gaps are resolved.

## 12. Acceptance scenarios

| AC ID | Scenario | Expected |
|---|---|---|
| N/A (ADM-049) | Create/update supervisor with role UUID | Saved admin account and assigned role returned; invalid UUID rejected. |
| N/A (ADM-049) | Create role with localized names and permissions | Role saved; selected UUIDs replace assignments on explicit update. |
| N/A (ADM-049) | Administrator lacks a route grant | In every environment, protected request is rejected; UI hides action. |
| N/A (ADM-049) | Toggle an active user | `is_active` flips; inactive user is refused on protected requests. |
| N/A (ADM-050) | Review audit entries | Filter and inspect actor/action/old/new data; full BRD reason and sensitive-event coverage remain unverified. |
| N/A (profile) | Change password with incorrect current value | 422; password unchanged. |
| N/A (TD-14) | Inspect all identifiers | Expected UUIDs; current profile and audit fail this expectation (§13). |

## 13. Open decisions and known limitations

| ID | Impact on this feature | Current behavior |
|---|---|---|
| ADM-049 / S1-05 | Suspension requires a defined policy and flow. | Toggle flips `is_active` and records `suspended_at` for admin accounts; it does not capture a reason or establish all BRD safeguards. |
| ADM-050 / S1-07 | Complete activity logging and reason coverage not established. | Generic CRUD pipeline writes audit entries; toggle/profile changes bypass that pipeline. Audit has no dedicated reason field and complete required sensitive-action coverage is not established. |
| S1-08 | Full BRD integration/acceptance remains open. | No acceptance evidence for every route or end-to-end Vue screen; no assumption of complete feature readiness. |
| TD-14 | Public UUID is mandatory, but current code conflicts. | Profile returns numeric user, role and country IDs; audit returns raw numeric actor/auditable IDs and numeric actor filter; supervisor role filter uses internal FK. Do not silently reinterpret them as UUIDs. |
| Enum contract | Consumers cannot rely on `{value,label}` for user type. | Resources return title-cased strings; filters expect raw values. |
| Response capture | Real examples required for each endpoint. | Profile GET and the five list responses above were captured by passing isolated PHPUnit runs; no other success examples were captured. Initial run encountered missing `NotifyOrchestrator` when queued work ran synchronously; the passing run faked the bus. Populated supervisor/role/permission/audit lists, details, writes, actions and full runtime acceptance remain outstanding. |
| List implementation | Some generic query options are unsafe or ineffective. | Role `name` filter/sort can target a nonexistent base column; generic sort is applied twice, and audit/user custom lists ignore generic filter and sort options. |

---

## Version history

| Version | Date | Type | Summary | Dashboard action required |
|---|---|---|---|---|
| 0.3.0 | 2026-11-15 | MINOR | Permission grid (`grid`, `delete` / `suspend` actions), roles in use cannot be deleted, suspension through edit. | Render the grid; handle the 422 on deleting a role in use. |
| 0.2.0 | 2026-11-15 | MINOR | Dashboard identities, explicit RBAC and protected role visibility. | Refresh role and permission integration. |
| 0.1.1 | 2026-11-12 | PATCH | Enforce admin identity and account suspension. | Handle inactive accounts. |
| 0.1.0 | 2026-11-12 | Initial | Document existing base access endpoints and known gaps. | Integrate cautiously after resolving §13 and capturing remaining responses. |

## Changelog

### 0.3.0 — 2026-11-15
- **Added:** `grid` in `GET /permissions`; `delete` and `suspend` permission actions; finance role grants (dashboard, reports, trainer packages / subscriptions / commission, hall commission); in-use role deletion refusal; suspension through supervisor edit. **Breaking:** No for payloads; **a supervisor who could delete through `*.edit` now needs `*.delete`** — re-save roles after running `db:seed --class=PermissionSeeder`. **BRD / decisions:** ADM-049, TD-28, TD-29.

### 0.2.0 — 2026-11-15
- Added explicit supervisor grants, route permission coverage and super-admin protection; removed role hierarchy.

### 0.1.1 — 2026-11-12
- Enforced admin identity and documented suspension timestamp and reactivation.

### 0.1.0 — 2026-11-12
- **Added:** Existing user, supervisor, role, permission, audit and profile contracts.
- **Changed:** —
- **Fixed:** —
- **Removed / Deprecated:** —
- **Breaking:** No
- **Endpoints affected:** All endpoints in Quick reference.
- **Dashboard action required:** Review §13 and verify missing real response examples before integration.
- **BRD / decisions:** ADM-049, ADM-050, BR-ACC-01, BR-ACC-06, BR-LOY-05, TD-14.
