Commit graph

45 commits

Author SHA1 Message Date
DEls
f06d545e02 fix(site-map): allow tab-separated survey files
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:52:19 +02:00
DEls
c4da9d4a65 feat(site-map): PocketBase collections for models, progress and survey imports 2026-10-14 07:05:11 +02:00
DEls
4493bbc0db feat(clock): pay against the real work schedule
Mon-Thu 07:00-16:30, Friday 07:00-15:00, last Friday of the month
07:00-12:00 (weekends use the Mon-Thu bands). Each shift end has a
10-minute band either side; arrivals up to 07:10 pay from 07:00.
Lunch now phases in from 5h to 5h30 instead of a 30-minute cliff, so
a normal day pays 9 / 7.5 / 5 hours.

The worker calendar judges each day against that day's expected hours,
so a normal Friday is green, and the worker page shows the schedule.
The recalc script recognises rows already rewritten by the first rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:51:24 +02:00
DEls
295f31fce6 fix(clock): recalc re-run counts already-paid rows as unchanged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:32:50 +02:00
DEls
75b178dd33 feat(clock): store paid hours, not the raw scan span
A scan in up to 07:10 pays from 07:00; a scan out between 16:20 and
16:40 pays to 16:30; a session over five hours loses a 30-minute lunch.
A normal day is therefore exactly 9 hours (green on the calendar and
matrix); late arrivals, early exits and overtime use the real times.
Applied on clock-out, in missed-scan corrections and their preview.
The calendar keeps the raw scan times and labels hours "Paid hours".

Includes a dry-run-first script to recalculate existing QR entries; it
only rewrites rows still equal to their raw scan total.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:01:17 +02:00
DEls
b32ad0bc8b feat(clock): fix a missing clock-in from Needs attention
When nobody scanned a worker in, the evening scan was stored as the IN
and the correction dialog only allowed a leaving time after it. The
dialog now shows an editable arrival and leaving time, with a one-click
"this scan was actually the clock-out" swap. Late scans (16:00+) get a
hint on the card. A corrected arrival is stored as the optional
actual_in_at on the correction row (migration script included, not
applied); the original QR scan is untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:57:09 +02:00
DEls
f72397259c feat(auth): add Office access and restrict Clock users 2026-10-07 14:22:57 +02:00
DEls
08becc6270 feat: journal MCP operations and fence purchasing writes 2026-10-05 01:38:15 +02:00
DEls
ed69b517e8 Integrate agent work and add dashboard attention indicators
Remove repeated Plant, Financial and Workforce shortcut cards. Add site-scoped Plant and Financial sidebar attention status with automatic refresh.

Include accumulated delivery processing, purchase order recovery, supplier reconciliation, plant data and user management changes and audit records.
2026-10-04 21:53:32 +02:00
DEls
32d4a82cdf feat: add employee search and save pending fleet updates 2026-09-30 00:42:38 +02:00
DEls
b700252932 fix(auth): verify PocketBase backup key before migration 2026-09-29 07:01:15 +02:00
DEls
f5259d852f feat(auth): add PocketBase logins and site access controls 2026-09-28 23:51:47 +02:00
DEls
5cdfaa3128 feat(clock): review missed scan-outs without changing phone flow 2026-09-25 07:27:12 +02:00
DEls
8bdf3754df Revert "feat(clock): add locked shift schema migration"
This reverts commit 5529c3f53d.
2026-09-25 07:06:28 +02:00
DEls
5d712b66c9 Revert "feat(clock): dry-run historical shift pairing"
This reverts commit e01c45765b.
2026-09-25 07:06:28 +02:00
DEls
e01c45765b feat(clock): dry-run historical shift pairing 2026-09-25 03:16:08 +02:00
DEls
5529c3f53d feat(clock): add locked shift schema migration 2026-09-25 03:13:06 +02:00
DEIs
87f75dd98f feat(plant): fleet map page with GPS-denormalised vehicles, clustering, and tile switcher
Adds /dashboard/plant/map: a Leaflet view of every vehicle scanned from
the MEGA Clock kiosk, plotted at its last known position.

Schema
- vehicles.last_latitude / last_longitude / last_scanned_at /
  last_location_accuracy_m denormalised off fuel_logs so the map page
  renders every vehicle in a single query. Same best-effort pattern as
  attention_dismissed_at; updated by /api/clock/vehicle-scan after every
  successful scan. PB init defaults new number cols to 0 (not null) —
  filter uses 'last_latitude > 0 || last_latitude < 0' to mean 'real fix'.
