Commit graph

110 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
300b1d2a07 feat(site-map): model upload script with progress-key parity 2026-10-14 07:06:13 +02:00
DEls
c4da9d4a65 feat(site-map): PocketBase collections for models, progress and survey imports 2026-10-14 07:05:11 +02:00
DEls
d66c213712 feat(site-map): model pipeline with 20 m chainage sections 2026-10-14 07:04:34 +02:00
DEls
337b77be3f feat(management): refresh workbook plans and reconcile cashflow 2026-10-14 05:48:01 +02:00
DEls
bab2ea7a43 feat(receiving): add Inbox 2 and strengthen duplicate checks 2026-10-12 20:20:27 +02:00
DEls
1fb65a9d9c chore(n8n): keep the stored PO-ingest callback in step with the published workflow
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:47:00 +02:00
DEls
c55676bff0 fix(po): photographed POs get their order date from the PO Creation Date
Text PDFs already get it from pdftotext; photos and scans have no text,
so 4 of 5 undated POs were photos that do print a PO Creation Date.
- n8n PO reader (stored patch): asked for order_date = 'PO Creation
  Date' (day/month/year), never Required By or Printed; the callback
  passes it on.
- process-batch accepts extracted.order_date (YYYY-MM-DD); the PDF text
  wins when both exist.
- create_purchase_order / update_purchase_order / move_to_purchase_orders
  take order_date, so agents record it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:45:14 +02:00
DEls
a41653ce87 fix(dn): date deliveries by the slip and orders by the PO's order date
The reader always read the slip date (and receiver, site name, customer
VAT, supplier registration), but dn_batch_notes never had those fields,
so they were dropped on save and every receipt was dated the moment it
was posted. The Financial dashboard dated orders by upload date, so 24
of 41 POs (R7.08M) sat in no month and deliveries piled up in the month
they were booked: 'R1.1M ordered, R2.4M delivered'.

- scripts/pb-add-dn-dates.py: adds the five header fields,
  delivery_notes.posted_at and purchase_orders.order_date (applied).
- scripts/dn-backfill-dates.py: filled 270 documents from the stored
  readings, moved 192 receipts to their slip date (booking time kept in
  posted_at) and read 36 PO order dates from the PDFs (applied; replaced
  values in docs/audits/2026-10-11-dates-backfill-backup.json).
- Posting refuses a slip without a delivery date instead of using today;
  receipts record posted_at. The review page shows the date and locks
  Post with a plain message when it is missing.
- PO intake reads 'PO Creation Date' from the PDF text (pdftotext).
- Dashboard tile and monthly chart date orders by order_date only; an
  order without one is undated, never put on its upload month.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:35:00 +02:00
DEls
cca41d5bb1 chore(clock): keep the pre-recalc paid-hours backups
Row ids with the hours_worked values from before the 2026-10-09
paid-hours recalculation, so the run can be reversed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 00:03:35 +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
cb463c0faa feat(workforce): add timesheet exports and duplicate protection
Add per-site CSV and PDF matrix reports, editable sick-note date ranges, and weekend calendar styling. Preserve verified duplicate uploads for audit while excluding their entries from hours and costs, and prevent repeat files and overlapping daily imports.
2026-10-07 23:53:47 +02:00
DEls
a21c728d61 feat(workforce): add calendar and sick notes 2026-10-07 14:27:44 +02:00
DEls
f72397259c feat(auth): add Office access and restrict Clock users 2026-10-07 14:22:57 +02:00
DEls
8d53e3688c feat(workforce): add active and inactive workers 2026-10-07 13:41:05 +02:00
DEls
0df711875c feat(workforce): add rate history and missing-rate editor 2026-10-07 13:19:50 +02:00
DEls
4922d649ae feat: add remote purchasing MCP to existing app 2026-10-05 02:46:36 +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
e6772bfb1f fix(ask): migrate created/updated to autodate, sort ties by id
ask_threads/ask_messages/ask_log shipped with created/updated typed as
plain `date` fields, which PocketBase never populates — every row's
created has been empty since. pb-add-ask-collections.py now emits
autodate fields (onCreate, plus onUpdate for updated) and migrates the
three live collections in place under --apply, re-adding
idx_ask_messages_thread_created if PocketBase drops it with the field.

--backfill (apply-only) just reports affected row counts: PocketBase
can only set an autodate field to *now*, never a historical value, so
there's nothing safe to backfill.

