# Verify: Attendance settings · spec 0003 · updated 2026-10-27
_Steps derived from spec 0003 acceptance criteria. `/check verify` runs these; `/test` locks the durable ones._

Use a school that already has the 0001 tables and a settings row `id = 1`. Sign in as admin, director, or backend (or staff with status admin). Open the live app at `https://design-changes.zap.ng/app/admin/`.

## UI / manual

- [x] Staff attendance nav group has two tiles for allowed roles: Locations (`href` `staff_attendance_locations.php`, icon `icon/attendance.png`, subtitle Named places and geofence radius) and Settings (`href` `staff_attendance_settings.php`, icon `icon/staff.png`, subtitle Late time, GPS, check out, face match). Locations stays selected on location edit. → AC-2, AC-3
- [x] Allowed roles can open `staff_attendance_settings.php`. Access is `staff_attendance_can_access`. → AC-1
- [x] Location edit toolbar back is `href` `staff_attendance_locations.php`. Locations list and Settings keep the history back control (`data-back-button`). → AC-4, AC-5
- [x] Settings GET fills from `SELECT id = 1`: late after shows `HH:MM`, GPS, minutes, checkout, `updated_at` as Lagos `j M Y, H:i`. Attendance and Location groups are present. There is no Face group and no percent input. Help lines match AC-5. Branch picker only when the switch list has more than one school; current school is checked and disabled. → AC-5
- [x] Missing 0001 tables shows `The attendance tables are missing.` Missing `id = 1` shows `The settings row is missing.` Both hide Save and Reset. Not the fatal retry screen. → AC-5, AC-7
- [x] Save stays on Settings, toasts `Settings saved.`, returns the current school row from `SELECT` `id = 1` after the `UPDATE` (stored integer percent, not a posted percent), stored fields, formatted `updated_at`, fresh `csrf_token`, and `branches[]`. Posted `face_match_percent` is silent success. Save and Reset disable until JSON returns; re-enable on fetch fail or a non JSON body. Current school success still toasts if an extra school fails. Current school fail skips extras, skips the success toast, and `notify`s the `msg` as error. → AC-6, AC-12
- [x] GPS `5`, empty checkout, and minutes `481` are refused (HTTP 200 `{status:error,msg}`), nothing written. Posted `face_match_percent` is ignored. `late_after` `08:30` stores `08:30:00` with no timezone conversion. → AC-7
- [x] Checkout off: visible minutes stay on screen, disabled, no `name`; hidden `min_checkout_minutes` holds the last value and is still saved. Never both named at once. → AC-8
- [x] Reset confirm wording: `This will set this school (and any checked schools) back to the original defaults.` Reset writes GPS `50`, checkout on, minutes `30`, and `08:00:00` plus PHP Lagos `updated_at`, leaves `face_match_threshold` unchanged, ignores posted field values, toasts `Settings reset to defaults.` → AC-9
- [x] Extra schools: picker from `$school_config['schools']`; current school always written via the session database and not sent as an extra; a `school_id` not on the list aborts before any write with `That school is not on your switch list.`; client never sends `db_name`; extra writes `SELECT` then `UPDATE` `id = 1`; partial extra success is kept; session database is restored after extras. `branches[].status` is `ok`, `missing_tables`, `missing_row`, or `error`. Flash uses `sas_branch_flash`. → AC-10, AC-12
- [x] flatpickr 4.6.13 with SRI posts `HH:MM` (`dateFormat` `H:i`). If the script is blocked, native `input type=time` `step=60` still saves `08:30:00`. → AC-11

## Commands

- [x] `SELECT id, face_match_threshold, max_gps_accuracy_m, allow_checkout, min_checkout_minutes, late_after, updated_at FROM staff_attendance_settings WHERE id = 1` after save: `face_match_threshold` is unchanged, late is `HH:MM:00`, `updated_at` is PHP Africa/Lagos, no `INSERT`, no second row. → AC-6, AC-7, AC-12
- [x] After reset, GPS, checkout, minutes, and late after are the 0001 seed plus a new `updated_at`, and `face_match_threshold` is still the value it had before reset. → AC-9
- [x] Dead session POST `action=save` returns HTTP 401 `{status:error, msg:unauthorized}` (JSON, not an HTML login page). Bad CSRF returns HTTP 403 `{status:error, msg:forbidden}`. → AC-12

## Value sourcing

- [x] Settings who may open it comes from `staff_attendance_can_access` (`table_name` admin/director/backend or `$info['status']` admin), not from hiding a tile. → AC-1
- [x] Settings form fields come from `SELECT` `staff_attendance_settings` `WHERE id = 1` in the session database. → AC-5, AC-12
- [x] JSON percent is `(int) round((float) $threshold * 100)` from `SELECT` `id = 1` after the `UPDATE` on the current school. Posted `face_match_percent` is ignored (silent success). Save and reset do not write `face_match_threshold`. Do not clamp an out of range stored value. → AC-6, AC-7, AC-9
- [x] GPS stored is POST matching `^[1-9][0-9]*$` then int 10 to 200 (refuse, not clamp). → AC-7
- [x] Minutes stored is POST matching `^(0|[1-9][0-9]*)$` then int 0 to 480. → AC-7
- [x] Checkout stored is POST exactly `0` or `1` (empty is an error). → AC-7, AC-8
- [x] `late_after` stored is hour 0 to 23 and minutes 00 to 59, optional seconds ignored, `sprintf('%02d:%02d:00')`, Lagos wall clock, not converted. → AC-7, AC-11
- [x] `updated_at` is PHP `date('Y-m-d H:i:s')` with boot timezone Africa/Lagos; display is `date('j M Y, H:i', strtotime($row['updated_at']))`. → AC-5, AC-6
- [x] Extra school databases come from POST `school_ids[]` intersected with `$school_config['schools']`; drop the current school id. → AC-10
- [x] Reset knobs for GPS, checkout, minutes, and late after come from spec 0001 seed constants, not from POST fields. `face_match_threshold` is omitted from the `UPDATE`. → AC-9
- [x] JSON uses `JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT`; live session is `isset($_SESSION['authenticated']) || isset($_SESSION['zap_auth'])`. → AC-12
- [x] flatpickr assets are jsDelivr `flatpickr@4.6.13` plus SRI copied at build. → AC-11

## Acceptance-criteria coverage

- AC-1 covered by settings role gate via `staff_attendance_can_access`
- AC-2 covered by the two Staff attendance nav tiles
- AC-3 covered by no hub page, two nav tiles only
- AC-4 covered by location edit back href
- AC-5 covered by settings GET, help lines, history back, and missing tables/row steps
- AC-6 covered by save stay/toast/JSON step
- AC-7 covered by refuse ranges and SELECT after save
- AC-8 covered by checkout switch minutes naming step
- AC-9 covered by reset confirm and seed SELECT
- AC-10 covered by extra school picker and off-list abort
- AC-11 covered by flatpickr and native time fallback
- AC-12 covered by tenant SELECT, CSRF/session JSON, engine contract
