6.3 KiB
Session: 2026-06-24 — Phase 3b·2 (Reports + all-users export)
Goal
Give the admin a Rapporten screen: pick a period (Deze week / Deze maand / Alles + manual
from/to) and optionally narrow by worker / insole type / activity, see headline production totals
plus three breakdowns (per medewerker / per handeling / per type), and export the underlying
detail rows (all workers) to CSV. Today's /api/export is self-scoped to the logged-in user;
this cycle adds the cross-user, filterable reporting + export. Second of three Phase 3b cycles
(after manual-sessions 3b·1; user management remains).
Spec: docs/superpowers/specs/2026-06-24-phase-3b2-reports-export-design.md;
plan: docs/superpowers/plans/2026-06-24-phase-3b2-reports-export.md.
Tracked as Plane epic SL-48 with tasks SL-49…SL-53.
Scope decisions (brainstorming)
- Both lenses on one screen (totals + breakdowns per worker AND activity AND type).
- Filters: date range (spine) + optional worker + type + activity; default this week.
- Completed-only counts toward totals/export (active/discarded excluded).
- All four metrics: gewerkte tijd, aantal zolen, aantal sessies, pauzetijd.
- CSV = detail rows, all workers (a leading Worker column), not the aggregated summary.
- Tables only — no chart library, no CSS bars (dependency-light).
Work done
Built via a 5-task TDD Workflow (w7ut902zo), one commit per task, run sequentially because the
tasks share one working tree:
- Task 1 — DRY CSV builder (
01fa18f). Extracted the inline header/row builder from the self-scoped/api/exportintobuildSessionsCsv(rows, { includeWorker })+ aSessionCsvRowinterface inapps/api/src/lib/csv.ts.sessions.tsnow maps its query rows intoSessionCsvRowand calls the shared builder.includeWorker:falsekeeps the worker export byte-identical (the existingexport.test.tsis the regression guard);includeWorker:trueprepends a Worker column. Newcsv.test.tscovers both header variants. - Task 2 — Report contracts +
GET /api/admin/report(4b213e2). AddedReportTotals,ReportWorkerRow,ReportActivityRow,ReportTypeRow,ReportResponseto@solelog/shared. Inadmin.ts:parseReportQuery(validatesfrom/torange,insole_type,activity_id),buildSessionFilters(completed-only + date range + optional worker/type/activity), and the report route — fetches the filtered joined rows and aggregatestotals+by_worker/by_activity/by_typein JS (sorted byworked_secondsdesc; empty range → zeros + empty arrays; null type bucketed under "Onbekend").report.test.ts(5 cases). - Task 3 —
GET /api/admin/export(d33fcb7). All-users filtered CSV reusingparseReportQuery+buildSessionFilters(Task 2) andbuildSessionsCsv(includeWorker:true)(Task 1); ordered bystart_timeasc; filenamesolelog-report_<from>_<to>.csv. 401/403 inherited from the/api/admin/*guard. Export tests appended toreport.test.ts. - Task 4 — Admin API client (
8ad2e69).apps/admin/src/api/reports.ts:ReportFilters,filtersToQuery(omits empty optionals),useReport(React Query), anddownloadExport— a rawfetchwith the bearer token →Blob→ object-URL download (a plain<a href>can't carry the token).reports.test.tscovers serialization + the bearer-fetch download + non-ok throw. - Task 5 — Rapporten screen + nav (
8d75be0). Purelib/date-range.tshelpers (thisWeek,thisMonth,allTime,dayStartISO/dayEndISO— local-day → ISO instants so there's no server tz ambiguity) with unit tests.screens/Reports.tsx: filter bar (presets + date inputs + worker/ type/handeling dropdowns), a headline totals line, and three breakdown tables, plus the Exporteer CSV button wired todownloadExport. Sidebar moves Rapporten intonavItems(soon = justGebruikers);App.tsxgets the/rapportenroute; report styles appended tostyles.css.
Deviations (fix-forward, test-only)
- Task 3: the API test DB is reset once per file (
test/setup.ts), so the WIDE-window export tests initially picked up sessions seeded by the earlier report tests. Fixed by scoping the two non-gating export tests to their own freshly-seeded activity via&activity_id=…(a real filter the route already supports). The route code is exactly as planned. - Task 5: the plan's
Reports.test.tsxhad a few assertions that collided with the screen's own markup (headline36vs a table cell;Jan/Kurkmatching dropdown<option>s). Tightened three assertions (scope to.reports-totals; targetgetByRole('cell', …)) and loosened the apiFetch mock signature for a teardown refetch. Screen behavior/markup unchanged.
Verification (independent of the workflow's self-reports)
git log— five task commits01fa18f → 8d75be0; clean tree.yarn workspace @solelog/api test— 85 passed (14 files), incl.csv,report, and the unchanged regressionexport.test.ts;typecheckclean.yarn workspace @solelog/admin test— 46 passed (10 files), incl.reports,date-range,Reports;typecheckclean;buildsucceeds (vite, 94 modules).npx oxlint— clean (exit 0).
Outcome
Phase 3b·2 is implemented and green across both touched workspaces. An admin opens Rapporten,
picks a period + optional filters, and sees correct headline totals (worked / zolen / sessies /
pauze) with three breakdown tables, then exports every matching completed session (all workers, with
a Worker column) to a range-named CSV. One CSV format now serves both the worker self-export and the
admin export. No DB migration (reuses existing work_sessions columns). Plane SL-48 + SL-49…SL-53
all marked Done. Roadmap Phase 3 note updated: 3b·2 done; user management is the last 3b cycle.
The maintainer's deploy WIP (.env.prod.example / docker-compose.prod.yml) was left untouched.
origin/main is behind (the pre-compaction push never landed); after this cycle main is ahead
~16 commits — a single git push origin main from the maintainer's terminal carries the batch
(and triggers Gitea CI).
Next (Phase 3b remainder)
- User management — create / set role / deactivate via better-auth
/api/auth/admin/*(theGebruikersscreen). Final 3b cycle.