117 lines
5.8 KiB
Markdown
117 lines
5.8 KiB
Markdown
# Phase 3a — Admin Panel (MVP) Design
|
|
|
|
- **Created:** 2026-06-17
|
|
- **Status:** Approved (brainstorming) — ready for implementation plan
|
|
- **Phase:** 3a (first of two slices of roadmap Phase 3; see `docs/roadmap.md` §9)
|
|
- **Tracker:** Plane (workspace `solelog`, project SoleLog)
|
|
|
|
## Goal
|
|
|
|
A working `apps/admin` desktop web app where an admin logs in, watches who is working
|
|
**live**, and manages **handelingen** (activities). Everything consumes existing backend
|
|
endpoints plus one tiny backend touch (`role` on `/api/me`). Reports/export, user
|
|
management, and manual session entry/edit are explicitly deferred to **Phase 3b**.
|
|
|
|
_Done when:_ an admin can sign in to the admin app, see who is working right now (auto-
|
|
refreshing), and add/edit/delete activities. A worker who signs in with valid credentials
|
|
is rejected with "Geen toegang".
|
|
|
|
## Scope decisions (confirmed during brainstorming, 2026-06-17)
|
|
|
|
1. **Slice** → MVP-first, two cycles. 3a = shell + admin login + live view + activity
|
|
management (existing endpoints). 3b = reports/export + user management + manual
|
|
entry/edit.
|
|
2. **Auth** → reuse the worker's bearer-token flow (`POST /api/auth/sign-in/email` →
|
|
capture `set-auth-token` → localStorage → `Authorization: Bearer`). Gate admin access
|
|
by reading `role` from `/api/me`; non-admins are signed out with "Geen toegang".
|
|
3. **Layout** → fixed left sidebar + content area (scales as 3b adds sections).
|
|
4. **Live refresh** → react-query `refetchInterval: 5000` (5s); elapsed time ticks
|
|
client-side every second.
|
|
5. **Live view is read-only in 3a** — no stop/fix button (admin-stopping another worker's
|
|
session needs a new backend endpoint → 3b).
|
|
|
|
## Architecture
|
|
|
|
```
|
|
apps/admin/ (new Vite + React 18 + TS SPA, dev port 5174)
|
|
src/
|
|
lib/ api.ts (apiFetch + signIn), auth-storage.ts ← copied from worker
|
|
auth/ AuthContext.tsx (signIn + admin-role gate via /api/me)
|
|
api/ admin-sessions.ts (useActiveSessions), activities.ts (CRUD hooks)
|
|
screens/ Login.tsx, Live.tsx, Activities.tsx
|
|
components/ Sidebar.tsx
|
|
lib/ elapsed.ts (HH:MM:SS formatter, ported from worker stopwatch)
|
|
App.tsx, main.tsx, styles.css
|
|
```
|
|
|
|
The admin app is a **client only** — it talks to the existing backend over HTTP with a
|
|
bearer token. No DB access. It mirrors `apps/worker`'s toolchain and conventions exactly
|
|
so the build can copy proven patterns.
|
|
|
|
### Backend change (the only one in 3a)
|
|
|
|
- `packages/shared/src/index.ts`: add `role: Role` to `PublicUser` (so `MeResponse.user`
|
|
carries it).
|
|
- `apps/api/src/routes/me.ts`: include `role` in the response (read from the session user,
|
|
default `'worker'`). The worker app ignores the extra field — no worker change needed.
|
|
|
|
## Components
|
|
|
|
- **`lib/api.ts` / `lib/auth-storage.ts`** — copied verbatim from worker; bearer token in
|
|
localStorage, `apiFetch<T>` adds the `Authorization` header.
|
|
- **`auth/AuthContext.tsx`** — `signIn(email,password)` calls the worker `signIn`, then
|
|
fetches `/api/me`; if `role !== 'admin'` it clears the token and throws so Login shows
|
|
"Geen toegang — alleen beheerders." `isAuthed` reflects a present token; an admin check
|
|
on mount (re-fetch `/api/me`) guards against a stale worker token.
|
|
- **`screens/Login.tsx`** — Dutch email+password form (no self-signup), error line.
|
|
- **`components/Sidebar.tsx`** — app name + signed-in email + logout (⏻); nav items **Live**
|
|
and **Handelingen**; a muted/disabled "binnenkort" group hints 3b (Rapporten /
|
|
Gebruikers / Handmatig).
|
|
- **`screens/Live.tsx`** — consumes `GET /api/admin/sessions/active` (`useActiveSessions`,
|
|
`refetchInterval: 5000`). One card per session: worker name, activity name, insole-type
|
|
pill, pair count, ticking elapsed timer (from `start_time`). Empty state "Niemand is nu
|
|
aan het werk."; header "Actief nu (N)".
|
|
- **`screens/Activities.tsx`** — port of the legacy worker `Settings.tsx` (git `decb158`):
|
|
add form (name + insole-type toggles), list with inline edit, delete-with-confirm —
|
|
desktop-styled. Uses `api/activities.ts` hooks against `/api/activities`.
|
|
|
|
## Data flow
|
|
|
|
1. Admin enters credentials → `signIn` → token stored → `/api/me` confirms `role==='admin'`.
|
|
2. Live screen polls `/api/admin/sessions/active` every 5s; each card computes elapsed from
|
|
`start_time` with a 1s client tick.
|
|
3. Activity CRUD hits `/api/activities` (GET open to any authed user; POST/PUT/DELETE are
|
|
admin-gated server-side — the admin bearer satisfies them).
|
|
|
|
## Error handling
|
|
|
|
- Login: 401/no-token → "Inloggen mislukt"; authenticated non-admin → "Geen toegang —
|
|
alleen beheerders" (token cleared).
|
|
- Live/Activities: react-query error → inline "Kon gegevens niet laden." with the data
|
|
hidden; loading → "Laden…".
|
|
- Delete activity: `window.confirm` warns that the activity's sessions are also deleted
|
|
(matches backend cascade in `activities.ts`).
|
|
|
|
## Testing
|
|
|
|
Mirror the worker's vitest + testing-library setup, mocking `apiFetch`:
|
|
|
|
- **Backend:** `/api/me` includes `role` for a worker and an admin (extend `me.test.ts`).
|
|
- **Auth/login:** admin signs in; worker credentials are rejected with "Geen toegang".
|
|
- **Shell:** sidebar renders nav; logout clears the token.
|
|
- **Live:** renders a card per active session (name/activity/type); empty state when none.
|
|
- **Activities:** add/edit/delete invoke the correct endpoints (mocked).
|
|
|
|
## Out of scope (→ Phase 3b)
|
|
|
|
- All-users filtered CSV export (current `/api/export` is self-scoped).
|
|
- User management via better-auth `/api/auth/admin/*`.
|
|
- Manual session entry/edit + admin stop/fix of another worker's session (needs new
|
|
backend endpoints).
|
|
|
|
## Build approach
|
|
|
|
spec → `writing-plans` → **one Workflow** that executes the plan task-by-task (TDD, commit
|
|
per task, final lint/typecheck/test/build + live verify), matching the Phase 2 pattern and
|
|
keeping this session's context clean.
|