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 && (