/* ============================================================= 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 (

Expiring stock

Couldn’t load: {err}
); } if (!data) { return (

Expiring stock

Loading…
); } // An empty list is honest: nothing has been flagged yet. It must not read as // "nothing is expiring", which is a different and much stronger claim. const nothingRecorded = data.items.length === 0; return (
SHOP FLOOR · RECORDED ON THE HANDHELD

Expiring stock

{EXP_WINDOWS.map((w) => ( ))}
setQ(e.target.value)} style={{height:34, fontSize:13}}/>
{data.expiredCount > 0 && ( {data.expiredCount} already expired )} {data.soonCount} expiring {expMoney(data.totalValueAtRiskCents)} at risk
{/* The value figure is only as complete as the counts behind it. Saying so keeps it from being read as the whole exposure. */} {data.withoutQuantity > 0 && (
{data.withoutQuantity} of these were flagged without a quantity, so they carry no value — the figure above is the value of what was actually counted.
)}
Product
Expiry
When
Counted
At risk
{nothingRecorded && (
Nothing flagged yet. Expiry dates are recorded on the handheld — scan a product, tap “Record expiry”, type the date off the pack.
)} {shown.map((i) => { const when = expWhen(i.daysLeft); return (
{i.name} {i.brand ? · {i.brand} : null} {i.batchNumber ? · batch {i.batchNumber} : null}
{expDate(i.expiryDate)}
{when.text}
{i.quantity == null ? : Number(i.quantity).toFixed(0)}
{expMoney(i.valueAtRiskCents)}
); })}
); } window.OFFICE_SCREENS = Object.assign(window.OFFICE_SCREENS || {}, { expiring: ExpiringStock });