---
feature: Admin users, roles, activity log
slug: access
surface: admin
consumer: vue
version: 0.2.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]
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.2.0 (2026-11-15); 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`, `destroy`, `delete`, `toggle`, `reply`, and `change-password` to `edit`. `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` and a `permissions` array, each entry containing `uuid`, `action`, and bilingual `label` (`ar`, `en`). 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 historical complete responses from a passing isolated PHPUnit run (SQLite in-memory, bus faked, one trainee fixture; other tables empty) on 2026-11-12. These captures predate 0.2.0 and do not establish the current permission shape.

**GET `/users`**

```json
{"status":"success","message":"Users retrieved","data":{"data":[{"id":"a3354be4-0bd6-43b8-a565-7d2bc258d3d9","full_name":"Jude Kshlerin","email":"sanford.dion@example.com","phone_code":null,"phone":null,"image":"https:\/\/ui-avatars.com\/api\/?name=U&background=dbeafe&color=1d4ed8&size=200","user_type":"Trainee","is_active":true,"is_ban":false,"is_banned":false,"created_at":"2026-11-12 14:16:58"}],"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":[],"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":null,"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":null,"total":0}}}
```

**GET `/roles`**

```json
{"status":"success","message":"Data retrieved successfully","data":{"roles":[],"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":null,"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":null,"total":0}}}
```

**GET `/permissions`**

```json
{"status":"success","message":"Permissions retrieved","data":[]}
```

**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`, PHPUnit temporary test with an authenticated factory user, SQLite in-memory, queue bus faked, assertion HTTP 200 on 2026-11-12. This historical capture used a trainee before admin identity enforcement was added; it illustrates the TD-14 numeric ID violation, but trainee access is now denied:

```json
{"status":"success","message":"Profile retrieved successfully","data":{"id":1,"role":null,"name":"Cydney Boyer","email":"david81@example.org","phone_code":null,"phone":null,"country":null,"image":"https://ui-avatars.com/api/?name=U&background=dbeafe&color=1d4ed8&size=200","gender":null,"birth_date":null,"user_type":"Trainee","is_active":true,"is_verified":true,"is_banned":false,"is_suspended":false,"settings":{"locale":"en","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.

| 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}`:

| 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.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.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.
