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 <noreply@anthropic.com>
This commit is contained in:
parent
5f4adcba6c
commit
d5e3b2ce2b
2 changed files with 179 additions and 0 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -28,3 +28,5 @@ kilo.json
|
|||
# Coolify production env template — placeholders only, real values stay local
|
||||
.env.coolify.local
|
||||
__pycache__/
|
||||
|
||||
.superpowers/
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
Loading…
Reference in a new issue