Commit graph

256 commits

Author SHA1 Message Date
DEls
b1d06c1aa7 feat(schema): add file_hash and queue fields to DNs and POs 2026-09-11 10:29:10 +02:00
DEls
9a8d637ff1 docs(plan): ingestion restructure implementation plan
20 tasks across four phases, each phase ending at a shippable state.
Phase 1 additive only, Phase 2 renames and document collections, Phase 3
route cutover, Phase 4 drops. Nothing is deleted before Phase 4, so every
earlier phase is reversible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 10:21:29 +02:00
DEls
8075f6798a docs(spec): add PO fulfilment model — outstanding is computed, not stored
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>
2026-09-11 10:09:12 +02:00
DEls
2814061b28 docs(spec): ingestion restructure — per-entity documents, honest status
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>
2026-09-11 10:02:42 +02:00
DEls
ce773322f7 Merge branch 'fix/slip-pdf-iframe-proxy' into main 2026-09-07 14:53:26 +02:00
DEls
8317238184 fix(purchasing): proxy PB files so PDFs render in the slip iframe
PocketBase sets `Content-Security-Policy: ...sandbox` on /api/files,
which makes Chromium treat the response as opaque-origin and silently
fail to render PDFs inside an <iframe> (downloads still work). Add a
same-origin proxy at /api/pb-files that forwards the bytes and strips
the CSP / X-Frame-Options headers so the iframe can render.

Files:
- new: src/app/api/pb-files/[collection]/[recordId]/[...filename]/route.ts
- SlipPreview now uses /api/pb-files/... for PDFs and PB directly
  for images (<img> isn't subject to the CSP sandbox, so the direct
  URL works there unchanged).

Path validation, ALLOWED_COLLECTIONS gate, path-traversal guard, and a
stable Content-Disposition so downloads keep the original filename.
2026-09-07 14:53:22 +02:00
DEls
f936efa356 Merge branch 'feat/dn-upload-hash-dedup' into main 2026-09-07 14:51:37 +02:00
DEls
448b1e3108 Merge branch 'feat/purchasing-po-search-picker' into main 2026-09-07 14:51:33 +02:00
DEls
78fa8da05c feat(delivery-notes): dedup by SHA-256 at upload time
Mirrors the existing /api/purchase-orders/upload pattern onto the
delivery-note path so byte-identical re-uploads short-circuit with
`200 + { duplicate: true, existing_dn_id, ... }` instead of
creating a second delivery_notes row.

Concretely:
- Hash the bytes with SHA-256 before any PB round-trip.
- Query `data_import_batches` for any processed batch with the same
  hash inside a 30-day window (the same window the PO route uses; the
  underlying `idx_batches_file_hash` index already covers it).
- On hit, return the existing DN + batch ids and skip creation. The
  n8n OCR pipeline is never invoked for a duplicate slip, so a
  double-clicking operator costs us nothing.
- On miss, stamp `file_hash` on the new batch row so subsequent
  re-uploads can dedup against it.
- Wire the response through `UploadDeliveryNoteButton` as a new
  'duplicate' panel showing the existing DN id plus a quick link to
  /dashboard/purchasing where the awaiting-PO inbox lives.

pb-types: adds file_hash?: string to DataImportBatchRecord so the
column the PB schema already has is reflected in the TS shape.
2026-09-07 14:49:33 +02:00
DEls
2d0f1b6703 feat(purchasing): add searchable picker for PO assignment
Replaces the static Select in the needs-review delivery-note modal
with a small PoPicker component that lets you type a PO number,
supplier, or project code to filter the list. The picker keeps the
same chosen-id contract so the assign flow is unchanged.

