Commit graph

532 commits

Author SHA1 Message Date
6c83282888 feat(settings): link to AI settings from the Settings page
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:36:23 +02:00
b212d0fcd8 feat(manager): a mobile orders board for the managers at /manager
A hidden, installable PWA: one link, opened on a phone, showing where
every order is. New / Processing / Completed, with a tap-to-preview
popup and the invoice/line detail Gary needs. Read-only, and NOT in
the sidebar — the route lives outside `(app)`, so `<Sidebar />` never
renders there.

The preview popup is reused, not rebuilt: the service emits
`UniversalHit`s and the page hands them to the existing
`SearchPreviewDialog`, so the manager gets the desk's quick-overview
verbatim.

Also fixes the /orders Completed tab, which was matching ZERO orders.
`delivered_at` is stamped on only 2 of 12,957 delivered lines and the
business stopped writing `delivered` on 2026-08-14 (the live pipeline
now ends at `invoiced` / `checked_in`), so the old filter never
matched. "Settled" is now defined once — `SETTLED_STATUSES` +
`SETTLED_DATE_FIELDS` — and dated per status: `invoiced_date` for
invoiced (the only properly stamped field, 2,199 lines), `delivered_at`
for delivered, `updated` only where no dedicated column exists.

`delivery_note` and `checked_in` move into `completed` as instructed,
and the standalone `post_processing` tab is retired (a DN is not a
stage of its own). `invoiced` stays in `collected` as well, so
invoiced orders don't vanish from the workflow tabs.

Completed tab: 0 -> 341 orders over 30 days, 112 over 7.
2026-10-26 23:09:01 +02:00
a6d53e3b2b feat(price-check): say whether the POS item is exact or only closest
The blue badge read the same whether the AI had matched the item
exactly or only found the nearest one, so a line priced from an M22
bolt looked like any other line.

Now an exact pick reads as before, a closest pick leads with
"Closest: {name} ({differs})" and raises a closest_only flag, and a
fuzzy match keeps its "Similar items:" wording. The popover shows the
pick and the differences at the top.

A refresh is still a no-AI recompute, but it keeps the stored pick
instead of throwing it away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 23:05:09 +02:00
9b3f30a01f feat(process): show the blue price check on both Process pages
The blue price-check badge now appears under the purple AI marker on
every line the AI filled (reviewed or not):

  - quote Process page, mobile card: its own row under the header, so
    the one-line summary is readable on a phone
  - quote Process page, desktop table: inside the Description cell
    rather than the cramped 32-px "#" column, which has no room for a
    summary
  - order Process page: its own full-width row under the markers, so
    the summary is readable on a phone

The summary truncates rather than wrapping — the popover always shows
it in full.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:45:46 +02:00
3248e3aa03 feat(ui): blue price-check badge with its popover
The blue badge that sits under the purple AI marker: a tag icon plus the
one-line summary, with an orange dot on the icon when the price check
found something worth a look.

