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>
19 -> ~16 was wrong. Ran the actual normalizeSupplierName() (trim +
whitespace-collapse + uppercase) against the three example pairs cited
as evidence: only the case-variant pair (Builders Warehouse) actually
collapses. The Ikhasi pair keeps a stray space after a hyphen that
differs between variants; the Global Precast pair differs by the
"(Pty) Ltd" suffix, not case or whitespace. Neither collapses under
exact-normalized matching.
This is not a defect -- it's the conservative-matching principle in
section 3 working as designed. The correct expectation against
production is 19 raw strings -> 18 groups, one merge. Both pairs that
don't collapse are exactly the kind of ambiguous case the spec already
says should stay separate for a human to resolve, not the kind that
should be forced together by loosening the normalizer.
Caught while grounding the implementation plan in real data, before
writing a single line of the plan itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New suppliers collection replacing purchase_orders.supplier (free text)
as the authoritative value, kept alongside it as OCR provenance rather
than dropped -- it genuinely is the supplier name, just inconsistently
formatted, unlike project_code which never held a real project code at
all.
Grounded in production: 24 POs, 19 distinct raw supplier strings, at
least 3 confirmed duplicates under a formatting variant. Migration
auto-merges only exact-normalized matches; anything ambiguous (e.g. two
"Vula" entries that may or may not be the same company) stays separate
rather than guessed.
The accounts-payable rollup (ordered/delivered/outstanding per supplier,
in Rand) is computed live from purchase_order_items and
delivery_note_items every read, never stored -- same reasoning as
po-fulfilment.ts: a stored balance has to be kept correct on every PO,
delivery, and correction, and PocketBase has no transactions to
guarantee that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reorganizes the flat 7-item sidebar into Workforce/Plant/Financial
groups, each with a landing dashboard and sub-pages split out of
today's combined Employees and Purchasing pages. Delivers the Delivery
Notes page and ingestion-status Uploads view that
2026-09-11-ingestion-restructure-design.md section 8 deliberately left
for later, now that the data model is settled.
Suppliers (new collection, PO relation, free-text-to-record matching
migration) is explicitly out of scope -- split into its own spec per
user request, same rigor as the ingestion restructure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Partial deliveries against a PO line are the reason qty_outstanding is
dropped rather than wired: a stored counter must be decremented correctly
on every receipt, OCR re-run and correction, and PocketBase has no real
transactions to guarantee that. The sum over delivery_note_items cannot
drift.
Records the relation chain, the two invariants (unmatched lines count
toward nothing; over-delivery is legal), and the fulfilment test cases.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the data_import_batches upload pool with five typed document
collections that each own their file, makes upload status reflect what
actually happened, puts a real site on every ingested row, and removes
the project_code duplication.
Audited against production on 2026-09-11:
- 19 plant_hours batches marked 'processed'; plant_hours_log has 1 row
- 147 of 320 fuel_logs have no site relation
- purchase_orders.project_code holds requisition numbers and site names,
not project codes — automatic site matching succeeds ~8% of the time
- PLANT collection and two data_import_batches fields have zero readers
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The vehicles PB collection already has a unit_type field (fuel |
plant, default fuel, made optional on 2026-08-28) and the
PATCH /api/vehicles/[id] endpoint accepts it with full test
coverage. Today there is no UI surface for assigning or changing a
vehicle's unit_type on either the Fleet page (/dashboard/fuel) or
the Plant page (/dashboard/plant), so the 53 currently mis-classified
vehicles cannot be triaged from the UI.
This spec adds that surface via the existing inline-edit pattern
already used on both pages for description editing: a Type column
renders a 'Fleet (L)' / 'Plant (h)' badge in display mode and a
Select in edit mode. Saving a flip calls PATCH /api/vehicles/[id];
on success router.refresh() re-pulls the page and the row drops
out of the page's filter, effectively moving it to the other page.
Scope is intentionally tight:
- Server-side filter on each page (Fleet: unit_type=fuel or empty;
Plant: unit_type=plant AND cost_per_hour > 0).
- New Type column on the existing Fleet and Plant tables (no new
pages, no new top-level nav).
- New shared <UnitTypePill> / <UnitTypeSelect> cell components.
- Reuses the generic useInlineEdit<K extends keyof T> hook (no
hook changes needed).
- No schema migration, no API changes, no new tests.
Spec self-review pass: tightened two claims (pb-types.ts UnitType
line 37 not 48; useInlineEdit is already generic so no hook edit).
Adds a design doc for capturing weekly plant-equipment hours via an Excel
template against the existing /dashboard/plant page. One new
plant_hours_log collection, one vehicles.unit_type field, three new
routes (import, template download, exception report CSV), and an
extension to PlantView.
Spec covers: lenient import with per-row errors, downloadable exception
report for fix-and-retry, snapshot of vehicle.unit_type on each row,
over-budget flag against a per-week budget, and a symmetric attention
panel on /dashboard/plant for type mismatches.
Per-vehicle 'I've seen this' control on the Fuel dashboard. Stamps
attention_dismissed_at on the vehicles collection; auto-undismisses when
a flagged fuel log with date_logged > dismissal arrives. Timestamp only,
no reason field. Tiny POST/DELETE route + filter line in page mapper +
Dismiss button on alert rows + Dismissed/Restore pill on vehicle detail.
Co-Authored-By: Claude <noreply@anthropic.com>
Two scoped follow-ups to the supplier-unknown flag (9897f47):
- Edit supplier on detail page when flagged as unknown
- Collapse received items per delivery note by default
Captures the async-queue redesign of the PO/DN import pipeline (see
spec + plan) and rewrites the n8n implementation doc to match the
new cron-driven architecture (workflow 005iI4jX5mq9DsTR). Also
gitignores cooler.env so the local-only deployment reference is no
longer tracked.
Co-Authored-By: Claude <noreply@anthropic.com>
Adds design spec and n8n handoff doc for the new PO upload flow:
browser -> Next.js /api/purchase-orders/upload -> n8n OCR ->
Next.js /api/purchase-orders/ingest -> PocketBase via pbAdmin().
Co-Authored-By: Claude <noreply@anthropic.com>