No new dependencies: a controlled input + filtered <ul> +
outside-click handler covers the use case without pulling in Radix
Combobox.
2026-09-07 14:20:16 +02:00
DEls
08ed89d9c8 feat(dashboard): enhance operations dashboard with rain outlook and coordinate support
Updated the operations dashboard to include a rain outlook feature, providing users with a 7-day forecast based on site coordinates. Added latitude and longitude fields to the site form for better weather data integration. Enhanced the fuel log and site management functionalities to utilize dynamic fuel pricing based on historical data. Improved overall data handling and user experience across various dashboard components.
2026-09-06 19:59:52 +02:00
DEls
56f6dd8efc feat(plant): implement independent pagination for plant log entries
Added a new pagination system for the "Plant log entries" table in PlantView, allowing for separate page management from the main plant data. Introduced constants for log page size and updated the rendering logic to accommodate paginated logs, enhancing user experience when navigating through log entries.
2026-09-02 12:36:55 +02:00
DEls
602c62ba06 refactor(plant): enhance PlantPage and PlantView for fuel log integration
Updated PlantPage to aggregate vehicle data from fuel_logs, including hours worked and total value calculations based on odo readings. Adjusted PlantView to support navigation to vehicle details and improved log display with new data structure. Removed legacy code related to plant_hours_log and refined data handling for better performance and clarity.
2026-09-02 12:36:10 +02:00
DEls
377335af4f feat(vehicles): implement DELETE endpoint for vehicle removal
Added a DELETE API endpoint for hard-deleting vehicles, including error handling for foreign-key constraints. Updated tests to cover successful deletion and various error scenarios. Enhanced the PlantPage component to aggregate vehicle data and display KPIs, including total hours and values. Updated PlantView to handle optimistic UI updates for deletions.
2026-09-02 11:52:39 +02:00
DEls
b04c1f0b69 fix(fuel): keep fuel-limit text on a single line
The 'fuel limit' cell text (e.g. '8 L per hour limit') was wrapping
to two lines in the table and on mobile cards. Add whitespace-nowrap
and a title tooltip in all three places fuel-limits renders:
- FleetMobileCard edit-mode (line ~1056)
- FleetMobileCard non-edit branch (line ~1106)
- FleetRow desktop table cell (line ~1312)
2026-08-31 14:20:41 +02:00
DEls
545343e086 fix(fuel): keep alert/warn count badges on a single line
The 'X alerts' and 'X warns' badges in AlertCard and AlertListRow
had no whitespace-nowrap, so '2 alerts' could wrap to two lines on
narrow viewports. Add whitespace-nowrap in both AlertCard and
AlertListRow variants of the alert-red and warn-amber badges.
2026-08-31 14:19:04 +02:00
DEls
006596d589 fix(components): keep UnitTypePill text on a single line
The Type column badge had no whitespace-nowrap, so 'Fleet (L)' /
'Plant (h)' wrapped inside the pill whenever the table column was
narrow. Add whitespace-nowrap to the badge className so the label
stays single-line at any column width.
2026-08-31 14:18:19 +02:00
DEls
8013fa109e fix(fuel): tighten Alert + Fleet cards on smaller screens
Tighten padding, gap, and rounding on the alert cards and the
Fleet mobile card so they take less vertical and horizontal real
estate on narrow viewports. Layout restores to the roomier
current sizing once the screen reaches the 'sm' breakpoint (640px).

Per-card changes:
- AlertCard (mobile): gap-3 -> gap-2 sm:gap-3; p-4 -> p-3 sm:p-4.
- AlertListRow (desktop list): px-4 py-3 -> px-3 py-2 sm:px-4 sm:py-3;
  gap-3 -> gap-2 sm:gap-3.
- FleetMobileCard (mobile): gap-2 -> gap-1.5 sm:gap-2; p-3 -> p-2.5
  sm:p-3.
2026-08-31 14:17:02 +02:00
DEls
475dd77c42 fix(fuel): truncate vehicle description in Alert + Fleet rows
The leading text in alert cards (mobile + desktop) and fleet rows
wrap to a multi-line layout when the description + project badge +
status pill can't all fit horizontally. The inner flex containers
holding the description span already had 'truncate' on the span,
but were missing 'min-w-0', so the flex children couldn't shrink
below their content size.

Add 'min-w-0' to four inner flex containers:
- AlertCard top row (mobile alert cards)
- FleetMobileCard edit-mode meta row (project badge + fuel-limits +
  UnitTypeSelect)
- FleetMobileCard non-edit meta row (project badge + UnitTypePill +
  fuel-limits)
- FleetRow top cell (desktop fleet row, Description or Link)

