docs(clock): design daily shifts and missed scan review

This commit is contained in:
DEls 2026-09-25 01:50:25 +02:00
parent 47ec4f1d1b
commit d694dde501

View file

@ -0,0 +1,52 @@
# MEGA Clock daily shifts and missed scan-out review
Date: 2026-09-25
Status: Written design for user review
## Goal and agreed behavior
The Clock App must treat each South African calendar day as its own attendance day. A worker whose last scan was an unmatched `in` on an earlier day must be able to scan **in** on the current day. The earlier shift remains a visible exception until a supervisor records the actual out date and time and explains the missed scan-out. Resolving that exception calculates the earlier day's hours once and locks the correction.
The user approved the four-column on-site board, a separate new-day clock-in, and midnight in `Africa/Johannesburg` as the day boundary. This means a scan after local midnight starts a new day's shift even if an earlier shift remains open. Any legitimate overnight shift must be closed through the supervised exception flow; the scanner must not silently extend it across days.
The immediate live case is seven unmatched 2026-09-23 `in` events at The Hills ext 12. They must appear in the attention queue after migration without inventing an out time or writing hours for an unresolved shift.
## Current behavior that changes
`/api/clock/employee-lookup` and `/api/clock/scan` now use the employee's latest event across all dates, so an unmatched `in` makes the next scan an `out`. The latter calculates every elapsed hour since that old scan and attributes it to the start date. The on-site board uses only each employee's latest event in a seven-day window, so the old exception can disappear once a new scan arrives or the window expires. The dashboard PIN is client-side and does not identify a supervisor.
## Operator flow
1. On `/clock`, scanning a badge shows the worker and the action before writing. The action comes from that worker's **current local day's** shift state: an open shift today means `Clock OUT`; otherwise it means `Clock IN`. If an older shift is unresolved, the confirmation also says that a prior day needs review, but today's scan proceeds independently.
2. The server decides the action again at submission time. A stale confirmation produces `409` and asks for a fresh scan; it never silently writes the opposite direction. One confirmed scan creates one raw `clock_events` row and one corresponding shift transition. A retry or concurrent submission cannot create a duplicate shift or count hours twice.
3. `/dashboard/workforce/on-site` shows four columns: **On site now**, **Clocked out today**, **Not arrived today**, and **Needs attention**. The first three describe activity on the current local day; an employee appears in exactly one of them according to their latest shift today. The attention column contains every unresolved shift from an earlier local day, plus a corrected shift whose payroll sync is pending or in conflict, without a seven-day expiry. One person may appear both on site today and in attention for an earlier day. The site filter applies to all four columns.
4. An attention card shows the worker, badge, site, original in date/time, and how long the exception has been open. Clicking it opens a correction form with the actual out date, actual out time, and required free-text reason. The form displays the calculated session hours before final confirmation. It makes clear that the original scan is retained.
5. A named supervisor signs in for the correction. Submission is rejected if the shift was already resolved, if the selected out instant precedes its in, or if it overlaps that worker's next shift. The server uses `Africa/Johannesburg` to interpret the entered date/time, stores a UTC instant, and attributes the hours to the local date of the original in. A closed correction cannot be edited through this flow; any later amendment needs a separate, audited process.
6. After a successful correction, the card leaves Needs attention. The timesheet projection for the original day includes the corrected session exactly once; today's shift and its eventual out remain independent.
## Data model
Add a durable `clock_shifts` collection as the source of truth for scan pairing and exception state. Each row has `employee`, `site` (both stamped at clock-in), `clock_in_event` (required, unique relation to `clock_events`), `clock_in_at` (UTC), `local_work_date` (`YYYY-MM-DD` in `Africa/Johannesburg`), optional `clock_out_event`, optional `clock_out_at`, `close_source` (`scan` or `supervisor`), optional `timesheet_entry`, and `payroll_status` (`open`, `pending`, `synced`, or `conflict`). An open row has no out time. An open row whose local work date precedes today is an attention item. Multiple sessions on one day remain possible for lunch breaks.
Add an append-only `clock_shift_corrections` collection with a unique `shift` relation, `actual_out_at`, `reason`, `supervisor_user`, and `recorded_at`. The original `clock_events` records are never rewritten or deleted. A supervised close records its actual out instant on the shift and a correction record; it does **not** pretend that a QR scan occurred at that time. Normal scan-outs continue to create raw `out` events. New collections deny direct browser writes; only narrow server services can change them.
`timesheet_entries` remains the existing payroll-facing daily total. Closed shifts for an employee and local work date determine the QR/direct total. Round each session to two decimals, then recompute the daily total from closed shifts when a shift closes, rather than adding the new duration to the previous value; this makes retries safe. Store `date_logged` as UTC midnight for the **South African local work date**, preserving the existing date-only representation. Existing imported timesheet entries are never altered. If an existing direct row cannot be tied unambiguously to the clock history, flag it as a `conflict` instead of merging by guesswork. A projection failure leaves `payroll_status=pending`, visible to the supervisor and retryable from persisted shift state. A correction leaves the attention queue only when its payroll status is `synced`; `conflict` and `pending` remain visible with the reason.
## Authentication and audit
The existing shared dashboard PIN and `?demo=1` bypass are not authorization for payroll correction. The correction API requires a server-verified PocketBase `users` session for a named supervisor. Add explicit `role` and allowed-site assignments to those users; do not infer either from the shared PIN. Store the session in a secure, HttpOnly, SameSite cookie and check the role, origin, and site permission on every correction. The server uses its admin client only after those checks. Provision named supervisor accounts and roles before enabling the correction UI; the one existing user account does not automatically gain that role.
The Clock App's existing scan route must remain a narrow employee-number operation. Its current client-only PIN protection is a known gap; the implementation plan must include a server-side kiosk credential/session for scan writes without adding a login step for every worker. Neither scan nor correction exposes the PocketBase admin token to the browser.
## Migration and rollout
1. Build a default **dry-run** migration that sorts existing `clock_events` by employee and scan time and pairs normal `in`/`out` events into shifts. It preserves the original event IDs, timesheet links, and totals. Consecutive `in` events, orphan `out` events, ambiguous timesheet links, duplicate direct entries, overlaps, and dates where the old UTC day differs from the South African local day are reported rather than guessed. Historical closed-shift totals are not rewritten during backfill. Unique `clock_in_event` makes reruns idempotent.
2. Compare dry-run totals with the live event and timesheet counts. Specifically confirm that the seven 2026-09-23 open scans become seven unresolved shifts and that no synthetic hours are created. Back up the relevant collections before an applied migration.
3. Deploy schema, backfill, scan logic, correction service, and dashboard in a coordinated rollout. The schema is additive; if the app must be rolled back, keep the new collections and original events intact, and revert only the app while scans are paused for workers with open earlier-day shifts. Until the new scan rule is live, operators must cancel a confirmation that says `Clock OUT` for one of the seven and record that arrival separately. No production correction or migration is authorized by approval of this design alone.
4. After rollout, read back the live scanner lookup for all seven: each must show `Clock IN` on a new local day. Check that the board shows seven attention cards and zero false on-site rows before any new scans. Verify one supervised correction only with a real, confirmed out time and reason, then read back its shift, audit row, and daily total.
## Required verification
- Unit and route tests cover the 23:59/00:00 South African boundary, prior-day open shift plus current-day in/out, repeated scans, stale lookup confirmation, duplicate requests, a worker in both current and attention columns, and exceptions older than seven days.
- Correction tests cover missing/unauthorized supervisor, missing reason, invalid or overlapping out time, stale/already-closed shift, retry after a partial projection failure, one-time total calculation, and preservation of imported timesheet rows.
- A local browser pass covers `/clock` confirmation and the four-column `/dashboard/workforce/on-site` board, including the correction form and site filter. Build/test success alone does not establish that the deployed PWA or production migration behaves correctly.