- Indexed via idx_vehicles_last_scanned.

Map UI
- Custom SVG markers per unit_type: car silhouette for fuel, tractor
  silhouette for plant, 24x24, drop-shadowed.
- Clustering via react-leaflet-cluster over Leaflet.markercluster.
  Cluster icons colour-ramp with size (green->sky->amber->rose) and
  scale 32->54px.
- Three-button base-layer switcher: Street (OSM), Dark (ESRI Dark
  Gray Canvas — CartoDB watermarks tiles without a key), Satellite
  (ESRI World Imagery). Last choice persists in localStorage.
- Site pins: small purple map-marker divIcon, click flies the map to
  zoom level 15.
- Empty-state is a floating chip on the map rather than a full-page
  card so the map is always visible.

APIs
- /api/clock/vehicle-scan: stores the GPS fix on the fuel_logs row
  AND denormalises onto the vehicle when present; missing-coord
  scan leaves last_* untouched.

Sidebar: new Map entry under Plant group.
2026-09-22 21:39:25 +02:00
DEls
a589b14ac9 feat(clock): enhance vehicle scan API with GPS support
Updated the vehicle scan API to include optional GPS coordinates (latitude, longitude) and location accuracy. The API now validates these fields, ensuring both latitude and longitude are provided together and within valid ranges. Enhanced error handling for invalid inputs and updated the API reference documentation accordingly. Additionally, modified the FleetScanner component to capture GPS data and reflect its status in the UI.
2026-09-22 19:59:44 +02:00
DEls
dc5ead1b7a feat(clock): add clock_events PocketBase collection 2026-09-12 00:14:13 +02:00
DEls
58b47eafee docs(project-calendar): add the project month/week plan; fix a stale doc reference
The migration's docstring pointed at a spec file that was never
written for this feature (the design was worked out via conversation,
not a separate spec doc) — points at the plan instead.
2026-09-11 22:35:41 +02:00
DEls
daf2344d99 feat(sites): add work_month_start_day field 2026-09-11 22:24:03 +02:00
DEls
5fa8edfc3e feat(suppliers): migrate existing PO supplier text onto supplier records 2026-09-11 18:14:05 +02:00
DEls
8025d297e6 feat(schema): add suppliers collection and purchase_orders.supplier_ref 2026-09-11 18:12:30 +02:00
DEls
13f76ac894 perf(schema): index the columns every per-site query filters on
timesheet_entries, fuel_logs and delivery_notes had no indexes at all --
fine at today's row counts, not once site count grows toward the 18-20
target. Added 8 indexes across fuel_logs, timesheet_entries,
delivery_notes, purchase_orders and plant_hours_log, unioned into each
collection's existing index array rather than replacing it.

Verified both pre-existing UNIQUE indexes (idx_po_number, load-bearing
for the placeholder-PO-number scheme; idx_plant_hours_log_qr_start,
prevents duplicate weekly imports) are byte-for-byte unchanged.
Idempotent: a second run touches nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 16:08:34 +02:00
DEls
31e4df30a9 refactor(schema): drop project_code, qty_outstanding, PLANT and data_import_batches
The irreversible step of the ingestion restructure. Guards proved clean
(0 no-site rows across all 8 target collections, PLANT empty, all 27
migrated documents present, no row still depending on the old batch
link), full schema + row-data backups taken to /tmp/pb-backup-2026-09-15
first, then applied.

Dropped project_code from employees, vehicles, fixed_costs,
income_claims, purchase_orders, delivery_notes, fuel_logs and
plant_hours_log. Dropped qty_outstanding from purchase_order_items.
Deleted the PLANT collection (0 rows, 0 code references — a deprecated
alias for vehicles) and DATA_IMPORT_BATCHES (86 rows, superseded by the
five document collections built across Tasks 6-15).

PocketBase refused the first attempt: it will not delete a collection
while any relation still points at it, and fuel_logs, plant_hours_log
and timesheet_entries all still carried a batch_reference relation to
data_import_batches that Task 7's migration made obsolete but never
removed. Confirmed live that nothing in src/ reads or writes that field
any more (only three stale type declarations in pb-types.ts), extended
the drop script to remove it from all three collections, re-ran, and the
deletion succeeded.

Cleaned up the now-dead DataImportBatchRecord interface and BatchType
union from pb-types.ts; BatchStatus survives as the shared type every
document/queue collection's processing_status now uses independently.