Add native 'title' attribute on the three truncatable description
spans (AlertCard, AlertListRow, FleetMobileCard non-edit) so the
full vehicle name is available on hover as a fallback for the
ellipsis truncation.
2026-08-31 14:14:23 +02:00
DEls
dc04987454 fix(inline-edit): stop wireMap leaking camelCase unitType on wire 2026-08-31 13:55:32 +02:00
DEls
d4862b37e7 feat(inline-edit): add wireMap option for snake_case keys; plant Type column 2026-08-31 13:11:10 +02:00
DEls
e79538ab96 feat(plant): filter vehicles to unit_type=plant + cost_per_hour>0 at server 2026-08-31 13:07:22 +02:00
DEls
8394084648 feat(fuel): add Type column to Fleet table (desktop + mobile) 2026-08-31 13:04:19 +02:00
DEls
dfc85385d1 feat(fuel): filter vehicles to unit_type=fuel at server, plumb unitType 2026-08-31 13:00:55 +02:00
DEls
a1d3e6350b feat(components): add shared UnitTypePill and UnitTypeSelect 2026-08-31 12:58:37 +02:00
DEls
2ea87ce56d docs(plan): implement vehicle unit_type inline-edit UI on Fleet + Plant 2026-08-31 12:57:18 +02:00
DEls
31c86b5b89 docs(spec): vehicle unit_type inline-edit UI (fleet + plant)
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).
2026-08-31 12:48:57 +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
395c0ff114 feat(chat): sidebar link, editable-while-waiting, sticky footer, limit warning
Three concerns from testing:

1. Chat typing UX: the textarea was disabled while a request was in
   flight, so the user couldn't draft the next question. Removed the
   disabled attribute; Enter is just a no-op while busy. Send button
   still gates actual submission.

2. Sticky footer: switched the chat wrapper to 100svh (mobile-keyboard
   aware) and made the composer sticky bottom-0 with a solid background
   so it no longer fades into the scroll area.

3. Limit-warning: the SSE stream now reads the 'done' event and surfaces
   a banner when the server reports the agent loop hit its iteration
   cap, so a quiet stream end isn't mistaken for a complete answer.

Sidebar: added 'Ask' nav item pointing at /dashboard/chat with the
Sparkles icon so the chat is reachable from the dashboard even if the
standalone-PWA install path keeps misbehaving.
2026-08-30 23:51:45 +02:00
DEls
e45d19900d fix(pwa): emit apple-mobile-web-app-capable for iOS Add-to-Home-Screen standalone mode
Next 16's appleWebApp metadata API only emits `mobile-web-app-capable`,
which iOS Safari ignores for Add-to-Home-Screen standalone mode. The
home-screen icon launches the page inside regular Safari with full
browser chrome instead of as a standalone app.

Adding the iOS-specific `apple-mobile-web-app-capable` tag to the root
layout's <head> so the home-screen bookmark launches without Safari UI.

Harmless on non-iOS browsers (those use `mobile-web-app-capable` which
Next already emits).
2026-08-30 23:28:48 +02:00
DEls
e0e2feb61c docs(pwa): manual install verification steps for real devices 2026-08-30 22:42:55 +02:00
DEls
0d056e116f fix(pwa): restore static manifest.json + remove dead next-pwa config
The next-pwa plugin's `manifest` option is silently ignored in
@ducanh2912/next-pwa@10.2.9 — only the service worker is generated.
The dashboard Web App Manifest now lives as a static file at
public/manifest.json, sourced from the next.config.js block we just
removed. This is the same shape the project shipped at HEAD~3, just
with the new short_name and id field added by Task 4.
2026-08-30 22:33:35 +02:00
DEls
95be557983 build(pwa): wire build-icons.mjs into the build pipeline 2026-08-30 22:22:42 +02:00
DEls
363410a735 feat(pwa): set MEGA OPS short_name + add id field to dashboard manifest 2026-08-30 22:21:51 +02:00
DEls
11d47fffda feat(pwa): rewrite chat.webmanifest with MEGA Ask metadata + chat icons 2026-08-30 22:20:48 +02:00
DEls
797a69c4ec chore(pwa): drop stale manifest.json (regenerated by next-pwa) 2026-08-30 22:19:56 +02:00
DEls
6ce3c24f12 refactor(pwa): address code review on build-icons.mjs 2026-08-30 22:18:25 +02:00
DEls
5a7d2931f9 feat(pwa): render chat icon set + per-route apple-touch-icons 2026-08-30 22:15:15 +02:00
DEls
055387cc6d docs(plan): MEGA Ask vs MEGA OPS PWA split-install icon plan 2026-08-30 22:12:30 +02:00
DEls
459905758f docs(spec): MEGA Ask vs MEGA OPS PWA split-install icon design 2026-08-30 22:11:02 +02:00
DEls
091030d978 fix(chat): strengthen the arithmetic ban after a probe found a violation
Probing 'combined value of the top three delivery notes' against the
production model returned R 3 285 785 — arithmetically correct, exactly
what the ban was written to prevent.

