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.
The plan spec called for default "fuel" on the new select so existing
vehicles rows (which would otherwise fail the required=True
constraint) stay valid. PocketBase applies the default at the field
level when adding it to an existing collection.
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.
Adds the schema needed for the weekly plant-equipment hours log:
- vehicles gets a unit_type select (fuel|plant), defaults to fuel
for backward-compat with existing rows
- plant_hours_log collection with the 13 plant-hours fields plus
vehicle_unit_type_at_import snapshot and composite unique index
(vehicle_qr, start_date)
- data_import_batches.batch_type gains plant_hours
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'.
Adds a design doc for capturing weekly plant-equipment hours via an Excel
template against the existing /dashboard/plant page. One new
plant_hours_log collection, one vehicles.unit_type field, three new
routes (import, template download, exception report CSV), and an
extension to PlantView.
Spec covers: lenient import with per-row errors, downloadable exception
report for fix-and-retry, snapshot of vehicle.unit_type on each row,
over-budget flag against a per-week budget, and a symmetric attention
panel on /dashboard/plant for type mismatches.
Per-vehicle 'I've seen this' control on the Fuel dashboard. Stamps
attention_dismissed_at on the vehicles collection; auto-undismisses when
a flagged fuel log with date_logged > dismissal arrives. Timestamp only,
no reason field. Tiny POST/DELETE route + filter line in page mapper +
Dismiss button on alert rows + Dismissed/Restore pill on vehicle detail.
Co-Authored-By: Claude <noreply@anthropic.com>
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.
5 tasks: API route (TDD), SupplierEditor component, wire into
PoDetailView with state, wrap received items in <details>,
final verification with Agent Browser.
Two scoped follow-ups to the supplier-unknown flag (9897f47):
- Edit supplier on detail page when flagged as unknown
- Collapse received items per delivery note by default
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).