From d5e3b2ce2b6f609fd3706d9b397ea95badfe57d2 Mon Sep 17 00:00:00 2001 From: DEls Date: Fri, 11 Sep 2026 19:04:34 +0200 Subject: [PATCH] docs(spec): Workers/Timesheets split and worker QR badges Workers becomes a lean roster (name, employee number, position, type, site, rate) with filters unchanged. Everything currently cluttering the list row -- total hours, cost, the 8-week sparkline, per-employee entry history -- moves to a new worker detail page that doesn't exist yet (today's "detail" is a dialog). Same pattern already used for Suppliers and Vehicles/Equipment this session: lean list, detail page carries depth. Timesheets keeps its flat entries table but gains a genuinely separate upload-status section -- the timesheets document collection already has this data, it's just currently squeezed into a per-row badge instead of being its own thing. The duplicated Import-timesheets button (currently on both pages) moves here only. QR badges: encodes employee_no directly, no new PocketBase field -- every worker already has a valid code the moment they exist, with no generation step. Explicitly a deliberate tradeoff (guessable numbers, no revocation) the user chose for now, pending seeing how the separate timekeeping PWA wants identity handled -- moving to a stored opaque token later is additive, not a rework. Card layout confirmed via the visual companion (centered, QR-forward). Both single-worker and batch-sheet print confirmed in scope. Rendering via the qrcode npm package + the browser's own print dialog (@media print), not a PDF library -- CSS handles real-world card dimensions more reliably than fighting a PDF engine's coordinate system. The timekeeping PWA itself is explicitly out of scope -- a separate project this spec only needs to stay compatible with. Co-Authored-By: Claude Opus 5 --- .gitignore | 2 + ...2026-09-11-workers-timesheets-qr-design.md | 177 ++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-11-workers-timesheets-qr-design.md diff --git a/.gitignore b/.gitignore index da39b36..65accad 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ kilo.json # Coolify production env template — placeholders only, real values stay local .env.coolify.local __pycache__/ + +.superpowers/ diff --git a/docs/superpowers/specs/2026-09-11-workers-timesheets-qr-design.md b/docs/superpowers/specs/2026-09-11-workers-timesheets-qr-design.md new file mode 100644 index 0000000..41e4c4b --- /dev/null +++ b/docs/superpowers/specs/2026-09-11-workers-timesheets-qr-design.md @@ -0,0 +1,177 @@ +# Workers / Timesheets split, plus worker QR badges + +> Splits the Workforce section's two overlapping pages into a lean +> roster (Workers) with a real detail page, and a Timesheets page that +> clearly separates "what was uploaded" from "every individual hour +> entry." Adds a printable QR badge per worker, derived from the +> existing employee number, in preparation for a separate timekeeping +> PWA that will scan these codes. +> +> Date: 2026-09-11 · Branch: `feat/workers-timesheets-qr` + +--- + +## 1. Why + +Today, `/dashboard/workforce/workers` and `/dashboard/workforce/timesheets` +both show individual `timesheet_entries` rows (date, hours), just at +different granularity — Workers nests them per-employee behind a dialog +alongside cost/hours aggregates and an 8-week sparkline; Timesheets shows +them as one flat table across everyone. Both pages also carry their own +"Import timesheets" button. This is the same category of duplication +already found and fixed once this session on the Purchase Orders / +Delivery Notes split — two pages doing overlapping jobs because a +combined page was split without deciding what each half is actually for. + +Separately, MEGA is building a standalone timekeeping app — installed as +its own PWA, used to scan a worker's badge and log their time directly. +**That app is not part of this spec.** What this spec builds is the +PocketBase-adjacent groundwork it will eventually read: a QR code per +worker that a phone camera can scan, and a way to print physical badges +for the whole workforce before that app exists to consume them. + +--- + +## 2. Workers — becomes a lean roster + +`/dashboard/workforce/workers` keeps: name, employee number, position, +worker type, site, rate/hour. Filterable by site, worker type, and +position, same filters as today. **Removed from the list/row itself:** +total hours, total cost, last-30-days hours, this-month hours, the 8-week +sparkline, and the per-employee timesheet-entries list currently shown in +a `Dialog`. All of that moves to a new detail page. + +### 2.1 New: worker detail page + +`/dashboard/workforce/workers/[employeeId]` (does not exist today — the +current "detail" is a dialog, not a route). Shows everything removed +from the list above: total hours, total cost, last-30/this-month +windows, the 8-week sparkline, and every individual `timesheet_entries` +row for that worker, newest first. Also hosts the QR badge (§4). + +This mirrors the pattern already established this session for Suppliers +(`suppliers-view.tsx` stays lean, `[supplierId]/supplier-detail-view.tsx` +carries the depth) and for Vehicles/Equipment — list pages stay scannable, +detail pages carry history. + +### 2.2 The duplicated upload button + +`ImportTimesheetsButton` currently appears in both pages' `Topbar`. +It moves to Timesheets only (§3) — a pure roster page has no reason to +host an upload action. + +--- + +## 3. Timesheets — two sections, not one blended table + +`/dashboard/workforce/timesheets` keeps its existing flat, filterable +`timesheet_entries` table (every entry, every worker, every site, +filterable by site and by upload-source), but gains a section above it +that doesn't exist today: + +**Upload status.** A table of the `timesheets` document collection +itself (from the ingestion restructure) — one row per uploaded sheet: +filename/date, `processing_status`, `rows_created`/`rows_failed`, +`last_error`. This data already exists and is already fetched by this +page's loader (`documents` in the current `page.tsx`), but today it only +surfaces as a small badge on individual entry rows. It becomes its own +first-class section — "which sheets did I upload and did they work" is a +different question from "show me every hour entry," and both are real, +already-answerable-from-existing-data questions that deserve their own +place rather than being squeezed into one row's badge. + +The two sections are visually and structurally separate — not a single +table with mixed row types. + +--- + +## 4. QR badges + +### 4.1 What the QR encodes + +**No new PocketBase field.** The QR encodes `employees.employee_no` +verbatim (e.g. `MCW058` — confirmed against live production data, +already a real, structured identifier, not a placeholder). Every worker +with an employee number already "has" a QR code the instant they exist; +there is no generation step, no button, nothing that can drift out of +sync with the roster. + +**This is a deliberate, acknowledged tradeoff, not an oversight.** +Employee numbers are sequential and could be guessed or typed in by +someone trying to clock in as a coworker, and a lost/stolen badge can't +be invalidated without renumbering the employee. The user has explicitly +chosen this simpler path for now, pending seeing how the (separately +developed) timekeeping app wants to handle identity — if it later needs a +stored, rotatable, opaque token instead of the employee number itself, +that is an additive schema change on top of this, not a rework: a new +`qr_token` field, a migration to backfill it, and swapping what string +gets encoded. Nothing in this spec needs to anticipate that shape now. + +### 4.2 Card design + +Layout, confirmed via the visual companion: **centered, QR-forward** — +company name, QR code, worker name, employee number, all centered on a +single card. Standard ID-badge dimensions (~85mm × 54mm, credit-card +sized) so it laminates cleanly. + +### 4.3 Where it appears, and the two export modes + +**Worker detail page (§2.1):** renders the card for that one worker, with +a "Print / Save as PDF" action. + +**Workers list page (§2):** an "Export QR badges" action opens a small +dialog offering either "all currently filtered/visible workers" or a +checkbox selection of specific rows, then prints a multi-card sheet +(8–10 cards per A4/Letter page, arranged for cut-and-laminate) using the +same card design. + +Both single-card and batch-sheet export are in scope — confirmed +explicitly, this is not an either/or. + +### 4.4 Technical approach + +QR rendering: a small, dependency-free client-side library (`qrcode` on +npm, ~20kb) — nothing like this exists in the project today, so this is +a new dependency either way, and it's the standard, minimal choice for +generating a QR as an SVG/canvas/data-URL in the browser. + +Output mechanism: **the browser's own print dialog** (`window.print()` +with `@media print` CSS), not a PDF-generation library. The single-card +and batch-sheet views are two different CSS layouts (one large centered +card vs. a CSS grid of many cards at real-world mm dimensions) behind the +same print trigger. The browser's print dialog already offers "Save as +PDF," so this produces a PDF without adding `jsPDF`/`html2canvas` as +dependencies or fighting a PDF library's coordinate system to get +physical card dimensions right — CSS `@media print` with `mm`/`in` units +is the more reliable tool for that than a PDF library's own layout +engine. + +--- + +## 5. What's explicitly out of scope + +- **The timekeeping PWA itself.** A separate application, a separate + project, not designed here. This spec's only obligation to it is that + the QR payload (the employee number) is something that app can scan + and look up against the same `employees` collection. +- **Moving to a stored/opaque QR token.** Deliberately deferred per §4.1 + — revisit once the timekeeping app's actual requirements are known. +- **Auto-generation of anything.** Because the QR is derived, not stored, + there is nothing to auto-generate on worker creation — every worker + already has a valid QR the moment they're created with an + `employee_no`. The only generation event in this spec is rendering the + printable artifact on demand, not a data-creation step. +- **Any change to the `employees` PocketBase schema.** None needed for + this spec. + +--- + +## 6. Testing + +| Area | What must be covered | +| --- | --- | +| Worker detail page | Renders the aggregates (hours, cost, sparkline, entries) that used to live in the list-row dialog, for a worker with entries and for a worker with none. | +| Timesheets upload-status section | Renders `timesheets` document rows with correct `processing_status`; a document collection returning zero rows doesn't break the page. | +| QR encoding | The rendered QR's payload is exactly `employee_no`, no prefix/suffix added silently. | +| Batch export selection | "All visible" respects the list's current site/type/position filters; explicit checkbox selection overrides it. | +| Print CSS | Manual verification only (this is a `@media print` concern vitest can't exercise) — confirm via the browser's print preview that single-card and multi-card layouts both render at the intended physical dimensions before considering this done. |