The popover shows POS stock (on hand, status, cost, sell, supplier), the
top three price-list matches, what we last paid each supplier, what we
quoted and ordered before, and how the match was made ("POS item the AI
picked" / "Matched by SKU" / "Similar items"). Refresh re-computes the
intel through the existing /api/v1/items/{collection}/{id}/intel/refresh
route and reads the line back afterwards.

Also closes the colour-token gap: globals.css mapped --color-info to
var(--info), but --info / --info-foreground were never defined, so
text-info rendered nothing. They now have a light and a dark fallback
next to the --ai tokens; themes that define their own --info still win.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:33:50 +02:00
793c9ea215 feat(orders): tint New-tab cards by the AI quote match
Purple when the order is linked to the quote the AI chose and every
line was paired "sure"; yellow when the AI matched a quote but not
every line for sure, or it is only a suggestion. New tab only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:52:59 +02:00
37933ccdc0 feat(orders): quote matching is AI-only
The rule-based matcher no longer writes or shows suggestions: on
2026-10-24 it had left 2,888 on unlinked orders, one quote suggested
to 309 of them.

- createOrder runs the AI order assistant (matchOrder) straight after
  intake when it is switched on, instead of the rule-based suggestion
- order page: Re-match / Try again and the rule-based suggestion card
  are gone; Match with AI is the action, and "Match by reference" saves
  the reference then runs the AI (the run route takes { reference })
- the unauthenticated /orders/[id]/quote-match route is removed
- the sweep route and MCP orders.quote_match_sweep run the AI batch
  (matchUnlinkedOrders, shared with the tick)
- scripts/2026-10-24-clear-rule-quote-suggestions.ts clears the old
  suggestions (dry run unless --apply); replaces the rematch script,
  which would have written rule-based ones again
- also fixes the setState-in-effect lint error on the order page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:49:26 +02:00
d3a52d97f2 feat(orders): purple review for AI-filled order lines
A line the AI filled now shows up purple until a person checks it, so
nothing the assistant priced is ever taken on trust.

- order-assistant-review.ts: the four client helpers — a line is
  pending only while it is filled, unreviewed and still priced from
  the quote; a human save keeps "ai_quote" / "quote" / "catalogue" and
  turns anything else into "manual"; the review patch records who
  checked the line and whether the prices were accepted unchanged.
- order-assistant-save.ts: the save-time merge, the order-side twin of
  quote-assistant-save.ts. The value the operator committed wins over
  the one the page was loaded with, so the one-click "use this cost"
  copy really does end the AI's provenance.
- The Process page applies the purple row class, shows the bot marker
  with the quote it was filled from, the reason and the doubts, and
  routes every line save through the merge.
- The Orders board shows an "AI x/y" badge: how many lines the AI
  filled, and how many still need a review. One query per page, and
  never fatal — the board has to render whether or not the assistant's
  columns exist yet.
- AiFillMarker grows an "order" variant instead of assuming a POS item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 14:25:11 +02:00
89e52bbd9d feat(orders): AI fills order lines from the matched quote
The assistant now prices the order's lines from the quote they are
paired with, as purple drafts a person then reviews.

- order-assistant-core.ts: isOrderLineWritable + buildOrderLineFillPatch.
  Only a pending line with no cost of its own, no person-set pricing
  source and no reviewed fill may be written. Supplier / SKU /
  matched_price_list_id only when the line has none; selling_price only
  when the line has none; markup + markup_percent written together.
  pricing_source becomes "ai_quote". A customer price that differs
  from the quoted price by more than 5% becomes a doubt.
- order-assistant-lines.ts (new): the shared, cycle-free helpers —
  idFilter, aiQuotePairs, writeAiPairs, clearAiPairs and
  fillOrderFromQuote. It lives in its own module so orders-service.ts
  can use it without an import cycle, since order-assistant-service.ts
  already imports setOrderQuote.
- The fill re-reads every line right before writing it, so a cost a
  person typed in the meantime wins. It stays quiet until the migration
  has added order_items.ai_fill — PB rejects a projection that names a
  column the collection does not have.
- setOrderQuote writes the AI's line pairs before pairOrderLines runs,
  so the AI's pairing wins over word matching, then fills the lines.
  A failure there is logged and never fails the link.
- The tick gains a second pass that fills the lines of orders already
  linked to a quote. Only orders the fill actually wrote to count
  towards the budget, so a skipped order cannot block the run.
- The run route fills after the match and reports the counts.

PocketBase filters have no IN operator, so every id list is an
OR-chain. fetchQuoteItemsForQuotesAdmin no longer tries an IN filter
first — that was a guaranteed 400 on every call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 14:24:56 +02:00
6fe09f7752 feat(orders): Settings → AI order assistant section
Task 9 of the AI order assistant plan.

- new "Order assistant" card in Settings → AI: enabled switch, run
  every (minutes, 5-1440), auto-link when sure, fill lines from the
  quote, the prompt with show/reset default, and the last-run summary
- parseSettingsForm reads oa_* and validates the interval; the last-run
  fields are never written by the form
- the page passes the new settings through as the initial state

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 13:16:09 +02:00
00d09b3b12 feat(orders): show AI quote match on the order page
Task 8 of the AI order assistant plan.

- new AiQuoteMatchPanel: "Linked by AI" / "AI suggests Q…", the
  reason, the doubts and the line-pair table (order line -> quote line
  with same_item)
- the quote match card gains a "Match with AI" button in every state,
  posts to /api/v1/orders/{id}/assistant/run with the PB token, toasts
  the 409 message verbatim, then reloads the order
- new card state: the AI suggested a quote -> the AI panel + Accept
  (links the AI's quote) / Match with AI / Dismiss
- the AI suggestion marks the order as quote_matching_attempted so the
  deterministic sweep cannot overwrite it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 13:13:28 +02:00
85533245bf feat(orders): order assistant routes, MCP tool and runbook
Task 7 of the AI order assistant plan.

- POST /api/v1/orders/assistant/tick (n8n, assertAgentAuth)
- POST /api/v1/orders/{id}/assistant/run (browser session; 409 for
  "AI is disabled" and "not set up yet")
- MCP orders.assistant_match { orderId } -> matchOrder(force) with a
  TOOL_HINTS entry; deliberately NOT in READ_TOOLS
- docs/order-assistant.md runbook and the AGENTS.md section

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 13:06:51 +02:00
e4fbcf3748 feat(orders): label lines split off a short collection
Shows "Short delivered · on <PO>" next to the description on /orders and
the order page so the buyer doesn't re-order units already on back order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:59:57 +02:00
2e55d21937 feat(collection): split short pickups on confirm, stop re-billing
Collection Confirm cuts a short line to what was collected (that part
invoices and closes) and creates the rest as a new pending line on the
same order, with Back order (same PO / straight to pool) on the process
page. Invoicing subtracts qty_invoiced so a part-invoiced line can't bill
twice. Supplier-side lines close at confirm when their order is billed,
and stragglers get a close button on Invoicing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 04:04:50 +02:00
33aa56cbf8 feat(driver): match an invoice photo to the driver's assigned lines
POST /api/v1/driver/invoice-match returns exact matches (same product,
invoice qty = ordered qty) and lines to review. Read-only for the lines.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 20:58:33 +02:00
5c46152df7 feat(collection-confirm): never hold collected stock lines back for outstanding ones
Confirm stock now sends the collected lines to stock booking and moves
lines not yet collected to a follow-on order (<PO>-2, -3, ...). The
invoice matcher follows a closed PO to its open follow-on.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 19:53:21 +02:00
9dd2da3e99 feat(driver): read-only invoice check endpoint for the driver app
POST /api/v1/driver/collection-check runs the collection AI check for a PO
and returns a per-line verdict (right item, right quantity) for drivers.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 19:38:38 +02:00
511f5dac6e feat(collection): return mistaken pickups to pool 2026-10-21 03:37:01 +02:00
42a20d7e72 fix(build): self-host the app fonts so the build doesn't need Google
next/font/google downloads every font from fonts.googleapis.com during
`next build`, and the Coolify build kept failing on it (Nunito/Outfit on
2026-10-17, Lato and JetBrains Mono on 2026-10-20).

The ten families (Geist, Geist Mono, Inter, Space Grotesk, DM Sans,
Playfair Display, Lato, Nunito, Outfit, JetBrains Mono) are now Latin
.woff2 files in src/app/fonts (344 KB, downloaded from Google Fonts),
loaded with next/font/local under the same CSS variable names and
weights. Verified: `npm run build` succeeds with no network access, and
the login page renders in the local Inter with no font errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:35:27 +02:00
d3132ab7f6 feat(collection-confirm): collected stock orders go through Collection Confirm
The driver app records the full quantity as received when it collects a
stock pickup, and Collection Confirm only listed stock lines that were
not fully received — so collected stock orders skipped it and sat at
"sent", outside the accountant's booking queue, until someone clicked
Mark received (29 stuck on 2026-10-19).

- STOCK_ON_PAGE_FILTER / stockLineOnPage: a collected stock line stays on
  Collection Confirm until its stock order is received; cancelled orders
  never show. Shared by the list and the AI check (so those lines get
  photos read and ticks too).
- Confirm stock: markStockOrdersReceived flips each stock order to
  "received" once every line is collected/booked (→ booking queue);
  orders with lines still out stay open, reported as stillOpen. The
  route returns { updatedIds, received, stillOpen }; the toast says so.
- Stock photos: `npm run pb:migrate:stock-photos` makes
  stock_purchases.additional_attachments multi-file;
  addStockPurchaseAttachment, Move photo and Delete photo add/remove with
  `+`/`-` instead of replacing the single slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:03:33 +02:00
f017a7f985 fix: number customer POs one at a time (duplicate PO numbers)
PO numbers were "read the month's highest, add one", so POs for several
suppliers on one order created together got the same number — 34 numbers
by 2026-10-19 (e.g. PO202610008LIF for BarloWorld CAT and John Ratcliffe,
created 71 ms apart).

- createPurchaseOrderWithNumber (po-number.ts): numbers and creates POs
  one at a time in the server, retries (up to 5) when the unique index
  rejects a clash; ignores any po_number passed in.
- buyer-tasks / MCP generate_po use it.
- The Process page's Send PO and in-stock PO buttons now call
  POST /api/v1/orders/[id]/purchase-orders (browser session) instead of
  numbering in the browser; the client generatePONumber is removed from
  pb-helpers.
- npm run pb:migrate:po-number-unique adds a partial unique index on
  purchase_orders.po_number for POs created from then on; the 34 existing
  duplicates are left as they are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:08:12 +02:00
924f7b764e fix: uploads to an order line add files instead of replacing them
A PocketBase file PATCH under the bare field name replaces the whole
list. On 2026-10-19 a buyer's "Add supplier invoice" on PO202610008LIF
deleted the driver's delivery-note photo (uploaded 14:38, read by the AI
14:40, replaced 14:41). Generating or regenerating a delivery-note PDF
did the same to every line on the DN.

- New src/lib/server/order-item-files.ts: addAttachmentForm() adds with
  `order_item_attachements+` and removes (`-`) only older stored copies
  of the same document; pickStoredCopy() finds a file by its uploaded
  name (PB stores "DN26101913.pdf" as "dn26101913_<10>.pdf").
- Used by the Collection Confirm invoice upload, DN create, DN regenerate
  (replaces only its own previous PDF) and /api/invoicing/delivery-note.
- /api/invoicing/delivery-note-pdf finds the DN PDF by name instead of
  taking the line's first file (falls back to the old behaviour for
  legacy names).
