Commit graph

42 commits

Author SHA1 Message Date
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
fa725f096c feat(purchasing): create the PO row at upload with a placeholder number
purchase_orders.purchase_order_number carries a UNIQUE index
(idx_po_number, confirmed live) so two unprocessed POs can no longer
share "". The upload route now creates the purchase_orders row
directly (dropping the shared data_import_batches pool for POs),
stamping it with a placeholder minted by src/lib/po-placeholder.ts
that n8n's extraction overwrites once OCR completes. Also requires
`site` at upload instead of waiting on OCR-derived site matching,
which only succeeds on ~8% of real documents per the project's audit
- the uploader already knows which site they're on.

Dedup now checks purchase_orders by file_hash directly. This
instance's purchase_orders collection has no working created/updated
autodate field (confirmed live - sort/filter by `created` 400s) and
no other timestamp field, so the dedup query drops the 30-day window
used by the old batch-based dedup and instead excludes only rows
whose last attempt failed.

process-batch/route.ts is intentionally untouched - it still creates
a fresh purchase_orders row from a batch, which is now inconsistent
with this upload shape. That inconsistency is expected until the
follow-up task rewrites process-batch to fill in the row born here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:14:19 +02:00
DEls
819678fb06 fix(timesheets): document-first import, site stamped per entry
Cuts the timesheet importer over to the document-first pattern already
proven for fuel logs and plant hours: creates a `timesheets` document
pending (site required, unlike the other two sheet types) before any
parsing happens, guards every early-exit path with a failDoc() helper
that writes a real `failed` outcome, and links entries via the
`timesheet` relation instead of `batch_reference`.

The actual point of this task: each timesheet_entries row now stamps
its own `site` from that row's employee (falling back to the
document's site only when the employee has none), rather than
inheriting the upload's site. Site attribution for payroll history is
resolved once, at write time, so moving a worker to a different site
later no longer rewrites their historical labour cost at read time.

Also updates ImportTimesheetsButton to the documentId response shape
(previously batchId), matching the Task 10/11 button updates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:07:01 +02:00
DEls
49f119ab5c fix(fuel): document-first import with honest status
The fuel-logs import route created its data_import_batches audit row
with status "processed" hardcoded before a single row was inserted —
the same production bug fixed for plant-hours in the prior commit.

Cut the route over to fuel_sheets (Task 6/7): the document is created
`pending` with the source file attached before any parsing, and the
real outcome is computed from actual insert counts via
resolveDocumentOutcome() and written back before the response
returns. A sheet whose vehicles match nothing now comes back `failed`
with rows_created: 0, not `processed`. Every fuel_logs row now links
via the `fuel_sheet` relation instead of `batch_reference`.

`fuel_sheets.site` is optional (a pump sheet can span sites; the
row-level site resolved from "Current Site" stays authoritative), so
it's only passed to the document create when the form supplied one.

Also renames ImportResult.batchId -> documentId in
import-fuel-logs-button.tsx to match the new response shape. No
exception-report route or separate orchestrator lib exists for fuel
logs (unlike plant-hours), so there was nothing else to rename.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:02:02 +02:00
DEls
610426776a fix(plant-hours): document-first import with honest status
The plant-hours import route used to create its audit row with
status "processed" hardcoded before a single insert ran. Production
ended up with 19 such batches, 18 of which created zero rows while
claiming success.

Cut the route over to plant_hours_sheets (Task 6/7): the document is
created `pending` with the source file attached before any parsing,
and the real outcome is computed from actual insert counts via
resolveDocumentOutcome() (Task 8) and written back before the
response returns. A sheet whose vehicles match nothing now comes back
`failed` with rows_created: 0, not `processed`.