Final collection count: 17, down from a peak of 20 mid-restructure (16
original + 3 new document collections - PLANT - data_import_batches -
2 renames counted once each). Every remaining collection's fields match
what code actually reads and writes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:05:42 +02:00
DEls
a3e4702159 refactor(schema): resolve project_code through site, not a stored copy
project_code was a text copy of the parent site's code, denormalised
onto 8 child collections that each already have a real site relation.
Written once at insert, never updated -- docs/data-model.md documents
the drift risk. On purchase_orders it never even held a real project
code: production had requisition numbers, site names, and the PO's own
number in that field.

Every read site now resolves the code through site/expand.site/siteById
instead, reusing each page's existing site map where one already
existed. Every create() call stops writing the column. The chat agent's
list_imports tool no longer reads data_import_batches -- it now reads
timesheets, fuel_sheets, plant_hours_sheets, purchase_orders and
delivery_notes filtered by processing_status, which was necessary before
the pool can be dropped (Task 20 would otherwise 404 the agent).

Also closed a Task 17 gap found while doing this: the agent tools' vehicle
and fuel-log handlers still resolved project code from the now-removed
vehicles.project_code column.

Wrote scripts/migrations/2026-09-15-drop-legacy-columns.py, the one
irreversible step in this whole restructure. Its guard against orphaning
fuel_logs rows checked the wrong condition -- "fuel_sheet is empty" --
which produced a false abort: 73 fuel_logs rows have no fuel_sheet
because they predate batch tracking entirely and never had a
batch_reference to begin with, so dropping data_import_batches costs
them nothing. Fixed the guard to check what it actually claims to check:
a row that HAD a batch_reference and was never relinked. Verified live
that no such row exists on any of the three affected collections.

Script run in dry-run only; not yet applied.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:01:14 +02:00
DEls
f007b5e8fb feat(schema): require site on every ingested collection
Tightened site from optional to required on fuel_logs, timesheet_entries,
delivery_notes and purchase_orders now that every route writes it and
every historical row has been backfilled. plant_hours_log was already
required (matches docs/data-model.md's documented invariant, confirmed
against production). timesheets (the document collection) was already
required from Task 6 -- a no-op here, not a regression.

fuel_sheets.site and plant_hours_sheets.site deliberately stay optional:
those sheets can legitimately span multiple sites.

Verified: creating a fuel_logs row with no site now gets a hard 400
(validation_required) from PocketBase itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:32:48 +02:00
DEls
4f9af4a2a6 fix(purchasing): restore the 30-day PO dedup window with a real timestamp
Task 13 moved PO creation to upload time and needed a hash-dedup window
matching the spec's "same 30-day dedup window as today" (design doc
section 4.1). purchase_orders has no working created/updated autodate
field on this PocketBase instance, and unlike delivery_notes it had no
other timestamp field either -- the first cut of Task 13 correctly found
this, but responded by dropping the time window entirely rather than
adding one. That's a real behaviour change from spec, not just a
workaround: a PO file legitimately re-sent long after the original would
have been flagged a duplicate forever, with no way to age out.

Added purchase_orders.date_uploaded (additive, mirrors how
delivery_notes already uses its own date_received) and wired it through:
stamped at create, filtered on in the dedup query.

Also brought PurchaseOrderRecord in pb-types.ts in line with what Task 1
already put on the live schema and what the upload route now writes --
processing_status/attempts/last_error/next_retry_at/last_attempt_at/
file_hash/date_uploaded were live in production but absent from the
TypeScript interface. docs/data-model.md section 7 documents this exact
gap as a standing foot-gun; closing it now while it's fresh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 14:17:42 +02:00
DEls
539ec93d2a feat(migration): move batch files into document collections
Migrates the 27 batches of type timesheets/fuel_logs/plant_hours out of
data_import_batches into the three document collections created in Task
6, preserving every source file and relinking every child row.

Status is derived from actual child count, not copied from the batch
row. Production proved the stored status cannot be trusted: every one of
these batches was written status=processed at creation time, before any
insert ran.

The bug is worse than the plan's own audit found. It predicted 18
phantom-processed batches, all plant_hours. Production has 21 -- three
timesheets batches are also phantom, including two byte-identical
re-uploads of the same wage sheet that both reported success and created
nothing. This hit payroll, not just plant-hour smoke tests.

Also: computed file_hash on migrated documents while the bytes were
already in hand (closes the same dedup gap Task 1 closed on delivery
notes and purchase orders), and split the schema-additive step
(--apply-fields) from the row migration (--apply) so the reversible part
can run independently of the irreversible one. The plan claimed
idempotency via a migrated_from_batch field that doesn't exist; replaced
with an explicit refuse-if-already-populated guard rather than pretend
resumability that isn't there.

