Merge branch 'feat/workers-timesheets-qr' into main

Adds the Workers/Timesheets split + worker QR badges spec and implementation plan, plus updated app icons/logo.
This commit is contained in:
DEls 2026-09-11 20:55:41 +02:00
commit 7c0925f787
9 changed files with 2274 additions and 0 deletions

2
.gitignore vendored
View file

@ -28,3 +28,5 @@ kilo.json
# Coolify production env template — placeholders only, real values stay local
.env.coolify.local
__pycache__/
.superpowers/

File diff suppressed because it is too large Load diff

View file

@ -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. |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 220 KiB

BIN
src/app/apple-icon_old.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB