# Daily Zone Check-in

Workstream 16 — campus staff log a daily self-regulation "zone" (Zones of
Regulation: blue/green/yellow/red). History is retained per day, and an eligible
user who has not checked in today is nudged on the dashboard, the
`/zones-of-regulation` page, and in the notification dropdown.

## "Today" is server-computed in the campus timezone

The check-in date is **not** decided by the client. Each campus carries a
required IANA `timezone` (`campuses.timezone`); the service computes the
campus-local date (`localDateInTimezone`, native `Intl`, DST-correct) so "today"
is independent of the caller's device clock/zone and correct across
organizations, campuses, and timezones.

## Storage (reuses `user_progress`)

No new table. A check-in is a `user_progress` row with
`progress_type = zone_checkin` and `item_id` = the campus-local date
(`YYYY-MM-DD`), `value` = the zone color. Because the per-user upsert key is
`(userId, progress_type, item_id)`, this yields **one row per user per day**, and
the set of rows is the history (`item_id` sorts chronologically). The thin
`services/zone-checkin.ts` wraps `UserProgressService` and owns the timezone/date
logic, keeping the generic `user_progress` endpoint generic.

## Routes (`/api/zone_checkins`)

All require explicit `ZONE_CHECKIN`; `globalAccess` alone does not imply this
personal workflow permission.

- `GET /today` → `{ date, zone, isCheckedInToday }` (campus-local date).
- `POST /` → record today's zone. Body `{ data: { zone } }` (blue|green|yellow|red).
  If the caller is a parent-scope user acting through a drilled child scope, the request is accepted
  as a no-op and returns today's existing personal state without saving a new row.
- `DELETE /today` → clear today's check-in.
- `GET /?from=&to=` → the caller's history (`{ rows: [{ date, zone }], count }`).

## Authorization

- `ZONE_CHECKIN` is seeded for `director`, `office_manager`, `teacher`, and
  `support_staff`. Other users can receive or lose it only through effective
  permissions (`custom_permissions` / `custom_permissions_filter`). The frontend
  and backend both gate this workflow by permission, not by role name.
  Reads/writes are scoped to the caller's own `userId` by `UserProgressService`.
- Check-ins persist only when the active scope is the user's own scope. Parent users drilled into a
  child school/campus/classroom do not see the personal check-in UI there, and the backend does not
  create reportable `user_progress` rows for that child scope.
- A user with no campus has no campus-local "today" — the service rejects with a
  validation error (only campus staff reach these routes).

## Tests

- **Unit** (`npm test`): `shared/constants/timezone.test.ts` (campus-local date
  incl. Phoenix no-DST + a DST zone; `isValidIanaTimezone`) and
  `shared/constants/zone-checkin.test.ts` (`isZoneCheckinColor`).
- **Frontend unit** (`vitest`): `business/zone-checkin/selectors.test.ts`
  (eligibility + nudge) and the top-bar notification builder (incl. the zones
  `href`).
- **Seeded e2e** (`frontend/tests/e2e/zone-checkins.seeded.e2e.ts`,
  `npm run test:e2e:content`): a campus-staff record/read-back/clear of today's
  zone, invalid-zone rejection, and external-role lockout.

## Open / deferred

- A manager-facing aggregate (campus self-regulation trends across staff) would
  need a cross-user report endpoint (`user_progress` is self-scoped) — deferred.
- Editing a campus `timezone` is part of the (design-gated) campus admin UI;
  for now it is seeded and validated at the API.