Also renames plant_hours_log's `batch_reference` writes to the new
`plant_hours_sheet` relation (src/lib/plant-hours-import.ts), and
renames the exception-report route folder from [batchId] to [sheetId]
to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 13:56:11 +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
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
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
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
a1d3e6350b feat(components): add shared UnitTypePill and UnitTypeSelect 2026-08-31 12:58:37 +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
394a5c0033 feat(plant-hours): ImportPlantHoursButton component
Mirrors ImportFuelLogsButton for the plant-hours weekly log upload
flow. Supports drag/drop, click-to-browse, shows import summary
(total / imported / mismatched / skipped), and links to the
exception-report CSV when rows are skipped.
2026-08-28 13:31:30 +02:00
DEls
da21518f42 refactor(fixed-cost-dialog): drop defaultType prop, use field-message lookup, local-date default 2026-08-28 13:22:07 +02:00
DEls
b59380455c feat(site-detail): split P&G card into monthly + one-off sections 2026-08-28 13:14:15 +02:00
DEls
b6fccac5a7 fix(po-detail): scope SlipPreview URL to correct PB collection 2026-08-27 00:10:45 +02:00
DEls
c9f3d61fc6 feat(purchasing): shorten upload button labels (Upload PO / Upload DN) 2026-08-26 23:59:21 +02:00
DEls
d2a4efa6e8 feat(needs-review): shrink modal to max-w-md for glance-and-pick flow 2026-08-26 23:26:26 +02:00
DEls
aa76470446 fix: pass recordId to SlipPreview from both callsites 2026-08-26 23:24:39 +02:00
DEls
ce3de2f9fb fix(slip-preview): build PB URL from recordId (was parsing filename) 2026-08-26 23:22:33 +02:00
DEls
0c2e092a3a feat(needs-review): compact modal with sticky header above slip 2026-08-26 23:10:05 +02:00
DEls
9ed7e55139 feat(purchasing): enhance purchasing page with received items tracking
Updated the PurchasingPage component to include received items data, allowing for accurate fulfillment percentage calculations based on authoritative data. The implementation now sums received quantities per purchase order item, replacing the previous denormalized field. Additionally, the needs-review delivery notes component has been improved with a modal for reviewing slips and assigning purchase orders, enhancing user interaction. The SlipPreview component has been integrated for better visual representation of delivery slips.
2026-08-26 23:05:05 +02:00
DEls
20d12f46d9 feat(slip-preview): render PDFs in an iframe instead of download-only 2026-08-26 23:03:41 +02:00
DEls
8dbf393446 feat(delivery-notes): enhance delivery note upload and processing
- Updated the delivery note upload API to include site ID for better project association.
- Modified the delivery note schema to support additional fields such as processing status, OCR confidence, and received by name.
- Enhanced the UI components for uploading delivery notes to allow file drop functionality and improved user feedback during file selection.
- Refactored purchasing page to integrate delivery notes that require review, providing better visibility and management of pending items.
- Added new KPIs to track active purchase orders and fulfillment percentages, improving overall dashboard insights.
2026-08-26 15:33:30 +02:00
DEls
2ace4365e6 feat(timesheets): enhance import functionality with site linking and employee creation
- Updated the timesheet import route to support optional metadata fields for site linking and auto-creation of missing employees.
- Enhanced parsing to detect multiple file formats, including Hills Casual Matrix and long-format CSV/XLSX.
- Modified response structure to include detected format and created employees in the JSON response.
- Improved UI components to allow project selection and options for creating new workers during import.
- Refactored import logic to ensure a seamless user experience when handling timesheet uploads.
2026-08-26 12:44:02 +02:00
DEls
f5c063765d feat(fuel-view): enhance inline editing and UI components
- Integrated inline editing functionality for vehicle descriptions, allowing users to edit directly within the FuelView.
- Added a Topbar with EditModeToggle and ImportFuelLogsButton for improved user interaction.
- Implemented EditModeBanner to provide feedback during editing sessions.
- Updated FleetMobileCard and FleetRow components to support inline editing, enhancing the overall user experience.
- Refactored FuelView to accept title and subtitle props for better contextual information.
2026-08-26 08:54:57 +02:00
DEls
472be99fd8 feat(dashboard): add job start date to site records and enhance site management
- Introduced a new optional field `job_start_date` in the site schema to track project start dates.
- Updated the SitesPage component to utilize `job_start_date` for determining site status and fallback logic.
- Enhanced the SitesView component to display the new status "upcoming" for sites with future start dates.
- Modified the SiteDetailPage to include `job_start_date` in the site details and added an EditSiteButton for site management.
- Updated the site templates to include `job_start_date` for better pre-filling in the site creation form.
2026-08-25 19:01:56 +02:00
DEls
1b9779f47e feat(timesheets): enhance import functionality to support XLSX and CSV formats
- Updated the import route to accept both long-format CSV and Hills-style wide-format XLSX files.
- Implemented a new parsing mechanism that detects file type and processes accordingly.
- Enhanced error handling to provide more informative messages during file parsing failures.
- Modified the response structure to include total rows processed and detailed error reporting.
- Updated the UI components to reflect changes in file selection and user instructions for importing timesheets.
2026-08-25 18:38:02 +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
18ebf6d217 diag: include step + detail in process-batch lookup_failed
When the route returns 500 with 'lookup_failed', include the failing
step name and the underlying error string in the response body so we
can pinpoint which catch block is firing (get_batch / dedup / match_site).
Production diagnostic - safe to revert once root cause is found.
2026-08-24 18:39:15 +02:00
DEls
0802afccf1 refactor(ui): tighten upload-button status pill (DRY, a11y, error UX)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 19:58:14 +02:00
DEls
029b9b3bf9 feat(ui): poll batch status + retry button on PO upload
After Task 7's upload returns 202 { batch_id, status: 'queued' }, the
button now polls /api/data-import-batches/[id]/status every 2s, renders
a state pill (queued/processing/processed/failed/dead), and provides a
Retry button that calls POST /retry for failed/dead batches. Polling
stops on terminal states and on unmount; no double-submit.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 19:52:53 +02:00
DEls
5abc96116f Import PO MIME/size constants from shared schema module
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 15:03:48 +02:00
DEls
0b873ae308 Add UploadPurchaseOrderButton component 2026-08-19 15:03:47 +02:00
DEls
20a16a27f8 Enhance Dashboard and Sidebar Navigation
- Updated the Dashboard page to improve card styling with a warm-amber glow strip and adjusted icon presentation for better visual appeal.
- Expanded the Sidebar navigation by adding new entries for "Sites," "Workforce," "Plant & Equipment," "Fleet & Fuel," and "Purchasing," each with corresponding icons and descriptions to enhance user navigation and access to key features.
2026-08-19 01:54:16 +02:00
DEls
c5b1acfc7f Update package dependencies and remove unused API routes
- Added `pocketbase` as a dependency in `package.json` and `package-lock.json`.
- Updated the `dev` script in `package.json` to run on port 6004 instead of 3000.
- Removed unused API routes: `src/app/api/health/route.ts`, `src/app/api/mcp/route.ts`, `src/app/api/upload/route.ts`, and their associated logic to streamline the codebase.
- Cleaned up `package-lock.json` by removing unnecessary peer dependencies and ensuring consistency in the dependency tree.
2026-08-19 00:14:47 +02:00
DEls
97cb5a48b5 Sidebar: switch breakpoint from xl (1280px) to lg (1024px)
The sidebar was hidden until 1280px wide, which meant laptops and
small desktops between 1024-1280px (incl. iPads in landscape, many
Windows laptops, Chromebooks) saw the hamburger instead of the
persistent sidebar. That felt wrong on what was clearly a desktop.

