/* ============================================================= CoreIQ Office — Duplicate review The consolidation queue (mirrors Pack review): product records that look like the SAME trade item, for a human to review + merge. - 'Barcode match' (green): live products sharing a canonical barcode — safe, high confidence. - 'Possible match' (amber): same-category products with very similar names but no shared barcode — the tail the barcode-first receiver can't catch; review the details before merging. Merge is reversible (audited in merge_records); stock is summed and both invoices stay reconcilable. 'Not a duplicate' dismisses it. No-fake-data: every candidate + number comes from /duplicates. ============================================================= */ const { useState: dupUseState, useEffect: dupUseEffect } = React; const dupMoney = (c) => (c == null ? "—" : "$" + (Number(c) / 100).toLocaleString("en-AU", { minimumFractionDigits: 2, maximumFractionDigits: 2 })); function DuplicateReviewScreen() { const { pushToast, activeUser } = window.useOffice(); const [groups, setGroups] = dupUseState(null); // null = loading const [err, setErr] = dupUseState(null); const [busy, setBusy] = dupUseState(null); // pairKey being acted on const [keepSel, setKeepSel] = dupUseState({}); // pairKey -> productId to keep const performedBy = (activeUser && (activeUser.email || [activeUser.first, activeUser.last].filter(Boolean).join(" "))) || "office-user"; const load = () => { setGroups(null); setErr(null); setBusy(null); window.OfficeAPI.duplicatesList() .then((g) => { const list = g || []; setGroups(list); const sel = {}; list.forEach((grp) => { // Default keep = the cheaper record (retire the pricier duplicate); operator can change it. const sorted = [...grp.products].sort((a, b) => (a.sellPriceCents ?? 1e12) - (b.sellPriceCents ?? 1e12)); sel[grp.pairKey] = sorted[0].id; }); setKeepSel(sel); }) .catch((e) => setErr((e && e.message) || String(e))); }; dupUseEffect(load, []); const doMerge = (grp) => { const keepId = keepSel[grp.pairKey]; const keep = grp.products.find((p) => p.id === keepId); const others = grp.products.filter((p) => p.id !== keepId); if (!keep || !others.length) return; if (!window.confirm(`Merge ${others.length} record(s) into "${keep.name}" (${dupMoney(keep.sellPriceCents)})?\n\nStock is summed, both invoices stay reconcilable, and this is reversible.`)) return; setBusy(grp.pairKey); const force = grp.confidence === "name"; // name matches don't share a barcode → force others .reduce((chain, o) => chain.then(() => window.OfficeAPI.mergeProducts(keep.id, o.id, performedBy, force)), Promise.resolve()) .then(() => { pushToast({ kind: "paid", icon: "check-circle", title: "Merged into one product", meta: keep.name }); load(); }) .catch((e) => { pushToast({ kind: "void", icon: "alert", title: "Merge failed", meta: String((e && e.message) || e) }); setBusy(null); }); }; const doDismiss = (grp) => { setBusy(grp.pairKey); window.OfficeAPI.dismissDuplicate(grp.pairKey, performedBy, null) .then(() => { pushToast({ kind: "paid", icon: "check", title: "Marked not a duplicate" }); load(); }) .catch((e) => { pushToast({ kind: "void", icon: "alert", title: "Couldn't dismiss", meta: String((e && e.message) || e) }); setBusy(null); }); }; const openInvoice = (invoiceId) => { window.OfficeAPI.supplierInvoicePdf(invoiceId) .then((blob) => window.open(URL.createObjectURL(blob), "_blank")) .catch((e) => pushToast({ kind: "void", icon: "alert", title: "Couldn't open invoice", meta: String((e && e.message) || e) })); }; // Decisive heuristic: records from the SAME supplier with DIFFERENT reorder // codes are almost always different products (a wholesaler gives one code per // product) — flag it so the operator leans to "Not a duplicate". const sameSupplierDiffCode = (grp) => { const suppliers = new Set(grp.products.map((p) => (p.supplierName || p.provider || "").toLowerCase()).filter(Boolean)); const codes = grp.products.map((p) => p.supplierCode).filter(Boolean); return suppliers.size === 1 && new Set(codes).size === grp.products.length && codes.length === grp.products.length; }; return (
Catalogue & stock · live from Aurora

Duplicate review

Records that might be the same product. If it's the same item entered twice, merge them — stock is summed, every supplier code + invoice is kept, and it's reversible. If they're different products (the reorder codes usually tell you), keep both.

{err && (
Couldn't load duplicates: {err}
)} {groups === null && !err && (
Scanning the catalogue…
)} {groups && groups.length === 0 && !err && (
No duplicate products — the catalogue is clean.
)} {groups && groups.map((grp) => { const isName = grp.confidence === "name"; const likelyDifferent = sameSupplierDiffCode(grp); const keepBothBtn = ( ); const mergeBtn = ( ); return (
{isName ? "Possible match" : "Barcode match"} {isName && No shared barcode — check the invoices.}
{likelyDifferent && (
Same supplier, different reorder codes — usually different products (variants/sizes), not a duplicate. Lean to “Keep both”.
)}
{grp.products.map((p) => { const keep = keepSel[grp.pairKey] === p.id; return ( ); })}
{likelyDifferent ? <>{keepBothBtn}{mergeBtn} : <>{mergeBtn}{keepBothBtn}}
); })}
); } window.OFFICE_SCREENS = Object.assign(window.OFFICE_SCREENS || {}, { duplicateReview: DuplicateReviewScreen });