27/27 migrated. 6/6 post-migration invariants verified: 3 fuel_sheets,
19 plant_hours_sheets, 5 timesheets, 1367/247/1 children relinked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 13:06:40 +02:00
DEls
3a71e7e884 feat(schema): add timesheets, fuel_sheets, plant_hours_sheets documents
Three document collections replacing data_import_batches for the
spreadsheet importers. Each owns its source file and carries honest
outcome counters (rows_total/created/failed, row_errors) — the counts the
current importers compute and then discard.

site is required on timesheets (a wage sheet belongs to one site) and
optional on the other two, which legitimately span sites since the fuel
importer resolves a site per row. cascadeDelete is false on all three, so
deleting a site cannot destroy its source spreadsheets.

Widened source_file mimeTypes beyond the plan's three entries to match
the pool being replaced. Browsers label CSVs text/csv, application/csv or
text/plain interchangeably and the importers branch on file extension,
not MIME — so the narrower list would have parsed a CSV successfully then
rejected it on write. The 2026-08-24 migration exists because that
already happened once.

Plan corrected: its TypeScript edit added a second `expand` property to
an interface that already had one, which does not compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 11:09:14 +02:00
DEls
0cdf385df2 refactor(schema): rename received_items and timesheets to their real names
RECEIVED_ITEMS -> DELIVERY_NOTE_ITEMS (55 rows): they are the lines on a
delivery note; the old name described an event, not a thing.

TIMESHEETS -> TIMESHEET_ENTRIES (1367 rows): this collection is the time
log, not a timesheet. The name is freed for the uploaded-document
collection created in Task 6.

PocketBase stores relations by collection id, so every relation survived
untouched; row counts asserted intact on both renames.

Two call sites the plan missed: src/app/api/timesheets/import/route.ts
and src/lib/ocr-result-tier.ts. Deliberately preserved the "timesheets"
string literals in pb-types.ts and the import route — those are
data_import_batches.batch_type select values, not collection names, and
changing them would have violated the select constraint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 11:01:40 +02:00
DEls
a0d335b235 fix(timesheets): stamp site on log rows, ending read-time drift
The timesheet log carried neither site nor project_code; attribution was
synthesised at read time from employees.site, so moving a worker to
another site silently migrated their entire payroll history with them.
docs/data-model.md flags this as the schema's worst drift risk.

Adds an optional site relation (maxSelect 1, cascadeDelete false so
deleting a site cannot destroy payroll history) and stamps all 1367 rows
from the employee's current site. 1367/1367 written, 0 orphans.

