# KPOD Digitization Monitor — Backend Plan

Laravel 12 API backing the `KPOD-FrontEnd` React app (`../KPOD-FrontEnd`).

The frontend is currently a self-contained prototype: all state lives in
`src/store/appReducer.ts`, all data comes from `src/data/seed.ts`, and auth is
mocked in `src/store/AuthProvider.tsx`. **There is no HTTP client in the
frontend at all.** This backend replaces those three with a real API, keeping
the exact same domain vocabulary so the port is a mechanical swap.

## Domain glossary (from the frontend)

| Frontend concept | Source | Backend home |
| --- | --- | --- |
| Programme / project | `state.projects` | `programs` table |
| Site (`DOC` \| `3D`) | `Site` in `types/index.ts` | `sites` table |
| Standard task template | `STD_TASKS` in `data/constants.ts` | `task_templates` table |
| Task on a site | `Task` | `tasks` table |
| Site operations totals (`site.ops`) | `SiteOps` | `site_metrics` table |
| Ops log / task log (`opsLog`, `task.log`) | `LogEntry` | `log_entries` table |
| Gallery (`galleryCount`) | `SiteGallery.tsx` | `site_images` table |
| Role (`viewer`/`admin`/`super`) | `utils/permissions.ts` | spatie role |
| Capability (`task.write`, …) | `Capability` | spatie permission |

Metric registry (`M` in `constants.ts`) has three modes that the backend must
honour on write: `add` cumulates, `set` overwrites, `site` flips a readiness
flag on the site.

---

## Phase 0 — Foundation

- [x] `ApiResponse` util, `EnforceJsonResponseForApiRequests` middleware, `ServiceException`, `lang/{en,ar}/messages.php` (ported from cash-move-backend)
- [x] `bootstrap/app.php` wired: api routing at `/api/v1`, middleware appended, exception renders
- [x] Create the `kpod_backend` MySQL database
- [x] Install `laravel/sanctum` for token auth
- [x] CORS config for the Vite dev origin (`CORS_ALLOWED_ORIGINS`, defaults to `:5173`)
- [x] Domain enums: `SiteTypeEnum`, `StatusEnum`, `TaskKindEnum`, `MetricEnum`, `MetricModeEnum`, `RoleEnum`, `PermissionEnum`
- [x] `HandlesSorting` trait already in place; `config/kpod.php` for programme constants

## Phase 1 — Auth & RBAC

- [x] Migration: `is_active`, `last_login_at` on `users`
- [x] Seed spatie roles `viewer` / `admin` / `super` and the 7 capabilities from `utils/permissions.ts`
- [x] `POST /auth/login` → token + user + role + capabilities (mirrors `signIn`)
- [x] `POST /auth/logout` (mirrors `signOut`)
- [x] `GET /auth/me` — session rehydration, replaces `readStoredSession`
- [x] `POST /auth/forgot-password` (mirrors `requestReset`)
- [x] `POST /auth/reset-password`
- [x] Seed demo users `admin@kpod.iq` / `viewer@kpod.iq` (password `kpod1234`) to match `DEMO_USERS`, plus `super@kpod.iq` (the prototype fakes Super client-side, so it has no counterpart)
- [x] Every API failure rendered into the envelope: 401, 403, 404, 409, 422, 429

## Phase 2 — Domain schema

- [x] `programs` — name, slug, is_active
- [x] `sites` — program_id, code (`NOC-STN`), name, type, lat, lng, area, tier, priority, is_clean, is_accessible, description, last_updated_on
- [x] `task_templates` — site_type, key, name, kind, sub, metrics(json), set_metrics(json), options(json), stages(json), auto_recv, position, is_custom
- [x] `tasks` — site_id, task_template_id, key, name, kind, status, planned/actual start+end, description, choice_value, stage_status(json), attach_link, attach_file, position
- [x] `site_metrics` — site_id, metric, value (the `site.ops` map)
- [x] `log_entries` — site_id, task_id, metric, value, value_label, doc_type, logged_on, user_id
- [x] `site_images` — site_id, path, caption, position
- [x] Eloquent models + relations + casts for all of the above

## Phase 3 — Derived metrics (server-side port of `utils/calc.ts`)

- [x] `Support\Num` — `pct`, `pctOrNull`, `sum`, `avg`, `nf` from `utils/math.ts` (rounding has to match exactly)
- [x] `SiteMetricsService`: `overall`, `ready`, `currentTask`, `r2sDone`, `displayStatus`, `needsAttention`, `sitePct`, `deliveryAvg`, `deliveryItems`, `opsBoxes`
- [x] `OpsTotalsService`: `opsTotals`, `opsSnapshot`
- [x] `BoardMetricsService`: `boardMetrics` (A1/A2/B1/B2, chains, lag), `documentFunnel`, `funnelMax`, `rankSites`
- [x] `KpiService`: `buildKpis` (6 Program Management cards) + `statusBreakdown`
- [x] **Verified against the prototype**: every board metric and all six KPI cards match `utils/calc.ts` run over the same seed data, pinned in `DashboardTest`

## Phase 4 — Read endpoints

- [x] `GET /programs`
- [x] `GET /sites` — filters `type`, `prio`, `stage`, `search` (mirrors `filterSites`)
- [x] `GET /sites/{code}` — full detail incl. tasks, ops, logs, images
- [x] `GET /task-types` — the `stdTasks` registry, grouped by site type
- [x] `GET /dashboard/executive` — board metrics, funnel, leaderboards, coverage table
- [x] `GET /dashboard/program` — KPI cards + status breakdown + ops snapshot
- [x] Resources: `SiteResource`, `SiteDetailResource`, `TaskResource`, `LogEntryResource`, `TaskTemplateResource`, `ProgramResource`, `UserResource`