Dropping to 'lg' (1024px):
- Phone portrait (< 768 px)  → hamburger ✅
- Tablet portrait (768-1024 px) → hamburger ✅
- iPad landscape / laptop (1024 px+) → sidebar ✅
- 932 px iPhone landscape cap is below 1024 px, so the hamburger
  still kicks in for all landscape phones.

Changed 4 occurrences: 3x 'xl:hidden' → 'lg:hidden', 1x
'hidden xl:block' → 'hidden lg:block' on the <aside> element.
2026-08-18 13:31:31 +02:00
DEls
3506640e4a Add Uploads page with 4 colored drop zones
New /dashboard/uploads page with a 2x2 grid of drop zones, one per
ingest source:

  - Orders (orange) — CSV / XLS / XLSX, max 10 MB
  - Delivery Notes (teal) — PDF / images, max 20 MB
  - Fuel Logs (yellow-green) — CSV / XLS / XLSX, max 5 MB
  - Time Sheets (green) — CSV / XLS / XLSX, max 5 MB

Each block has:
  - A coloured left stripe + icon disc in the accent colour
  - Title, format-count badge, and a long description of what to upload
  - File-format chips and a max-size hint
  - A drag-and-drop drop zone that also accepts files via click
  - A primary 'Upload X' button in the accent colour
  - A 'Recent' list showing the last 4 uploads with status badges

When files are dropped or picked, the client POSTs to
/api/upload with a 'type' field. The endpoint validates MIME and size
against the kind-specific rules in src/lib/uploads.ts and returns a
JSON descriptor with accepted/rejected entries. New entries are
prepended to the page's recent-uploads list in real time, and a
bottom-right toast shows accepted/rejected counts.

The sidebar gets a new 'Uploads' nav item with the upload icon.

Files:
  - src/lib/types.ts                    type definitions
  - src/lib/uploads.ts                   acceptance rules + mock history
  - src/app/api/upload/route.ts          type-aware upload handler
  - src/app/dashboard/uploads/page.tsx   the new page
  - src/components/layout/sidebar.tsx    Uploads nav entry
2026-08-09 19:30:00 +02:00
SJCoetz
df459de0f5 Remove Active Sites card from sidebar
The card showed hardcoded '5 projects · 3 flagged · 2 on schedule'
stats that were never updated from the mock data and duplicated
information in the Sites module. Removing it gives the sidebar a
cleaner Logo → Nav → Lock structure and means the Lock screen button
is visible without scrolling on standard heights.

Also align the mobile drawer's close-button breakpoint from lg to xl
to match the rest of the sidebar's breakpoint logic.
2026-08-09 03:02:59 +02:00
SJCoetz
eeea5b7592 Initial commit: MEGA INFRASTRUCTURE dashboard + Coolify Dockerfile 2026-08-09 01:37:40 +02:00