- AGENTS.md: always add, never replace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:46:37 +02:00
e66ea11845 feat(collection-confirm): Delete photo; drop the Run agent scan button
- Delete photo next to "Add supplier invoice" removes the photo open in
  the previewer (wrong upload, or one of several for the same PO) after an
  inline "Delete …? Yes, delete" confirm; the confirm resets when you
  switch photos. Not offered for the PO PDF.
- POST /api/v1/collection-confirm/delete-photo (browser session) →
  deleteCollectionPhoto: takes the file off every order line carrying it
  (or the stock purchase's photo slot) and deletes the AI's reads of it
  plus the checks built on them (shared forgetReads with Move photo).
- Removed the header's blue "Run agent scan" button and its results panel
  (not in use); the /api/v1/agent/scan route stays for the agent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:26:10 +02:00
af4cdc66da feat(collection-confirm): spot a photo on the wrong PO and move it
Drivers photograph invoices per item and can't see which order a photo
lands on. B&R invoice 00282979 (stock order PO26100702STK) ended up on
the thinners line of PO202610017BLY; the AI read "PO26100702STK" off it
but the card only said "not found".

- The card list now carries each read's po_refs and source file, and sets
  ai_doc.other_po when the document names a PO that exists and isn't the
  row's PO (none named, or this card's PO among them: no hint).
