# tasks.md — Bilal Bin Rabah Platform: Frontend ↔ Backend Integration

## Executive summary

This plan connects the **mock-driven Next.js 16 frontend** (`bilal`) to the **Laravel 12 + Sanctum +
spatie/laravel-permission backend** (`bilal-backend`, MySQL) so the platform runs on a real, tested,
permission-gated API while preserving the exact data-layer contract the frontend already speaks.

- **What it connects.** The frontend's `createResourceService` seam (today in-memory dummy data) ⇄ a
  versioned `/api/v1` Laravel REST surface. The wire contract is fixed: list envelope `{ data, links, meta }`
  (snake_case `meta`/`links` + query params), camelCase resource fields, camelCase 422 `errors` keys, integer
  BIGINT ids, Sanctum PAT Bearer in `localStorage`.
- **Cutover model (incremental, reversible).** A per-resource allowlist `NEXT_PUBLIC_LIVE_RESOURCES`
  (read in `env.ts`, consumed in `createResourceService`) promotes ONE entity to the live API at a time while
  everything else stays on mock. When every entity is listed, flip `NEXT_PUBLIC_USE_MOCK_DATA=false` and clear
  the allowlist. Rollback = flip the flag back; no component changes.
- **Key decisions.** Multi-role = **UNION** (no role switching, no `X-Session-Role`); one seeded **super-user**
  (bypasses permissions via `Gate::before` + the FE hidden-features mask, never assignable); **per-action**
  `<domain>.<action>` Policy gating on the backend with the frontend **HIDING** (never disabling) controls the
  user lacks; self-service **/profile** (single `phone`) for every authenticated user; runtime role↔permission
  matrix at **/admin/roles** gated by `roles.managePermissions` (super-user only).
- **Infra constraints.** **No Redis on this server** — `CACHE_STORE=database`, `QUEUE_CONNECTION=database`.
  The database cache driver does NOT support cache tags, so caching uses **explicit namespaced keys**
  (e.g. `content:home`, `lookups:nationalities`) invalidated with `Cache::forget(key)` on write (a small
  key-registry clears a group); public reads still send `Cache-Control`/`ETag` for Next.js ISR.
- **Phase overview.** Phase 0 environment/CORS/contract freeze + CI. Phase 1 backend auth/profile/RBAC +
  OpenAPI scaffold. Phase 2 frontend auth integration. Phase 3 data-layer contract upgrade. Phase 4 RBAC
  enforcement wiring. **Phase 4.5 vertical-slice proof spike** (nationalities end-to-end, FE wired live).
  Phase 5 entity-by-entity buildout with **per-entity sub-checklists**. Phase 6 cutover & testing.

## Table of contents