Since old rows keep an empty created either way, the read side sorts
ask_messages by `created,id` instead of `created` alone, so tied
(all-empty) rows land in a stable, deterministic order rather than
whatever order PocketBase happens to return them in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:05:23 +02:00
DEls
b211ce1c8b feat(ask): PocketBase schema for threads, messages and log
Additive, dry-run-by-default script creating ask_threads, ask_messages
and ask_log (change/memory/request), following the
pb-add-dn-staged-ingestion.py pattern. --dry-run confirms all three
collections are new; --apply is a human step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:51:26 +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
1031a103c0 chore(dn): add two read-only checks for the staged rollout
dn-snapshot prints row counts across the delivery-note collections. The
two that matter are delivery_notes and delivery_note_items: the staged
pipeline must never move them on its own, so a reading before and after a
test says plainly whether anything reached the ledger.

dn-board renders the inbox board as text, which is how the pipeline can
be read over SSH or while the app is mid-deploy.

Both are read-only. The other scripts written while diagnosing this work
were one-off probes and are not kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 01:00:44 +02:00
DEls
14be7e7e21 fix(dn): keep an operator's correction through the next run
Picking the right order did nothing. The route stored the corrected
number, the re-run rebuilt the candidate from its page reads, and
po_number_raw went back to the original misread — so the operator
answered the same question again, having been told it was saved.

Nothing in dn_batch_notes distinguished a value a person chose from one
the model read, so the runner could not tell a correction from stale OCR
and overwrote both. A manual_overrides field now records the decision,
and the runner applies it before resolving rather than after, so the
corrected number is what the PO is matched on.

Only fields a run rebuilds are recorded this way. Quantity, unit and the
masses survive on their own; page ranges are the assembler's answer and
are corrected through the assembly route, which keeps the whole batch
consistent. An override must carry a value — clearing a box is not a
decision that a document has no number.

Verified end to end on a slip whose order number was read with a zero for
the letter O: the correction survived the run, resolved the order, moved
the match from fuzzy to exact, and left the document ready to post.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 23:55:56 +02:00
DEls
252dd7eb6a fix(dn): let staged ingestion run against the real PocketBase schema
Three faults stopped the staged pipeline from ingesting anything once it
met a live PocketBase rather than a test double.

PocketBase treats 0 as blank for a required number field, and two staged
fields legitimately hold zero: dn_batch_sources.ordinal is 0 for the
first source in a batch, and dn_batch_pages.region is 0 for a whole-page
read. Both creates were rejected with validation_required, so the first
file of every upload failed and no page ever persisted. The migration now
declares them optional, and pb-relax-dn-zero-fields.py relaxes instances
that already have them required.

The staged runner verifies its batch lease and commits the protected
write in one transaction, which needs PocketBase's batch API. That is off
by default, so the runner failed closed with
dn_pipeline_transactions_required and no deployment note explained why.
pb-enable-batch-api.py turns it on and raises the 3s transaction timeout,
which is tight for a commit carrying page images.

compare-dn-pipelines used top-level await while resolving as CJS, so the
command in the rollout runbook always failed to transform. Renamed .mts.

The tests missed all of this because they mock PocketBase, and the mock
accepts what the real server rejects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 23:49:58 +02:00
DEls
1a97d7db44 fix(migration): preserve quoted index semantics 2026-09-19 19:36:50 +02:00
DEls
bee2b3dd17 fix(migration): normalize PocketBase index definitions 2026-09-19 19:33:39 +02:00
DEls
bbd7a7bb23 fix(dn): harden staged assembly and page identity 2026-09-19 19:17:46 +02:00
DEls
11bc1a4b6b fix: verify audit collisions and purchase orders 2026-09-19 18:58:07 +02:00
DEls
310219d09b fix: keep staged migration dry run additive 2026-09-19 18:54:24 +02:00
DEls
2914eefacf fix: harden delivery note posting review gates 2026-09-19 18:53:25 +02:00
DEls
5d094183e6 feat: add delivery note posting and pipeline comparison 2026-09-19 18:45:30 +02:00
DEls
e388e460d0 fix(dn): harden staged lease and candidate reuse 2026-09-19 08:20:13 +02:00
DEls
33be75b821 fix(dn): preserve supplier evidence in resolver 2026-09-19 07:49:40 +02:00
DEls
ab57bafc4a feat(dn): assemble candidates and support revisions 2026-09-18 19:53:53 +02:00
DEls
498b25858a fix(dn): reconcile staged ingestion indexes by name 2026-09-18 19:02:47 +02:00
DEls
ad23b6572e fix(dn): harden staged ingestion schema migration 2026-09-18 18:59:36 +02:00
DEls
34dbdeb8bd feat(dn): add staged ingestion schema 2026-09-18 18:55:19 +02:00