**Deviation from the original plan:** `GET /sites` is *not* paginated. The map,
the KPI cards and the status chart all read the same list, so a page of it
would make every one of them wrong. The portfolio is bounded by how many sites
a programme has.

## Phase 5 — Write endpoints

Every one is gated by the capability its reducer case checks.

- [x] `POST /programs` — `program.add`
- [x] `POST /sites` — `site.add` (server-side code generation, mirrors `makeSiteId`)
- [x] `PATCH /sites/{code}` — `site.edit` (priority + description)
- [x] `POST /sites/{code}/images` — `site.addImage`
- [x] `PATCH /sites/{code}/readiness` — `task.write` (clean/access + log entry)
- [x] `PUT /tasks` — `task.write`, the full `task/save` draft incl. multi-site apply
- [x] `PATCH /sites/{code}/tasks/{key}/choice` — `task.write`
- [x] `PATCH /sites/{code}/tasks/{key}/yes-no` — `task.write` (auto-completes the task)
- [x] `PATCH /sites/{code}/tasks/{key}/stages` — `task.write` (rolls up to task status)
- [x] `POST /sites/{code}/ops-log` — `ops.log` (honours add/set/site metric modes)
- [x] `POST /sites/{code}/tasks/{key}/submissions` — `ops.log` (docType submission log)
- [x] `POST /task-types` — `taskType.add`

**Deviations from the original plan**, each with a reason:

- `PUT /tasks` is programme-level, not `PUT /sites/{site}/tasks/{task}` — one
  save can target several sites, which a site-scoped URL cannot express. An
  unknown code fails the whole request; there is no partial apply.
- Site codes are generated per-prefix, not by total site count. `makeSiteId`
  numbers from `state.sites.length`, which collides the moment two sites share
  a three-letter prefix.
- Metrics are checked against the site type. Logging `actScan` on a document
  site is a 422 rather than a silently stored figure that would then skew the
  per-type dashboard totals.
- Custom task types are scoped to one programme, so adding one does not
  reshape every other programme's sites.

## Phase 6 — Seeding

- [x] `Support\DemoRandom` — the mulberry32 PRNG and FNV-1a hash from `utils/math.ts`, ported with 32-bit masking and a hand-rolled `Math.imul`
- [x] Port `SITE_SEEDS` + `createInitialSites()` from `data/seed.ts` so a fresh DB reproduces the exact demo portfolio the prototype shows
- [x] `TaskTemplateSeeder` from `STD_TASKS`
- [x] `DatabaseSeeder` chains: permissions → users → templates → programme + sites
- [x] **Verified**: the PRNG output is byte-identical to the frontend's for all 11 site seeds

## Phase 7 — Tests & docs

- [x] Pest feature tests — **94 passing, 2280 assertions**
  - `AuthTest` (10) — login, capabilities per role, enumeration resistance, suspension, rehydration, revocation
  - `PermissionTest` (24) — every write route refused to a Viewer and to an anonymous caller, Super-only routes refused to an Admin
  - `SiteTest` (14) — filters, derived values, code generation, duplicates, readiness, gallery
  - `TaskTest` (14) — choice/yes-no/stage rules, single- and multi-site save, open-text tasks, set-metrics, readiness write-back
  - `OpsLogTest` (10) — add/set/site metric modes, dual logging, type mismatches, submissions
  - `DashboardTest` (9) — board metrics and KPI cards pinned to the prototype's own output
  - `ProgramTest` (9) — programmes, task-type registry, per-programme scoping
  - `DemoRandomTest` (unit, 4) — PRNG pinned to golden values captured from the frontend
- [x] `docs/API.md` — endpoint reference for the frontend port
- [x] `docs/FRONTEND_INTEGRATION.md` — how to swap `appReducer` dispatches for API calls
- [x] Pint clean
- [x] Smoke-tested over HTTP against MySQL: login → token, `GET /sites`, executive board, a live `ops-log` write recalculating A1, and a Viewer 403

## Not built (out of scope so far)

- Real image uploads — `site_images` rows exist with a nullable `path` and
  `POST /sites/{code}/images` creates a slot, matching the prototype's
  placeholder behaviour. Wiring multipart upload onto that column is the
  remaining step.
- Password-reset email templating — the broker is wired and tested; it sends
  Laravel's default notification.
- The `stages` task kind is fully implemented but no seeded template uses it;
  the prototype's `STD_TASKS` has none either.

---

## Conventions

- Every response goes through `App\Utils\ApiResponse` — `{status, message, data, errors}`.
- Column-backed list filters live in `app/ModelFilters/` and run via
  `Model::filter($filters)` ([EloquentFilter](https://github.com/Tucker-Eric/EloquentFilter)).
  Filters that depend on a *derived* value — like a site's current task status —
  cannot be where clauses and are applied in PHP instead; say so in the filter
  class so nobody adds a method that would be silently ignored.
- Business-rule failures throw `App\Services\Exceptions\ServiceException`; the
  bootstrap handler renders them into the same envelope.
- Controllers stay thin: validate in a `FormRequest`, delegate to a service.
- Capability checks live in middleware/policies, never inline in a controller body.
