/* ============================================================= CoreIQ Office — Expiring stock (LIVE) The back-office half of what the handheld records in the aisle: the shop floor scans a product and types the date off the pack, and those sightings land in product_expiry_checks. Without this screen that is data entry into a table nobody reads — worse than not collecting it, because it costs staff real time and looks like it is working. Deliberately NOT a stock screen. On-hand is a projection of the movements ledger; an expiry sighting is an observation about a shelf. The quantity shown is what the operator counted at that moment, and is labelled as such. ============================================================= */ const { useState: expUseState, useEffect: expUseEffect, useMemo: expUseMemo } = React; function expMoney(cents) { if (cents == null) return "—"; return "$" + (Number(cents) / 100).toFixed(2); } // Australian order, matching what the handheld asks the operator to type. function expDate(iso) { if (!iso) return "—"; const d = new Date(iso); if (isNaN(d)) return String(iso).slice(0, 10); const dd = String(d.getDate()).padStart(2, "0"); const mm = String(d.getMonth() + 1).padStart(2, "0"); return dd + "/" + mm + "/" + d.getFullYear(); } // "Expired" and "expiring" are different jobs: one comes off the shelf today, // the other is a markdown decision. The wording has to keep them apart. function expWhen(daysLeft) { const n = Number(daysLeft); if (n < 0) return { text: "Expired " + Math.abs(n) + "d ago", tone: "void" }; if (n === 0) return { text: "Expires today", tone: "void" }; if (n <= 30) return { text: n + " days", tone: "hold" }; return { text: n + " days", tone: "" }; } const EXP_WINDOWS = [30, 60, 90, 180]; function ExpiringStock() { const { scope } = window.useOffice(); const [windowDays, setWindowDays] = expUseState(90); const [data, setData] = expUseState(null); const [err, setErr] = expUseState(null); const [busyId, setBusyId] = expUseState(null); const [reloadKey, setReloadKey] = expUseState(0); const [q, setQ] = expUseState(""); expUseEffect(() => { let alive = true; setData(null); setErr(null); const siteId = scope === "all" ? undefined : scope; window.OfficeAPI.getExpiring(windowDays, siteId) .then((d) => { if (alive) setData(d); }) .catch((e) => { if (alive) setErr(String((e && e.message) || e)); }); return () => { alive = false; }; }, [windowDays, scope, reloadKey]); const shown = expUseMemo(() => { if (!data) return []; const needle = q.trim().toLowerCase(); if (!needle) return data.items; return data.items.filter((i) => (i.name || "").toLowerCase().includes(needle) || (i.sku || "").toLowerCase().includes(needle) || (i.brand || "").toLowerCase().includes(needle)); }, [data, q]); async function resolve(item) { setBusyId(item.id); try { await window.OfficeAPI.resolveExpiring(item.id); setReloadKey((k) => k + 1); } catch (e) { setErr(String((e && e.message) || e)); } finally { setBusyId(null); } } if (err) { return (