docs: spec + plan for phase 3b.1 manual session entry/edit + admin stop/fix

This commit is contained in:
Bas van Rossem
2026-06-17 22:25:28 +02:00
parent 70ac27ec8e
commit 9d21576014
2 changed files with 407 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
# Phase 3b·1 — Manual Session Entry/Edit + Admin Stop/Fix — Design
- **Created:** 2026-06-17
- **Status:** Approved (brainstorming) — ready for implementation plan
- **Tracker:** Plane (workspace `solelog`, project SoleLog)
- **Cycle:** First of three Phase 3b cycles (then reports/export, then user management)
- **Touches:** `packages/shared`, `apps/api`, `apps/admin`, `apps/worker`
## Goal
An admin can find any work session, **create** a manual one for a worker, **edit/correct** any
session, and **stop or cancel** a worker's stuck active session — the "manual fallback wherever
something fails" from the vision. Because the backend is the source of truth, the **worker
converges** to admin changes (no stuck stopwatch).
_Done when:_ an admin can list/filter all sessions, hand-create a completed session for a worker,
edit any session's fields (server recomputes duration), and stop/cancel a worker's active
session — and within ~15s the worker's stopwatch reflects a stop/cancel done by the admin.
## Scope decisions (confirmed during brainstorming, 2026-06-17)
1. **Slicing:** three sequenced 3b cycles; **this one first** (manual entry/edit + admin
stop/fix), then reports/export, then user management.
2. **Sessions UI:** a full **"Sessies"** admin screen (the all-sessions list deferred from 3a
lands here; the reports cycle reuses it).
3. **Editable fields:** start/end time, activity, insole type, pair count, paused seconds,
notes, status. The worker is chosen **only on create** — no reassignment on edit. Duration is
always **derived** server-side (`end start paused`), never typed.
4. **Worker convergence:** folded into this cycle — the worker polls and reconciles to server
truth (an admin stop/cancel reflects on the phone within ~15s; no stuck state).
## A. Backend (under the existing admin-gated `/api/admin/*` guard in `routes/admin.ts`)
- `GET /api/admin/users``{ id, name, email }[]` from the `user` table (ordered by name), to
populate the create form's worker picker. Direct DB query — no better-auth client dependency.
- `POST /api/admin/sessions` — manual create. Body `CreateManualSessionInput`:
`user_id, activity_id, insole_type, pair_count, start_time, end_time, paused_seconds?, notes?`.
Produces a **completed** session, `source='manual'`, `paused_at=null`,
`duration_seconds = max(0, round((endstart)/1000) paused_seconds)`.
- `PUT /api/admin/sessions/:id` — edit any session. Body `AdminUpdateSessionInput`:
`activity_id, insole_type, pair_count, start_time, end_time(nullable), paused_seconds, notes,
status`. Recomputes `duration_seconds` from times paused when `end_time` is present; when
`status='active'`/`end_time` null, `duration_seconds=null`. No user reassignment.
- `POST /api/admin/sessions/:id/stop` — quick "stop now": fold any open pause, `end=now`,
`status='completed'`, recompute duration.
- `POST /api/admin/sessions/:id/discard``status='discarded'`, `end=now`.
New `@solelog/shared` contracts: `CreateManualSessionInput`, `AdminUpdateSessionInput`. **No DB
migration** — reuses existing `work_sessions` columns (incl. the pause fields). Responses use the
existing `toWorkSession` mapper (so they carry `user_name`/`activity_name` where joined).
### Validation
`end ≥ start`; `pair_count ≥ 1`; `paused_seconds ≥ 0` and `≤ (endstart)`; activity must exist;
user must exist (create); `insole_type` a valid `InsoleType`. Invalid → 400; missing
session/user → 404. No hard delete — cancellation is `status='discarded'` (already excluded from
exports).
## B. Admin UI
- **`components/Sidebar.tsx`** — add `{ to: '/sessies', label: 'Sessies' }` to `navItems`; drop
`'Handmatig'` from the muted `soonItems` (now built → leaves `['Rapporten', 'Gebruikers']`).
- **`App.tsx`** — add `<Route path="/sessies" element={<Sessions />} />`.
- **`screens/Sessions.tsx`** — lists all sessions via `useAllSessions` (`GET /api/admin/sessions`,
newest first), a status filter (alle / actief / voltooid / geannuleerd), and a
`+ Nieuwe registratie` button. Each row: worker · activity · type · worked (+ pauze) · date.
**Active** rows show `[Stop]` `[Annuleer]`; **all** rows show ✎ edit.
- **`components/SessionForm.tsx`** — shared create/edit form: worker picker (create only, from
`useAdminUsers`), activity dropdown, insole-type, pair count, start/end datetime-local, paused,
status (edit only), notes, and a live **"gewerkt"** preview. Submits create or update.
- **`api/admin-sessions.ts`** — add `useAllSessions`, `useAdminUsers`, `useCreateManualSession`,
`useUpdateSession`, `useAdminStopSession`, `useAdminDiscardSession` (all invalidate the
`['admin','sessions']` query family; keep the existing `useActiveSessions`).
- **`screens/Live.tsx`** — add `[Stop]` `[Annuleer]` to each `LiveCard`, wired to the admin
stop/discard hooks (invalidate the active query).
## C. Worker convergence (`apps/worker`)
- **`api/sessions.ts`** — give `useActiveSessions` a `refetchInterval: 15000` (poll), plus the
default refetch-on-window-focus.
- **`screens/Stopwatch.tsx`** — extend the active-session effect to **reconcile**: if a session is
running locally (`sessionId` set) but the latest `activeSessionsQuery.data` no longer contains a
matching **active** session for it, the session was stopped/cancelled elsewhere → reset the
timer and surface a brief notice **"Deze sessie is door de beheerder gestopt."** Also: treat a
**409** from the worker's own stop/discard as already-closed → reset locally instead of erroring.
## Error handling
- Worker reconciliation notice is transient (dismissible / auto-clears on next start).
- Admin form: inline validation errors mirror the API 400s (end before start, count < 1).
- Stop/discard on a non-active session → 409; admin UI refetches and the row reflects truth.
## Testing
- **API** (`admin.test.ts`): create computes duration + `source='manual'`; edit recomputes and
rejects `end<start`; stop/discard act on **another** user's session; all 401/403 gated;
`GET /api/admin/users` returns the roster.
- **Admin:** Sessions list renders + status filter; create form posts the right body; edit
prefills + PUTs; Live `Stop`/`Annuleer` fire the right mutations.
- **Worker:** when the active query returns without a locally-running session, the stopwatch
resets + shows the notice; a 409 on stop resets instead of erroring.
## Out of scope (later 3b cycles)
- Aggregated/on-screen **reporting** + all-users filtered CSV (reports cycle).
- Full **user management** UI — create/role/deactivate via `/api/auth/admin/*` (user-mgmt cycle);
this cycle only *reads* the roster (`GET /api/admin/users`) for the picker.
- Real-time push (SSE) — polling is sufficient at this scale.
## Build approach
spec → `writing-plans` → one **Workflow** (~7 TDD tasks), commit per task, final verify. Tracked
as a Plane epic.