From c2faca98bdfebaa3007fc43500480a16e38d3015 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 26 Oct 2026 10:54:09 +0200 Subject: [PATCH] feat: ask to collect a PO's items after its invoice photo uploads An invoice photo collects nothing, and the green invoice button made it look done, so lines stayed "assigned" and never reached Collection Confirm (EWSS and G-Man on 2026-10-26). After an upload the driver now gets "Collect these items?" with every item ticked. Without the AI match it opens straight away; with it, once the match sheet closes, listing only what the match left. Co-Authored-By: Claude Opus 5.5 --- app/page.tsx | 36 +++++++++++++ components/CollectPrompt.tsx | 95 +++++++++++++++++++++++++++++++++ components/InvoiceMatchHost.tsx | 13 +++-- 3 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 components/CollectPrompt.tsx diff --git a/app/page.tsx b/app/page.tsx index 41c9453..435b3ec 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -12,6 +12,8 @@ import CollectModal from '@/components/CollectModal'; import ProblemModal from '@/components/ProblemModal'; import ItemDetailModal from '@/components/ItemDetailModal'; import InvoiceMatchHost from '@/components/InvoiceMatchHost'; +import CollectPrompt from '@/components/CollectPrompt'; +import { invoiceCheckBase } from '@/lib/invoiceCheck'; import { getMyAssignedItems, getMyAssignedStockItems, @@ -108,6 +110,12 @@ export default function CollectionsPage() { const [showDetailModal, setShowDetailModal] = useState(false); const [showProblemModal, setShowProblemModal] = useState(false); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null); + // "Collect these items?" after an invoice photo. With the AI match on it + // waits in pendingPrompt until the match sheet closes, so it only lists + // what the match left behind. + type PromptState = { poLabel: string; ids: string[] }; + const [collectPrompt, setCollectPrompt] = useState(null); + const pendingPrompt = useRef(null); // One layout for every driver: compact rows, grouped supplier → PO. // The supplier/invoice/area and compact/detailed switches were removed // on 2026-10-18 — six layouts behind small icons just confused drivers. @@ -393,6 +401,13 @@ const loadItems = useCallback(async (showLoading = true) => { const files = await uploadInvoicePage({ target, poNumber, page, file }); setItems((prev) => withTargetFiles(prev, target, files)); showToast(`Invoice page ${page} uploaded${poNumber ? ` · PO ${poNumber}` : ''}`); + // The photo collects nothing by itself: ask, so the lines reach + // Collection Confirm whether or not the AI match runs. + if (statusFilter === 'assigned') { + const prompt = { poLabel: poNumber ? `PO ${poNumber}` : 'this PO', ids: poItems.map((i) => i.id) }; + if (poNumber && invoiceCheckBase()) pendingPrompt.current = prompt; + else setCollectPrompt(prompt); + } } catch (err) { console.error('Invoice photo upload failed:', err); const reason = err instanceof Error ? err.message : 'unknown error'; @@ -918,8 +933,29 @@ const loadItems = useCallback(async (showLoading = true) => { showToast(`Invoice matched · ${ids.length} item${ids.length === 1 ? '' : 's'} marked collected`); }} onRestored={() => { void loadItems(false); }} + onClosed={() => { + if (pendingPrompt.current) setCollectPrompt(pendingPrompt.current); + pendingPrompt.current = null; + }} /> + {collectPrompt && (() => { + const left = items.filter((i) => collectPrompt.ids.includes(i.id)); + if (left.length === 0) return null; + return ( + setCollectPrompt(null)} + onCollect={(chosen) => { + setCollectPrompt(null); + void handleBulkCollect(collectPrompt.poLabel, chosen); + }} + /> + ); + })()} + {/* Toast */} {toast && (
void; + onClose: () => void; +} + +// Shown right after an invoice photo uploads. The photo alone collects +// nothing, and the green invoice button made it look done, so drivers left +// lines "assigned" and off Collection Confirm. Works with or without the AI +// match: it only lists what is still on the list. Every item starts ticked; +// a short or different item is unticked and collected on its own card. +export default function CollectPrompt({ poLabel, items, onCollect, onClose }: CollectPromptProps) { + const [picked, setPicked] = useState>(() => new Set(items.map((i) => i.id))); + + const toggle = (id: string) => + setPicked((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + + const chosen = items.filter((i) => picked.has(i.id)); + + return ( + +

+ The photo is saved, but the items aren’t collected yet. Tick what you picked up. +

+
    + {items.map((item) => { + const on = picked.has(item.id); + return ( +
  • + +
  • + ); + })} +
+

+ Short or different from the PO? Untick it and use Collect on that item to enter what you got. +

+
+ + +
+
+ ); +} diff --git a/components/InvoiceMatchHost.tsx b/components/InvoiceMatchHost.tsx index aae224a..c83796b 100644 --- a/components/InvoiceMatchHost.tsx +++ b/components/InvoiceMatchHost.tsx @@ -19,13 +19,15 @@ interface InvoiceMatchHostProps { onCollected: (ids: string[]) => void; /** Lines put back: reload the driver's list. */ onRestored: () => void; + /** The sheet closed: the page asks about anything still to collect. */ + onClosed?: () => void; } // Beta helper, mounted once on the driver page. Renders nothing until a PO's // "Check invoice" runs. It marks only exact matches collected (same product, // invoice quantity = ordered quantity); everything uncertain is listed for the // driver and left alone. Every auto-collected line can be undone. -export default function InvoiceMatchHost({ onCollected, onRestored }: InvoiceMatchHostProps) { +export default function InvoiceMatchHost({ onCollected, onRestored, onClosed }: InvoiceMatchHostProps) { const [open, setOpen] = useState(false); const [busy, setBusy] = useState(false); const [poNumber, setPoNumber] = useState(''); @@ -125,8 +127,13 @@ export default function InvoiceMatchHost({ onCollected, onRestored }: InvoiceMat if (!open) return null; + const close = () => { + setOpen(false); + onClosed?.(); + }; + return ( - setOpen(false)}> + {busy && (