- PO card: "Photo may be for <PO>" in the header; when open, "Invoice <no>
  is for <PO>, not this PO" with a Move photo button. Nothing moves on its
  own.
- POST /api/v1/collection-confirm/move-photo (browser session) →
  movePhotoToPo: copies the photo to the target (a collected line of a
  customer PO, or the stock purchase's single photo slot — refused when
  taken), removes it from the source, deletes the AI's read of it and the
  checks built on it so it is re-read where it now belongs.
- normalizePoReference moves to the client-safe src/lib/po-reference.ts
  (re-exported from the matching service).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:08:47 +02:00
dade4592be docs(mcp): collection_confirm.ai_check may set a discounted cost
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:40:22 +02:00
d73fb6f0ab feat(collection-ai): read invoice discounts and set the discounted cost
HYCO (I009753) prints full prices on the lines and "Discount 25.00%" in
the totals; Swagelok (622831) has a "Disc %" column on each line. The AI
compared our cost (already the discounted price) with the printed price,
so the Price tick went red on both.

- The read now captures a line's discount_percent and a whole-document
  doc_discount_percent (prompt rule 5b) and stores net_unit_price on each
  line (both discounts stacked).
- The Price tick compares the cost with the price after discount; the
  tooltip and the document-lines panel spell it out
  ("R15.10 less 25% invoice discount = R11.33").
