Commit graph

186 commits

Author SHA1 Message Date
980f40aa79 feat(delivery-notes): see what's on a note, colour-coded progress, POS-aligned stock search
The delivery-notes list was close to unusable: company groups initialised
to an empty Set, so `isOpen` was false for every group and the page landed
on a stack of company headers with nothing under them. The code comment
claimed the opposite of what the code did.

Screen
- Track *collapsed* group names instead of open ones. Open is now the
  default with no initialisation, and it survives a list re-fetch.
- Rows expand to show the real line items (description, qty, supplier,
  SKU, price, subtotal) via the existing /detail read model, cached after
  first open. Ported from /quotes, which already had this affordance.
- Whole-row click removed so expanding can't be mistaken for opening; the
  DN number and "Open full delivery note ->" navigate instead.
- Row tint as a derived progress ladder — no new stored statuses:
    none = draft · yellow = signed copy · blue = quoted · green = po_received
  `invoiced` leaves the Active tab, so Active is now genuinely what is
  outstanding. Hovering the status badge explains the colour.
- DataTable gained an optional `selectionHeaderLabel`; the checkboxes
  feed one action (Combine) and now say so.

Stock search
- The create-delivery-note dialog was `getFullList`-ing every stock item
  with expand=supplier on each open, before a character was typed. Now a
  debounced typeahead against /api/v1/stock-items/search.
- Deliberately NOT the existing quick-draft search route: that one is
  served with an admin client and no session check, so its result isn't
  gated on the listRule. The new route uses assertBrowserSession.

Fixes
- The PDF preview modal was clamped to 384px wide and 900px tall despite
  passing max-w-[1100px]. DialogContent's default `sm:max-w-sm` is a
  *variant*, so twMerge kept both and the variant won on cascade order
  (verified against the compiled stylesheet: 88,536 vs 258,517). Both
  preview variants now repeat the `sm:` prefix so the conflict resolves.
- Expand-panel fetch was missing the Authorization header. Dashboard auth
  lives in localStorage, so route handlers cannot read it from cookies and
  the request 401'd. A 401 now reports an expired session instead of a
  bare status code.

Notes
- 25 new unit tests. The stage ladder and the PB filter construction are
  the two places where a silent wrong answer looks like a UI bug.
- Unauthenticated write routes on delivery-notes (create, combine,
  create-quote, customer-po, mark-invoiced) have no session check. Left
  as-is and reported separately — locking them is an auth-behaviour change.
2026-10-04 20:40:13 +02:00
d98c263f45 feat(scan): standalone delivery-note scanner PWA at /scan
Scan the QR printed on a delivery note, see what it is, and act on it:
reprint, photograph the signed copy, or walk it toward invoicing. It is its
own route group outside `(app)` so it gets a clean full-bleed layout and its
own manifest — "add to home screen" should give the operator a scanner, not
the CRM. Mirrors the pattern `(counter)` already uses.

