docs: spec + plan for phase 3b.1 manual session entry/edit + admin stop/fix
This commit is contained in:
@@ -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((end−start)/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 `≤ (end−start)`; 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.
|
||||
Reference in New Issue
Block a user