- Owner's call: when a "sure" tax-invoice line has a discount and our
  cost isn't the discounted price, the AI sets the cost to it (customer
  lines through updateOrderItemCost, which adds a cost note; stock lines
  through updateStockPurchaseItem) and records why in ai_check.cost_changed,
  shown in purple under the line. No discount, a delivery note or a
  "likely" match: the cost is never touched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:18:21 +02:00
9ddb811afd feat(collection-confirm): AI checks on/off switch in the page header
The n8n tick was calling in every 5 minutes but WFDASH answered
"disabled" because Collection checks was off in Settings → AI, and
nothing on Collection Confirm said so.

- New "AI checks" switch next to Refresh, the same setting as Settings →
  AI → Collection checks. Purple and "On" when running, with the last
  run in the tooltip; a gear links to /settings/ai.
- GET/PUT /api/v1/collection-confirm/ai/enabled (browser session). Only
  reads/writes collection_ai_enabled and the last run — no key, model or
  prompt goes through it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:18:13 +02:00
d4cdde7384 fix(collection-ai): Add to POS never duplicates SKU-less rows, failures are per row
I5: a customer row without a SKU returns needs_sku and creates nothing; the
Add to POS button is disabled for it ("Set a SKU first").
I6: a createStockItem failure is caught per row (status failed + error), the
route still 200s, and the page toasts "N added, M failed, K need a SKU".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:16:21 +02:00
457ad30f12 fix(collection-ai): stock rows tick and show their own line SKU
I4: ConfirmRow.line_sku carries stock_purchase_items.sku; the SKU tick,
the AI robot and (for stock rows) the SKU column use line_sku before the
catalogue sku via effectiveSku. The SKU pencil keeps line_sku in step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:14:17 +02:00
8c3cfd45fc feat(collection-ai): settings section, runbook
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:55:44 +02:00
137b458a06 fix(collection-ai): page handler tests, full document lines, snapshot dialog defaults, tick aria detail
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:45:12 +02:00
c1656f5ccd feat(collection-ai): ticks, findings and POS buttons on Collection Confirm
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:41:52 +02:00
cc3e1d040b feat(collection-ai): add to POS, AI routes and MCP tool
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:33:56 +02:00
78ba560096 fix(quote-assistant): refuse to fill until the ai_fill field exists
PocketBase drops unknown fields on update, so before the migration runs
the AI's prices would land with no purple marker and no notes. The
manual button / MCP now return 409 'not set up yet' instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 00:26:13 +02:00
6604e8b837 fix(quote-assistant): review minors (templates, forced order, bounds, UI)
- M1: fillQuote and the tick refuse templates ("quote is not open
  (template)"); both quote pages gate the fill button with
  canAiFillQuote (new/follow_up/claimed, not a template).
- M4: run summary keeps <= 20 errors of <= 300 chars (boundRunErrors).
- M5: forced runs offer never-tried lines before tried ones.
- M6: counts route 400s on ids that aren't PB record ids.
- M7: a fill never replaces a supplier already on the line.
- M8: warn dot on the AI marker when a pending fill has doubts.
- M9: MCP hint states the real eligibility rule.
- M10: detail-page price dialog re-reads the line before recording an
  AI review.
- M11: exported QUOTE_NOT_OPEN_ERROR_PREFIX / AI_DISABLED_ERROR used by
  the run route.
- M14: prompt textarea maxLength 50000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:41:28 +02:00
dd3ab2ad9a fix(quote-assistant): clone/save-as-template don't copy unreviewed AI prices
A source line whose AI fill is still pending review (isAiFillPending) is
copied without cost, selling price, markup or supplier, so the purple
draft price can't resurface as an ordinary price on the copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:30:22 +02:00
4e0f7a5c5c fix(quote-assistant): AI fill on Process page keeps unsaved typing
After "Let AI fill this quote" the page re-pulls the quote's lines and
merges them with mergeItemsAfterAiRun: rows unchanged since the last load
take the server copy (so fills show), rows with unsaved changes stay as
typed, with a toast saying how many were kept. Previously onDone ran
fetchData, which replaced every row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:29:04 +02:00
a9160de1d6 fix(quote-assistant): retry unanswered lines, bound and serialise ticks
- decideWithAi throws on stop_reason max_tokens instead of returning a
  partial answer; fillQuote marks a requested line the AI did not answer
  as error (retried after 1 h), never a permanent no_match.
- QUOTE_MAX_LINES 30 -> 15; the tick also caps lines per quote so the
  60-line budget reaches other quotes.
- runAssistantTick stops starting quotes after a 90 s time budget and
  returns reason running while a newer start than the last summary is
  under 10 minutes old (lease).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:26:28 +02:00
144971779f feat(quote-assistant): settings section, n8n tick, runbook
Quote assistant card in Settings, AI (on/off, interval, min confidence,
editable prompt with reset, last-run line); parseSettingsForm handles the
qa_* fields. docs/quote-assistant.md has the runbook and the n8n tick
workflow spec; AGENTS.md gets a short section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:10:02 +02:00
fa854060c4 feat(quote-assistant): AI x/y badge on quotes board
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:01:43 +02:00
40094be3f3 feat(quote-assistant): purple AI lines on process and quote pages
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:56:02 +02:00
ef6193367d feat(quote-assistant): ai colour token, marker, fill button
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:45:44 +02:00
d34be52a66 feat(quote-assistant): tick/run/counts routes, MCP tool, re-price guard
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:38:38 +02:00
a1a2cc73e2 feat(search): invoice + delivery-note search, richer colour-coded preview
- Orders now match on customer invoice numbers and delivery-note
  numbers carried on their lines (tagged "invoice" / "delivery note").
- New Delivery Notes source: DN number, customer PO, customer and
  company names; preview pulls lines and invoices for visible hits only.
- Quick-overview dialog moved to search-preview.tsx and redesigned:
  type-tinted header, status pills, stat tiles, invoice chips with
  QuickBooks links, line list, icon timeline with dates and gaps.
  Fixes the dialog being pinned at 24rem by the primitive's sm:max-w-sm.
- Result list: per-type colour (icon tile, accent border, badge, row
  hover), status pills per row, and a jump bar; mobile-tightened.
- Stock-order preview now fetches ordered_value/created (Value row and
  age chip were always empty); supplier names keep their original case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:45:44 +02:00
a3a831d183 fix(quotes): list renders the customer's company, not the stale copy
Third and last place that read `quotes.company` directly. The list page
showed "Mre" next to Pierre Visser, who works for Harmony — the same
drift already reported on the detail page.

This one was a deliberate perf shortcut rather than an oversight.
`QUOTE_LIST_EXPAND` dropped `customer.company` to "save one IN query per
page load" and the page read the quote's own `company` relation
instead. That relation is a denormalised copy that PocketBase never
re-cascades, so the shortcut read a field nothing keeps correct — and
the column it fed is what a rep reads to decide whether to approve, so
a stale company there is worse than a slightly larger query.

The 3-level expand is back, on both the list and the search path (the
search results feed the same columns, so they needed it too). Search
now also RANKS on the customer's company rather than the stale copy —
otherwise a query for "Mre" would surface quotes whose results then
render "Harmony", contradicting themselves on screen. It falls back to
the quote's own copy for the ~4% of customers with no company relation,
where the two are equal by construction.

Applies to both the desktop table column and the mobile card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-16 01:07:50 +02:00
8d83065964 feat(giellie): tighten the agent tool policy and MCP surface
Aligns the tool policy, the system prompt, the MCP route and the skill
doc so they describe the same boundary, with tests pinning the policy
and prompt text.

The docs and the executable policy had drifted apart — the skill doc
advertised capabilities the policy does not grant, which is worse than
not documenting them: an agent reading the doc will plan around a tool
it isn't allowed to call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-16 00:44:52 +02:00
09e4d8f1e1 feat(collection-pool): back-to-pending, collection date, and a safe notes feed
Gives the operator two new controls on a collection-pool stop, and fixes
the audit feed they write into.

Back-to-pending (`items/[id]/back-to-pending`)

  A stop has to come back when the driver can't take it — no-show,
  wrong date, supplier pushed the delivery — or when the supplier
  confirmation was premature. A row can be assigned to a driver and/or
  marked ready at the same time, so the route clears both in one write
  rather than making the operator unwind them in the wrong order.

Collection date (`items/[id]/collection-date`)

  `order_items.collection_date` was a PB date field that existed but
  nothing read or wrote it. On a PENDING stop it is a forecast — the
  supplier said goods land on the 20th, so the stop stays
  un-assignable because promising a driver a pickup for goods that
  don't exist yet would be a lie. On a READY stop it is the confirmed
  collection day.

Notes feed (`src/lib/note-feed.ts`)

  `notes` on order_items / stock_purchase_items is NOT free text — it
  is a JSON array of NoteEntry objects, and every consumer parses it as
  one. The assign route's "mark ready" path wrote a plain string into
  it, which silently destroys the entire history: the next parse falls
  back to an empty feed. All writers now go through `appendNote` with
  the row's current value. Lives in `src/lib` rather than a page view
  model because both the client pages and the server routes need it,
  and an API route must never import from `app/(app)`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-16 00:44:49 +02:00
3b926570c7 fix(quotes): delivery-note quotes skip approvals; company follows customer
Two quote-flow defects found while auditing Q202610315.

1. Delivery-note quotes were being dumped into the rep approval queue.

   A DN quote is never sent on its own — it goes out with the note, via
   the note's "Send the delivery note and quote to the customer" button.
   But `generateQuotePdf` auto-flipped every non-terminal quote to
   `approval_pending`, which overwrote the DN status before the
   skip-approval branch in `sendQuoteEmail` could ever run. Ten DN
   quotes were sitting in Pending Approval as a result.

   - `createQuoteFromDeliveryNote` now creates the quote `approved`
     rather than `delivery_note`.
   - The PDF render no longer flips DN quotes at all, as a backstop.
   - The quote page gets a non-sending action row pointing back at the
     note. No "send" button — a second way to email a customer the same
     invoice is a liability, not a feature.

   Also fixes a latent duplicate-invoice bug: the duplicate guard
   pinned `status = "delivery_note"`, so once the quote had been sent
   it fell through and a second quote could be raised for the same
   note. Verified by test — the old guard silently minted a duplicate.

2. `quotes.company` never followed a customer change.

   `quotes.company` is a denormalised copy of `customer.company` and
   PB does not re-cascade relations, so swapping the customer left the
   old company behind forever (Q202610315 read "Mre" while its
   customer sat at Harmony — two contradictory companies on one row).

   The customer swap ran as a raw client-side PB PATCH that wrote only
   `customer`, which also bypassed the server's placeholder guard. It
   now goes through `PATCH /api/v1/quotes/[id]/pdf`, which cascades
   both relations atomically.

   The customer-facing PDF already read `customer.expand.company`,
   which is why this never reached a customer — but every consumer of
   the quote's own `company` got the wrong answer. A read-only audit
   (`scripts/audit-quote-company-drift.mjs`) puts it at 2,591 of 9,950
   quotes. That data is NOT backfilled here.

Also drops the duplicate standalone "COMPANY" column on the quote
detail page, and the stale-company fallback in approvals, so this
class of bug can't hide in the UI again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-16 00:44:45 +02:00
248c390184 chore: update route.ts 2026-10-15 16:28:37 +02:00
d35c25a9e7 feat(collection-pool): badge suppliers with driver-reported problems
A supplier whose stops include a driver-reported problem (out of
stock, damaged, short supply, wrong item, no-show) now carries a red
badge beside its name showing the count.

The point is the COLLAPSED case. Previously an operator had to expand
every supplier group to discover which ones had problems — the signal
only existed on the expanded rows. Now it is visible at the group
level, which is where an operator starts reading.

Not colour-only: the badge carries an AlertTriangle icon plus the count
and the word "issue"/"issues", and its tooltip names the affected stops
so the operator knows what they are about to open.

New pure helpers in view-model (unit-tested, no PB needed):
  isProblemRow / getProblemLabel  — per-row flag + human label
  summarizeSupplierProblems      — count + tooltip detail lines for a
                                   supplier group

Also drops the now-unused PoolRow / getAssignmentBadge / getPoolAgeLabel
imports from page.tsx — dead imports flagged as lint warnings while
working on the search page.

Committed at the operator's request alongside the search work; the
changes were already present and verified green (tsc, lint, full suite
3967 passing) before committing.
2026-10-15 16:06:33 +02:00