Two detectors, not one. `BarcodeDetector` is fast and allocation-free but is
Chrome-only: Safari/iOS still has it "Under Consideration" (WebKit bug
#254573) and Firefox does not ship it. A scanner that only used it would
simply not work on an iPhone. So native where it exists, jsQR everywhere
else — pure JS, so no 2 MB WASM payload on a weak warehouse signal, and the
delivery-note QR is always a QR code. Both run against the same canvas frame
so the caller never branches. Also requires HTTPS, which the page says
plainly when it isn't.

The QR carries the human number, so the first call is a new resolve endpoint
mapping number -> PB id. Everything after that speaks ids, which is what every
existing route already takes. The number is shape-validated before any network
call, so a supplier's packing label can't fire a lookup.

STATUS LADDER. mark-invoiced hard-refuses anything that isn't po_received, and
customer-po refuses a note that isn't sent/loaded — so rather than offering a
button that 500s, each action only appears when the server would accept it, and
the card shows which step comes next. It routes draft -> loaded -> po_received
-> invoiced via the existing load-dn, NOT draft -> sent, because draft -> sent
means "create a quote and email it" and a warehouse scan must never trigger
that silently.

The signed-copy upload deliberately uses the upload-only endpoint, not
/api/v1/deliveries/mark-signed-copy — that one flips the note to `completed`
in the same write, which would jump a note straight past invoicing.

Manual entry is always available, so the page works on a device with no camera
or a browser that won't scan.

Tests decode the REAL delivery-note QR through the production fallback path at
print size, and assert the shape check rejects a supplier QR, a random string
and an over-long number.
2026-10-04 13:15:56 +02:00
ad869aa6f8 fix(pdf-print): open the PDF in a new tab instead of printing the iframe
`iframe.contentWindow.print()` does not work for a PDF in Chrome. Chrome backs
every PDF — src URL or blob URL — with its internal `pdf_embedder` extension
rather than a real DOM document, so the frame's `contentDocument` is an empty
shell:

    <html><head><link rel="stylesheet"
      href="chrome-extension://…/pdf_embedder.css"></head>
    <body></body></html>

The PDF *paints* — you can see it in the preview — but `print()` runs against
that empty body, so the operator gets a print dialog full of blank pages and
no error to explain why. Read straight out of a live page to confirm.

A new tab gets Chrome's real PDF viewer, whose own print button works. Same
document, no blank pages.

Affects the delivery-note preview, the quote PDF viewer, and the second quote
viewer in the same file. Popup-blocked is now reported explicitly rather than
appearing to do nothing.
2026-10-04 13:15:08 +02:00
b8116d32eb fix(giellie): pin Approve/Reject above a scrolling list on long approvals
A long batch (e.g. 16 stock_items.create) made the approval card taller than
the page; it sits outside the message scroller, so the Approve button was
clipped and unreachable. The card now has a bounded height (60dvh), a pinned
header holding Approve/Reject, and only the list of changes scrolls.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 08:32:04 +02:00
97fc698383 fix(giellie): stale-tab approval guard, size guard, SSE heartbeat, shared in-flight set
- decision endpoint requires pendingKey; mismatch with the stored pending is a 409
- refuse to continue chats over 1.5M chars; compact pending.completed on save
- SSE ": ping" heartbeat every 15s, cleared on finish/cancel
- in-flight set lives on globalThis (survives HMR, shared across bundles)
- resolveOrigin ignores non-http(s) env values; log sanitised cause on tool-load 503
- approval card shows the confirm reason; trust caption updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 00:36:55 +02:00
d2e33c5eed fix(giellie): mobile overlay, dvh height, markdown image block, per-chat trust, keep draft on failure
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 00:12:56 +02:00
b28d57bfc8 feat(giellie): chat UI, approval card, photo/file composer, sidebar rename
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 00:06:50 +02:00
7555196166 feat(audit): implement audit trail for delivery notes and orders
- Added an audit trail feature that logs every create, update, and delete action on delivery notes and orders, capturing detailed changes per field.
- Introduced the `AuditTimeline` component to display the history of changes for delivery notes and orders, enhancing traceability.
- Updated the `page.tsx` files for delivery notes and orders to fetch and display audit logs, ensuring users can view the history of modifications.
- Enhanced the `package.json` with a new migration script for setting up the audit log functionality.

This feature improves accountability and transparency in the management of delivery notes and orders.
2026-09-30 01:48:44 +02:00
b99aacb020 feat(rep): add /rep/visits map with heatmap, per-customer clusters
New page lets a rep see their own rep_visits on a Leaflet map.
Defaults to last 7 days; toggleable to 14 / 30 / 90.

  /rep/visits       ── server-side fetch + re-export to client
  /rep/visits map   ── per-customer cluster, count badge, last-visit
                       label, popup with full per-visit timeline + notes
                       + 'manual' badge, side-list of customers in window
                       + search filter + heatmap toggle

Service + route (no pb-helpers):
  visits-service.getVisitsForRep(repId, {from?, to?, limit?})
    - admin PB (session-scoped at the route layer)
    - expands customer relation, filters position_source != 'tracker'
      server-side, PB date format gotcha (T → space), 5000-row cap
  /api/v1/rep-visits/list   GET, browser-session auth, scopes repId
                            from user.id (never a query param — same
                            hardening as POST /api/v1/visits)

Heatmap:
  leaflet.heat wrapper at src/components/leaflet-heatmap-layer.tsx
  - useMap() + useEffect lifecycle
  - brand gradient (deep blue → green → amber → yellow → crimson)
  - reads on both light OSM and dark CARTO tile sets
  - maxZoom default 10 — leaflet.heat scales intensity by
    1/2^(maxZoom-currentZoom); without this the OSM default maxZoom
    of 18 makes points render at ~5% opacity at our default zoom 10

Other fixes landed mid-feature:
  - KPI cards collapsed from 3 into 1 (clusters count is a
    zoom-dependent implementation detail, not a glanceable stat)
  - added 'View my visits on a map' nav link under /rep header

Dep: +leaflet.heat@^0.2.0, +@types/leaflet.heat@^0.2.5
     (react-leaflet@5, leaflet@1.9.4 already present)

Tests: 50 + 34 = 84 pass in the touched (rep) area + visits-service
       + rep-visits API. Pre-existing failures on main (sidebar,
       pocketbase-admin, quotes-service) confirmed unrelated by
       stashing this branch.
2026-09-20 23:50:03 +02:00
dce54934f0 fix(invoicing): bill only what's collected, support partial pickups + kit splits
The invoice qty rule used to read the customer's order qty regardless
of how much the driver actually collected. QuickBooks ended up billing
8-of-10 on one line and 7-of-10 on the other for Kusasalethu PO 390588
(INV10924) — a 5-unit overbilling that the mine's receiving team flags
as suspicious.

The rule now caps the invoice qty at min(qty_collected, quantity):
  - Short pickup → bill what's collected, the row stays at
    item_status="ready_invoice" with qty_invoiced < quantity, and a
    follow-up invoice fires automatically when the rest arrives.
  - MOQ overflow → still capped at the customer's order qty; the
    surplus-book flow continues to route the overflow to stock.

The Invoicing page now shows "N short" / "N units short" badges on
short-pickup rows, a "N hidden" badge when supplier-side rows are
suppressed, and a "Show supplier-side lines" toggle (default OFF) so
the invoicing operator doesn't see kit-component remainders unless she
needs to. A split-remainder dialog on the order process page lets the
operator explicitly flag a remainder as exclude_from_invoice=true when
the original line was a kit (e.g. "M20 bolt comes with nut and washer"
split across 3 suppliers).

PB schema additions (one-time, run scripts/nss-add-invoice-columns.ts):
  - order_items.qty_invoiced            (number)
  - order_items.customer_invoice_number (text)
  - order_items.invoiced_date           (date)

These three columns were referenced throughout the codebase but never
existed on the live schema, so writes were being silently dropped. The
schema fix unblocks the reconcile flow that stamps them after every
QB webhook returns.

Backfill for the live Kusasalethu order: scripts/nss-fix-kusasalethu-inv10924.ts
updates order_items.quantity 10→8 / 10→7 to match what was collected,
stamps the new columns, and flips order.invoiced=true.

New server helper reconcileOrderItemInvoicedStatus() runs after each
QB webhook and per-row decides:
  - item_status = "invoiced"     if qty_invoiced >= quantity (fully billed)
  - item_status = "ready_invoice" otherwise (still partial — stays in
    the to-invoice tab for a follow-up invoice once the rest arrives)

Wired into both the bulk invoicing page and the delivery-note-invoice
route. markOrderInvoiced now compares against qty_invoiced vs
quantity rather than the flat item_status check, and its error
message names the outstanding shortfall per row.
2026-09-17 13:53:36 +02:00
1bc0da40e7 feat(customers): lift active/inactive toggle to top of customer form
The active/inactive customer switch was buried under the
'Details' section after the sales rep and description — easy
to miss when the only thing the operator wants to do is mark
a customer as inactive.

Moved it to the second row of the form, immediately after
the customer name field, where it's the first decision the
operator makes. Visual treatment bumped to match the new
prominence: a green-tinted container when active, muted
grey when inactive, so the state is readable from across
the room instead of requiring the operator to read the
trigger label.

  state semantics (unchanged from bbfa888):
  - 'status' is a PB bool, true = active, false = inactive
  - new customers default to true via the form's
    'status: customer?.status ?? true' initializer
  - legacy rows with status undefined render as Active on
    both the detail and list pages (same default-active
    contract on read) — no migration needed
  - the only way to render an inactive customer is to flip
    the switch and save it; PB then stores false
2026-09-16 11:04:39 +02:00
bbfa888cc1 feat(customers): add status toggle and site/location field
Two new fields added to the customer add/edit form, both
operator-facing for the active/inactive customer workflow:

  - customers.status (bool): flips the customer active/inactive.
    Legacy rows pre-date the field and load as undefined; both
    the form and the read side collapse that to active=true so
    the migration date doesn't silently flip the existing base.

  - customers.site_location (text): free-text site/shaft label
    keyed off the customer's company. Distinct from the existing
    'site' free-text mine tag owned by the pin tool. Storage is
    plain text on purpose, with a /api/v1/customers/sites
    endpoint returning distinct existing values for an input
    + datalist autocomplete under the company combobox.

  surfaces:
  - Active/Inactive badge on customer detail and list rows
  - site_location row in the customer Details card and under
    the company line on the list (desktop + mobile card)
  - Switch under the sales-rep section of the form to flip
    status; existing customers stay active by default

  also fixes a UX trap on edit: previously opening the edit
  form showed 'Select company...' / 'Select rep...' as placeholders
  for an instant until the lookup lists resolved, making a real,
  saved company/rep look unlinked. CompanySearch and UserSearch
  now accept a selectedLabel fallback; CustomerForm passes the
  resolved customer.expand.company / customer.expand.rep names
  through so the triggers display the linked names immediately.

  listDistinctSiteLocations helper added to customers-service.ts
  alongside the existing listDistinctSites.
2026-09-16 09:47:09 +02:00
cd359b75a9 feat(quotes): split attachments into internal vs outbound with operator upload UI
Splits the quote attachment model along the seam that actually matters:
'operator-only references' (drawings the operator consulted, supplier
emails they filed away) vs 'customer-facing files' (supplier quotes,
drawings, NDAs that need to ride along on the email).

Schema (PB): new `internal_attachments` + `outbound_attachments` fields
on the `quotes` collection, maxSelect 10 each. Existing `attachments`
field kept for back-compat but is no longer the source of truth for
outbound emails.

API:
- GET/POST/DELETE /api/v1/quotes/[id]/internal-attachments[/{filename}]
- GET/POST/DELETE /api/v1/quotes/[id]/outbound-attachments[/{filename}]
- POST /api/v1/quotes/[id]/attachments/upload kept for legacy `attachments`

MCP: `quotes.send_email` description now correctly states
`selectedAttachmentIds` are PB file tokens from `outbound_attachments`
(supplier quotes, drawings, NDAs), not IDs of the legacy `attachments`
collection.

Frontend: `ProcessOutboundAttachmentsCard` component lets the operator
upload/remove customer-facing files during the process page; approval +
process pages now read the new fields.

Outbound Send workflow (deployed as `PIEAkPYYYZLGl6Pj`):
- Old workflow (id `WF_Email_Send_SMTP`) and stale repo file
  (`WFDASH — Quote Outbound Send.json`) replaced.
- New revision intersects `quotes.outbound_attachments` with
  `email_outbound_attachment_ids`, downloads each from
  `/api/files/quotes/<id>/<token>`, and emits
  `options.attachments: [{name, content, type}]` consumed by
  `Send Email (SMTP)` (PO Send pattern).
- Failure path flipped from `send_failed` to `mail_failed` so
  `retryQuoteEmail` can find failed sends (it only accepts the
  latter). Previously failed sends were stranded.

Email Monitor (v5) workflow fixes:
- Reply-branch auth: any authenticated request now passes through;
  pre-fix bug rejected branches that the IMAP trigger hadn't seen yet.
- Reply detection: bulletproof subject regex
  (/Q-?\d{4}-?\d{2}-?\d{3}/) accepts both legacy `Q-2026-09-246`
  and current `Q202609246` formats; canonicalised digits-only for
  PB `~` substring match.

Delivery notes: new pricing endpoint at
/api/v1/delivery-notes/items/[id]/pricing + service-layer methods.

Snapshots kept for forensics: PRE-FIX-REPLY-{BRANCH-AUTH,DETECTION}.json
and push-payload.baseline.json are the previously-deployed workflow
shapes, retained so we can diff/rollback if regression hits.

Verified:
- PUT workflow PIEAkPYYYZLGl6Pj -> 200, active, 25 nodes
- quotes-service.test.ts: 117 pass (4 pre-existing failures unchanged)
- mcp/route.test.ts: 118 pass (2 pre-existing failures unchanged)
- node --check on new Code node JS passes

Docs: docs/quotes-outbound-attachments-deploy.md covers the deploy
procedure; docs/api-mcp-layer.md description sync.
2026-09-09 13:43:56 +02:00
f895b3b933 chore(sidebar): temporarily hide Quote Sprint and Order Sprint nav entries
The sprint boards are disabled pending a rebuild. Operators
process quotes through /quotes/[id]/process and orders through
/orders/[id] until they are re-enabled. The original nav
entries are commented in place so they can be restored by
uncommenting a single line each.
2026-09-08 18:55:46 +02:00
56476e3ca0 fix(quote-sprint): propagate description edits to quote_items.description
The sprint-board pricing modal was writing operator edits only to
quote_items.quote_description, but the quote detail page renders
quote_items.description. The 'process and open' flow writes to
description directly, which is why edits appeared there but not
after saving from the sprint board.

Mirror the trimmed description into both quote_description and
description on save, matching the process page's write path.

Add three regression guards (edited description, cleared
description, untouched save) to sprint-item-pricing-modal.test.tsx.
2026-09-08 18:45:13 +02:00
c8b7e1f51b feat(rep): visit check-in PWA — phone GPS, offline outbox
The rep app: open it on site, it finds who's around you, tick who you
saw, done. Third instance of the per-route-group PWA pattern already
running here (/m boss phone, /counter), so it reuses MobileAuthGate and
gets its own manifest + service worker.

Phone GPS is the primary position source, not the vehicle tracker. The
tracker knows where the bakkie is; the phone knows where the rep is,
and those diverge by a few hundred metres once the rep walks into the
plant. GPS also needs no network, which matters because mine shafts
have poor signal. The tracker is the secondary path
(GET /api/v1/visits/position) for when location is denied or
unavailable. The independently-collected vehicle trail corroborates
check-ins afterwards, so honesty doesn't depend on the phone.

Matching runs on the phone, not the server. The app caches a compact
customer snapshot and runs rankNearbyCustomers locally, so there's one
code path that behaves identically online and off — rather than a
server path plus an offline fallback that only gets exercised in the
places it's hardest to debug.

- (rep)/rep/            layout, page, check-in view, outbox
- visits-service.ts     getCustomerSnapshot, recordVisits
- POST /api/v1/visits           record a check-in
- GET  /api/v1/visits/customers snapshot for the offline cache
- GET  /api/v1/visits/position  tracker fallback
- public/manifest-rep.json, public/sw-rep.js, pickSwPath() branch

These routes authenticate with the browser session, not AGENT_API_KEY —
the caller is a logged-in rep. The rep id comes from the validated
session rather than the request body, so a client can't log a visit as
someone else.

The outbox uses localStorage rather than IndexedDB deliberately: the
payloads are tiny and localStorage is synchronous, which matters when
the rep hits Save and immediately pockets the phone. Entries are only
removed once the server acknowledges them, so a failed flush retries
instead of dropping a visit — duplicates are preferred over loss, and
surfaced rather than merged. Permanently-rejected entries (4xx other
than 401/408/429) are dropped so one bad row can't wedge the queue.

recordVisits writes each visit independently: a stale customer id
shouldn't cost the other four visits in a queued batch.

BLOCKED until one PB admin change: rep_visits.position_source does not
yet allow "phone" — verified against the live instance, PB returns
400 "Invalid value phone". Add it to the select. Separately,
accuracy_m is silently accepted and discarded (PB ignores unknown
fields), so GPS accuracy is thrown away until that number field is
added; not blocking.

47 new tests. Verified on :4001: /rep serves 200 with the right
metadata, manifest-rep.json and sw-rep.js are served.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 21:09:27 +02:00
61c5d8c906 feat(visits): office pin tool for seeding customer coordinates
Radius-based rep check-in has nothing to match against until customers
carry real positions: 0 of 616 had any — every `coordinates` geoPoint
was PocketBase's `{lon: 0, lat: 0}` default, and most company addresses
are P.O. Boxes, so geocoding can't seed them either. This is the tool
that seeds the ~20 main West Rand shafts by hand.

- /visits            landing page; a launcher until the visit-history
                     view has data to show
- /visits/pin-tool   pick a customer, click the map, save
- setCustomerCoordinates() in customers-service.ts, with lat/lng range
  validation so a string off a form input can't write a poisoned row
- PATCH /api/v1/customers/[id]/coordinates for agent/MCP callers
- Visits entry in the sidebar's Management group, plus MOBILE_TITLE_MAP

The browser writes through a Server Action, not the /api/v1/ route:
that route sits behind `assertAgentAuth` and a browser has no
AGENT_API_KEY. Both paths share `setCustomerCoordinates`. Mirrors the
existing saveAiSettingsAction split.

Pure view logic lives in pin-tool-helpers.ts so it can be tested away
from the Leaflet runtime.

Also refreshes the PB schema export — `rep_visits`, `rep_stops`,
`rep_vehicle_positions` and `users.tracker_vehicle_id` now exist on the
live instance.

32 tests. The suite's 9 pre-existing failing files are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 16:58:07 +02:00
aed3f5c861 feat: implement discount functionality in POS system
Added support for discounts in the POS checkout process, allowing operators to apply either a fixed amount or a percentage off the gross total. Updated the state management to include discount details and modified the receipt and cart rendering to display pre- and post-discount values. Enhanced the checkout API to handle discount inputs and ensure proper calculations. Updated relevant tests to cover new discount logic.
2026-09-03 12:19:21 +02:00
bdd0ac46e3 feat: implement per-route service worker selection
Updated the service worker registration logic to allow for specific service workers based on the route. Added support for a mobile service worker at `/m`, while maintaining existing functionality for `/counter` and `/new-order` routes. This enhances the PWA experience by serving optimized service workers for different app sections.
2026-09-02 07:49:38 +02:00
e3b1c09704 chore: update package.json 2026-09-02 00:41:31 +02:00
f7b5de4c85 feat(charts): allow RadialGauge to override its center label 2026-09-01 01:33:16 +02:00
7a1239a187 fix(orders): bill the customer quantity, not the supplier order_qty
`order_items.order_qty` is the SUPPLIER-facing quantity — what prints
on the PO. Five call sites read it as the customer quantity, which is a
money bug in both directions:

  - supplier MOQ (customer 4, we buy 10) → invoiced 10, over-billing 6
  - shorted line (customer 100, supplier covers 40) → invoiced 40,
    under-billing 60 until the operator splits the remainder

The invoicing screen never fetched `quantity` at all (zero occurrences
in page.tsx), so it could not bill the customer quantity even in
principle. Added to the PB field projection, which is load-bearing:
drop it and PB returns undefined and every line silently falls back to
order_qty. A page-level test now fails if the projection drifts.

`maxSurplusFor` measured the surplus as `collected - order_qty`, which
on an MOQ line is always 0 — the overflow was mathematically unbookable
and vanished from inventory by construction, not by operator error.

Four call sites built delivery-note lines by hand; two passed
`quantity` and two forgot it, so the same DN printed the customer's 4
via the service layer and the supplier's 10 via the API routes. All
four now share `buildDeliveryNoteLine`, so they cannot diverge again.

`order_qty` had no doc comment at all next to an 11-line one on
`quantity` — that absence is why five subsystems guessed differently.
Now documented, with a new `src/lib/order-item-qty.ts` holding
`customerQty` / `supplierQty` as the single definition.

driver-stops.ts and collection-confirm/po-card.tsx read `order_qty`
CORRECTLY (they check what the supplier delivered against what we
ordered from them) and carry comments saying so, so the next cleanup
doesn't "fix" them into breaking.

23 new tests. Full suite: no regressions — the 15 failures are
identical before and after (verified by stashing and re-running).

Audit: docs/superpowers/specs/2026-08-30-quantity-handling-audit.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 18:28:33 +02:00
33fc549e10 feat(orders): add shared quote-line context component
Shared presentation of a paired quote line — description (when it
differs from the order), buyer note, line attachments, quote
attachments, plus a 'possible match' warning banner for the
operator's confirm button.

Used by both the order detail table (Task 13) and the process page
(Task 12) so the two never drift. The process page used to own the
only copy; the detail page showed nothing.

tier='possible' framing is the load-bearing part: a buyer is never
shown a note from the wrong line without being told it might be
wrong.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 12:20:39 +02:00
63e4a654bf refactor(orders): extract AttachmentList into a shared component
Pure move — bodies copied verbatim from process/page.tsx into
src/components/orders/attachment-list.tsx. The order detail page
(Task 13) will now import the same component instead of growing a
second copy.

The 4 icon imports (FileText, Image as ImageIcon, FileQuestion,
ExternalLink) became unused after the move and were removed from
process/page.tsx. Re-exported from the new file alongside the
MatchedAttachment type, attachmentHref helper, and inferKind helper.

No behaviour change. tsc clean. No new lint errors in the touched
files.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 12:16:31 +02:00
7c3083c017 feat(quotes): add quote-level "Quoted before?" panel to the process page
When a mine puts a spot out to tender, several engineering companies
each ask us to price the same item list. Before pricing a new one the
operator wants to see what the earlier quote went out at.

The per-line answer already existed: populateItemIntel powers the ⓘ
popover on each row and returns recent_quote (prior quote number,
company, selling price, days ago). This adds no new matching logic —
it runs that same lookup across every line of a quote and rolls the
results up, so the operator gets one panel instead of clicking ⓘ on
each row.

What the roll-up adds over the per-line popover:
  - "N of M lines have been quoted before" at a glance
  - relatedQuotes ranked by HOW MANY of this quote's lines they also
    carried; a prior quote covering 5 of 5 is very likely the same
    tender from a different bidder, one covering 1 of 5 is a shared
    commodity line

Two honesty details, both driven by what the first live run showed:
  - match confidence is surfaced as close/likely/loose. The underlying
    matcher accepts anything above 0.5, and a live check returned a
    R150 hit against a magnetic base drill — a loose row must not look
    as trustworthy as a close one.
  - an unpriced prior quote renders "unpriced", never R0.00, which
    would read as "we quoted it at zero".

Read-only: passes write:false so viewing the panel doesn't persist
intel onto rows the operator only looked at. On-demand rather than
auto-loading, since it costs one intel computation per line
(~400-650ms each, batched 5 at a time, capped at 40 lines).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 08:52:19 +02:00
113735d0ba fix(quotes): speed up archive search and add item preview to archive rows
The archive's search fetched the entire recency-windowed matching set
(thousands of rows once "Include older" is on) and substring-filtered
it in memory, taking several seconds per keystroke; it now delegates
to the same ranked pushdown search service the main /quotes list uses.
Also fixes pagination being hidden while searching (results past page
1 were unreachable) and the match count showing the current page's
size instead of the true total.

Adds the chevron item-preview toggle to archive rows (extracted into
a shared QuoteItemsExpanded component, backed by a new GET
/api/v1/quotes/[id]/items route), removes a duplicate "Customer or
Company" label in the new-quote form, and drops the stray
warning-colored search box styling that leaked from DataTable onto
every list page that uses it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 02:30:14 +02:00
0efbae8dc5 feat(quotes): enhance search functionality and user experience
- Updated the search input handling to improve responsiveness by integrating a debounced search mechanism, reducing latency from 550 ms to approximately 300 ms.
- Added keyboard shortcut support for focusing the search box, enhancing accessibility and usability for frequent users.
- Improved the search placeholder to reflect all searchable fields, providing clearer guidance to operators.
- Introduced a clear-search button to streamline user interactions, allowing quick resets of the search input.
- Enhanced the display of search results with a live result count badge, providing immediate feedback on search outcomes.

These changes collectively enhance the search experience for operators, making it more intuitive and efficient.
2026-08-28 20:13:44 +02:00
Dave
7986784eaf chore(tests): fix ~60 pre-existing TS errors across 11 test files
Typecheck cleanup after the deploy blocker push — `tsc --noEmit` was
silent on `d11a184` baseline but ~60 errors hid across test files
that only the compiler surfaces (the build was clean because test
files are excluded from the prod bundle). Touched:

- src/app/api/mcp/route.test.ts (16) — generic on `vi.fn()` for
  tuple inference; `as unknown as` two-step cast on record literals
- src/app/api/mcp/po-agent-workflow.test.ts (1) — spread instead of
  duplicate key
- src/components/buyer-tasks/buyer-task-group-card.test.tsx (5) —
  `PurchaseOrderEmailPayload` now requires `subject`/`body`
- src/components/collection-confirm/confirm-stock-dialog.test.tsx (1)
  — removed shadowed `id` from `makeRow` overrides
- src/components/item-intel-popover.test.tsx (2) — missing row
  fields + boolean→object for `verified`
- src/components/quotes/process-item-card.test.tsx (2) — full
  QuoteItem stub; generic on `vi.fn()`
- src/components/sprint-item-pricing-modal.test.tsx (3) — full
  SprintItem/QuoteItemAttachment/QuoteAttachment stubs
- src/lib/server/orders-service-review.test.ts (~60) — annotated
  `getOne` return type to break union widening on the expand option
- src/lib/server/orders-service.test.ts (2) — removed bogus
  `includeArchivedCounts` param; `rfq_number` now whitelisted on Order
- src/lib/server/quotes-service.test.ts (3) — two-step cast on
  Quote header; `unit?` now on QuoteItem
- src/lib/server/stock-purchase-items-service.test.ts (~10) — typed
  `pbMock` via `StockPurchaseItemsPb` so `*WithPb(pb,...)` checks

Also added two optional fields to `src/lib/types.ts`:
- `QuoteItem.unit?` — pre-2026 rows have no unit, so it's optional
- `Order.rfq_number?` — populated by the n8n email agent, inherited
  verbatim by `splitOrder`

Build: clean. Typecheck: 0 errors. Tests: 14 pre-existing runtime
failures + 1 pre-existing unhandled error in `supplier-form.test.tsx`
remain — confirmed they exist on `d11a184` baseline, so they are
outside the cleanup scope and unchanged by this commit.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 04:43:40 +02:00
Mika
a105c2c449 fix(delivery-notes): move 'Add item' to per-card button after each row
Step A — move the 'Add item' button from the top of the items
section to the bottom of the items list.

Step B — match the quote-form.tsx pattern: every item card is
followed by its own ghost + dashed-border 'Add another item'
button, so the operator never has to scroll back to the top
to add the next line. The dashed-border styling keeps it from
being mistaken for a submit/finish affordance.

The data-testid="dn-adhoc-add-item" selector is preserved on
the per-card buttons so any future test selectors continue to
work.

Fixes PINW-18
2026-08-26 01:32:46 +02:00
Dave
a0afa51600 refactor(orders): replace OrdersReviewModal with OrdersReviewModalMount and streamline attachment handling
- Updated the orders page to use `OrdersReviewModalMount` for improved order review functionality.
- Removed the inline `orderAttachementsToQuotes` function to simplify the code structure and enhance maintainability.
- Introduced a review modal in the order detail page, allowing users to trigger the review process with a dedicated button.
- Ensured consistent behavior between the orders list and detail pages regarding order review interactions.
2026-08-24 07:26:18 +02:00
Dave
139a3f3bfe feat(quotes): enhance scoring logic to include price matching
- Updated the `scoreQuote` function to incorporate price scoring, allowing quotes to be evaluated based on selling and cost prices when the query is numeric.
- Introduced new weights for item selling and cost prices, ensuring they contribute to the overall score only when relevant.
- Enhanced the test suite to cover new price matching scenarios, ensuring accurate scoring behavior for quotes based on price inputs.
- Refactored related functions to support the new price scoring logic, maintaining consistency across the scoring process.
2026-08-24 07:09:39 +02:00
Dave
663c018a70 feat(quotes): add "Include older" toggle for searching full quote history
- Introduced a new toggle in the quotes and archive pages to allow users to include older quotes beyond the default 90-day cutoff.
- Updated the search parameters in the quotes and archive APIs to handle the new `includeOlder` flag, enabling full history searches.
- Enhanced the user interface to reflect the current search scope, providing clear feedback on whether the search is limited to recent quotes or includes the full archive.
- Adjusted the quotes search service to support the new functionality, ensuring efficient retrieval of older quotes when requested.
- Added relevant tests to verify the correct behavior of the new search feature and its integration with existing functionality.
2026-08-20 10:18:44 +02:00
Dave
43bbdfc458 feat(tv-dashboard): enhance TV dashboard with driver stops and performance metrics
- Updated the TV dashboard to include a new section for driver stops, displaying active deliveries and pickups with GPS coordinates.
- Refactored the dashboard data structure to incorporate year-over-year performance metrics alongside existing monthly and weekly stats.
- Replaced the supplier map with a driver stops map for improved clarity on driver activities.
- Enhanced the user performance metrics to include detailed counts for quotes and orders processed by each representative.
- Removed the obsolete supplier map component to streamline the dashboard's functionality.
2026-08-19 10:16:49 +02:00
Dave
bfcddb9d82 feat(quote-form): allow empty quantity input for operator convenience
- Updated the `DraftQuoteItem` interface to allow `quantity` to be an empty string, enabling operators to type counts directly without needing to delete a default value.
- Modified the `QuoteFormItemCard` to handle empty input gracefully, falling back to 1 on submission if the field is left blank.
- Adjusted the `addItem` function in `QuoteForm` to initialize `quantity` as an empty string, enhancing user experience during item addition.
2026-08-18 07:28:25 +02:00
Dave
8ed0952364 refactor(forms): update layout and structure of form components
- Adjusted the `SheetContent` width in multiple form components to improve responsiveness.
- Enhanced the `CompanyForm` layout by reorganizing fields for better clarity and usability, including the addition of coordinates input.
- Updated the `CustomerForm` and `SupplierForm` to maintain consistency in design and structure.
- Improved the `DataTable` component's card styling for better visual appeal and user interaction.
- Refined the `QuotesPage` card styling to enhance the user experience when selecting quotes.
2026-08-17 23:34:30 +02:00
Dave
50944a3526 feat(quotes): add operator-facing notes functionality to quotes
- Introduced a new `quote_notes` field to store operator-facing free-text notes, distinct from the existing `notes` field used for rejection audit logs.
- Updated the quote processing page to include a `QuoteNoteCard` for editing and displaying the operator notes, ensuring seamless integration into the PDF generation flow.
- Enhanced the PDF generation logic to render the operator notes in a blue callout below the totals, improving visibility for end-users.
- Implemented a new service function `setQuoteNotes` to handle the saving of notes, including trimming whitespace and handling non-string inputs defensively.
- Added unit tests for the `setQuoteNotes` function to ensure correct behavior and data integrity when updating notes.
- Updated relevant types and documentation to reflect the new functionality and its intended use cases.
2026-08-17 15:22:16 +02:00
Dave
437ed3529c feat(collection-confirm): implement confirmation dialog for stock collection
- Introduced a new `ConfirmStockDialog` to handle stock collection confirmations, allowing operators to review per-line quantities and any short lines before finalizing the action.
- Updated the `CollectionConfirmPage` to manage the new confirmation state and trigger the dialog upon clicking the "Confirm collection" button.
- Enhanced tests to cover the new dialog functionality, ensuring correct behavior for both full and short quantity lines during the confirmation process.
- Improved documentation to clarify the flow and user interactions with the new confirmation dialog.
2026-08-17 11:41:55 +02:00
Dave
d0fb8e81f0 feat(collection-confirm): add quantity collection handling and deduplication improvements
- Introduced a new `handleQtyChange` function to manage per-row quantity updates for both customer and stock items, ensuring correct data submission to the appropriate endpoints.
- Enhanced the `dedupeAttachments` function to handle additional document sources, including parent purchase orders, and skip empty filenames to prevent 404 errors.
- Updated the `ItemsTable` and `PoCard` components to support quantity editing, allowing users to modify collected quantities directly in the UI.
- Added tests to validate the new quantity handling logic and attachment deduplication, ensuring robust functionality across various scenarios.
2026-08-17 02:17:27 +02:00
Dave
4a59288a01 feat(deliveries): enhance delivery notes management and UI
- Introduced a new `DeliveryNoteRow` type to track delivery notes alongside items, improving the organization of delivery data.
- Updated the `DeliveryListResponse` type to include delivery notes, allowing for better integration with the existing delivery management system.
- Enhanced the `groupByCustomerOrder` function to accommodate delivery notes, ensuring they are grouped correctly with their respective orders.
- Implemented state management for delivery notes in the `DeliveriesPage` component, including loading and error handling.
- Added search functionality for delivery notes, enabling operators to filter by delivery note number, customer PO, and order details.
- Updated the UI to display delivery notes alongside items, improving the overall user experience in the deliveries management interface.
- Enhanced tests to cover new delivery notes functionality and ensure correct behavior across various scenarios.
2026-08-17 01:27:03 +02:00
Dave
1640c1e09a feat(delivery-notes): implement QuickBooks invoice auto-close functionality
- Added a new service function `markDeliveryNoteCompletedByInvoice` to auto-close delivery notes after successful QuickBooks invoice creation, updating the status to "completed" and appending an audit banner to the notes.
- Enhanced the delivery note detail page with a modal for creating invoices via QuickBooks, including customer selection and billing company options.
- Implemented state management for the invoice modal, including loading states and error handling for customer fetching.
- Updated tests to cover the new auto-close functionality and ensure correct behavior across various scenarios.
- Improved documentation for the delivery notes service to reflect the new functionality and its usage.
2026-08-16 22:24:08 +02:00
Dave
46e0683635 feat(duplicate-orders): enhance duplicate order detection and UI
- Introduced a new `DuplicateKind` type to differentiate between "order_number" and "line_aggregate" duplicates.
- Updated the `DuplicateOrdersDialog` component to support tabbed navigation between order number and line aggregate views, improving user experience.
- Enhanced the data structure for duplicate records to include `totalQuantity` and `totalSellingPrice`, facilitating more accurate duplicate detection.
- Implemented logic to auto-flip to the tab with available content, ensuring operators do not encounter empty views.
- Expanded test coverage for the `findDuplicateOrders` function to validate new line-aggregate matching logic and ensure accurate detection of duplicates across different scenarios.
- Improved documentation for the duplicate order handling process, clarifying the criteria for identifying duplicates.
2026-08-16 11:25:00 +02:00
Dave
e7672ab7c9 feat(mcp): implement structured MCP error handling for upstream fetches
- Introduced structured error handling for MCP upstream fetches, distinguishing between network errors (`-32010`) and upstream errors (`-32011`). This allows the agent to provide more informative feedback based on the type of failure encountered.
- Updated the `fetchUpstreamJson` function to throw `McpUpstreamError` with detailed error data, including hints and next actions for specific tools.
- Enhanced the `TOOL_HINTS` mapping to provide tool-specific guidance for handling errors, improving the agent's ability to respond to various failure scenarios.
- Added comprehensive unit tests to validate the new error handling logic and ensure correct behavior across different error cases.
- Updated documentation in `docs/agents/hermes-skill.md` to include a new section on structured MCP errors, detailing the error codes and their meanings.
2026-08-15 22:32:11 +02:00
Dave
151ae8d67f feat(quotes): introduce quotes.submit tool for approval transition
- Added a new `quotes.submit` tool that transitions a quote from `new` to `approval_pending` once all line items are priced. This tool ensures that only fully priced quotes can be submitted for approval, enhancing the integrity of the approval process.
- Updated the API documentation in `docs/api-mcp-layer.md` to include the new `quotes.submit` tool and its usage details.
- Enhanced the `quotes` status enum in `docs/agents/hermes-skill.md` to reflect the new transition and its implications.
- Regenerated documentation to ensure accuracy, increasing the total tool count from 126 to 127.
- Added comprehensive tests for the `submitQuote` function to cover various scenarios, including idempotency and error handling for unpriced items.
2026-08-15 19:01:52 +02:00
Dave
48053a6205 feat: add support for stock purchase attachments
- update backend service to fetch stock purchase attachments
- refactor dedupe logic to avoid filename collisions between customer and stock attachments
- adjust PoCard to generate correct file URLs for stock attachments
- update test expectations for the modified service query
2026-08-14 18:25:05 +02:00
Dave
59f245d798 feat(collection-confirm): add stock item edits
- Add new POST API endpoints at `/api/v1/stock-purchase-items/[id]/sku` and `/api/v1/stock-purchase-items/[id]/cost` for updating stock purchase item details
- Update ItemsTable to support cost and SKU edits for all row types, add supplier SKU column and editing for stock rows
- Extend PoCard to pass edit callbacks and add stock-specific invoice upload functionality
- Update all related test suites to cover new editing and UI features
- Adjust invoice upload messaging to differentiate between customer and stock purchase orders
2026-08-14 16:39:46 +02:00
Dave
43de97501c feat(tv): add TV dashboard + refine workflows
Add full PIN-protected TV dashboard stack including auth, redirects, map view, APIs, and server services.

Also include these critical refinements:
- Overhaul PO number generation to use per-company monthly sequences
- Rework quote rejection to bounce quotes back to operator queues instead of marking them terminal
- Add post_processing order tab group for clearer workflow visibility
- Fix collection pool tooltip positioning, overflow clipping, and default collapsed state
- Refactor customer list API, clean up unused form props, and fix item intel popover target ID handling
- Add comprehensive tests for new components and update existing test expectations
2026-08-13 20:55:22 +02:00
Dave
617c5b3422 feat(delivery-notes): add inline qty editing and bulk combine features
Add inline quantity editing for delivery note items:
- New PATCH API endpoint and service function to update item quantities
- Optimistic UI updates on the detail page with error and loading states
- Input validation and status gating for draft/sent delivery notes

Add bulk delivery note combine functionality:
- New POST API endpoint and service to merge multiple draft/sent notes
- Row selection support in DataTable across pagination
- Confirmation modal with selected notes preview and optional order linking
- Automatic audit trail updates for merged and cancelled records

Extend OrderItem TypeScript interface with optional order expand field
Fix minor type casting issues in createAdHocDeliveryNote utility
2026-08-13 11:24:05 +02:00
Dave
18397bc9be feat(counter): add standalone counter order PWA
This commit adds a full unauthenticated PWA for in-store retail order taking:
- Dedicated `/new-order` route with complete order form, customer/company picker
- Public `/api/counter/*` endpoints for search, order creation, and photo uploads
- Photo capture and gallery selection with 10-photo per order limit
- Offline caching via dedicated service worker and app manifest
- Shortcut redirect at `/counter` for quick installation
- Comprehensive setup and usage documentation in `docs/counter-pwa.md`
- Minor cleanup to root layout metadata and service worker registration
- Small layout fix for collection pool row text truncation

Orders submitted via the PWA appear immediately in the main CRM's New tab, and the app is designed for trusted office kiosk phones without requiring user login.
2026-08-13 09:27:20 +02:00
Dave
c0ec3c99ef feat(orders,invoicing): add duplicate order cleanup and soft cancellation
Add a complete workflow for managing duplicate intake orders:
- Add new server API endpoints for single order cancellation, duplicate detection, and bulk duplicate cancellation
- Add client UI components for bulk duplicate cleanup and single order soft cancellation
- Add a "Duplicates" button to the orders dashboard for quick access to the cleanup tool
- Refactor invoicing module: rename customerNameSimilarity to nameSimilarity, update QB customer picker to use radio buttons and remove automatic preselection
- Add comprehensive test coverage for the new order service utilities
2026-08-13 09:02:27 +02:00
Dave
6344948b0b feat: add driverless collection confirm and new order tooling
This commit adds several key features, improvements, and cleanups across the codebase:
- Add `order_items.confirm_collected` MCP/REST endpoint to mark order items as collected without requiring driver assignment
- Add standalone order creation and order split APIs for flexible order management
- Update order list sort to oldest-first to align with user workflow of working through oldest orders first
- Improve order search to show cross-tab matches and display source tab indicators on search results
- Add customer name similarity check to warn about mismatched QuickBooks customers during invoice creation
- Remove supplier name column from delivery note PDFs and clean up related data passing across delivery note routes
- Add validation and debug logging to ItemIntelPopover for missing item ID on apply actions
- Remove unused OrderItemContextPopoverTrigger component from order detail page
- Add delivery note regeneration script and update test suites for new and existing functionality
- Update documentation and add test coverage for the new collection confirm tool
2026-08-12 01:13:46 +02:00