The previous prompt said 'never do arithmetic' but did not say what
to do instead when the user explicitly asks for an aggregate. The
model picked the path of least resistance and summed them.

New prompt lists the verbs that count as arithmetic (including average,
percent, ratio, weighted, combined, total, sum) and gives the model a
concrete response shape: report each row's value, declare the
aggregate 'not available from me', tell the user which number is
missing.

Re-probed — the model now lists the three values individually and
declines the sum.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:02:06 +02:00
DEls
e528589e9c feat(chat): save a single answer as PDF via the print dialog
Hides app chrome via a 'printing-answer' class on body, leaves only the
.print-target visible. The class is removed in a finally so a cancelled
print dialog can't leave the app invisible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 13:09:06 +02:00
DEls
7d066f37dc feat(chat): take or attach a photo and ask about it
Adds a camera button next to the paperclip that opens the device camera
on phones (capture='environment') and a file picker otherwise. Photos
are read into base64 and sent with the next user turn.

Pending photos render as a thumbnail strip above the composer with a
remove button each. The state had to be hoisted above the composer
handlers — useCallback closes over it at handler-creation time, so
declaring it lower in the file produces a TS2448.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 13:03:21 +02:00
DEls
c5dbb6cf64 feat(chat): accept images in a user turn on both providers
Anthropic adapter: image content block before text.
OpenAI-compatible adapter: image_url with a data: URL.
Route normalizer carries images through the trust boundary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 12:57:52 +02:00
DEls
6274d90b36 docs(env): default MiniMax-M3 (vision + tools verified)
M2.7 is text-only; M3 is the first MiniMax model that accepts both the
tools parameter and image inputs on the OpenAI-compatible endpoint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 12:56:30 +02:00
DEls
00269b90b5 feat(agent): add six listing and ranking tools
Closes the coverage gaps: delivery notes, plant hours, fixed costs,
income claims, the employee roster and import batches. Each takes an
optional site, an enumerated sortBy and a limit, so list/highest/lowest
questions work across every collection.

Verified live against the dashboard:
  - list_delivery_notes sorts by value; top result AS0002086 at R3 255 444
  - list_employees returns 72 (16 permanent + 56 casual)
  - list_imports returns 44 batches, none failed
  - list_plant_hours returns 0 rows — same as /dashboard/plant
    (the production plant_hours_log collection is empty today)
  - list_fixed_costs / list_income_claims wired through same shape

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 12:38:33 +02:00
DEls
94c393b279 feat(agent): add listing loaders for the uncovered collections
Six new loaders: delivery notes (with received items bucketed for
value computation), plant hours, fixed costs, income claims, employees
and import batches. Each takes optional filters rather than full
queries, so callers always run through typed tool handlers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 12:36:04 +02:00
DEls
0c7cd5b7d5 feat(metrics): compute delivery note value from received items
delivery_notes stores no value; a note is worth the quantity received on
each line times the matched PO item's rate. Unmatched lines contribute
zero rather than a guessed rate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 12:34:51 +02:00
DEls
818a679922 feat(types): add IncomeClaimRecord
Queried by two dashboard pages and the agent loader, but previously
untyped — those reads compiled only because untyped getFullList returns
loose records.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 12:33:44 +02:00
DEls
b2b72dacfe docs(plan): agent coverage gaps, vision and PDF export
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 12:27:38 +02:00