Emits the canonical xlsx with a sample row derived from the first
vehicle in PocketBase. Falls back to a hardcoded sample if the
vehicles collection is empty.
Two follow-up fixes from the Task 5 code-quality review:
- scripts/generate-plant-hours-template.ts uses
fileURLToPath(import.meta.url) instead of __dirname. The
project's tsconfig has module: esnext, so the moment the
package flips to ESM the original would throw
ReferenceError: __dirname is not defined.
- src/lib/plant-hours-template.ts documents that the frozen
header row is a best-effort request — SheetJS Community
Edition silently drops the !freeze property (only Pro emits
frozen-pane XML). The function still produces the canonical
13-column workbook, but the doc comment no longer claims a
behaviour that doesn't happen.
Threaded attentionDismissedAt from the page mapper into the view.
When set, the header shows a pill with the timestamp and a Restore
button that DELETEs /api/vehicles/{id}/attention-dismiss and
refreshes the page. Errors render inline below the pill.
Slug-to-prose map for restore failures mirrors the dismiss-side
pattern from commit c633681 — RESTORE_ERROR_COPY at module scope,
friendly copy in window-less inline error slot.
- Disable all dismiss buttons while any is in flight so rapid
clicks on a second vehicle don't silently no-op.
- Map API error slugs (bad_id, not_found, update_failed) to
user-facing copy so window.alert shows prose, not machine tags.
- Add aria-busy to the dismiss buttons.
- Drop redundant e.stopPropagation(); preventDefault() is
sufficient to suppress the wrapping <Link>.
buildPlantHoursTemplateWorkbook() emits the canonical 13-column xlsx
with a frozen header row, typed cells (date/time/number), a sample
row using the supplied vehicle, and a Notes column widened for
readability. Used by both /api/plant-hours/template and the seed
script that writes RAW_DOCS/Plant_Hours_Template_2.xlsx.
Adds two tests that lock in behaviour the previous test file relied on
implicitly:
- validatePlantHoursRow rejects when the supplied vehicle's qr_code_id
doesn't match row.vehicle_qr (the guard the schema extends beyond
the original spec)
- computePlantHoursFields rounds value to 2 decimal places (a
fractional-input case the integer-only assertion didn't exercise)
Pure validator covering all the error/warning cases from the spec:
unknown QR, bad date range, end_hours < start_hours, bad time format,
missing cost (cell + vehicle), duplicate week, and the type-mismatch
warning. computePlantHoursFields() derives hours/value/over_budget/
hours_mismatch and the vehicle_unit_type_at_import snapshot.
Click on an AlertCard or AlertListRow to hide the vehicle from
'Attention required' instantly. POST /api/vehicles/{id}/attention-dismiss
stamps the field; optimistic state mirrors that locally so the row
disappears before the network roundtrip. Error path reverts the
overlay and shows window.alert (same pattern as inline-edit).
Moves normHeader/resolveColumn/parseDate/toNumber out of fuel-import-parse
into a new src/lib/import-parse.ts so plant-hours can reuse them. Adds
parseTime (HH:MM, 24h, single-digit hour normalised). Adds the
PLANT_COL_ALIASES table for the plant-hours route. fuel-import-parse
becomes a thin re-export shim — the fuel-logs route and its existing
test file are unchanged.
Server mapper now emits attentionDismissedAt and maxFlaggedDate on
each VehicleSummary. The flaggedVehicles memo uses the new helper
to hide vehicles whose current flagged logs are all at-or-before
the user's most recent dismissal. Auto-undismiss is implicit: a
new fuel log with date_logged > attentionDismissedAt brings the
vehicle back on the next page load.
Pure predicate: returns true when a vehicle has been dismissed and
no flagged log is strictly newer than the dismissal. Six Vitest cases
covering each branch and the auto-undismiss boundary.
Sets or clears vehicles.attention_dismissed_at via pbAdmin().
15-char PB id pattern, 404 when PB reports the record is missing,
500 on any other write failure. Test mocks pbAdmin() so no live PB
needed.
Wrap the received-items list in <details>/<summary>. Default
collapsed; click to expand. Hidden when dn.receivedItems is
empty (preserved). The 'Raw OCR data' <details> lower in the
same card is independent and unaffected.
PoDetailView now mirrors its detail prop into local state and
passes a handleSupplierSaved callback that updates state.supplier
and triggers the existing refresh(). SupplierEditor replaces the
static Badge when the supplier is unknown.
Badge-as-button + Input + Save/Cancel client component. Only
renders when isUnknownSupplier(currentSupplier) is true. Save
POSTs to /api/purchase-orders/{poId}/supplier and invokes
onSaved(next). Esc cancels; Enter saves when valid.
Not yet rendered — Task 3 wires it into PoDetailView.
Validates { supplier } (trimmed, 1-200 chars) and PATCHes the
purchase_orders.supplier field via pbAdmin(). 400/404/500 with
explicit error codes; mirrors the retry-route pattern.
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.
Updated the OCR result processing to include validation of matched PO items before determining the verification tier. This change ensures that if the local PO items fetch is empty but n8n supplied matched IDs, the tier will downgrade to draft, requiring user review. Additionally, the `resolveOcrTier` function has been modified to account for item verifiability, improving the accuracy of tier assignments based on confidence levels and PO resolution status. Unit tests have been added to cover these new scenarios.
Updated the OCR result processing to include a more robust PO matching mechanism. The new implementation prioritizes exact matches, followed by normalized comparisons, and finally employs a Levenshtein-based fuzzy match for improved accuracy. This change ensures that delivery notes are correctly linked to purchase orders, even when OCR introduces minor errors. Additionally, the behavior for handling confidence levels and PO resolution has been clarified in the documentation, enhancing overall processing reliability.
OCR is noisy. Instead of exact-then-fuzzy-fallback, always fetch
all ~15 POs from PocketBase, normalise both sides (strip non-
alphanumeric, lowercase), score each with Levenshtein similarity,
and return the best match above 0.75 threshold. Single-char
insert/delete (MEGCP09239 vs MEGCPO9239) scores ~0.9 — matches.
Completely different POs (similarity <0.5) don't match.
Plus the existing 'return 200 with empty items' fallback if no
match — keeps the workflow loop alive even when OCR is bad.
OCR sometimes mis-reads PO numbers (e.g. 'MEGCP09239' instead of
'MEGCPO9239') — when that happens the PO lookup 404'd, which
terminated the n8n workflow loop even with onError set on the
HTTP node.
Return 200 with {po: null, items: [], note: 'po_not_found'} instead.
The downstream 'Match items to PO' Code node already treats an
empty items array as 'no PO to match against' and leaves every
OCR item unmatched. The OCR result route then marks the DN as
needs-review and the user can fix the PO link manually.
OCR sometimes prefixes PO numbers with '#' from the slip header
(e.g. "#MEGCPO9165"). The workflow URL would otherwise break on
the # being a URL fragment separator, and the exact-match PB
filter would also fail (the fuzzy fallback worked, but a clean
sanitise is cheaper).
TS strict-mode rejects direct cast from RecordModel because the
two types don't sufficiently overlap. The fix is the standard
'as unknown as' double-cast. Was previously caught only by the
Docker build's stricter typecheck, not by the local dev
typecheck (which was polluted by .next/dev/types stale cache).
GET /api/purchase-orders/by-number/[poNumber]/items
- Auth via shared x-ingest-secret
- Returns { po: {id, purchase_order_number, supplier}, items: [{id, description, qty, qty_outstanding, unit}] }
- Exact lookup first, then fuzzy fallback (strip non-alphanumeric + lowercase)
- Used by the Delivery Note OCR workflow to enrich the LLM extraction
with PO context so each line item can be linked back to a
purchase_order_items row.
PB's filter parser only accepts && as the boolean AND operator; the
literal word AND returns HTTP 400 'Something went wrong while processing
your request'. The rest of the codebase already uses && (see
purchase-orders/upload/route.ts:81).
Caused updateBatchRow to silently no-op after every successful OCR
run — the batch row never transitioned to 'processed'.
- upload: create data_import_batches row when DN is uploaded
(batch_type=delivery_notes, processed_record_id=<dnId>)
- ocr-result: mark the batch processed on success, failed/dead on
failure (uses nextBatchState from batch-state.ts for retries)
- new /api/delivery-notes/[id]/slip proxy route: lets n8n fetch the
slip file via shared secret instead of embedding the PocketBase
admin JWT in workflow headers (which was leaking in the workflow JSON)
Backfill: existing pending DN ekko6xkhynoekas needs a manually
created data_import_batches row until it's reprocessed.
- 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.
- 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.
- 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.
- Improved the layout of the detail drawer using flexbox for better responsiveness.
- Implemented a scrollable container for the timesheet table to prevent overflow.
- Ensured a clean and organized appearance for displaying employee details.
- Adjusted the DialogContent to use flex layout and set a maximum height for better responsiveness.
- Wrapped the timesheet table in a scrollable div to prevent overflow and enhance user experience.
- Ensured the layout maintains a clean and organized appearance while displaying employee details.
- 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.
- 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.