Tightened to required in Task 16, once the import route writes it on
every new row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 10:48:51 +02:00
DEls
9117b73cd4 fix(fuel): backfill site on 147 orphaned fuel logs
147 of 320 fuel_logs had no site relation; their only attribution was the
denormalised project_code text, which Task 18 deletes. Backfilled before
that column goes, resolving each row twice (by project_code and via the
vehicle's own site) and writing only on agreement.

147/147 written, 0 skipped, 0 remaining.

Recorded a caveat the dry run exposed: with one site in production both
resolvers draw from a candidate set of size one, so "both agree" is
guaranteed rather than evidence. The write is still the only possible
correct value, but the guard only bites once more sites exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 10:44:19 +02:00
DEls
5274ac4981 fix(migration): separate download failures from missing files
Review found a transient 500 during download landed in the "no stored
file" bucket, so a broken run printed as a clean one and exited 0. Now
counted separately and exits non-zero.

Also: 404 is no longer swallowed on mutations (a PATCH against a deleted
record was counted as written), the target host and mode are printed, the
apply path reports a final written count, and filenames quote with
safe="".

Plan Task 1 Step 4 still carried the superseded copy-from-batch code
block under the rewritten prose, contradicting it. Replaced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 10:39:54 +02:00
DEls
147ce7e77a feat(schema): hash entity files directly instead of copying batch hashes
Production disproved the copy-from-batch approach in two ways:
- 10 delivery-note batches carry no file_hash at all (they predate
  78fa8da), and all 10 sit inside the 30-day dedup window, so copying
  would have left a live gap rather than a historical one
- one PO row was produced from two different files with different
  hashes via the dedup-by-PO-number branch, so copying gives
  last-write-wins on an arbitrary one

Hashing the stored bytes has neither problem and does not depend on the
collection we are about to delete. 56/56 records covered, none skipped.

Immediately surfaced a real duplicate the old path missed: two delivery
notes numbered IN132202 with identical bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 10:32:41 +02:00
DEls
b1d06c1aa7 feat(schema): add file_hash and queue fields to DNs and POs 2026-09-11 10:29:10 +02:00
DEls
658d83dcff fix(pb): allow PDF and image MIME on data_import_batches.original_file
The PO upload route (and the delivery-notes and xlsx import
paths) writes the uploaded file into the DATA_IMPORT_BATCHES.
original_file field. That field was bootstrapped with a CSV/Excel
allowlist only:

  text/csv, text/plain, application/vnd.ms-excel, application/csv

When the upload route was rewritten on 2026-08-19 (commit f0f7c82)
to write directly to PB instead of forwarding the file to n8n, every
PDF and image upload started failing PB field validation with
validation_invalid_mime_type, surfacing in the API as a 500 with
error='batch_create_failed' and the real cause buried in
pb_data.original_file. The 2026-08-24 migration that should have
added xlsx was also never applied to prod (verified in PB:
xlsx missing too).

Migration: scripts/migrations/2026-08-31-data-import-batches-allow-pdf-images.py
- Strictly additive: unions the new mimeTypes onto the existing
  allowlist, exits no-op if already applied (verified idempotent).
- Adds application/pdf, image/jpeg, image/png, image/webp (PDF /
  image support for the PO and delivery-note flows) and
  application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
  (xlsx, completing the missed 2026-08-24 migration).
- No code change in the upload routes required - they were already
  validating against ALLOWED_MIME in purchase-order-schema.ts and
  expecting PB to accept the same set.

Verification on https://pb.mega.infra.apphub.co.za:
  Before: POST /api/purchase-orders/upload (PDF) -> HTTP 500,
    pb_data.original_file.code = 'validation_invalid_mime_type'.
  After:  same POST -> HTTP 202 with batch_id, PB row created
    (batch_type=purchase_orders, status=pending, file_hash
    populated, original_file stored).
2026-08-31 12:20:41 +02:00
DEls
9ea0a1f65e fix(pb): make vehicles.unit_type optional
53 vehicles predate the unit_type field and have unit_type=''. PB
validates the WHOLE record on any PATCH, so as long as unit_type was
required, no record with an empty value could be updated (Dismiss,
inline-edit, anything calling .update()). Making the field optional
unblocks all 53 immediately.

The default='fuel' still applies to new inserts.

Note: the live PB schema lost its 'default' value during the PATCH
(see review below) — only 'required' was actually changed in the
stored schema. Fresh records will be inserted with unit_type='' (or
null) until the application sets it explicitly. The bootstrap
definition still has default='fuel' so fresh environments get it.

Idempotent migration: scripts/migrations/2026-08-28-vehicles-unit-type-optional.py
applied successfully; re-run is a no-op.
2026-08-28 15:26:33 +02:00
DEls
0290ced51a fix(schema): ensure is_recurring=false writes are preserved
The live PB fixed_costs collection never received the is_recurring field
added in 87bb179 — the bootstrap script is collection-create-only and
errors out on already-existing collections, and no migration added the
field. Smoke-test rows sent is_recurring=false and got it silently
dropped (PB ignores unknown fields on write), then the partition helper
treated the missing value as monthly (its !=== false fallback).

Two changes:

1. Drop 'default: true' from the bootstrap schema definition. A bool
   default only applies when the field is absent from the write payload,
   so this is belt-and-braces — but it removes any doubt that a default
   could ever silently override an explicit false. The partition helper
   still treats absent values as monthly, and the backfill migration
   patches pre-existing rows to true.

2. Make the backfill migration self-contained: if the field is missing
   from the collection, add it (bool, required=false, no default) before
   patching rows. Idempotent — no-op if already present.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 13:43:38 +02:00
DEls
70522ed1ae feat(migrations): backfill is_recurring=true on fixed_costs 2026-08-28 12:43:25 +02:00
DEls
fb7d3d51b1 feat(fuel): PB schema migration for attention_dismissed_at
Adds the optional date field to VEHICLES via a one-shot idempotent
migration, and keeps scripts/pb-bootstrap-schema.py in sync so freshly
bootstrapped environments also get it. No data backfill — existing
rows have the field set to null, which the UI treats as 'not yet
dismissed'.
2026-08-28 12:26:23 +02:00
DEls
1d06baafee fix(data-import-batches): allow xlsx MIME on original_file
When the new fuel-import route started accepting .xlsx uploads, the
real xlsx MIME type (application/vnd.openxmlformats-officedocument
.spreadsheetml.sheet) hit PB's mimeTypes allowlist on data_import_
batches.original_file and got rejected with:

    validation_invalid_mime_type: mime type must be one of:
    application/pdf, image/jpeg, image/png, image/webp, text/csv,
    text/plain, application/vnd.ms-excel, application/csv.

The old CSV-only route never hit this (text/csv was already on the
list), so the audit-trail batch attach worked fine.

Adds the OOXML xlsx MIME to the allowlist via the same migration
pattern as 2026-08-19 and 2026-08-24-fuel-logs. Idempotent: re-run
is a no-op once applied. No app-code changes - the route was
already sending the correct MIME for the file extension.

Verified: batch create succeeds against live PB with the exact
xlsx payload the route sends.
2026-08-24 22:17:45 +02:00
DEls
7bce0a4e80 feat(fuel): drop-in xlsx/csv import for pump sheets
The Aug 2026 pump sheet from The Hills (and any future sheet from
the same source) uses different column names and a different date
format than the legacy CSV the import route was built for. Adapts
the route so the same endpoint accepts either Excel (.xlsx) or CSV,
maps the new column names onto the existing FUEL_LOGS schema, and
persists their 'Latest Consumption' column verbatim.

What's new
  • SheetJS (xlsx) replaces PapaParse for parsing. Single code path
    handles both .xlsx and .csv uploads.
  • Resolves columns case-insensitively against an alias table so
    legacy CSVs (vehicle_description, odo_reading) still work
    alongside the new shape (Date Logged, Vehicle QR, Km's/ Hours,
    Litres Pumped, Current Site, Latest Consumption).
  • Date parser: DD/MM/YYYY first (with leading-space tolerance),
    falls back to YYYY-MM-DD, then to Excel serial numbers.
  • Vehicle match: QR code first (clean identifier in the source
    data), falls back to description text for legacy CSV.
  • Site match: by sites.name (one round-trip, cached for the
    import). project_code is derived from the resolved site; falls
    back to the vehicle's project_code.
  • Latest Consumption persisted as a new FUEL_LOGS field
    (latest_consumption, number, optional). Empty source cells
    stay empty rather than getting coerced to 0.

PocketBase change
  • scripts/migrations/2026-08-24-fuel-logs-latest-consumption.py
    adds the new field. Idempotent; follows the pattern of the
    2026-08-19 migration. Run it with PB_ADMIN_EMAIL +
    PB_ADMIN_PASSWORD before importing the new sheets, or the
    import will fail with 'latest_consumption is not a field'.

UI
  • Import button now accepts .xlsx and CSV; SAMPLE_HEADERS +
    SAMPLE_OPTIONAL show the new column expectations and the
    DD/MM/YYYY date format hint.
  • fuel-view.tsx shows a 'Consumption (src)' column on the row
    table so operators can eyeball what their fuel team sent
    against the app's recomputed variance.

Out of scope (intentionally untouched)
  • vehicles, sites, data_import_batches and every other
    collection — no schema changes there.
  • PO / timesheets / delivery-notes imports.
  • fuel/page.tsx business logic (variance calc, vehicle summaries).

Verified
  • Dry-run against 'The Hills Diesel 1-24 August.xlsx': 134/134
    rows parse cleanly, 0 errors. 12/134 rows correctly carry an
    empty Latest Consumption (matches the source).
  • vitest: 20/20 fuel-import-parse + 35/35 PO-schema tests pass.
  • tsc --noEmit clean.
  • One pre-existing failure in
    src/app/api/purchase-orders/process-batch/route.test.ts
    unrelated to fuel; confirmed failing on main before this change.
2026-08-24 22:03:08 +02:00
DEls
c534572590 feat(migration): add async-queue fields to data_import_batches
Extends the data_import_batches collection for the PO/DN async-import
queue redesign (see spec docs/superpowers/specs/2026-08-19-po-import-queue-redesign.md
and plan docs/superpowers/plans/2026-08-19-po-import-queue-implementation.md).

Adds 8 fields:
  content_type, status (select: pending|processing|processed|failed|dead),
  attempts, last_error, last_attempt_at, next_retry_at, processed_at,
  processed_record_id

Extends the batch_type enum to include purchase_orders and delivery_notes.

Adds index idx_batches_pending on (status, batch_type, next_retry_at) for
the cron query.

Idempotent: safe to re-run.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 19:19:17 +02:00