- [Executive summary](#executive-summary)
- [Definition of Done (DoD)](#definition-of-done-dod)
- [Phase 0 — Environment, CORS & contract freeze](#phase-0--environment-cors--contract-freeze) (incl. **CI & quality gates**)
- [Phase 1 — Backend auth, profile & RBAC foundation](#phase-1--backend-auth-profile--rbac-foundation-no-frontend-wiring-yet) (incl. **OpenAPI spec scaffold**)
- [Phase 2 — Frontend auth integration](#phase-2--frontend-auth-integration-token-bearer-401-carve-out-loginlogoutmeprofile)
- [Phase 3 — Data-layer contract upgrade](#phase-3--data-layer-contract-upgrade-envelope--server-pagination--debounce--abort--server-sort)
- [Phase 4 — RBAC enforcement wiring](#phase-4--rbac-enforcement-wiring-fe-consumes-me-roles-be-policies--scoping)
- [Phase 4.5 — Vertical-slice proof spike](#phase-45--vertical-slice-proof-spike-first-entity-end-to-end-fe-wired-live)
- [Phase 5 — Entity-by-entity endpoint buildout](#phase-5--entity-by-entity-endpoint-buildout-dependency-first-incremental-cutover-via-liveresources)
- [Phase 5.1 — Per-entity sub-checklists (concrete, one per entity)](#phase-51--per-entity-sub-checklists-concrete-one-per-entity)
- [Phase 6 — Cutover & testing](#phase-6--cutover--testing)
- [Cross-cutting](#cross-cutting-apply-to-every-pageendpoint)

## Definition of Done (DoD)

> **Reference this everywhere.** A task (and every per-entity slice) is DONE only when ALL of the following hold:
- [ ] [S][BE+FE] Code follows the `CLAUDE.md` rules (thin page → feature → service / thin controller → Action → model; no hardcoded values; tokens + `lib/config` + i18n / `config()` only).
- [ ] [S][BE+FE] Tests written AND green — BE: a feature test per endpoint covering happy path + 401 + 403 + 404 + 422; FE: vitest for services/hooks/critical utils (incl. error/reload paths).
- [ ] [S][BE+FE] Permission-gated — BE Policy enforces `<domain>.<action>` (403 is the real boundary); FE HIDES (never disables) the matching control/page.
- [ ] [S][BE] Eager-loaded (no N+1), indexed (every filtered/sorted/FK column), and paginated (default 25, cap 100; only the deliberate `listAll()`/report path is unbounded).
- [ ] [S][BE] OpenAPI 3 spec for `/api/v1` updated for the touched endpoints and validated in CI.
- [ ] [S][BE+FE] Lint/format clean — BE `pint --test`; FE `eslint`.
- [ ] [S][BE+FE] Verification gates pass — BE: `php artisan test`, `pint --test`, `php artisan migrate:fresh --seed`; FE: `npx tsc --noEmit`, `eslint`, `vitest run`, `next build` (no `MISSING_MESSAGE`).


> **Purpose.** A phased, top-to-bottom developer task list derived from the Integration Roadmap (`bilal-backend/docs/frontend-integration-roadmap.md`, Next.js 16 frontend ↔ Laravel 12 backend). Work it in order; tick each box `[ ]` → `[x]` as you finish.
>
> **How to use.** Do the phases sequentially. Inside a phase, do Backend tasks and Frontend tasks as marked; finish a phase only when every `- [ ] Acceptance:` box in it is checked. Do not skip ahead — later phases assume earlier ones are green.
>
> **Repos (siblings).** Backend repo = `bilal-backend` (`E:\Companies\Clenicode\bilal\bilal\bilal-backend`, Laravel 12 + Sanctum + spatie/laravel-permission, MySQL). Frontend repo = `bilal` (`E:\Companies\Clenicode\bilal\bilal\bilal`, Next.js 16 App Router). All paths below are relative to one of these two repo roots.
>
> **Non-negotiable conventions folded in throughout:** resource fields = camelCase; pagination `meta`/`links` + list/filter query params = snake_case; 422 `errors` keys = camelCase (map camel→snake in the Action, never in `prepareForValidation`); IDs = integer BIGINT; auth = Sanctum PAT Bearer in `localStorage`; **multi-role = UNION** (no active-role selection, no in-session switcher, no `X-Session-Role` header); one seeded **super-user** (bypasses permissions via `Gate::before` + bypasses the frontend hidden-features mask, never assignable); **per-action permission gating** (`<domain>.<action>` Policy on the backend, frontend HIDES — never disables — the matching buttons/pages); self-service **/profile** for every authenticated user (owner-only, never edits roles/permissions/branch/status/linkage).
>
> **Ratified clarifications folded into this list (resolving the verification findings — apply everywhere below):**
> 1. **Single `phone` column.** The data model adds ONE `phone` column (migration + `UserResource` emit a single `phone`). Every "self-editable fields" mention is therefore **name, phone, email, photo** (singular `phone`). There is no phones table / JSON array / plural handling — drop any plural-phone wording.
> 2. **Assignable auth roles = exactly 4** (`supervisor`, `teacher`, `parent`, `employee`). The 6 known `UserRole` literals are `super-user | supervisor | teacher | parent | employee | student`; of these, `super-user` is **seed-only** (M3) and `student` is **reserved** (no portal), leaving **4 truly assignable**. The **employee-form role picker offers only `{supervisor, teacher, employee}`** (parents are provisioned by the student-approval flow, not the employee form). Backend FormRequests constrain submitted auth roles with `Rule::in(['supervisor','teacher','parent','employee'])` and reject `super-user`/`student`.
> 3. **bulk-delete reuses `<domain>.delete`** — there is no separate `bulk-delete` permission; the `bulk-delete` endpoint authorizes against `<domain>.delete`.
> 4. **Custom (non-CRUD) actions are gated identically** — every custom action also maps to its own `<domain>.<action>` permission + Policy gate + FE hide (see Cross-cutting).

---

## Phase 0 — Environment, CORS & contract freeze

> Goal: both apps talk locally; the wire contract + all ratified decisions are committed to both repos; nothing wired yet.

**Backend (`bilal-backend/`)**
- [x] [S][BE] Verify `config/cors.php`: `paths` includes `api/*`, `allowed_origins` reads `FRONTEND_URL` (default `http://localhost:3000`), `supports_credentials` is `true`.
- [x] [S][BE] Verify/update `config/sanctum.php`: `stateful` includes `localhost:3000`, `guard = web`; set finite `expiration` (recommended `525600` = 1 year); optional `SANCTUM_TOKEN_PREFIX` (e.g. `bilal_`). Plan to **surface `expiresAt`** in the Phase 1 auth response.
- [x] [M][BE] Update `.env` and `.env.example`: add `APP_LOCALE=ar`, `FRONTEND_URL`, `SANCTUM_STATEFUL_DOMAINS`, DB creds; ensure all are read via `config()` only (never `env()` in app code).
- [x] [S][BE] Confirm `routes/api.php` exposes `GET /api/v1/health` returning `200 ok`.

**Frontend (`bilal/`)**
- [x] [M][BE+FE] `src/lib/config/env.ts`: add typed `authTokenKey` (`NEXT_PUBLIC_AUTH_TOKEN_KEY`, default `bilal.auth.token`) via the `publicEnv` helper.
- [x] [M][BE+FE] `src/lib/config/env.ts`: add typed `liveResources` (`NEXT_PUBLIC_LIVE_RESOURCES`, default `''`) parsed to `string[]` (split on `,`, trimmed); keep `apiBaseUrl`/`apiVersion`/`useMockData`; never read `process.env` anywhere else.
- [x] [M][FE] `.env.local` + `.env.example`: set `NEXT_PUBLIC_API_BASE_URL=http://localhost:8000`, `NEXT_PUBLIC_API_VERSION=v1`, `NEXT_PUBLIC_USE_MOCK_DATA=true`, `NEXT_PUBLIC_LIVE_RESOURCES=`.
- [x] [M][BE+FE] `src/lib/config/app-config.ts`: add `SEARCH_DEBOUNCE_MS` (e.g. `300`) and confirm a single default `PAGE_SIZE` (`25`, matching backend) used by `useResourceList` and `DataTable`.

**Contract freeze (both repos)**
- [x] [S][BE+FE] Commit the §2 wire-contract table + §3 decisions into both repos' `CLAUDE.md` (or a shared `INTEGRATION.md`): envelope `{ data, links, meta }`, snake_case meta/query params, camelCase fields + 422 error keys, `expiresAt` surfaced in the auth response.
- [x] [S][BE+FE] Document the multi-role **UNION** model (no `X-Session-Role`, no role selection, no in-session switcher).
- [x] [S][FE] Document the seed-only **super-user** (bypasses permissions + hidden-features mask, never assignable).
- [x] [S][BE] Document the **per-action gating** rule (`<domain>.<action>` Policy = boundary; FE hides controls; bulk-delete reuses `<domain>.delete`; custom actions gated the same way).
- [x] [S][BE+FE] Document the self-service **/profile** endpoints (`GET /auth/me`, `PATCH /auth/profile`, `POST /auth/password`) — owner-only, single `phone`, never roles/permissions/branch/status/linkage.

**CI & quality gates**
- [ ] [M][BE] Backend CI pipeline (e.g. GitHub Actions `.github/workflows/backend.yml`): `composer install`, `cp .env.example .env`, `php artisan key:generate`, `php artisan migrate`, `php artisan test`, `./vendor/bin/pint --test` — fail the build on any non-zero.
- [ ] [M][FE] Frontend CI pipeline (e.g. `.github/workflows/frontend.yml`): `npm ci`, `npx tsc --noEmit`, `eslint`, `vitest run`, `next build` — fail on any error or `MISSING_MESSAGE`.
- [ ] [M][BE+FE] Minimum test-coverage gate as a MERGE gate (services / policies / critical hooks must meet a configured threshold); a PR cannot merge to `master` until both pipelines + the coverage gate are green. Referenced again in Phase 6.

**Acceptance**
- [x] [S][BE] Acceptance: `curl http://localhost:8000/api/v1/health` → `200`.
- [x] [S][BE] Acceptance: browser `fetch('http://localhost:8000/api/v1/health')` from `localhost:3000` succeeds with no CORS error.
- [x] [S][BE+FE] Acceptance: contract table + all §3 decisions (incl. union model, super-user, per-action gating, /profile) committed to both repos.

---

## Phase 1 — Backend auth, profile & RBAC foundation (no frontend wiring yet)

> Goal: a tested `/api/v1/auth/*` surface (login/logout/me/forgot-password **+ profile + password**) plus seeded roles/permissions, returning the exact envelope/casing. Profile/password endpoints are built HERE — they are part of the auth surface (§7/§9) and the FE profile page (Phase 2) depends on them.

**Backend (`bilal-backend/`)**
- [x] [M][BE] Create migration `database/migrations/<ts>_add_domain_fields_to_users_table.php`: add `username` (nullable, unique), `phone` (single column, nullable, indexed), `user_type` (`staff`|`parent`|`student`), `archived_at` (nullable timestamp — archive flag, NOT `SoftDeletes`); reversible `down()`.
- [x] [L][BE] Performance — indexes & strict models: in THIS migration and EVERY later migration, add a DB index on every filtered/sorted/foreign-key column (here: `username` unique, `phone`, `user_type`, `archived_at`); enable `Model::shouldBeStrict()` in non-prod (e.g. `AppServiceProvider::boot()` guarded by `!app()->isProduction()`) to surface N+1/lazy-load violations early (see Cross-cutting "Performance & caching").
- [x] [M][BE] `app/Models/User.php`: update `$fillable` (`name`, `username`, `email`, `phone`, `password`, `user_type`, `archived_at`).
- [x] [M][BE] `app/Models/User.php`: set `$casts` (`password` => `'hashed'`, `email_verified_at` => `'datetime'`, `archived_at` => `'datetime'`) and `$hidden` (`password`, `remember_token`); keep `HasApiTokens` + `HasRoles`.
- [x] [M][BE] `app/Models/User.php`: add `parent`/`children`/`teacherCircles` relationships and a `scopeNotArchived`.
- [x] [M][BE] `database/seeders/RoleSeeder.php`: `Role::firstOrCreate(['name'=>$n,'guard_name'=>'web'])` for `super-user`, `supervisor`, `teacher`, `employee`, `parent` (+ `student` reserved); idempotent; `super-user` is seed-only (never granted via API/UI).
- [x] [L][BE] `database/seeders/PermissionSeeder.php`: `Permission::firstOrCreate(...)` for every §5.5 catalog entry — including the **non-CRUD custom permissions** (`students.transfer/archive`, `students.pending.approve/reject`, `parents.resetPassword`, `student_exams.publish`, `subjects.assignTeachers`, `employees.assignRoles`, `memorization.record`, `attendance.record`, `grades.record`) PLUS the **full per-resource CRUD permission sets for the administration resources** (`evaluations.{viewAny,view,create,update,delete}`, `warnings.*`, `certificates.*`, `letters.*` — these are resources, not single custom actions) PLUS the **content permission sets** (`news.{viewAny,view,create,update,delete}`, `library.*`, `album.*`, `site_settings.{view,update}`, `landing.{view,update}`); attach to roles via `Role::findByName($r)->givePermissionTo([...])`; add per-job permission bundles (secretary/accountant/coordinator/driver); call `app(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions()` at the end; grant NOTHING to `super-user`.
- [x] [L][BE+FE] `database/seeders/UserSeeder.php` + refactor `database/seeders/DatabaseSeeder.php`: order `RoleSeeder` → `PermissionSeeder` → `UserSeeder`; create demo users mirroring FE `DEMO_USERS` (supervisor, teacher w/ circle 1, parent w/ one child) with `assignRole(...)`, plus exactly one `super-user` account via `firstOrCreate`.
- [x] [M][BE] `app/Http/Requests/Api/V1/Auth/LoginRequest.php`: rules `login` (string, required — email OR username OR phone) + `password` (required); NO `prepareForValidation()` (keep error keys camelCase); Arabic messages.
- [x] [M][BE] `app/Http/Requests/Api/V1/Auth/ForgotPasswordRequest.php`: `email` rule; generic flow (no user enumeration).
- [x] [M][BE] `app/Http/Requests/Api/V1/Auth/UpdateProfileRequest.php` (new): validate self-editable fields ONLY (`name`, `phone`, `email`, `photo`); `authorize()` returns owner-only (acting user == target); Arabic messages; reject any roles/permissions/branch/status/linkage key if present.
- [x] [M][BE] `app/Http/Requests/Api/V1/Auth/ChangePasswordRequest.php` (new): validate `currentPassword` (required, must match the stored hash) + `newPassword` (required, confirmed, strong) with camelCase error keys; `authorize()` owner-only; Arabic messages.
- [x] [L][BE] `app/Http/Resources/UserResource.php`: camelCase output `{ id, name, username, email, phone, photoUrl, userType, roles: string[], permissions: string[], teacherId?, circleIds?, parentId?, childIds?, archivedAt }` (single `phone`); `roles` = `getRoleNames()`, `permissions` = `getAllPermissions()->pluck('name')` (UNION); eager-load `with('roles.permissions')`; assert roles ⊆ the 6 known `UserRole` literals.
- [x] [L][BE] `app/Actions/Auth/AuthenticateUserAction.php`: resolve by `where('email',$login)->orWhere('username',$login)->orWhere('phone',$login)`, `Hash::check`, reject archived users, `createToken($deviceName, $abilities)` in a DB transaction; return `{ user, token, expiresAt }` (works for staff email/username and parent email/phone).
- [x] [L][BE] `app/Http/Controllers/Api/V1/AuthController.php` (or a dedicated `ProfileController`): thin `login(LoginRequest)` → `{ data: { token, expiresAt, user: UserResource } }`; `logout(Request)` → `currentAccessToken()->delete()`, `204`; `me(Request)` → `UserResource`; `forgotPassword(ForgotPasswordRequest)` → password broker, generic success; **`updateProfile(UpdateProfileRequest)` → owner-only patch of name/phone/email/photo, returns the refreshed `UserResource`, NEVER mutates roles/permissions/branch/status/linkage**; **`changePassword(ChangePasswordRequest)` → verify current, set new hashed password, `200`/`204`**; REPLACE raw `GET /api/v1/user` with `GET /api/v1/auth/me`.
- [x] [M][BE] `routes/api.php`: add the `auth` group inside `v1` — `POST auth/login` + `POST auth/forgot-password` (both `throttle:login`); behind `auth:sanctum`: `POST auth/logout`, `GET auth/me`, **`PATCH auth/profile`**, **`POST auth/password`**.
- [x] [M][BE] `app/Providers/AppServiceProvider.php::boot()`: register `RateLimiter::for('login', fn($r) => Limit::perMinute(5)->by($r->input('login').'|'.$r->ip()))`.
- [x] [M][BE] `app/Providers/AppServiceProvider.php::boot()`: register super-user bypass `Gate::before(fn ($u) => $u->hasRole('super-user') ? true : null)`.
- [x] [M][BE] `bootstrap/app.php`: confirm the `api` middleware group is applied to `routes/api.php`; register any `role:`/`permission:` middleware aliases here (Laravel 12 has NO `Kernel.php`).
- [x] [M][BE] Lang: add `lang/ar/validation.php`, `lang/ar/auth.php`, `lang/ar/passwords.php` (Arabic login/reset/profile/password messages).
- [ ] [M][BE] OpenAPI 3 spec scaffold for `/api/v1`: stand up a hand-authored `openapi.yaml` (or wire a Laravel generator such as `dedoc/scramble`) covering the auth surface (`/auth/login`, `/auth/forgot-password`, `/auth/logout`, `/auth/me`, `/auth/profile`, `/auth/password`) with the `{ data, links, meta }`/`{ data }` envelopes, camelCase schemas, and 401/403/404/422 responses; add a CI step that VALIDATES the spec; optionally generate/validate FE TS types from it. Keep it in sync per entity thereafter (see the per-entity OpenAPI step).
- [x] [L][BE] Tests: `tests/Feature/Auth/LoginTest.php` (happy, invalid email, wrong password → 401/422), `LogoutTest.php` (401 w/o token; token revoked after), `MeTest.php` (returns user + roles + unioned permissions; 401 unauth; asserts only known role names), **`ProfileTest.php` (PATCH /auth/profile persists name/phone/email/photo owner-only; rejects a roles/permissions/status payload; cannot edit another user → 403)**, **`ChangePasswordTest.php` (POST /auth/password requires the correct current password → 422 on mismatch; new password takes effect; owner-only)**; use `RefreshDatabase`.

**Acceptance**
- [x] [S][BE] Acceptance: `POST /api/v1/auth/login` (seeded supervisor) → `200 { data: { token, expiresAt, user: { roles:["supervisor"], permissions:[…] } } }` in camelCase.
- [x] [S][BE] Acceptance: `GET /api/v1/auth/me` with that bearer → same user; `401` without it.
- [x] [S][BE] Acceptance: `POST /api/v1/auth/logout` revokes the token; reusing it on `/me` → `401`.
- [x] [S][BE] Acceptance: `PATCH /api/v1/auth/profile` updates name/phone/email/photo for the owner only and returns the refreshed user; a payload attempting to set roles/permissions/branch/status/linkage is ignored/rejected; editing another user → `403`.
- [x] [S][BE] Acceptance: `POST /api/v1/auth/password` rejects a wrong `currentPassword` with `422` and succeeds with the correct one; only the owner can change their password.
- [x] [S][BE+FE] Acceptance: a two-role user returns `roles:["teacher","supervisor"]` + unioned `permissions[]`; a test confirms no role name outside the 6 known literals is ever emitted.
- [x] [S][BE] Acceptance: the seeded super-user passes an arbitrary `$this->authorize('anything')` via `Gate::before`; only ONE super-user row exists after `migrate:fresh --seed`.
- [x] [S][BE] Acceptance: `php artisan migrate:fresh --seed` is idempotent; `php artisan test` + `pint --test` green.

---

## Phase 2 — Frontend auth integration (token, Bearer, 401 carve-out, login/logout/me/profile)

> Goal: real login/logout/session persistence + the self-service profile page against Phase 1, while the rest of the app stays on mock data. (The profile page is safe here because Phase 1 already shipped `PATCH /auth/profile` + `POST /auth/password`.)

**Frontend (`bilal/`)**
- [x] [M][FE] `src/store/use-app-store.ts`: add `token: string|null`, `isAuthenticated: boolean`, `isBootstrapping: boolean`; actions `setSession({ user, token })` + `clearAuth()`; NO `setSessionRole` (union, not switching).
- [x] [M][FE] `src/store/use-app-store.ts`: persist `token` to `localStorage[env.authTokenKey]` on set; restore on init.
- [x] [L][FE] `src/store/use-app-store.ts`: extend `AppUser` with `roles: UserRole[]` (full held set) + `permissions: string[]` (union from `/me`); keep a derived single `role` (primary by precedence `super-user > supervisor > teacher > employee > parent`) for legacy call sites; `roles?` optional (fall back to `[role]`).
- [x] [M][FE] `src/store/use-app-store.ts`: in mock mode, demo `loginAs()` buttons set a sentinel token (`'mock-session'`) via `setSession` so `isAuthenticated` is true; keep `DEMO_USERS` behind the mock flag only (must not satisfy real login).
- [x] [L][BE+FE] `src/lib/auth/auth-service.ts` (new): `login(login, password)`, `logout()`, `me()`, `forgotPassword(email)`, `updateProfile(fields)` (`PATCH /auth/profile`), `changePassword(currentPassword, newPassword)` (`POST /auth/password`) against `/auth/*` via `httpClient`; returns unwrapped payloads; separate from `createResourceService`; ALWAYS live (ignores `useMockData`/`liveResources`).
- [x] [M][FE] `src/lib/api/http-client.ts`: inject `Authorization: Bearer <token>` (from store/`localStorage`) when present, merged over existing headers; NO `X-Session-Role` header.
- [x] [M][FE] `src/lib/api/http-client.ts`: add `getList<T>()` returning `{ items: T[]; meta }` (no list-shape unwrap — pass `data`+`meta` through); `get/post/put/patch` unwrap `payload.data`; keep a raw escape hatch for `/health`.
- [x] [L][FE] `src/lib/api/http-client.ts`: implement the 401 carve-out — call `clearAuth()` + redirect to `/login` ONLY when (a) a token was attached AND (b) path is NOT `/auth/login` or `/auth/forgot-password`; for those, let 401/422 propagate; always `throw new ApiError(...)`; redirect via a registered handler; keep `ApiError`/`buildUrl`/version prefixing.
- [x] [L][BE+FE] `src/app/(auth)/login/page.tsx`: replace the 800ms simulation + `DEMO_USERS.supervisor` with `authService.login()`; on success `setSession({ user, token })` from `/me`'s `roles[]`/`permissions[]` (NO role-selection step), then `router.push(homeForRole(roles))`; map 401→invalid-credentials, 422→field errors, network→generic via i18n.
- [x] [M][FE] `src/components/layout/topbar.tsx`: replace hardcoded `Link to="/login"` with `onClick` → `authService.logout()` → `clearAuth()` → redirect; on network failure still `clearAuth()`; add loading state; keep mobile icon-only styling; NO role switcher.
- [x] [M][BE+FE] `src/app/(auth)/forgot-password/page.tsx`: call `authService.forgotPassword(email)`; generic success regardless; error state.
- [x] [M][FE] `src/components/layout/route-guard.tsx`: order — (a) if `isBootstrapping` render nothing/spinner (do NOT redirect during `/me`); (b) auth check `!isAuthenticated` → `/login`; (c) existing `isHiddenRoute()` + `requiredRolesForPath()` check; mock sentinel token keeps demo buttons working.
- [x] [L][BE+FE] `src/lib/auth/use-auth.ts` (new) + `src/app/(dashboard)/layout.tsx`: on mount skip when `env.useMockData`; otherwise if a token exists call `authService.me()` to repopulate `currentUser` (full `roles[]` + unioned `permissions[]` + scope fields); on 401 `clearAuth()`; gate render on `isBootstrapping`; optional cross-tab `storage` listener for logout sync.
- [x] [L][FE] i18n: add `i18n/messages/ar/auth.json` (invalid-credentials, validation, network, session-expired, login-required, logout-failed — NO `role-select` key) and `i18n/messages/ar/profile.json` (own-data fields + change-password copy); register `auth` and `profile` in `i18n/request.ts` `DOMAIN_NAMESPACES` (fill `ar` only, defer `en`).
- [x] [L][BE+FE] `src/app/(dashboard)/profile/page.tsx` (new): self-service profile for every authenticated user — own-data form (name, **single phone**, email, photo) → `authService.updateProfile()`, and change-password form (current + new + confirm) → `authService.changePassword()`; NO role/branch/status/permissions/linkage controls; on success refresh `currentUser` via `setSession`/`me()`; loading/empty/error states; Arabic `profile` namespace.

**Acceptance**
- [x] [S][FE] Acceptance: login with seeded credentials stores a token, lands on `homeForRole(roles)`, and survives refresh (restored via `/me`).
- [x] [S][BE+FE] Acceptance: a multi-role user (teacher + supervisor) lands in a single session with combined nav/permissions — no role-selection prompt, no switcher anywhere.
- [x] [S][BE+FE] Acceptance: wrong credentials show the Arabic invalid-credentials message inline; the 401 interceptor does NOT redirect or loop on `/login`.
- [x] [S][BE+FE] Acceptance: logout revokes server-side + clears local state; the back button cannot re-enter the dashboard.
- [x] [S][BE+FE] Acceptance: a forced 401 on an authenticated request (revoke token in DB, then navigate) redirects to `/login` and clears state.
- [x] [S][BE+FE] Acceptance: `/profile` loads for any authenticated user; name/phone/email/photo persist via `PATCH /auth/profile` (live endpoint built in Phase 1); password change requires the current password (`POST /auth/password`); neither path can alter roles/permissions.
- [x] [S][BE+FE] Acceptance: mock mode still serves all dashboards (flag `true`, demo buttons work via sentinel token).

---

## Phase 3 — Data-layer contract upgrade (envelope + server pagination + debounce + abort + server sort)

> Goal: FE data layer speaks `{ data, links, meta }` + server pagination/sort, with mock mode honoring the same `ListResult` so UI paths are identical. Flag stays `true`, `liveResources` empty.

**Frontend (`bilal/`)**
- [x] [M][FE] `src/lib/services/base/resource-service.ts`: add `ListResult<T> = { items: T[]; total: number; page: number; perPage: number; lastPage: number; hasMore: boolean }`.
- [x] [M][FE] `src/lib/services/base/resource-service.ts`: extend `ListQuery` with `page?`, `perPage?`, `sort?`, `dir?: 'asc'|'desc'` (keep `search?`, `signal?`).
- [ ] [M][FE] `src/lib/services/base/resource-service.ts`: change `list()` → `Promise<ListResult<T>>`; add `listAll(query?): Promise<T[]>` for aggregate/report consumers (§6).
- [ ] [L][FE] `src/lib/services/base/resource-service.ts`: `createMockService.list` applies `search` → optional `sort` (`localeCompare('ar')`) → slice `(page-1)*perPage … page*perPage`; computes `total`/`lastPage`/`hasMore`; honors `signal`; keeps `MOCK_LATENCY_MS`; `listAll` returns the full filtered collection in input order (no sort).
- [ ] [L][FE] `src/lib/services/base/resource-service.ts`: `createHttpService.list` sends snake_case params `{ search, page, per_page, sort, dir }` + `signal` to `httpClient.getList`; maps `{ items, meta }` → `ListResult` (derive `hasMore = meta.current_page < meta.last_page`); `listAll` calls the report path or `?per_page=all`; `getById/create/update` rely on central `.data` unwrap.
- [x] [M][BE+FE] `src/lib/services/base/resource-service.ts`: change `removeMany` → `POST <resourcePath>/bulk-delete { ids }` (replaces N parallel DELETEs).
- [x] [M][BE+FE] `src/lib/services/base/resource-service.ts`: per-resource live override — `createResourceService` returns http when `!env.useMockData || env.liveResources.includes(config.resourcePath)`.
- [ ] [M][FE] `src/features/shared/hooks/use-resource-list.ts`: add `page`/`setPage` (default 1), `pageSize` (from `app-config.PAGE_SIZE`), `total`, `lastPage`, `hasMore`, `sort`/`dir` state.
- [ ] [M][FE] `src/features/shared/hooks/use-resource-list.ts`: debounce `search` by `app-config.SEARCH_DEBOUNCE_MS`; thread `AbortSignal` into `service.list({ search, page, perPage, sort, dir, signal })`; add `search`/`page`/`sort`/`dir` to effect deps; abort the prior request on change.
- [ ] [M][FE] `src/features/shared/hooks/use-resource-list.ts`: reset `page → 1` whenever `search`/`sort`/`dir` change; in `.catch` ignore `AbortError`, let the global handler own `ApiError.status === 401`, otherwise set error state; REMOVE the client-side `filtered` `useMemo`.
- [ ] [M][FE] `src/features/shared/hooks/use-resource-list.ts`: new return `{ items, total, page, setPage, pageSize, lastPage, hasMore, sort, dir, setSort, loading, error, reload, search, setSearch, create, update, remove }`.
- [ ] [L][FE] `src/components/shared/data-table.tsx`: make fully controlled — props `data: T[]`, `total`, `currentPage`, `onPageChange`, `pageSize`, `sort`, `dir`, `onSortChange`; remove internal `page` state, internal slicing, and the `localeCompare('ar')` sort; header sort buttons call `onSortChange`; keep zebra/RTL/horizontal-scroll/a11y (`aria-sort`); select-all = current page by default + optional "select all N matching filter" affordance.
- [ ] [M][FE] `src/components/shared/pagination.tsx`: receive `totalPages = ceil(total / pageSize)` + `currentPage` + `onPageChange` (API otherwise unchanged).
- [ ] [M][FE] `src/components/shared/error-state.tsx`: accept optional `status`; show "session expired / login required" for 401 and HIDE the retry button for 401.
- [x] [M][BE+FE] `src/lib/services/index.ts`: re-export updated `ResourceService<T>` / `ListResult<T>` types (no logic change; concrete services inherit the factory unchanged).
- [ ] [L][FE] Migrate the blast radius — grep `service.list(` and `\.filtered` across `src/` (~30+ files) and switch each from old `filtered`/`list()` shape to `items` + lifted pagination props: includes `src/features/students/hooks/use-students.ts`, `use-circles.ts`, and the `transport`/`levels`/`parents`/`employees` pages + any `features/*/hooks/*` calling `service.list()`.
- [ ] [L][BE+FE] Route aggregate/report consumers to `listAll()` (NOT paginated `list()` — see §6). Switch each of these specific files/areas: `src/features/memorization/hooks/use-monthly-report.ts`; `src/features/students/hooks/use-students-report.ts`; `src/features/employees/hooks/use-teacher-work-report.ts`; `src/app/(dashboard)/education/reports/*` (daily, circles, students, attendance); `/education/exams/level-report`; `/parent/reports`; and finance reports (when re-exposed).

**Acceptance**
- [ ] [S][FE] Acceptance: with `useMockData=true`, every list screen paginates via mock `ListResult` (e.g. 20 seed rows, `perPage=10` → 2 pages); search is debounced, resets to page 1, and cancels in-flight requests; sort toggles re-query (mock sorts the full set, not one page); loading/empty/error intact.
- [ ] [S][FE] Acceptance: every report/aggregate page listed above derives its totals from `listAll()` (full dataset), NOT a paginated `list()` page-1 slice.
- [ ] [S][FE] Acceptance: `npx tsc --noEmit` 0, `eslint` clean, `vitest` green, `next build` exit 0 with no `MISSING_MESSAGE`.

---

## Phase 4 — RBAC enforcement wiring (FE consumes `/me` roles; BE policies + scoping)

> Goal: navigation/guards driven by the real `/me` roles/permissions; backend enforces authorization on every endpoint (FE hiding is UX only). The corrected §5.4 route/section matrix is honored exactly.

**Frontend (`bilal/`)**
- [x] [M][FE] `src/lib/constants/navigation.ts`: feed the user's full `roles[]` into `filterNavigation()` and `requiredRolesForPath()` (no rule changes, real data instead of `DEMO_USERS`); resolution is any-of (a route shows if any held role grants it → union nav).
- [x] [M][FE] `src/lib/constants/navigation.ts`: super-user short-circuits to the full nav and bypasses the `hidden-features` mask (M3).
- [x] [L][FE] `src/lib/constants/navigation.ts`: honor the corrected §5.4 matrix — keep `/admin/students/cards` under `ROUTE_ROLE_OVERRIDES` (supervisor/`ADMIN`); keep the education admin-only items (`/admin/levels`, `/admin/subjects*`, `/education/exams/archive`, `/education/teacher-log`) gated to `ADMIN`; `/education/reports` stays `TEACHING`.
- [x] [L][FE] Per-action button gating: read the unioned `permissions[]` from `/me` and HIDE (never disable) Add/Edit/Delete buttons, row actions, bulk actions, AND custom-action controls when the matching `<domain>.<action>` permission is absent (e.g. hide "publish to parent" without `student_exams.publish`, hide "transfer"/"archive" without `students.transfer`/`students.archive`, hide "reset password" without `parents.resetPassword`, hide create without `<domain>.create`).
- [x] [L][FE] `src/components/layout/route-guard.tsx`: block whole pages whose required role/permission the user lacks; when `roles[]` includes `super-user`, bypass BOTH the role filter and the `hidden-features` mask; `/education/statistics` stays net-HIDDEN for all non-super-user roles; `/messages/notices` is treated as authenticated-only (do not mirror the FE TEACHING label as a guard).
- [x] [M][FE] `src/components/layout/sidebar.tsx`: compute `useMemo(filterNavigation(currentUser.roles), [currentUser.roles])` once (any-of union); super-user renders the full tree incl. hidden modules; no in-session switch handling.
- [x] [L][BE+FE] `src/lib/config/hidden-features.ts`: keep as the global mask, applied for all roles EXCEPT `super-user` in `filterNavigation` + `RouteGuard`, regardless of backend roles; `/education/statistics`, `/admin/audit`, `/finance` remain net-hidden for everyone but super-user (`/content` is NO LONGER hidden — it is a managed CMS shown to supervisor; see the Phase 5 Content CMS re-expose task).
- [x] [L][FE] `src/lib/auth/session.ts`: fix `ROLE_HOME.student` (§5.6 — point at a future student landing or short-circuit to a "no portal yet" screen / `/login`, never `/dashboard`); add `ROLE_HOME['super-user']='/dashboard'`; ensure `homeForRole(roles)` resolves the primary by precedence `super-user > supervisor > teacher > employee > parent`.
- [x] [M][FE] `src/lib/types/user.ts`: ensure `UserRole = 'super-user' | 'supervisor' | 'teacher' | 'parent' | 'employee' | 'student'`; `src/lib/auth/roles.ts`/`ROLE_LABELS` add a `super-user` label (excluded from assignable pickers).

**Backend (`bilal-backend/`)**
- [x] [L][BE] `app/Policies/*` (registered in `AppServiceProvider`): per model gate `viewAny/view/create/update/delete` via `$this->authorize(...)`; each maps to a `<domain>.<action>` permission; **`bulk-delete` authorizes against `<domain>.delete`** (no separate permission); **custom actions map to their own `<domain>.<action>` permission** (e.g. `students.transfer`, `students.archive`, `students.pending.approve`, `parents.resetPassword`, `student_exams.publish`); 403 is the real boundary; super-user passes via `Gate::before`.
- [ ] [M][BE] Data scoping via query scopes keyed to the MOST PERMISSIVE held role (M1, no session role): `supervisor`/`super-user` → unrestricted; else `teacher` → own `circleIds`' students; else `parent` → own `childIds`/`parentId`; never trust frontend filtering.
- [ ] [M][BE] Optional `role:`/`permission:` route middleware only for coarse gates; prefer policies.

**Acceptance**
- [x] [S][BE+FE] Acceptance: a teacher sees TEACHING + common nav; a supervisor sees ADMIN; a parent sees the parent portal; teacher + supervisor sees the UNION of both — all from `/me` `roles[]`, no switcher.
- [x] [S][BE+FE] Acceptance: the corrected §5.4 matrix is honored — `/admin/students/cards` is supervisor-only, `/education/statistics` is hidden for all non-super-user roles, the education admin-only items are ADMIN-only, and `/messages/notices` is reachable by any authenticated user.
- [ ] [S][BE+FE] Acceptance: a teacher (without supervisor) calling a supervisor-only endpoint directly → 403 despite FE hiding.
- [ ] [S][BE] Acceptance: a multi-role teacher + supervisor user gets unrestricted student data in a single session (most-permissive scope = supervisor), proving union scoping.
- [x] [S][BE+FE] Acceptance: a non-super-user lacking `<domain>.create` (or a custom permission like `student_exams.publish`) sees NO matching control and is 403'd if it forges the request; the super-user sees and can do everything, incl. hidden modules.
- [x] [S][BE+FE] Acceptance: `/me` never returns a role outside the 6 known literals (M2 test green).

---

## Phase 4.5 — Vertical-slice proof spike (first entity end-to-end, FE wired live)

> Goal: before scaling to all entities, prove the WHOLE contract on ONE real entity — **nationalities** (a
> dependency-free lookup table). Build the full backend per-entity stack AND wire the frontend live for it via
> `NEXT_PUBLIC_LIVE_RESOURCES=/nationalities`, validating every gate end-to-end with zero other entities touched.

**Backend (`bilal-backend/`)**
- [x] [M][BE] Nationalities: migration `create_nationalities_table` — `name` (string), `code` (string, nullable), indexes on filtered/sorted columns, reversible `down()`.
- [x] [S][BE] Nationalities model: `$fillable`, `$casts`, ordering scope; tight mass-assignment.
- [x] [S][BE] Factory + seeder mirroring `lib/dummy/lookups.ts` nationalities so mock and live datasets match.
- [x] [M][BE] `StoreNationalityRequest`/`UpdateNationalityRequest`: validate camelCase payload, map camel→snake IN THE ACTION (not `prepareForValidation`), Arabic messages, camelCase 422 keys.
- [x] [S][BE] `NationalityResource`: camelCase output exactly matching the FE TS type.
- [x] [M][BE] `NationalityPolicy`: `viewAny/view/create/update/delete` → `nationalities.<action>`; `bulk-delete` reuses `nationalities.delete`; super-user passes via `Gate::before`; register in `AppServiceProvider`.
- [x] [S][BE] Add `nationalities.{viewAny,view,create,update,delete}` to `PermissionSeeder` + attach to roles; `forgetCachedPermissions()`.
- [x] [M][BE] Thin `NationalityController` (`index/show/store/update/destroy` + `bulkDelete`) returning Resources; `index` paginated via `NationalityResource::collection($paginator)`; honor `sort`/`per_page` over whitelisted columns.
- [x] [S][BE] Routes in the `v1` group incl. `POST /nationalities/bulk-delete { ids }`.
- [x] [S][BE] Cache the public/lookup read with an EXPLICIT namespaced key `lookups:nationalities` (`Cache::remember`), invalidated with `Cache::forget('lookups:nationalities')` on every write; send `Cache-Control`/`ETag`.
- [ ] [S][BE] Update the OpenAPI spec for `/nationalities` and validate in CI.
- [x] [L][BE] Feature tests: list (paginated `{data,links,meta}`) / show / create / update / delete / bulk-delete + 401/403/404/422; `RefreshDatabase`.

**Frontend (`bilal/`)**
- [x] [S][FE] Set `NEXT_PUBLIC_LIVE_RESOURCES=/nationalities` so `createResourceService` returns the http service for nationalities only (everything else stays mock).
- [x] [M][BE+FE] Wire the nationalities screen live: list renders from the API (`{data,links,meta}`), server pagination + server sort + debounced search work, fields arrive camelCase, create/update/delete round-trip through the form dialog, bulk-delete hits `POST /nationalities/bulk-delete`.
- [x] [S][FE] Confirm the 401 carve-out, per-action button HIDE (no `nationalities.create` → no Add button), and that the page falls back to mock when the path is removed from the allowlist.

**Acceptance**
- [x] [S][BE+FE] Acceptance: nationalities CRUD is live end-to-end; the `{ data, links, meta }` envelope, server pagination+sort, camelCase fields, 401 carve-out, per-action permission gating + FE button HIDE, the cutover flag, and caching all behave per contract; all DoD gates are green; the FE renders nationalities entirely from the live API; NO other entity is touched.

---
## Phase 5 — Entity-by-entity endpoint buildout (dependency-first), incremental cutover via `liveResources`

> Goal: ship backend endpoints one vertical slice at a time; promote each to live via `NEXT_PUBLIC_LIVE_RESOURCES` once green. No per-entity frontend change (the factory consumes them).

**Build order (do entities in this sequence)**
- [ ] [L][BE] 1. Reference/lookup tables (no scoping, FK targets): nationalities, emirates/regions, levels & sections, subjects, exam-types (`/exam-types`), section-subjects (`/section-subjects`), buses, drivers — paths `/admin/transport/*`, `/admin/levels`, `/admin/subjects`, `/exam-types`, `/section-subjects`. Archive-aware: sections of an archived level stay queryable, and a section's `levelId` is reassignable to another level.
- [ ] [L][BE] 2. Employees (`/admin/employees`): backend accepts `roles: string[]` constrained by `Rule::in(['supervisor','teacher','parent','employee'])` (the assignable auth literals — `super-user`/`student` rejected if submitted) PLUS the separate `Employee.job_role` data column (teacher/secretary/accountant/supervisor/coordinator/driver) — distinct from the Spatie auth `roles` (per ratified clarification #2: exactly 4 assignable auth roles; the employee-form picker offers only {supervisor, teacher, employee}); teachers are employees with auth role `teacher` (no separate Teachers table); job→permission bundles per §5.5 (deps: roles & permissions seeded).
- [ ] [L][BE] 3. Students (`/admin/students`, archive, transfer — gated by `students.archive`/`students.transfer`) + pending-students approval (`/admin/students/pending`): `POST /students/pending`, `PATCH /students/pending/{id}/approve` (gated by `students.pending.approve`; assign `levelId`+`sectionId`, optional `busId`; enforce `section.levelId === levelId` server-side; bus region-filtered), `DELETE /students/pending/{id}/reject` (gated by `students.pending.reject`, supervisor-only); approval also creates the Parent `User` with a server-generated hashed password returned once (P1), collecting/confirming a parent identifier (email or phone). Whitelisted server-side filter params (validated + whitelisted, never raw user input into queries): `level_id`, `section_id`, `gender`, `nationality_id`, `emirate_id`, `region_id`, `transport_type`, `is_orphan`, `has_id`. Archived students retain their cached `levelName`/`sectionName` snapshot (deps: levels, sections, nationalities, emirates, regions, buses).
- [ ] [M][BE] 4. Parents (`/admin/parents`) + parent↔student linkage + password reset (`PATCH /parents/{id}` resets, supervisor-only via `parents.resetPassword`, never parent-editable) (deps: students).
- [ ] [L][BE] 5. Education/teaching (session-scoped to teacher circles): memorization (`memorization.record`), attendance (`attendance.record`), plans, exams, sub-subject-grades (`grades.record`), teacher-logs (`/teacher-logs`, admin-only per §5.4), reports, mushaf/reading + the §6 aggregate report endpoints (enumerated in the Reports & aggregate endpoints subsection below); student-exam publish gated by `student_exams.publish`. Memorization data model: an achievement-type enum (memorization/حفظ vs revision/مراجعة) matching the FE type; `memorization.record` does NOT carry attendance status — present/absent/remote/excused is recorded only via the dedicated attendance endpoint (`attendance.record`) (deps: students, circles, levels, exam-types).
- [ ] [L][BE] 6. Parent portal (read-only, scoped): `/parent/children` (add sibling → creates pending student per §5/P1, collecting a parent identifier), `/parent/attendance`, `/parent/reports`, `/parent/exams` (only `publishToParent=true`), `/parent/notices` (source = the `messages` table scoped to notice-type rows for the parent's children — NOT a separate notices resource; cross-reference the `/messages/notices` authenticated-only rule in Cross-cutting), `/parent/reading` (deps: students, attendance, exams, messages).
- [ ] [L][BE] 7. Messages (ownership/admin gates on reply/delete). Profile endpoints (`GET /auth/me`, `PATCH /auth/profile`, `POST /auth/password`) were already built in **Phase 1** and wired on the FE in **Phase 2** — verify they remain owner-only and never touch roles/permissions/branch/status/linkage; user/employee role pickers exclude `super-user` (deps: employees, parents).
- [ ] [L][BE+FE] 8. Administration (supervisor-only, `/admin/administration/*` — visible per §5.4, NOT hidden): full CRUD endpoints for `/evaluations`, `/warnings`, `/certificates`, `/letters`, each running the per-entity checklist and gated by its own per-resource `<domain>.viewAny/view/create/update/delete` Policy + permissions (these are full CRUD RESOURCES, not single `administration.*` custom actions); promote each path via `NEXT_PUBLIC_LIVE_RESOURCES` (deps: employees).
- [ ] [L][BE+FE] 9. Dashboard statistics: `GET /dashboard/statistics` returning role-scoped aggregate counts with gender split (students male/female, attendance, …) consumed by the `/dashboard` widgets (the ALL_STAFF landing) — a dedicated server-computed aggregate gated like a report endpoint (most-permissive-role scope); or document that the dashboard derives these via `listAll()` aggregates per §6. Without it, the default landing of most roles has no live data source after cutover (deps: students, attendance, exams).
- [ ] [L][BE] 10. Content CMS — landing-sections / homepage CMS (IN scope, admin/super-admin managed): build the landing/homepage sections as a real DB-backed resource (hero slider, **OWNER section — the homepage owner block is now DB-backed, NOT static**, "why us", FAQ, CTA, footer text) running the full per-entity checklist; content permissions `landing.view`/`landing.update` granted to `supervisor` (super-user passes via `Gate::before`); PUBLIC unauthenticated `GET` read endpoint feeds the public landing page sections; write/manage endpoints gated by `landing.update`.
- [ ] [L][BE] 11. Content CMS — news & announcements (الأخبار, `/content/news`): full CRUD resource running the per-entity checklist; content permissions `news.viewAny/view/create/update/delete` granted to `supervisor` (super-user via `Gate::before`); PUBLIC unauthenticated `GET` list + show endpoints for the public news pages; write/manage endpoints content-permission gated.
- [ ] [L][BE] 12. Content CMS — library (المكتبة, `/content/library`) + photo album (الألبوم, `/content/album`): full CRUD resources running the per-entity checklist; content permissions `library.viewAny/view/create/update/delete` and `album.viewAny/view/create/update/delete` granted to `supervisor` (super-user via `Gate::before`); PUBLIC unauthenticated `GET` read endpoints for the public library + photo-album pages; write/manage endpoints content-permission gated.
- [ ] [L][BE] 13. Content CMS — site settings (contact info, social links, footer, basic SEO/site metadata): singleton resource running the per-entity checklist; content permissions `site_settings.view`/`site_settings.update` granted to `supervisor` (super-user via `Gate::before`); PUBLIC unauthenticated `GET` read endpoint exposes the public site settings; update gated by `site_settings.update`.
- [ ] [L][BE+FE] 14. Content CMS frontend re-expose & wiring: remove the `/content` prefix from `HIDDEN_ROUTE_PREFIXES` in `src/lib/config/hidden-features.ts` so the Content module (`/content/website`, `/content/news`, `/content/library`) shows for `supervisor` (super-user already bypasses); re-show any dashboard quick-access / nav entry for those routes; wire the PUBLIC landing/news/album/library pages — and the homepage owner section — to consume the LIVE API public read endpoints via the existing `newsService`/`albumService` (mock) plus new landing/library/site-settings services, so admin edits render on the public pages immediately.
- [ ] [L][BE] 15. Roles & Permissions management (super-user): the runtime role↔permission matrix surface — endpoints `GET /roles`, `GET /permissions`, `PUT /roles/{role}/permissions`, all gated by a new `roles.managePermissions` permission held ONLY by the super-user; the seeder provides the default grants and the page edits them at runtime; the permission **catalog** (the universe of `<domain>.<action>` strings) stays code-defined/seeded — only the role→permission **grants** are editable; the `super-user` role is never editable/assignable and `student` is reserved (editable roles = `supervisor`/`teacher`/`employee`/`parent`). See the dedicated "Roles & Permissions management (super-user) — R1" subsection below.
- [ ] [S][BE] Scope note — hidden modules: Finance (`/finance`) and Audit (`/admin/audit`) (and Statistics `/education/statistics`) are OUT OF SCOPE for backend buildout and remain mock-only; they get NO live endpoints. The super-user still sees them via the hidden-mask bypass, but they render mock/empty data (reconciled with the Phase 6 super-user smoke check). **Content (`/content`) is IN scope** — an admin (supervisor) + super-admin (super-user) managed CMS whose public pages (landing/news/library/album + site settings) render LIVE from the API (build items 10–14 above); it is NOT mock-only and is REMOVED from the hidden mask. If Finance/Audit/Statistics must go live later, add explicit Phase 5 build items + per-resource CRUD permissions.

**Reports & aggregate endpoints (§6 — build each alongside its owning entity in the order above)**
> Each backend report endpoint is classified as either a **dedicated server-computed aggregate** (totals computed in SQL/the Action) OR a **`listAll()` consumer** (the FE aggregates the full unsliced collection). A report NEVER derives totals from a paginated `list()` page (see Cross-cutting "Wire & data discipline").
- [ ] [M][BE] `GET /education/reports/daily` — dedicated server-computed aggregate (daily attendance/memorization totals, gender-split); FE consumer `use-monthly-report` / `education/reports/daily`.
- [ ] [M][BE] `GET /education/reports/circles` — dedicated server-computed aggregate (per-circle rollups); FE consumer `education/reports/circles`.
- [ ] [M][BE+FE] `GET /education/reports/students` — `listAll()` consumer over the scoped students collection; FE consumer `use-students-report` / `education/reports/students`.
- [ ] [M][BE] `GET /education/reports/attendance` — dedicated server-computed aggregate (attendance rollups); FE consumer `education/reports/attendance`.
- [ ] [M][BE] `GET /education/exams/level-report` — dedicated server-computed aggregate (per-level exam results); FE consumer `/education/exams/level-report`.
- [ ] [M][FE] `GET /admin/transport/report` — `listAll()` consumer over the buses/students transport collection.
- [ ] [M][BE] `GET /admin/employees/work-report` — dedicated server-computed aggregate (teacher work totals); FE consumer `use-teacher-work-report`.
- [ ] [M][BE+FE] `GET /parent/reports` — `listAll()` consumer scoped to the parent's children.
- [ ] [M][BE] Each report endpoint is gated by the most-permissive-role scope, returns camelCase, and cross-references Cross-cutting "Wire & data discipline" (reports never derive totals from a paginated `list()`).

**Frontend supporting changes**
- [ ] [L][FE] `src/features/registration/components/registration-form.tsx`: switch `pendingStudentsService.create()` → real `POST /students/pending` (or `authService.register`); remove the `import { generateAutoPassword } from '@/lib/types/student'` usage and stop calling it; send no password; `POST /students/pending` returns nothing secret.
- [ ] [M][FE] `src/features/employees/components/employee-form-dialog.tsx`: role picker lists only `{supervisor, teacher, employee}` (the realistic employee-assignable set) — excludes `super-user` and `student` (backend rejects a forged value).

**Roles & Permissions management (super-user) — R1 (build item 15)**
> A super-user-only admin surface that makes RBAC grants editable at runtime. The permission **catalog** stays code-defined/seeded; only the role→permission **grants** are editable. The `super-user` role is never editable/assignable; `student` is reserved. Editable roles = `supervisor`, `teacher`, `employee`, `parent`.

_Backend (`bilal-backend/`)_
- [ ] [M][BE] `database/seeders/PermissionSeeder.php`: add the `roles.managePermissions` permission to the catalog and grant it to NO editable role — only the seed-only `super-user` holds it (via `Gate::before`); call `forgetCachedPermissions()` after seeding.
- [ ] [L][BE] `app/Http/Controllers/Api/V1/RoleController.php` + `PermissionController.php` (or one `RolePermissionController.php`): thin actions — `index` → `GET /api/v1/roles` (each role + its `permissions: string[]`); `GET /api/v1/permissions` → the full catalog grouped by domain; `PUT /api/v1/roles/{role}/permissions` → sync the role's permissions; return Resources (camelCase); never raw models; all gated by `roles.managePermissions`.
- [ ] [L][BE] `app/Http/Requests/Api/V1/Role/SyncRolePermissionsRequest.php` (new): validate `permissions: string[]` ⊆ the seeded catalog; `authorize()` via the `roles.managePermissions` gate; REJECT the `{role}` param being `super-user` (and reject any attempt to edit/assign `super-user`); allow only `Rule::in(['supervisor','teacher','employee','parent'])` (`student` reserved/not editable); Arabic messages.
- [ ] [M][BE] `app/Policies/RolePolicy.php` (or a dedicated gate): map `managePermissions` to the `roles.managePermissions` permission; register in `AppServiceProvider`; super-user passes via `Gate::before`.
- [ ] [M][BE] Sync Action: persist the new grants in a DB transaction, then call `app(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions()` so the change takes effect immediately.
- [ ] [M][BE] `routes/api.php`: behind `auth:sanctum` add `GET roles`, `GET permissions`, `PUT roles/{role}/permissions`, all gated by `roles.managePermissions`.
- [ ] [L][BE] Tests: `tests/Feature/Roles/RolePermissionTest.php` — super-user GETs `/roles` + `/permissions` and PUTs a role's permissions (grant takes effect after `forgetCachedPermissions`); a non-super-user (incl. supervisor) is `403` on all three; editing/assigning `super-user` is rejected (422/403); `student` is not editable; `RefreshDatabase`.

_Frontend (`bilal/`)_
- [ ] [L][FE] `src/lib/services/rbac-service.ts` (new — or `roles-service.ts` + `permissions-service.ts`): `getRoles()` (`GET /roles`), `getPermissions()` (`GET /permissions`, grouped by domain), `syncRolePermissions(role, permissions)` (`PUT /roles/{role}/permissions`) via `httpClient`; ALWAYS live; export from `lib/services/index.ts`.
- [ ] [L][BE+FE] `src/app/(dashboard)/admin/roles/page.tsx` (new): super-user-only roles × permissions matrix (rows = editable roles `supervisor`/`teacher`/`employee`/`parent`; columns = the catalog grouped by domain) with per-cell toggles → `rbacService.syncRolePermissions()` on save; the `super-user` row and `student` are never shown/editable; loading/empty/error states; Arabic namespace.
- [ ] [M][FE] `src/lib/constants/navigation.ts`: add the `/admin/roles` route with a nav item visible ONLY to `super-user` (never to any assignable role).
- [ ] [M][FE] `src/components/layout/route-guard.tsx`: guard `/admin/roles` as super-user-only — block every non-super-user from direct-URL access.
- [ ] [M][FE] After a successful save, re-fetch `/auth/me` (via `authService.me()` → `setSession`) so the current session picks up the changed grants; note other users see changes on their next `/me`/login.
- [ ] [M][FE] i18n: add `i18n/messages/ar/roles.json` (matrix labels, per-domain group headings, save/confirm copy); register `roles` in `i18n/request.ts` `DOMAIN_NAMESPACES` (fill `ar` only, defer `en`).

**Acceptance (Roles & Permissions management)**
- [ ] [S][BE+FE] Acceptance: the super-user GETs `/roles` (each with its permissions) + `/permissions` (catalog grouped by domain) and PUTs a role's permissions; the change takes effect immediately (cache forgotten) and is reflected on the next `/me`.
- [ ] [S][BE+FE] Acceptance: a non-super-user (incl. supervisor) is `403` on all three endpoints; any attempt to edit or assign the `super-user` role is rejected; `student` is not editable.
- [ ] [S][BE+FE] Acceptance: `/admin/roles` is reachable ONLY by the super-user (hidden from nav for everyone else and `403`/redirected by RouteGuard); the matrix saves and re-fetches `/me`.

**Composite / screen (bootstrap) endpoints — R2 (Performance §14 — build each alongside its owning entity)**
> One request per screen: when a page needs several models, return them in ONE endpoint instead of N round-trips. Prefer one well-shaped response (or `?include=`) over the FE firing parallel calls; the FE calls one aggregate endpoint per screen when live (mock mode returns the same shaped response).
- [ ] [M][FE] `GET /students/form-data` (composite student-form lookups): nationalities + emirates + regions + levels + sections + buses in ONE response — replaces the ~7 parallel service calls on the students page + `student-form-dialog`.
- [ ] [M][BE] `GET /students/pending/form-data` (pending-approval sets): levels + sections + buses (region-filtered) in ONE response.
- [ ] [M][BE] `GET /memorization/session?circleId=&date=` (memorization session bundle): circle(s) + students + sessions + group achievements for a circle+date in ONE response.
- [ ] [M][BE] `GET /buses/form-data` (buses-form lookups): emirates + regions + drivers (`role=driver`) + supervisors (`role=supervisor`) in ONE response.
- [ ] [M][BE] `GET /levels/with-sections` (levels & sections bundle): levels + sections + teachers (for `primaryTeacher`) in ONE response.
- [ ] [M][BE] `GET /dashboard/statistics` (dashboard statistics): role-scoped aggregate counts with gender split in ONE endpoint (same endpoint as build item 9 — one composite aggregate, not N list calls).
- [ ] [M][FE] FE wiring: switch each multi-service screen to call its single aggregate endpoint when live — students page + `student-form-dialog`, pending-approval, memorization session, buses form, levels & sections, and the dashboard — instead of firing parallel `service.list()` calls.

**Caching — public CMS + lookups — R2 (Performance §14)**
- [ ] [S][BE] Choose a config-driven cache store via `config/cache.php` + `.env`/`.env.example` (`CACHE_STORE` = `database`, `QUEUE_CONNECTION` = `database` — **no Redis on this server**) — never hardcode.
- [ ] [L][BE] Cache rarely-changing data via `Cache::remember` with EXPLICIT namespaced keys (e.g. `content:home`, `lookups:nationalities`) — the `database` cache driver does NOT support tags: public/CMS content (landing/homepage incl. the owner block, news, album, library, site-settings) + lookup tables (nationalities, emirates, regions).
- [ ] [M][BE] Bust the cache on write: every CMS/lookup create/update/delete calls `Cache::forget(key)` for its explicit namespaced key(s) (in the owning Action/observer; keep a small key-registry to clear a group) so edits show immediately.
- [ ] [M][FE] Public read endpoints send `Cache-Control`/`ETag` headers so the FE (Next.js ISR/route cache) can cache them.
- [ ] [M][BE] NEVER cache per-user/role-scoped or frequently-changing lists (students/attendance/memorization/etc.).
- [ ] [M][FE] FE: enable Next.js ISR / route caching on the public landing/news/library/album/site-settings pages (revalidate window) consuming the cache-headed public read endpoints.

**Per-entity checklist (repeat for EACH entity in the build order)**
> **Per-entity custom-action matrix** — the Policy / feature-test / acceptance lines below reference THIS instead of saying "every custom action" generically; each entity's custom actions are:
> - `students` → `transfer`, `archive`, `pending.approve`, `pending.reject`
> - `employees` → `assignRoles`
> - `parents` → `resetPassword`
> - `subjects` → `assignTeachers`
> - `student_exams` → `publish`
> - `memorization` → `record`
> - `attendance` → `record`
> - `sub-subject-grades` (grades) → `record`
> - `administration` → `evaluations`/`warnings`/`certificates`/`letters` are full CRUD RESOURCES (gated as CRUD, NOT as custom actions)
- [ ] [M][BE] Migration created/verified (if missing) with reversible `down()`.
- [ ] [M][FE] Performance — indexes & pagination: index every filtered/sorted/FK column for the entity (per its whitelisted filter/sort params); paginate the list (default 25, cap 100) — only the deliberate `listAll()`/report path is unbounded; wrap multi-step writes in a DB transaction; queue slow side-effects.
- [ ] [M][BE] Model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [M][BE] Factory + seeder mirroring `lib/dummy/*` for that entity.
- [ ] [M][BE] `FormRequest`(s): validate the camelCase payload as received; map camel→snake IN THE ACTION (not `prepareForValidation`); constrain any submitted auth role with `Rule::in(['supervisor','teacher','parent','employee'])` (reject `super-user`/`student`); Arabic messages.
- [ ] [M][BE] `Resource`: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Paginated list via `XResource::collection($paginator)` (default `{ data, links, meta }` — no custom collection); honor `sort`/`per_page` over whitelisted columns only.
- [ ] [M][BE] Policy: per-action `<domain>.<action>` permissions for `viewAny/view/create/update/delete` + every custom action (per the custom-action matrix above); `bulk-delete` reuses `<domain>.delete`; registered; super-user passes via `Gate::before`; data-scope to the most permissive held role.
- [ ] [M][BE] Add the entity's `<domain>.<action>` permissions (CRUD + custom) to `PermissionSeeder` and attach to roles.
- [ ] [M][BE] Controller: thin `index/show/store/update/destroy` + `bulkDelete` + any custom actions returning Resources; never return raw models.
- [ ] [M][BE] Routes: register in the `v1` group, including `POST /<resource>/bulk-delete { ids }` (transactional, partial-failure reported) and any custom-action routes.
- [ ] [M][BE] Feature tests: list/show/create/update/delete + bulk-delete + custom actions (per the custom-action matrix) + 401/403/404/422; `RefreshDatabase`.
- [ ] [S][BE] Update the OpenAPI 3 spec for this resource (paths, request/response schemas, error responses) and re-run CI spec validation.
- [ ] [M][FE] Promote: add the resource path to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green.

**Acceptance (per entity)**
- [ ] [S][BE] Acceptance: live endpoint returns the exact camelCase Resource the FE TS type expects; list returns the Laravel-default paginated envelope; sort/`per_page` honored over whitelisted columns.
- [ ] [S][BE] Acceptance: create/update round-trips through the FE form dialog; bulk-delete is transactional and authorized against `<domain>.delete`.
- [ ] [S][BE+FE] Acceptance: the FE form dialog round-trips create/update against the live endpoint (moved here from the per-entity checklist — it is an acceptance criterion, not a build step).
- [ ] [S][BE] Acceptance: each of `index/show/create/update/delete/bulk-delete` AND every custom action (per the custom-action matrix) is gated by its own `<domain>.<action>` permission + Policy and returns 403 for a missing permission or out-of-scope role; the FE hides the corresponding buttons/pages when the permission is absent.
- [ ] [S][BE+FE] Acceptance: the super-user passes every action; user/employee role pickers exclude `super-user`/`student` and the backend rejects them if forged.
- [ ] [S][FE] Acceptance: aggregate/report pages for the entity use `listAll()`/report endpoints; feature tests green.

---

## Phase 5.1 — Per-entity sub-checklists (concrete, one per entity)

> Each entity below runs the full per-entity template (migration → model → factory/seeder mirroring
> `lib/dummy/*` → FormRequest camel→snake in the Action → Resource camelCase → Policy with per-action +
> custom permissions → controller incl. bulk-delete → routes → OpenAPI update → feature tests → FE
> round-trip → promote via `NEXT_PUBLIC_LIVE_RESOURCES`). The reusable template above still applies; these
> make each entity concrete with its key fields, custom permissions, and scope. Build them in the Phase 5
> dependency-first order. Each line carries effort/owner PM tags per the global convention.

### 5.1.1 Nationalities (`/nationalities`)
> Key fields: `name`, `code` · Scope: lookup (no scoping) · Custom actions: none
- [x] [M][BE] Nationalities: migration — columns `name`, `code`, index every filtered/sorted/FK column, reversible `down()`
- [x] [M][BE] Nationalities model: relationships, query scopes, `$casts`, tight `$fillable`.
- [x] [S][BE] Nationalities factory + seeder mirroring `lib/dummy/lookups.ts`.
- [x] [M][BE] Nationalities FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [x] [S][BE] Nationalities Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [x] [M][BE] Nationalities Policy: `viewAny/view/create/update/delete`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: lookup (no scoping). Add the permissions to `PermissionSeeder` + attach to roles.
- [x] [M][BE] Nationalities controller: thin `index/show/store/update/destroy` + `bulkDelete`, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [x] [S][BE] Nationalities routes in the `v1` group incl. `POST /nationalities/bulk-delete { ids }`.
- [x] [S][BE] Nationalities: PUBLIC unauthenticated `GET` read endpoint + cache via explicit namespaced key (`Cache::remember`/`Cache::forget` on write) + `Cache-Control`/`ETag`; write/manage endpoints permission-gated.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/nationalities` and re-run CI validation.
- [x] [L][BE] Nationalities feature tests: list/show/create/update/delete + bulk-delete + 401/403/404/422; `RefreshDatabase`.
- [x] [M][BE+FE] Nationalities FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [x] [S][FE] Nationalities: promote by adding `/nationalities` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green.

### 5.1.2 Emirates & Regions (`/emirates, /regions`)
> Key fields: emirate `name`; region `name` + `emirateId` · Scope: lookup · Custom actions: none
- [x] [M][BE] Emirates & Regions: migration — columns emirate `name`; region `name` + `emirateId`, index every filtered/sorted/FK column, reversible `down()` (deps: emirates before regions)
- [x] [M][BE] Emirates & Regions model: relationships, query scopes, `$casts`, tight `$fillable`.
- [x] [S][BE] Emirates & Regions factory + seeder mirroring `lib/dummy/lookups.ts`.
- [x] [M][BE] Emirates & Regions FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [x] [S][BE] Emirates & Regions Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [x] [M][BE] Emirates & Regions Policy: `viewAny/view/create/update/delete`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: lookup. Add the permissions to `PermissionSeeder` + attach to roles.
- [x] [M][BE] Emirates & Regions controller: thin `index/show/store/update/destroy` + `bulkDelete`, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [x] [S][BE] Emirates & Regions routes in the `v1` group incl. `POST /emirates/bulk-delete { ids }`.
- [x] [S][BE] Emirates & Regions: PUBLIC unauthenticated `GET` read endpoint + cache via explicit namespaced key (`Cache::remember`/`Cache::forget` on write) + `Cache-Control`/`ETag`; write/manage endpoints permission-gated.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/emirates, /regions` and re-run CI validation.
- [x] [L][BE] Emirates & Regions feature tests: list/show/create/update/delete + bulk-delete + 401/403/404/422; `RefreshDatabase`.
- [x] [M][BE+FE] Emirates & Regions FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [x] [S][FE] Emirates & Regions: promote by adding `/emirates` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: emirates before regions)

### 5.1.3 Levels & Sections (`/admin/levels, /sections`)
> Key fields: level `name`, `order`; section `name`, `levelId`, `primaryTeacherId` · Scope: admin · Custom actions: none
- [x] [M][BE] Levels & Sections: migration — columns level `name`, `order`; section `name`, `levelId`, `primaryTeacherId`, index every filtered/sorted/FK column, reversible `down()` (deps: employees (for primaryTeacher))
- [x] [M][BE] Levels & Sections model: relationships, query scopes, `$casts`, tight `$fillable`.
- [x] [S][BE] Levels & Sections factory + seeder mirroring `lib/dummy/levels.ts, lib/dummy/sections.ts`.
- [x] [M][BE] Levels & Sections FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [x] [S][BE] Levels & Sections Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [x] [M][BE] Levels & Sections Policy: `viewAny/view/create/update/delete`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: admin. Add the permissions to `PermissionSeeder` + attach to roles.
- [x] [M][BE] Levels & Sections controller: thin `index/show/store/update/destroy` + `bulkDelete`, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [x] [S][BE] Levels & Sections routes in the `v1` group incl. `POST /admin/levels/bulk-delete { ids }`.
- [x] [M][BE] Levels & Sections composite endpoint: `GET /levels/with-sections` (levels + sections + teachers in one response).
- [ ] [S][BE] Update the OpenAPI 3 spec for `/admin/levels, /sections` and re-run CI validation.
- [x] [L][BE] Levels & Sections feature tests: list/show/create/update/delete + bulk-delete + 401/403/404/422; `RefreshDatabase`.
- [x] [M][BE+FE] Levels & Sections FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [x] [S][FE] Levels & Sections: promote by adding `/admin/levels` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: employees (for primaryTeacher))

### 5.1.4 Subjects (`/admin/subjects`)
> Key fields: `name`, `levelId` · Scope: admin · Custom actions: none
- [ ] [M][BE] Subjects: migration — columns `name`, `levelId`, index every filtered/sorted/FK column, reversible `down()` (deps: levels)
- [ ] [M][BE] Subjects model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Subjects factory + seeder mirroring `lib/dummy/subjects.ts`.
- [ ] [M][BE] Subjects FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Subjects Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Subjects Policy: `viewAny/view/create/update/delete`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: admin. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Subjects controller: thin `index/show/store/update/destroy` + `bulkDelete`, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Subjects routes in the `v1` group incl. `POST /admin/subjects/bulk-delete { ids }`.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/admin/subjects` and re-run CI validation.
- [ ] [L][BE] Subjects feature tests: list/show/create/update/delete + bulk-delete + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Subjects FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Subjects: promote by adding `/admin/subjects` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: levels)

### 5.1.5 Subject-assignments (section-subjects) (`/section-subjects`)
> Key fields: `sectionId`, `subjectId`, `teacherId` · Scope: admin · Custom actions: `subjects.assignTeachers`
- [ ] [M][BE] Subject-assignments (section-subjects): migration — columns `sectionId`, `subjectId`, `teacherId`, index every filtered/sorted/FK column, reversible `down()` (deps: subjects, sections, employees)
- [ ] [M][BE] Subject-assignments (section-subjects) model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Subject-assignments (section-subjects) factory + seeder mirroring `lib/dummy/subjects.ts`.
- [ ] [M][BE] Subject-assignments (section-subjects) FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Subject-assignments (section-subjects) Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Subject-assignments (section-subjects) Policy: `viewAny/view/create/update/delete` + custom action(s) `subjects.assignTeachers`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: admin. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Subject-assignments (section-subjects) controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Subject-assignments (section-subjects) routes in the `v1` group incl. `POST /section-subjects/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/section-subjects` and re-run CI validation.
- [ ] [L][BE] Subject-assignments (section-subjects) feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Subject-assignments (section-subjects) FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Subject-assignments (section-subjects): promote by adding `/section-subjects` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: subjects, sections, employees)

### 5.1.6 Exam-types (`/exam-types`)
> Key fields: `name` · Scope: lookup · Custom actions: none
- [ ] [M][BE] Exam-types: migration — columns `name`, index every filtered/sorted/FK column, reversible `down()`
- [ ] [M][BE] Exam-types model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Exam-types factory + seeder mirroring `lib/dummy/exams.ts`.
- [ ] [M][BE] Exam-types FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Exam-types Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Exam-types Policy: `viewAny/view/create/update/delete`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: lookup. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Exam-types controller: thin `index/show/store/update/destroy` + `bulkDelete`, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Exam-types routes in the `v1` group incl. `POST /exam-types/bulk-delete { ids }`.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/exam-types` and re-run CI validation.
- [ ] [L][BE] Exam-types feature tests: list/show/create/update/delete + bulk-delete + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Exam-types FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Exam-types: promote by adding `/exam-types` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green.

### 5.1.7 Drivers (`/drivers`)
> Key fields: `name`, `phone`, `licenseNumber` · Scope: admin · Custom actions: none
- [x] [M][BE] Drivers: migration — columns `name`, `phone`, `licenseNumber`, index every filtered/sorted/FK column, reversible `down()`
- [x] [M][BE] Drivers model: relationships, query scopes, `$casts`, tight `$fillable`.
- [x] [S][BE] Drivers factory + seeder mirroring `lib/dummy/drivers.ts`.
- [x] [M][BE] Drivers FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [x] [S][BE] Drivers Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [x] [M][BE] Drivers Policy: `viewAny/view/create/update/delete`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: admin. Add the permissions to `PermissionSeeder` + attach to roles.
- [x] [M][BE] Drivers controller: thin `index/show/store/update/destroy` + `bulkDelete`, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [x] [S][BE] Drivers routes in the `v1` group incl. `POST /drivers/bulk-delete { ids }`.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/drivers` and re-run CI validation.
- [x] [L][BE] Drivers feature tests: list/show/create/update/delete + bulk-delete + 401/403/404/422; `RefreshDatabase`.
- [x] [M][BE+FE] Drivers FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [x] [S][FE] Drivers: promote by adding `/drivers` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green.

### 5.1.8 Buses (`/buses`)
> Key fields: `name`, `plateNumber`, `driverId`, `supervisorId`, `regionId`, `capacity` · Scope: admin · Custom actions: none
- [x] [M][BE] Buses: migration — columns `name`, `plateNumber`, `driverId`, `supervisorId`, `regionId`, `capacity`, index every filtered/sorted/FK column, reversible `down()` (deps: drivers, emirates, regions, employees)
- [x] [M][BE] Buses model: relationships, query scopes, `$casts`, tight `$fillable`.
- [x] [S][BE] Buses factory + seeder mirroring `lib/dummy/lookups.ts`.
- [x] [M][BE] Buses FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [x] [S][BE] Buses Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [x] [M][BE] Buses Policy: `viewAny/view/create/update/delete`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: admin. Add the permissions to `PermissionSeeder` + attach to roles.
- [x] [M][BE] Buses controller: thin `index/show/store/update/destroy` + `bulkDelete`, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [x] [S][BE] Buses routes in the `v1` group incl. `POST /buses/bulk-delete { ids }`.
- [x] [M][BE] Buses composite endpoint: `GET /buses/form-data` (emirates + regions + drivers + supervisors in one response).
- [ ] [S][BE] Update the OpenAPI 3 spec for `/buses` and re-run CI validation.
- [x] [L][BE] Buses feature tests: list/show/create/update/delete + bulk-delete + 401/403/404/422; `RefreshDatabase`.
- [x] [M][BE+FE] Buses FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [x] [S][FE] Buses: promote by adding `/buses` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: drivers, emirates, regions, employees)

### 5.1.9 Employees (`/admin/employees`)
> Key fields: `name`, `phone`, `altPhone`, `email`, `jobRole` (teacher/secretary/accountant/supervisor/coordinator/driver), auth `roles[]` · Scope: admin · Custom actions: `employees.assignRoles`
> Note: FormRequest constrains submitted auth `roles` with `Rule::in(['supervisor','teacher','parent','employee'])` (rejects `super-user`/`student`); the employee-form picker offers only `{supervisor, teacher, employee}`. `phone`+`altPhone` live on the Employee entity (edited here, not on /profile).
- [x] [M][BE] Employees: migration — columns `name`, `phone`, `altPhone`, `email`, `jobRole` (teacher/secretary/accountant/supervisor/coordinator/driver), auth `roles[]`, index every filtered/sorted/FK column, reversible `down()` (deps: roles & permissions seeded)
- [x] [M][BE] Employees model: relationships, query scopes, `$casts`, tight `$fillable`.
- [x] [S][BE] Employees factory + seeder mirroring `lib/dummy/employees.ts`.
- [x] [M][BE] Employees FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys; constrain auth `roles` with `Rule::in(['supervisor','teacher','parent','employee'])`.
- [x] [S][BE] Employees Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [x] [M][BE] Employees Policy: `viewAny/view/create/update/delete` + custom action(s) `employees.assignRoles`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: admin. Add the permissions to `PermissionSeeder` + attach to roles.
- [x] [M][BE] Employees controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [x] [S][BE] Employees routes in the `v1` group incl. `POST /admin/employees/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/admin/employees` and re-run CI validation.
- [x] [L][BE] Employees feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [x] [M][BE+FE] Employees FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [x] [S][FE] Employees: promote by adding `/admin/employees` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: roles & permissions seeded)

### 5.1.10 Students (`/admin/students`)
> Key fields: `name`, `studentCode`, `levelId`, `sectionId`, `nationalityId`, `emirateId`, `regionId`, `gender`, `isOrphan`, `hasId`, `busId`, `transportType` · Scope: most-permissive role (supervisor→all; teacher→own circles) · Custom actions: `students.transfer`, `students.archive`
> Note: Whitelisted filter params: `level_id`, `section_id`, `gender`, `nationality_id`, `emirate_id`, `region_id`, `transport_type`, `is_orphan`, `has_id`. Archived students keep their `levelName`/`sectionName` snapshot.
- [ ] [M][BE] Students: migration — columns `name`, `studentCode`, `levelId`, `sectionId`, `nationalityId`, `emirateId`, `regionId`, `gender`, `isOrphan`, `hasId`, `busId`, `transportType`, index every filtered/sorted/FK column, reversible `down()` (deps: levels, sections, nationalities, emirates, regions, buses)
- [ ] [M][BE] Students model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Students factory + seeder mirroring `lib/dummy/students.ts`.
- [ ] [M][BE] Students FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Students Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Students Policy: `viewAny/view/create/update/delete` + custom action(s) `students.transfer`, `students.archive`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: most-permissive role (supervisor→all; teacher→own circles). Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Students controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Students routes in the `v1` group incl. `POST /admin/students/bulk-delete { ids }` + custom-action routes.
- [ ] [M][BE] Students composite endpoint: `GET /students/form-data` (nationalities + emirates + regions + levels + sections + buses in one response).
- [ ] [S][BE] Update the OpenAPI 3 spec for `/admin/students` and re-run CI validation.
- [ ] [L][BE] Students feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Students FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Students: promote by adding `/admin/students` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: levels, sections, nationalities, emirates, regions, buses)

### 5.1.11 Pending students (approval) (`/admin/students/pending`)
> Key fields: registration payload (student + parent identifier email OR phone) · Scope: supervisor · Custom actions: `students.pending.approve`, `students.pending.reject`
> Note: `PATCH /students/pending/{id}/approve` assigns `levelId`+`sectionId` (enforce `section.levelId === levelId` server-side) + optional region-filtered `busId`, and creates the Parent `User` with a server-generated hashed password returned once (P1). `POST /students/pending` returns nothing secret.
- [ ] [M][BE] Pending students (approval): migration — columns registration payload (student + parent identifier email OR phone), index every filtered/sorted/FK column, reversible `down()` (deps: levels, sections, buses, students, parents)
- [ ] [M][BE] Pending students (approval) model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Pending students (approval) factory + seeder mirroring `lib/dummy/pending-students.ts`.
- [ ] [M][BE] Pending students (approval) FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Pending students (approval) Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Pending students (approval) Policy: `viewAny/view/create/update/delete` + custom action(s) `students.pending.approve`, `students.pending.reject`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: supervisor. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Pending students (approval) controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Pending students (approval) routes in the `v1` group incl. `POST /admin/students/pending/bulk-delete { ids }` + custom-action routes.
- [ ] [M][BE] Pending students (approval) composite endpoint: `GET /students/pending/form-data` (levels + sections + region-filtered buses in one response).
- [ ] [S][BE] Update the OpenAPI 3 spec for `/admin/students/pending` and re-run CI validation.
- [ ] [L][BE] Pending students (approval) feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Pending students (approval) FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Pending students (approval): promote by adding `/admin/students/pending` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: levels, sections, buses, students, parents)

### 5.1.12 Parents (`/admin/parents`)
> Key fields: `name`, `phone`, `email`, `childIds` · Scope: admin · Custom actions: `parents.resetPassword` (supervisor-only, never parent-editable)
- [ ] [M][BE] Parents: migration — columns `name`, `phone`, `email`, `childIds`, index every filtered/sorted/FK column, reversible `down()` (deps: students)
- [ ] [M][BE] Parents model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Parents factory + seeder mirroring `lib/dummy/parents.ts`.
- [ ] [M][BE] Parents FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Parents Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Parents Policy: `viewAny/view/create/update/delete` + custom action(s) `parents.resetPassword` (supervisor-only, never parent-editable); `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: admin. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Parents controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Parents routes in the `v1` group incl. `POST /admin/parents/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/admin/parents` and re-run CI validation.
- [ ] [L][BE] Parents feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Parents FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Parents: promote by adding `/admin/parents` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: students)

### 5.1.13 Memorization (`/memorization`)
> Key fields: `studentId`, `circleId`, `date`, `fromSurah`/`fromAyah`, `toSurah`/`toAyah`, `achievementType` (memorization حفظ / revision مراجعة) · Scope: teacher circles · Custom actions: `memorization.record`
> Note: `memorization.record` does NOT carry attendance status — present/absent/remote/excused is recorded only via the attendance endpoint.
- [ ] [M][BE] Memorization: migration — columns `studentId`, `circleId`, `date`, `fromSurah`/`fromAyah`, `toSurah`/`toAyah`, `achievementType` (memorization حفظ / revision مراجعة), index every filtered/sorted/FK column, reversible `down()` (deps: students, circles)
- [ ] [M][BE] Memorization model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Memorization factory + seeder mirroring `lib/dummy/memorization.ts`.
- [ ] [M][BE] Memorization FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Memorization Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Memorization Policy: `viewAny/view/create/update/delete` + custom action(s) `memorization.record`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: teacher circles. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Memorization controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Memorization routes in the `v1` group incl. `POST /memorization/bulk-delete { ids }` + custom-action routes.
- [ ] [M][BE] Memorization composite endpoint: `GET /memorization/session?circleId=&date=` (circle + students + sessions + group achievements in one response).
- [ ] [S][BE] Update the OpenAPI 3 spec for `/memorization` and re-run CI validation.
- [ ] [L][BE] Memorization feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Memorization FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Memorization: promote by adding `/memorization` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: students, circles)

### 5.1.14 Attendance (`/attendance`)
> Key fields: `studentId`, `date`, `status` (present/absent/remote/excused) · Scope: teacher circles · Custom actions: `attendance.record`
- [ ] [M][BE] Attendance: migration — columns `studentId`, `date`, `status` (present/absent/remote/excused), index every filtered/sorted/FK column, reversible `down()` (deps: students, circles)
- [ ] [M][BE] Attendance model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Attendance factory + seeder mirroring `lib/dummy/attendance.ts`.
- [ ] [M][BE] Attendance FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Attendance Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Attendance Policy: `viewAny/view/create/update/delete` + custom action(s) `attendance.record`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: teacher circles. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Attendance controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Attendance routes in the `v1` group incl. `POST /attendance/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/attendance` and re-run CI validation.
- [ ] [L][BE] Attendance feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Attendance FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Attendance: promote by adding `/attendance` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: students, circles)

### 5.1.15 Plans (`/plans`)
> Key fields: `circleId`, plan range fields · Scope: teacher circles · Custom actions: none
- [ ] [M][BE] Plans: migration — columns `circleId`, plan range fields, index every filtered/sorted/FK column, reversible `down()` (deps: circles)
- [ ] [M][BE] Plans model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Plans factory + seeder mirroring `lib/dummy/plans.ts`.
- [ ] [M][BE] Plans FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Plans Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Plans Policy: `viewAny/view/create/update/delete`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: teacher circles. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Plans controller: thin `index/show/store/update/destroy` + `bulkDelete`, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Plans routes in the `v1` group incl. `POST /plans/bulk-delete { ids }`.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/plans` and re-run CI validation.
- [ ] [L][BE] Plans feature tests: list/show/create/update/delete + bulk-delete + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Plans FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Plans: promote by adding `/plans` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: circles)

### 5.1.16 Exams (`/exams`)
> Key fields: `name`, `examTypeId`, `levelId`, `date` · Scope: teaching · Custom actions: none
- [ ] [M][BE] Exams: migration — columns `name`, `examTypeId`, `levelId`, `date`, index every filtered/sorted/FK column, reversible `down()` (deps: exam-types, levels)
- [ ] [M][BE] Exams model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Exams factory + seeder mirroring `lib/dummy/exams.ts`.
- [ ] [M][BE] Exams FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Exams Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Exams Policy: `viewAny/view/create/update/delete`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: teaching. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Exams controller: thin `index/show/store/update/destroy` + `bulkDelete`, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Exams routes in the `v1` group incl. `POST /exams/bulk-delete { ids }`.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/exams` and re-run CI validation.
- [ ] [L][BE] Exams feature tests: list/show/create/update/delete + bulk-delete + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Exams FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Exams: promote by adding `/exams` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: exam-types, levels)

### 5.1.17 Student-exams (`/student-exams`)
> Key fields: `studentId`, `examId`, `publishToParent`, grade fields · Scope: teaching · Custom actions: `student_exams.publish`
- [ ] [M][BE] Student-exams: migration — columns `studentId`, `examId`, `publishToParent`, grade fields, index every filtered/sorted/FK column, reversible `down()` (deps: students, exams)
- [ ] [M][BE] Student-exams model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Student-exams factory + seeder mirroring `lib/dummy/exams.ts`.
- [ ] [M][BE] Student-exams FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Student-exams Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Student-exams Policy: `viewAny/view/create/update/delete` + custom action(s) `student_exams.publish`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: teaching. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Student-exams controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Student-exams routes in the `v1` group incl. `POST /student-exams/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/student-exams` and re-run CI validation.
- [ ] [L][BE] Student-exams feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Student-exams FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Student-exams: promote by adding `/student-exams` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: students, exams)

### 5.1.18 Sub-subject-grades (`/sub-subject-grades`)
> Key fields: `studentExamId`, `subSubject`, `grade` · Scope: teaching · Custom actions: `grades.record`
- [ ] [M][BE] Sub-subject-grades: migration — columns `studentExamId`, `subSubject`, `grade`, index every filtered/sorted/FK column, reversible `down()` (deps: student-exams)
- [ ] [M][BE] Sub-subject-grades model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Sub-subject-grades factory + seeder mirroring `lib/dummy/sub-subject-grades.ts`.
- [ ] [M][BE] Sub-subject-grades FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Sub-subject-grades Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Sub-subject-grades Policy: `viewAny/view/create/update/delete` + custom action(s) `grades.record`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: teaching. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Sub-subject-grades controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Sub-subject-grades routes in the `v1` group incl. `POST /sub-subject-grades/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/sub-subject-grades` and re-run CI validation.
- [ ] [L][BE] Sub-subject-grades feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Sub-subject-grades FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Sub-subject-grades: promote by adding `/sub-subject-grades` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: student-exams)

### 5.1.19 Teacher-logs (`/teacher-logs`)
> Key fields: `teacherId`, `date`, `note` · Scope: admin-only (§5.4) · Custom actions: none
- [ ] [M][BE] Teacher-logs: migration — columns `teacherId`, `date`, `note`, index every filtered/sorted/FK column, reversible `down()` (deps: employees)
- [ ] [M][BE] Teacher-logs model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Teacher-logs factory + seeder mirroring `lib/dummy/teacher-log.ts`.
- [ ] [M][BE] Teacher-logs FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Teacher-logs Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Teacher-logs Policy: `viewAny/view/create/update/delete`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: admin-only (§5.4). Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Teacher-logs controller: thin `index/show/store/update/destroy` + `bulkDelete`, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Teacher-logs routes in the `v1` group incl. `POST /teacher-logs/bulk-delete { ids }`.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/teacher-logs` and re-run CI validation.
- [ ] [L][BE] Teacher-logs feature tests: list/show/create/update/delete + bulk-delete + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Teacher-logs FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Teacher-logs: promote by adding `/teacher-logs` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: employees)

### 5.1.20 Administration — Evaluations (`/evaluations`)
> Key fields: `employeeId`, `date`, content/body fields · Scope: supervisor-only · Custom actions: none (full CRUD resource, NOT a single `administration.*` action)
- [ ] [M][BE] Administration — Evaluations: migration — columns `employeeId`, `date`, content/body fields, index every filtered/sorted/FK column, reversible `down()` (deps: employees)
- [ ] [M][BE] Administration — Evaluations model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Administration — Evaluations factory + seeder mirroring `lib/dummy/administration.ts`.
- [ ] [M][BE] Administration — Evaluations FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Administration — Evaluations Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Administration — Evaluations Policy: `viewAny/view/create/update/delete` + custom action(s) none (full CRUD resource, NOT a single `administration.*` action); `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: supervisor-only. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Administration — Evaluations controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Administration — Evaluations routes in the `v1` group incl. `POST /evaluations/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/evaluations` and re-run CI validation.
- [ ] [L][BE] Administration — Evaluations feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Administration — Evaluations FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Administration — Evaluations: promote by adding `/evaluations` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: employees)

### 5.1.21 Administration — Warnings (`/warnings`)
> Key fields: `employeeId`, `date`, content/body fields · Scope: supervisor-only · Custom actions: none (full CRUD resource, NOT a single `administration.*` action)
- [ ] [M][BE] Administration — Warnings: migration — columns `employeeId`, `date`, content/body fields, index every filtered/sorted/FK column, reversible `down()` (deps: employees)
- [ ] [M][BE] Administration — Warnings model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Administration — Warnings factory + seeder mirroring `lib/dummy/administration.ts`.
- [ ] [M][BE] Administration — Warnings FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Administration — Warnings Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Administration — Warnings Policy: `viewAny/view/create/update/delete` + custom action(s) none (full CRUD resource, NOT a single `administration.*` action); `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: supervisor-only. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Administration — Warnings controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Administration — Warnings routes in the `v1` group incl. `POST /warnings/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/warnings` and re-run CI validation.
- [ ] [L][BE] Administration — Warnings feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Administration — Warnings FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Administration — Warnings: promote by adding `/warnings` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: employees)

### 5.1.22 Administration — Certificates (`/certificates`)
> Key fields: `employeeId`, `date`, content/body fields · Scope: supervisor-only · Custom actions: none (full CRUD resource, NOT a single `administration.*` action)
- [ ] [M][BE] Administration — Certificates: migration — columns `employeeId`, `date`, content/body fields, index every filtered/sorted/FK column, reversible `down()` (deps: employees)
- [ ] [M][BE] Administration — Certificates model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Administration — Certificates factory + seeder mirroring `lib/dummy/administration.ts`.
- [ ] [M][BE] Administration — Certificates FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Administration — Certificates Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Administration — Certificates Policy: `viewAny/view/create/update/delete` + custom action(s) none (full CRUD resource, NOT a single `administration.*` action); `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: supervisor-only. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Administration — Certificates controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Administration — Certificates routes in the `v1` group incl. `POST /certificates/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/certificates` and re-run CI validation.
- [ ] [L][BE] Administration — Certificates feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Administration — Certificates FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Administration — Certificates: promote by adding `/certificates` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: employees)

### 5.1.23 Administration — Letters (`/letters`)
> Key fields: `employeeId`, `date`, content/body fields · Scope: supervisor-only · Custom actions: none (full CRUD resource, NOT a single `administration.*` action)
- [ ] [M][BE] Administration — Letters: migration — columns `employeeId`, `date`, content/body fields, index every filtered/sorted/FK column, reversible `down()` (deps: employees)
- [ ] [M][BE] Administration — Letters model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Administration — Letters factory + seeder mirroring `lib/dummy/administration.ts`.
- [ ] [M][BE] Administration — Letters FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Administration — Letters Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Administration — Letters Policy: `viewAny/view/create/update/delete` + custom action(s) none (full CRUD resource, NOT a single `administration.*` action); `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: supervisor-only. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Administration — Letters controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Administration — Letters routes in the `v1` group incl. `POST /letters/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/letters` and re-run CI validation.
- [ ] [L][BE] Administration — Letters feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Administration — Letters FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Administration — Letters: promote by adding `/letters` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: employees)

### 5.1.24 Messages (`/messages`)
> Key fields: `senderId`, recipient/scope, `body`, `type` (incl. notice rows) · Scope: ownership/admin gates on reply/delete · Custom actions: reply/delete ownership gates
> Note: `/messages/notices` is authenticated-only on the backend; parent `/parent/notices` reads notice-type rows scoped to the parent's children (NOT a separate resource).
- [ ] [M][BE] Messages: migration — columns `senderId`, recipient/scope, `body`, `type` (incl. notice rows), index every filtered/sorted/FK column, reversible `down()` (deps: employees, parents)
- [ ] [M][BE] Messages model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Messages factory + seeder mirroring `lib/dummy/messages.ts`.
- [ ] [M][BE] Messages FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Messages Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Messages Policy: `viewAny/view/create/update/delete`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: ownership/admin gates on reply/delete. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Messages controller: thin `index/show/store/update/destroy` + `bulkDelete`, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Messages routes in the `v1` group incl. `POST /messages/bulk-delete { ids }`.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/messages` and re-run CI validation.
- [ ] [L][BE] Messages feature tests: list/show/create/update/delete + bulk-delete + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Messages FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Messages: promote by adding `/messages` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green. (deps: employees, parents)

### 5.1.25 Transport report (`/admin/transport/report`)
> Key fields: buses + students transport rollup · Scope: admin · Custom actions: none (aggregate, `listAll()` consumer)
- [ ] [M][BE] Transport report: dedicated endpoint that aggregates over the FULL (unsliced) buses/students collection via `listAll()`-style query — never a paginated `list()` page; camelCase output; gated by the most-permissive-role scope. (deps: buses, students)
- [ ] [S][BE] Update the OpenAPI spec + feature test asserting totals reflect the full dataset.
- [ ] [S][BE+FE] FE consumer derives totals from the report endpoint (full dataset), not page 1.

### 5.1.26 Exam level-report (`/education/exams/level-report`)
> Scope: teaching · Aggregate (dedicated server-computed)
- [ ] [M][BE] Exam level-report: dedicated server-computed aggregate (per-level exam results), camelCase, scope-gated. (deps: exams, levels)
- [ ] [S][BE] Update the OpenAPI spec + feature test.
- [ ] [S][BE+FE] FE `/education/exams/level-report` consumes it.

### 5.1.27 Exam archive (`/education/exams/archive`)
> Scope: admin-only (§5.4)
- [ ] [M][BE] Exam archive: archived-exams read endpoint (admin-only Policy), camelCase, paginated. (deps: exams)
- [ ] [S][BE] Update the OpenAPI spec + feature test (401/403 for non-admin).
- [ ] [S][BE+FE] FE archive page consumes it.

### 5.1.28 Reports (aggregate endpoints) (`/education/reports/*`, `/parent/reports`, `/admin/employees/work-report`)
> Each report is a dedicated server-computed aggregate OR a `listAll()` consumer — NEVER totals from a paginated `list()`.
- [ ] [M][BE] `/education/reports/daily` (dedicated aggregate, attendance/memorization totals + gender split). (deps: attendance, memorization)
- [ ] [M][BE] `/education/reports/circles` (dedicated aggregate, per-circle rollups). (deps: circles)
- [ ] [M][BE] `/education/reports/students` (`listAll()` over scoped students). (deps: students)
- [ ] [M][BE] `/education/reports/attendance` (dedicated aggregate). (deps: attendance)
- [ ] [M][BE] `/admin/employees/work-report` (dedicated aggregate, teacher work totals). (deps: employees)
- [ ] [M][BE] `/parent/reports` (`listAll()` scoped to the parent's children). (deps: students)
- [ ] [S][BE] Each report: gated by most-permissive-role scope, camelCase, OpenAPI updated, feature test asserting full-dataset totals.

### 5.1.34 Profile (`/auth/me`, `/auth/profile`, `/auth/password`)
> Built in Phase 1, wired in Phase 2 — this sub-checklist VERIFIES, it does not rebuild.
- [ ] [S][BE] Verify `GET /auth/me`, `PATCH /auth/profile`, `POST /auth/password` remain owner-only and self-editable fields = `name`, single `phone`, `email`, `photo` (Employee `phone`+`altPhone` are NOT here).
- [ ] [S][BE] Verify profile edits NEVER touch roles/permissions/branch/status/linkage; a forged privileged key is rejected/ignored (422 or silently dropped).
- [ ] [S][BE] Verify OpenAPI covers the three endpoints; ProfileTest/ChangePasswordTest green.
- [ ] [S][FE] Verify `/profile` page round-trips own-data + password change against the live endpoints.

### 5.1.35 Roles & Permissions (`/roles`, `/permissions`, `/roles/{role}/permissions`)
> Super-user-only runtime grant matrix — catalog stays code-seeded, only grants are editable.
- [ ] [S][BE] Verify `roles.managePermissions` is seeded to NO editable role (super-user passes via `Gate::before`).
- [ ] [M][BE] `RoleController`/`PermissionController` (or `RolePermissionController`): `GET /roles`, `GET /permissions` (grouped by domain), `PUT /roles/{role}/permissions` (sync) returning Resources (camelCase); all gated by `roles.managePermissions`.
- [ ] [M][BE] `SyncRolePermissionsRequest`: `permissions[] ⊆` seeded catalog; reject `{role}` ∈ {`super-user`,`student`}; allow only `Rule::in(['supervisor','teacher','employee','parent'])`; Arabic messages.
- [ ] [S][BE] Sync Action persists in a DB transaction then calls `forgetCachedPermissions()`.
- [ ] [S][BE] Update the OpenAPI spec for the three endpoints.
- [ ] [L][BE] `RolePermissionTest`: super-user GET/PUT works (grant takes effect after `forgetCachedPermissions`); non-super-user (incl. supervisor) 403 on all three; editing/assigning `super-user` rejected; `student` not editable; `RefreshDatabase`.
- [ ] [M][FE] `rbac-service` + `/admin/roles` matrix page (rows = editable roles; columns = catalog by domain); super-user-only nav + RouteGuard; re-fetch `/me` on save.
- [ ] [S][BE+FE] Promote `/roles` + `/permissions` (always-live RBAC service); verify super-user-only access end-to-end.

### 5.1.36 Dashboard statistics (`/dashboard/statistics`)
> Composite role-scoped aggregate — one endpoint, not N list calls.
- [ ] [M][BE] `GET /dashboard/statistics`: role-scoped aggregate counts with gender split (students male/female, attendance, exams, …) computed server-side; most-permissive-role scope; gated like a report endpoint. (deps: students, attendance, exams)
- [ ] [S][BE] Update the OpenAPI spec + feature test per role scope.
- [ ] [S][BE+FE] FE `/dashboard` widgets consume the single aggregate endpoint (not N parallel `list()` calls).


### 5.1.29 Content CMS — Landing/Homepage sections (`/content/website (landing-sections)`)
> Key fields: hero slider, **OWNER block (DB-backed)**, "why us", FAQ, CTA, footer text · Scope: supervisor (super-user via Gate::before) · Custom actions: `landing.view`, `landing.update`
- [ ] [M][BE] Content CMS — Landing/Homepage sections: migration — columns hero slider, **OWNER block (DB-backed)**, "why us", FAQ, CTA, footer text, index every filtered/sorted/FK column, reversible `down()`
- [ ] [M][BE] Content CMS — Landing/Homepage sections model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Content CMS — Landing/Homepage sections factory + seeder mirroring `public landing dummy`.
- [ ] [M][BE] Content CMS — Landing/Homepage sections FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Content CMS — Landing/Homepage sections Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Content CMS — Landing/Homepage sections Policy: `viewAny/view/create/update/delete` + custom action(s) `landing.view`, `landing.update`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: supervisor (super-user via Gate::before). Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Content CMS — Landing/Homepage sections controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Content CMS — Landing/Homepage sections routes in the `v1` group incl. `POST /content/website (landing-sections)/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Content CMS — Landing/Homepage sections: PUBLIC unauthenticated `GET` read endpoint + cache via explicit namespaced key (`Cache::remember`/`Cache::forget` on write) + `Cache-Control`/`ETag`; write/manage endpoints permission-gated.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/content/website (landing-sections)` and re-run CI validation.
- [ ] [L][BE] Content CMS — Landing/Homepage sections feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Content CMS — Landing/Homepage sections FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Content CMS — Landing/Homepage sections: promote by adding `/content/website (landing-sections)` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green.

### 5.1.30 Content CMS — News & announcements (`/content/news`)
> Key fields: `title`, `body`, `publishedAt`, media · Scope: supervisor · Custom actions: full CRUD · Permissions: `news.{viewAny,view,create,update,delete}`
- [ ] [M][BE] Content CMS — News & announcements: migration — columns `title`, `body`, `publishedAt`, media, index every filtered/sorted/FK column, reversible `down()`
- [ ] [M][BE] Content CMS — News & announcements model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Content CMS — News & announcements factory + seeder mirroring `lib/dummy/news.ts`.
- [ ] [M][BE] Content CMS — News & announcements FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Content CMS — News & announcements Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Content CMS — News & announcements Policy: `viewAny/view/create/update/delete` + custom action(s) full CRUD; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: supervisor. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Content CMS — News & announcements controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Content CMS — News & announcements routes in the `v1` group incl. `POST /content/news/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Content CMS — News & announcements: PUBLIC unauthenticated `GET` read endpoint + cache via explicit namespaced key (`Cache::remember`/`Cache::forget` on write) + `Cache-Control`/`ETag`; write/manage endpoints permission-gated.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/content/news` and re-run CI validation.
- [ ] [L][BE] Content CMS — News & announcements feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Content CMS — News & announcements FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Content CMS — News & announcements: promote by adding `/content/news` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green.

### 5.1.31 Content CMS — Photo album (`/content/album`)
> Key fields: `title`, photos[] · Scope: supervisor · Custom actions: full CRUD · Permissions: `album.{viewAny,view,create,update,delete}`
- [ ] [M][BE] Content CMS — Photo album: migration — columns `title`, photos[], index every filtered/sorted/FK column, reversible `down()`
- [ ] [M][BE] Content CMS — Photo album model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Content CMS — Photo album factory + seeder mirroring `lib/dummy/album.ts`.
- [ ] [M][BE] Content CMS — Photo album FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Content CMS — Photo album Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Content CMS — Photo album Policy: `viewAny/view/create/update/delete` + custom action(s) full CRUD; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: supervisor. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Content CMS — Photo album controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Content CMS — Photo album routes in the `v1` group incl. `POST /content/album/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Content CMS — Photo album: PUBLIC unauthenticated `GET` read endpoint + cache via explicit namespaced key (`Cache::remember`/`Cache::forget` on write) + `Cache-Control`/`ETag`; write/manage endpoints permission-gated.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/content/album` and re-run CI validation.
- [ ] [L][BE] Content CMS — Photo album feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Content CMS — Photo album FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Content CMS — Photo album: promote by adding `/content/album` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green.

### 5.1.32 Content CMS — Library (`/content/library`)
> Key fields: `title`, `file`/`url`, category · Scope: supervisor · Custom actions: full CRUD · Permissions: `library.{viewAny,view,create,update,delete}`
- [ ] [M][BE] Content CMS — Library: migration — columns `title`, `file`/`url`, category, index every filtered/sorted/FK column, reversible `down()`
- [ ] [M][BE] Content CMS — Library model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Content CMS — Library factory + seeder mirroring `lib/dummy/library.ts`.
- [ ] [M][BE] Content CMS — Library FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Content CMS — Library Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Content CMS — Library Policy: `viewAny/view/create/update/delete` + custom action(s) full CRUD; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: supervisor. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Content CMS — Library controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Content CMS — Library routes in the `v1` group incl. `POST /content/library/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Content CMS — Library: PUBLIC unauthenticated `GET` read endpoint + cache via explicit namespaced key (`Cache::remember`/`Cache::forget` on write) + `Cache-Control`/`ETag`; write/manage endpoints permission-gated.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/content/library` and re-run CI validation.
- [ ] [L][BE] Content CMS — Library feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Content CMS — Library FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Content CMS — Library: promote by adding `/content/library` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green.

### 5.1.33 Content CMS — Site settings (`/site-settings`)
> Key fields: contact info, social links, footer, basic SEO/site metadata (singleton) · Scope: supervisor · Custom actions: `site_settings.view`, `site_settings.update`
- [ ] [M][BE] Content CMS — Site settings: migration — columns contact info, social links, footer, basic SEO/site metadata (singleton), index every filtered/sorted/FK column, reversible `down()`
- [ ] [M][BE] Content CMS — Site settings model: relationships, query scopes, `$casts`, tight `$fillable`.
- [ ] [S][BE] Content CMS — Site settings factory + seeder mirroring `site-settings dummy`.
- [ ] [M][BE] Content CMS — Site settings FormRequest(s): validate camelCase payload, map camel→snake IN THE ACTION, Arabic messages, camelCase 422 keys.
- [ ] [S][BE] Content CMS — Site settings Resource: camelCase output matching the FE TS type exactly; eager-load to avoid N+1.
- [ ] [M][BE] Content CMS — Site settings Policy: `viewAny/view/create/update/delete` + custom action(s) `site_settings.view`, `site_settings.update`; `bulk-delete` reuses `<domain>.delete`; super-user via `Gate::before`; data-scope: supervisor. Add the permissions to `PermissionSeeder` + attach to roles.
- [ ] [M][BE] Content CMS — Site settings controller: thin `index/show/store/update/destroy` + `bulkDelete` + custom actions, returning Resources; paginated `Resource::collection($paginator)`; whitelist `sort`/`per_page`.
- [ ] [S][BE] Content CMS — Site settings routes in the `v1` group incl. `POST /site-settings/bulk-delete { ids }` + custom-action routes.
- [ ] [S][BE] Content CMS — Site settings: PUBLIC unauthenticated `GET` read endpoint + cache via explicit namespaced key (`Cache::remember`/`Cache::forget` on write) + `Cache-Control`/`ETag`; write/manage endpoints permission-gated.
- [ ] [S][BE] Update the OpenAPI 3 spec for `/site-settings` and re-run CI validation.
- [ ] [L][BE] Content CMS — Site settings feature tests: list/show/create/update/delete + bulk-delete + custom actions + 401/403/404/422; `RefreshDatabase`.
- [ ] [M][BE+FE] Content CMS — Site settings FE round-trip: create/update/delete through the form dialog against the live endpoint; per-action buttons HIDE when the permission is absent.
- [ ] [S][FE] Content CMS — Site settings: promote by adding `/site-settings` to `NEXT_PUBLIC_LIVE_RESOURCES` once the slice is green.

---

## Phase 6 — Cutover & testing

> Goal: flip remaining entities to live confidently and verify the whole system end to end.

**Cutover & verification**
- [ ] [S][FE] Cutover: add each green resource to `NEXT_PUBLIC_LIVE_RESOURCES`; once all entities are listed, set `NEXT_PUBLIC_USE_MOCK_DATA=false` and clear the allowlist (no component changes — centralized seam).
- [ ] [S][BE] Smoke matrix per role (super-user / supervisor / teacher / employee / parent) AND a multi-role employee (teacher + supervisor) as a single union session: login → home (primary by precedence) → each accessible list (pagination + debounced search + server sort) → one create/update/delete + one bulk-delete where permitted → `/profile` edit + password change → logout.
- [ ] [S][BE] Verify the union user sees combined nav/permissions with NO switcher; the super-user sees everything incl. the hidden modules — but those modules (Finance/Audit) render mock/empty because they have NO live endpoints (Phase 5 scope note), so the super-user check does NOT assert live data for them; **Content is LIVE — it renders real data from the Content CMS (not mock)**; missing-permission controls (CRUD AND custom) are HIDDEN (not disabled); 403 on out-of-scope or missing-permission direct API calls.
- [ ] [S][BE+FE] Verify the Content CMS end to end: a supervisor (and the super-user) can manage landing-sections/homepage (incl. the DB-backed owner block), news, library, photo album, and site settings; the PUBLIC landing/news/library/album pages render those edits LIVE from the public read endpoints; teacher/employee/parent do NOT see content-management controls; public read endpoints require no auth while write/manage endpoints 403 without the content permission.
- [ ] [S][BE+FE] Verify the corrected §5.4 matrix is honored end to end: `/admin/students/cards` supervisor-only, `/education/statistics` net-hidden for non-super-user, education admin-only items ADMIN-only, `/messages/notices` authenticated-only.
- [ ] [S][BE+FE] Aggregate verification: every report page (§6) shows totals over the FULL dataset, not page 1.
- [ ] [S][BE+FE] Performance smoke — no N+1: assert key list/show + composite endpoints issue a bounded number of queries via a query-count assertion (e.g. `DB::enableQueryLog`/telemetry) and that `Model::shouldBeStrict()` raises no lazy-load violations in non-prod.
- [ ] [S][BE] Performance smoke — caching: public pages (landing/news/library/album/site-settings) are a cache HIT on the second request (served from cache, `Cache-Control`/`ETag` honored); a CMS/lookup write busts the cached key (`Cache::forget`) so the next read reflects the change; per-user/role-scoped lists are never cached.
- [ ] [S][BE+FE] Performance smoke — composite endpoints: each live screen issues ONE aggregate request (not N parallel `list()` calls) for its lookups/data.
- [ ] [S][BE+FE] Roles & Permissions smoke: the super-user opens `/admin/roles`, toggles a grant for `supervisor`/`teacher`/`employee`/`parent`, saves (cache forgotten + `/me` re-fetched); a non-super-user cannot see `/admin/roles` or call its endpoints (`403`); `super-user`/`student` are never editable.
- [ ] [S][BE+FE] Refresh/expiry: refresh restores session via `/me`; an expired/revoked token forces `/login`; the 401 carve-out never traps `/auth/*`.
- [ ] [S][BE+FE] Rollback verified: flip `NEXT_PUBLIC_USE_MOCK_DATA=true` (and clear `liveResources`) restores mock mode with no code change.

**Acceptance (gates)**
- [ ] [S][FE] Acceptance: FE `npx tsc --noEmit` 0, `eslint` clean, `vitest` green, `next build` exit 0 (no `MISSING_MESSAGE`).
- [ ] [S][BE] Acceptance: BE `php artisan test` green, `pint --test` clean, `migrate:fresh --seed` idempotent.

---

## Cross-cutting (apply to every page/endpoint)

> These rules hold for every new page, component, resource, and endpoint — not just the ones above.

**Per-action permission gating (G1/D3/D5)**
- [ ] [M][BE] Backend: every `index/show/create/update/delete` + `bulk-delete` endpoint is gated by a `<domain>.<action>` permission + Policy checked with `$this->authorize(...)`; 403 is the real boundary; `bulk-delete` authorizes against `<domain>.delete`.
- [ ] [L][BE] Backend: every **custom/non-CRUD action** (`students.transfer/archive`, `students.pending.approve/reject`, `parents.resetPassword`, `student_exams.publish`, `subjects.assignTeachers`, `employees.assignRoles`, `memorization.record`, `attendance.record`, `grades.record`, …) maps to its own `<domain>.<action>` permission + Policy method, gated identically. (The administration resources — `evaluations`/`warnings`/`certificates`/`letters` — are full CRUD RESOURCES, NOT custom actions: they each get a full per-resource CRUD permission set + a Policy, not a single `administration.*` permission.)
- [ ] [M][BE] Backend: any new resource adds its `<domain>.<action>` permissions (CRUD + custom) to `PermissionSeeder` AND a Policy (shipping a resource without both is a defect).
- [ ] [M][FE] Frontend: read the unioned `permissions[]` from `/me` and HIDE (never disable) the Add/Edit/Delete buttons, row actions, bulk actions, and custom-action controls when the matching permission is absent; block whole pages via `RouteGuard`.
- [ ] [M][FE] Frontend: a missing permission HIDES the control — never render a greyed-out/disabled control for a permission the user lacks.

**Super-user handling (M3)**
- [ ] [L][BE] Exactly one seeded super-user (`firstOrCreate`); never assignable via API/UI; omitted from every user/employee role picker; any request to grant `super-user` is rejected by a concrete FormRequest rule (`Rule::in(['supervisor','teacher','parent','employee'])` / `Rule::notIn(['super-user','student'])`) on the employee/user store+update requests — enforced, not merely asserted.
- [ ] [M][BE] `Gate::before(fn ($u) => $u->hasRole('super-user') ? true : null)` makes it pass every permission/policy check; no explicit grants in `PermissionSeeder`.
- [ ] [M][FE] Frontend: super-user bypasses BOTH the role filter and the `hidden-features` mask in `filterNavigation`/`RouteGuard` — the only identity that sees Finance/Audit/Statistics (Content is no longer hidden — it is a managed CMS visible to supervisor too).
- [ ] [L][BE] Runtime RBAC: role→permission grants are editable ONLY by the super-user via `GET /roles`, `GET /permissions`, `PUT /roles/{role}/permissions` (gated by `roles.managePermissions`); the permission **catalog** stays code-seeded — only the grants change; `super-user`/`student` are never editable/assignable; every change calls `forgetCachedPermissions()` and affected sessions pick it up on their next `/me`/login.

**Multi-role UNION model (M1)**
- [ ] [M][BE+FE] Session is the union of all held roles — combined nav + permissions, in ONE session; no active-role selection at login, no in-session switcher, no `X-Session-Role` header anywhere.
- [ ] [M][FE] Effective permissions = `getAllPermissions()` (union); navigation/route resolution is any-of (`filterNavigation`/`requiredRolesForPath`/`homeForRole` resolve against the full `roles[]`).
- [ ] [M][BE+FE] Data scoping uses the MOST PERMISSIVE held role (super-user/supervisor → unrestricted; else teacher → own circles; else parent → own children), computed server-side; never trust the frontend.

**Self-service profile (PR1/D4)**
- [ ] [M][BE+FE] Backend ships `GET /auth/me`, `PATCH /auth/profile`, `POST /auth/password` (Phase 1) with `UpdateProfileRequest`/`ChangePasswordRequest`; all owner-only; profile edits NEVER change roles/permissions/branch/status/linkage; self-editable fields = name, **single phone**, email, photo.
- [ ] [M][BE] `/profile` stays in `PUBLIC_DASHBOARD_PREFIXES` (role-agnostic) by design — any authenticated user, owner-scoped; the "public prefix" is not an escalation hole because the backend authorizes owner-only and rejects privileged fields.

**Wire & data discipline**
- [ ] [M][BE] Resource fields camelCase; pagination `meta`/`links` + list/filter query params snake_case; 422 `errors` keys camelCase (camel→snake mapped in the Action).
- [ ] [M][FE] Lists use `list()` (paginated); reports/aggregates/exports use `listAll()` or a `/reports/*` endpoint — a report page never derives totals from a paginated `list()` (§6).
- [ ] [M][BE] Bulk delete is a single transactional `POST /<resource>/bulk-delete { ids }`; select-all = current page by default with an explicit "select all N matching" affordance.
- [ ] [M][BE+FE] No hardcoded colours/sizes/strings/env values: tokens + `lib/config` + i18n only (Arabic catalogs filled, `en` deferred); every data view has loading/empty/error states; pages stay thin (page → feature → service); no page imports `lib/dummy/*` directly.
- [ ] [M][BE+FE] `/messages/notices` gated as authenticated-only on the backend (do not mirror the FE TEACHING-label quirk); hidden routes are feature-flag-off, not a permission boundary.

**Performance & caching (§14)**
- [ ] [M][FE] Every list is paginated (default 25, cap 100); only the deliberate `listAll()`/report path is unbounded.
- [ ] [M][BE] Every endpoint eager-loads every serialized relation (no N+1; `Model::shouldBeStrict()` in non-prod); select only needed columns; index every filtered/sorted/FK column.
- [ ] [L][BE] Rarely-changing data (public CMS + lookup tables) is cached (`Cache::remember` with explicit namespaced keys — the `database` driver has no tag support) and invalidated on write via `Cache::forget(key)`; public reads send `Cache-Control`/`ETag`; per-user/role-scoped/frequently-changing lists are NEVER cached.
- [ ] [M][BE+FE] A screen needing several models calls ONE composite/aggregate endpoint, not N round-trips.

> **Design notes / rationale carried from roadmap §5.8 + §10 (non-task context for executors):**
> - **Parent credential (P1) — contradiction resolved:** the CLAUDE.md §6 vs §7 ambiguity is pinned to "the **server Action** issues AND hashes the parent password **at approval**" (`ApprovePendingStudentAction`), returned once. The FE stops generating `autoPassword`; "store as-is" is deleted.
> - **CSRF:** not required for PAT-Bearer auth; only relevant if you ever switch to the stateful-cookie flow (CORS already supports credentials) — then only `http-client`/Sanctum config change.
> - **Token in `localStorage` (XSS):** acceptable for PAT-Bearer; mitigate with a strict CSP and never place tokens in URLs.
> - **Spatie permission cache:** call `forgetCachedPermissions()` after seeding/role/permission changes (24h cache), and use `firstOrCreate` everywhere for idempotency.
> - **Homepage/landing owner content (decision — RATIFIED):** the public homepage owner/landing section is **DB-backed via the landing-sections Content CMS (admin/super-admin managed)** — the homepage OWNER block, hero, "why us", FAQ, CTA, and footer are stored in the Content CMS and the public landing page renders them LIVE from the public read endpoint (it is NOT static frontend content). See the Phase 5 Content CMS build items (10–14); never hardcode dynamic copy.

**Global verification gates (run before claiming any task done)**
- [ ] [M][FE] Frontend: `npx tsc --noEmit` 0, `eslint` clean, `vitest` green, `next build` exit 0 (no `MISSING_MESSAGE`).
- [ ] [M][BE] Backend: `php artisan test` green, `pint --test` clean, `php artisan migrate:fresh --seed` idempotent.