From 802565d2d83123776b743cf55bb757a0ea0a7a02 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 19 Oct 2026 13:50:27 +0200 Subject: [PATCH] feat(collection-confirm): check printed prices on delivery notes; show AI reading notes Plumblink hands out delivery notes with prices on them. The Price tick was always grey on a delivery note, so a card could go purple without its prices ever being checked, and the AI's reading notes ("delivery note, not a tax invoice", "year not printed") were never shown. - Price tick: compare whenever the document line has a price, invoice or delivery note; grey only when no price is printed. The AI still only corrects a cost from a tax invoice. - ai_doc now carries the read's notes; cardNotes() unions them per card and the AI findings box lists them under "Reading notes" (the box now shows when there are notes but no findings). Co-Authored-By: Claude Opus 5.5 --- docs/collection-ai-checks.md | 4 ++- .../collection-confirm/ai-findings.tsx | 31 ++++++++++++++----- .../collection-confirm/po-card-ai.test.tsx | 19 ++++++++++++ src/components/collection-confirm/po-card.tsx | 4 ++- src/lib/collection-ai-card.test.ts | 12 ++++++- src/lib/collection-ai-card.ts | 13 ++++++++ src/lib/collection-ai-ticks.test.ts | 13 ++++++-- src/lib/collection-ai-ticks.ts | 3 +- .../server/collection-confirm-service.test.ts | 2 ++ src/lib/server/collection-confirm-service.ts | 6 +++- 10 files changed, 91 insertions(+), 16 deletions(-) diff --git a/docs/collection-ai-checks.md b/docs/collection-ai-checks.md index 6feeca8..c5ce304 100644 --- a/docs/collection-ai-checks.md +++ b/docs/collection-ai-checks.md @@ -102,11 +102,13 @@ Collection checks, and stored in | Green | The document agrees with our line: our SKU equals the document code after normalising (uppercase, letters and digits only; this is also how a SKU the AI filled goes green), quantity equals what was collected and what was ordered, the AI is sure it is the same item, unit price after any discount within 1 cent | | Amber | Close but worth a look: our SKU differs from the document code; collected quantity matches the document but the ordered quantity differs; the AI thinks it is the same item but is only "likely" sure | | Red | Mismatch or missing: no SKU on our line, quantity differs from the document, price differs or we have no cost, or the line is not on the document or the AI says it is a different product (all four red) | -| Grey | Can't check, per tick: **SKU** when the document line shows no code; **Qty** when it shows no quantity; **Price** on a delivery note or when the document line shows no price; **all four** when the AI couldn't pair the document (see the AI findings box for the reason; **Check with AI** retries) | +| Grey | Can't check, per tick: **SKU** when the document line shows no code; **Qty** when it shows no quantity; **Price** when the document line shows no price (a delivery note that prints prices, like Plumblink's, is compared like an invoice — but the AI only ever corrects a cost from a tax invoice); **all four** when the AI couldn't pair the document (see the AI findings box for the reason; **Check with AI** retries) | For stock purchase lines the SKU tick and robot use the line's own SKU (the one a rep or the AI set on the purchase line), not the catalogue SKU. +The AI findings box also lists the AI's **reading notes** — what it noticed on the photo (a delivery note rather than an invoice, the year missing from the date, a rotated or partly covered page). + Hover a tick for the values compared (ours vs the document). A robot icon marks a SKU the AI filled. Click the document chip on a card to see the extracted lines next to the photo. diff --git a/src/components/collection-confirm/ai-findings.tsx b/src/components/collection-confirm/ai-findings.tsx index f5211b6..a548445 100644 --- a/src/components/collection-confirm/ai-findings.tsx +++ b/src/components/collection-confirm/ai-findings.tsx @@ -7,9 +7,12 @@ import { docPriceLabel, docUnitCost, roundCents } from "@/lib/collection-ai-tick const zar = new Intl.NumberFormat("en-ZA", { style: "currency", currency: "ZAR" }); -/** Box of document-level observations from the AI. Renders nothing when empty. */ -export function AiFindings({ findings }: { findings: string[] }) { - if (findings.length === 0) return null; +/** + * Box of document-level observations from the AI, plus its reading notes + * (what it noticed on the photo). Renders nothing when both are empty. + */ +export function AiFindings({ findings, notes = [] }: { findings: string[]; notes?: string[] }) { + if (findings.length === 0 && notes.length === 0) return null; return (
AI findings

-
    - {findings.map((finding, i) => ( -
  • {finding}
  • - ))} -
+ {findings.length > 0 ? ( +
    + {findings.map((finding, i) => ( +
  • {finding}
  • + ))} +
+ ) : null} + {notes.length > 0 ? ( +
+

Reading notes

+
    + {notes.map((note, i) => ( +
  • {note}
  • + ))} +
+
+ ) : null}
); } diff --git a/src/components/collection-confirm/po-card-ai.test.tsx b/src/components/collection-confirm/po-card-ai.test.tsx index d802e97..fbf6375 100644 --- a/src/components/collection-confirm/po-card-ai.test.tsx +++ b/src/components/collection-confirm/po-card-ai.test.tsx @@ -100,6 +100,25 @@ describe("PoCard AI layer", () => { expect(screen.getByTestId("ai-findings")).toHaveTextContent("Freight not on PO"); }); + it("shows the AI's reading notes in the findings box even when there are no findings", () => { + const row = makeAiRow({ + ai_doc: { + read_id: "r", + doc_type: "delivery_note", + document_no: "2010/02893666", + doc_date: "2026-10-09", + supplier_name: "Plumblink", + findings: [], + notes: ["Year not printed on the document date"], + lines: [], + }, + }); + render(); + const box = screen.getByTestId("ai-findings"); + expect(box).toHaveTextContent("Reading notes"); + expect(box).toHaveTextContent("Year not printed on the document date"); + }); + it("shows a line's own findings (e.g. a pairing failure) in the findings box", () => { const row = makeAiRow({ ai_check: makeCheck({ diff --git a/src/components/collection-confirm/po-card.tsx b/src/components/collection-confirm/po-card.tsx index 6ad0f3e..fe143cd 100644 --- a/src/components/collection-confirm/po-card.tsx +++ b/src/components/collection-confirm/po-card.tsx @@ -16,6 +16,7 @@ import { import { cardDocument, cardFindings, + cardNotes, documentChipLabel, } from "@/lib/collection-ai-card"; import type { CollectionDocLine } from "@/lib/types"; @@ -204,6 +205,7 @@ export function PoCard({ const [docPanelOpen, setDocPanelOpen] = useState(false); const aiDoc = useMemo(() => cardDocument(rows), [rows]); const aiFindings = useMemo(() => cardFindings(rows), [rows]); + const aiNotes = useMemo(() => cardNotes(rows), [rows]); // Every line the AI read from the chip's document; lines paired with a row // on this card are marked "matched". Falls back to the paired lines alone // when the document's full line list is unavailable. @@ -890,7 +892,7 @@ export function PoCard({ )} - +