# Al-Azeem API Reference

For the mobile app. Covers everything under `routes/api.php`: login and the
daily checklist endpoints. See [CLAUDE.md](../CLAUDE.md) for the domain
background (Oracle login source, checklist schema, known gaps).

## Conventions

- Base path: `/api`
- All requests: `Accept: application/json`. Bodies are JSON
  (`Content-Type: application/json`).
- Auth: Bearer token (Laravel Sanctum). Every endpoint except `POST /login`
  requires `Authorization: Bearer {token}`.
- Missing/invalid token → `401`, body `{"message": "Unauthenticated."}`.
- Validation and business-rule errors both come back `422` in the same
  shape (Laravel's `ValidationException`):

  ```json
  {
    "message": "The employee_id field is required.",
    "errors": {
      "employee_id": ["The employee_id field is required."]
    }
  }
  ```

  Business-rule rejections (e.g. "already submitted") use this same shape
  with the key `submission` instead of a field name — see each endpoint.
- **Gotcha — two different date formats for the same field.** Hand-built
  JSON responses (`GET /today`, `GET /submissions/{id}`) render
  `submission_date` as a bare date: `"2026-09-22"`. Responses that serialize
  the Eloquent model directly (`POST /submit`, `POST /verify`, `GET
  /submissions` list) render it as a full ISO-8601 timestamp:
  `"2026-09-22T00:00:00.000000Z"` (the `date` cast stores a `00:00:00` time
  component — see CLAUDE.md). Same logical value, two shapes depending on
  endpoint. Parse both as a date on the client.

---

## Auth

### `POST /api/login`

No auth required. Rate-limited: 6 requests/minute per IP.

Authenticates against the live Oracle `UM_USERS` table directly (not
against local data). See [CLAUDE.md](../CLAUDE.md) — `U_PASS` is plain
text there.

**Request body**

| Field | Type | Required |
|---|---|---|
| `employee_id` | string | yes |
| `password` | string | yes |

**Success — `200`**

```json
{
  "token": "1|nUVSKghMFwROfxTQgfKdnKtyJWCVGRnHUycPkUeR740df855",
  "employee": {
    "id": 1,
    "employee_id": "138"
  }
}
```

Use `token` as the Bearer token on every subsequent request.

**Errors**

- `422` — missing `employee_id`/`password`, or wrong credentials:
  ```json
  {
    "message": "The provided credentials are incorrect.",
    "errors": { "employee_id": ["The provided credentials are incorrect."] }
  }
  ```
  (Deliberately doesn't distinguish "unknown employee" from "wrong
  password".)
- `429` — rate limited.

---

### `POST /api/logout`

Revokes the token used on the request. **`204` No Content**, no body.

---

### `GET /api/user`

Returns the authenticated employee.

**Success — `200`**

```json
{
  "id": 1,
  "oracle_user_id": 39,
  "employee_id": "138",
  "role": "caretaker",
  "last_login_at": "2026-09-22T18:37:24.000000Z",
  "created_at": "2026-09-22T18:37:25.000000Z",
  "updated_at": "2026-09-22T18:37:25.000000Z"
}
```

`role` is `null` until it's been set — see **Known limitations** below.

---

## Checklists

Every checklist endpoint operates on the **caller's own** daily checklist,
resolved from `employee.role` — there's no `{employee}` in any of these
URLs.

### `GET /api/checklists/today`

Returns the caller's checklist template plus today's submission and every
item's current completion state. Creates today's submission (and a blank
completion row per item) on first call of the day; safe to call repeatedly.

**Success — `200`**

```json
{
  "submission": {
    "id": 1,
    "status": "in_progress",
    "submission_date": "2026-09-22",
    "staff_note": null,
    "submitted_at": null,
    "verified_at": null,
    "verifier_note": null
  },
  "template": {
    "role": "caretaker",
    "name": "Sanctuary Caretaker Hour-by-Hour Operational Checklist",
    "reports_to": "Sanctuary In-Charge",
    "evidence_options": ["whatsapp", "salesforce"],
    "staff_signoff_label": "Caretaker Signature",
    "supervisor_signoff_label": "Sanctuary In-Charge Verification",
    "sections": [
      {
        "id": 7,
        "title": "1. Biometric Attendance, Staff Hygiene & Entry Non-Negotiables",
        "items": [
          {
            "id": 20,
            "time_label": "Pre-Shift",
            "task": "Personal Hygiene: Shower with soap & shampoo, apply deodorant, wear clean uniform, fresh vest, and fresh underwear daily.",
            "completion": {
              "is_done": false,
              "evidence": [],
              "note": null,
              "completed_at": null
            }
          }
        ]
      }
    ]
  }
}
```

`template.evidence_options` tells the client which evidence checkboxes to
render for every item in this checklist — always one of:
- `["whatsapp", "salesforce"]` — Caretaker, Sweeper
- `["physical_log", "verified"]` — In-Charge, Security Guard

**Errors**

- `404` — the employee's `role` has no matching template (usually means
  `role` was never set):
  ```json
  { "message": "No checklist template is configured for role []." }
  ```

---

### `PUT /api/checklists/items/{item}`

Update one item's completion within **today's** submission. `{item}` is a
`ChecklistItem` id (from the `today`/`show` response).

**Request body**

| Field | Type | Required | Notes |
|---|---|---|---|
| `is_done` | boolean | yes | |
| `evidence` | array of string | no | Each value must be one of the template's `evidence_options`. |
| `note` | string, nullable | no | |

**Success — `200`** — the updated completion row:

```json
{
  "id": 1,
  "submission_id": 1,
  "item_id": 20,
  "is_done": true,
  "evidence": ["whatsapp"],
  "note": "done at shift start",
  "completed_at": "2026-09-22T19:01:58.000000Z",
  "created_at": "2026-09-22T19:00:08.000000Z",
  "updated_at": "2026-09-22T19:01:58.000000Z"
}
```

`completed_at` is set to the server time when `is_done: true`, and cleared
(`null`) when unchecked.

**Errors**

- `422` — invalid `evidence` value for this role's template:
  ```json
  {
    "message": "The selected evidence.0 is invalid.",
    "errors": { "evidence.0": ["The selected evidence.0 is invalid."] }
  }
  ```
- `422` — the submission is no longer editable:
  ```json
  {
    "message": "This checklist has already been submitted and can no longer be edited.",
    "errors": { "submission": ["This checklist has already been submitted and can no longer be edited."] }
  }
  ```
  (`status` in the message is whichever of `submitted`/`verified` it
  currently is.)
- `404` — no submission exists yet for today (call `GET /today` first), or
  `{item}` doesn't belong to the caller's own template.

---

### `POST /api/checklists/submit`

Staff sign-off: locks today's submission and moves it to `submitted`, ready
for a supervisor to verify. Doesn't require every item to be `is_done` —
matches the paper form, where not every task applies every day (e.g. "As
Needed", "2x / Week").

**Request body**

| Field | Type | Required |
|---|---|---|
| `staff_note` | string, nullable | no |

**Success — `200`** — the updated submission:

```json
{
  "id": 1,
  "template_id": 2,
  "employee_id": 1,
  "submission_date": "2026-09-22T00:00:00.000000Z",
  "status": "submitted",
  "staff_note": "All morning tasks logged.",
  "submitted_at": "2026-09-22T19:02:08.000000Z",
  "verified_by_employee_id": null,
  "verified_at": null,
  "verifier_note": null,
  "created_at": "2026-09-22T19:00:08.000000Z",
  "updated_at": "2026-09-22T19:02:08.000000Z"
}
```

**Errors**

- `422` — already submitted/verified (same shape as above).
- `404` — no submission exists yet for today.

---

### `GET /api/checklists/submissions`

List submissions — defaults to the caller's own history, most recent day
first, paginated (20/page, Laravel's standard paginator envelope).

**Query params**

| Param | Type | Notes |
|---|---|---|
| `status` | string | Filter to `in_progress`, `submitted`, or `verified`. E.g. `?status=submitted` for a "pending verification" queue. |
| `all` | boolean | If truthy, returns **every** employee's submissions, not just the caller's. ⚠️ Not permission-checked — see Known limitations. |

**Success — `200`**

```json
{
  "data": [
    {
      "id": 1,
      "template_id": 2,
      "employee_id": 1,
      "submission_date": "2026-09-22T00:00:00.000000Z",
      "status": "verified",
      "staff_note": "All morning tasks logged.",
      "submitted_at": "2026-09-22T19:02:08.000000Z",
      "verified_by_employee_id": 2,
      "verified_at": "2026-09-22T19:02:08.000000Z",
      "verifier_note": "Spot-checked, looks good.",
      "created_at": "2026-09-22T19:00:08.000000Z",
      "updated_at": "2026-09-22T19:02:08.000000Z",
      "template": { "id": 2, "role": "caretaker", "...": "..." },
      "employee": { "id": 1, "employee_id": "138", "...": "..." }
    }
  ],
  "links": { "first": "...", "last": "...", "prev": null, "next": null },
  "meta": { "current_page": 1, "last_page": 1, "per_page": 20, "total": 1 }
}
```

This list view does **not** include sections/items/completions — for the
full checklist content of one submission, use `GET
/submissions/{id}`.

---

### `GET /api/checklists/submissions/{submission}`

Same response shape as `GET /today`, but for a specific past (or
in-progress) submission by id — full template + sections + items +
completions as they stood for that submission. ⚠️ Not ownership-checked —
see Known limitations.

**Errors**

- `404` — no submission with that id.

---

### `POST /api/checklists/submissions/{submission}/verify`

Supervisor sign-off: moves a `submitted` submission to `verified`.

**Request body**

| Field | Type | Required |
|---|---|---|
| `verifier_note` | string, nullable | no |

**Success — `200`** — the updated submission (same shape as `POST
/submit`'s response, with `status: "verified"`, `verified_by_employee_id`,
`verified_at`, `verifier_note` populated).

**Errors**

- `422` — not yet submitted:
  ```json
  {
    "message": "Only a submitted checklist can be verified.",
    "errors": { "submission": ["Only a submitted checklist can be verified."] }
  }
  ```
- `422` — the caller is the same employee who submitted it:
  ```json
  {
    "message": "You cannot verify your own checklist.",
    "errors": { "submission": ["You cannot verify your own checklist."] }
  }
  ```
- `404` — no submission with that id.

---

## Known limitations

Carried over from [CLAUDE.md](../CLAUDE.md) — worth the mobile client
knowing about, not just the backend:

- **No role hierarchy.** `verify()` only blocks an employee from verifying
  *their own* submission. It doesn't check that the verifier actually holds
  a supervisory role over the submitter (e.g. nothing stops a Sweeper from
  verifying a Security Guard's checklist, even though the paper process
  reserves that for Admin HOD). Don't build a client-side assumption that
  only supervisors can call this endpoint.
- **`GET /submissions/{id}` and `?all=1` on the list endpoint are not
  ownership-checked.** Any authenticated employee can read any other
  employee's submissions, including `staff_note`/`verifier_note`. Fine for
  an internal-staff app; don't reuse this endpoint shape somewhere that
  needs per-employee privacy.
- **`employees.role` is not set anywhere automatically** — including by
  `POST /login`. It has to be set manually (currently only via `tinker` on
  the backend) before `GET /checklists/today` will return anything but
  `404`. If the mobile app needs to handle "my account has no role yet",
  that 404 is the signal.
