/* =============================================================
CoreIQ Office — Analytics (LIVE · legacy archive)
Business intelligence over the 17-year retention archive: sales reporting,
inventory intelligence, and debtor/supplier analytics. Every figure is a real
aggregate from Aurora (/analytics/*) — nothing fabricated.
============================================================= */
const { useState: anUseState, useEffect: anUseEffect } = React;
const anM = (c) => "$" + (Number(c || 0) / 100).toLocaleString("en-AU", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const anMk = (c) => { const d = Number(c || 0) / 100; return d >= 1000 ? "$" + (d / 1000).toFixed(d >= 100000 ? 0 : 1) + "k" : "$" + d.toFixed(0); };
const anN = (n) => Number(n || 0).toLocaleString("en-AU");
const anErr = (e) =>
{e}
;
const anLoad = (t) => ;
// Vertical bar chart (months / hours / day-of-week)
function VBars({ items, valueKey, labelKey, height = 120, color = "var(--brand)" }) {
const mx = Math.max(1, ...items.map((d) => Number(d[valueKey]) || 0));
return (
24 ? 2 : 4, height}}>
{items.map((d, i) => (
))}
);
}
// Horizontal bars (tender mix / shrinkage)
function HBars({ items, valueKey, labelKey, fmt }) {
const mx = Math.max(1, ...items.map((d) => Math.abs(Number(d[valueKey]) || 0)));
return (
{items.map((d, i) => (
{d[labelKey]}
{fmt(d[valueKey])}
))}
);
}
// Interactive revenue mix — a stacked bar of Retail + Script co-pay + Govt PBS
// subsidy; tick any series on/off and the bar recomputes so you can play with it.
function RevenueMix({ split }) {
const SERIES = [
{ key: "retail", label: "Retail (front of shop)", value: split.privateCents, color: "var(--brand)" },
{ key: "copay", label: "Script patient co-pay", value: split.pbsCents, color: "var(--hold)" },
{ key: "subsidy", label: "Government PBS subsidy", value: split.govtSubsidyCents, color: "var(--navy-600)" },
];
const [on, setOn] = anUseState({ retail: true, copay: true, subsidy: true });
const active = SERIES.filter((s) => on[s.key]);
const total = active.reduce((a, s) => a + s.value, 0);
const denom = Math.max(1, total);
return (
Revenue mix
{anM(total)} · tick to add/remove
{SERIES.map((s) => (
))}
{active.length === 3 ? "Total income" : active.length === 0 ? "Nothing selected" : "Selected total"}
{anM(total)}
);
}
// -----------------------------------------------------------------
// 1) SALES ANALYTICS
// -----------------------------------------------------------------
function SalesAnalytics() {
const [years, setYears] = anUseState([]);
const [year, setYear] = anUseState(null);
const [data, setData] = anUseState(null);
const [loading, setLoading] = anUseState(true);
const [err, setErr] = anUseState(null);
anUseEffect(() => {
window.OfficeAPI.analyticsMeta().then((m) => { setYears(m.years || []); setYear(m.latest); }).catch((e) => setErr(String(e.message || e)));
}, []);
anUseEffect(() => {
if (!year) return;
let alive = true; setLoading(true); setErr(null);
window.OfficeAPI.analyticsSales(year)
.then((s) => { if (alive) { setData(s); setLoading(false); } })
.catch((e) => { if (alive) { setErr(String(e.message || e)); setLoading(false); } });
return () => { alive = false; };
}, [year]);
const DOW = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const dowFull = data ? DOW.map((d, i) => data.byDayOfWeek.find((x) => x.dow === i) || { day: d, dow: i, revenueCents: 0 }) : [];
const hoursFull = data ? Array.from({ length: 24 }, (_, h) => data.byHour.find((x) => x.hour === h) || { hour: h, revenueCents: 0 }).filter((x) => x.hour >= 6 && x.hour <= 21) : [];
const split = data?.revenueSplit;
const splitTotal = split ? Math.max(1, split.privateCents + split.pbsCents) : 1;
return (
live from Aurora
Sales analytics
{err && anErr("Couldn't load analytics: " + err)}
{loading && !data &&
Crunching {year}…
}
{data && (
<>
Revenue by month · {year}
{data.byMonth.length === 0 ?
No sales.
:
({ label: m.month.slice(5), revenueCents: m.revenueCents }))} valueKey="revenueCents" labelKey="label" height={150}/>}
By day of week
({ label: d.day, revenueCents: d.revenueCents }))} valueKey="revenueCents" labelKey="label" height={120} color="var(--teal-600,var(--brand))"/>
By hour of day
({ label: d.hour, revenueCents: d.revenueCents }))} valueKey="revenueCents" labelKey="label" height={120} color="var(--navy-600,var(--brand))"/>
Top sellers · {year}
by revenue
#
Product
SKU
Units
Revenue
{data.topProducts.map((p, i) => (
{i + 1}
{p.name}
{p.sku || "—"}
{anN(p.units)}
{anM(p.revenueCents)}
))}
By operator
Staff
Sales
Revenue
{data.byOperator.map((o) => (
{o.name}
{anN(o.sales)}
{anM(o.revenueCents)}
))}
>
)}
);
}
// -----------------------------------------------------------------
// 2) INVENTORY INTELLIGENCE
// -----------------------------------------------------------------
function InventoryAnalytics() {
const { setScreenState, setScreen } = window.useOffice();
const [data, setData] = anUseState(null);
const [err, setErr] = anUseState(null);
anUseEffect(() => {
window.OfficeAPI.analyticsInventory().then(setData).catch((e) => setErr(String(e.message || e)));
}, []);
const open = (pid) => { if (pid) { setScreenState({ activeProductId: pid, productFrom: "inventoryAnalytics" }); setScreen("productDetail"); } };
if (err) return {anErr("Couldn't load: " + err)}
;
if (!data) return anLoad("Inventory intelligence");
return (
Dead & slow stock
on-hand > 0, no outward movement since {data.cutoff} · highest value first
{data.deadStock.length === 0 ?
No dead stock — everything's moving.
:
Product
SKU
On hand
Value
Last out
{data.deadStock.map((d) => (
open(d.productId)}>
{d.name}
{d.sku || "—"}
{anN(d.onHand)}
{anM(d.valueCents)}
{d.lastOut || "never"}
))}
}
Shrinkage by reason
anN(v) + " u"}/>
Fastest movers
units out, last 12 months
Product
Units out
{data.topMovers.slice(0, 12).map((m) => (
open(m.productId)}>
{m.name}
{anN(m.unitsOut)}
))}
);
}
// -----------------------------------------------------------------
// 3) FINANCIAL ANALYTICS — aged debtors + supplier spend
// -----------------------------------------------------------------
function FinancialAnalytics() {
const [tab, setTab] = anUseState("debtors");
const [deb, setDeb] = anUseState(null);
const [sup, setSup] = anUseState(null);
const [err, setErr] = anUseState(null);
anUseEffect(() => { window.OfficeAPI.analyticsDebtors().then(setDeb).catch((e) => setErr(String(e.message || e))); }, []);
anUseEffect(() => { window.OfficeAPI.analyticsSuppliers().then(setSup).catch((e) => setErr(String(e.message || e))); }, []);
return (
{err && anErr(err)}
{tab === "debtors" && deb && (
<>
Account
Customer
Balance
Last activity
Last payment
{deb.topDebtors.map((d) => (
{d.accountNumber || "—"}
{d.name}
{anM(d.balanceCents)}
{d.lastActivity || "—"}
{d.lastPayment || "—"}
))}
>
)}
{tab === "suppliers" && sup && (
<>
Spend by supplier
all-time, GST-inclusive
Supplier
Invoices
Spend
GST
Active
{sup.bySupplier.map((s, i) => (
{s.supplier}
{anN(s.invoices)}
{anM(s.spendCents)}
{anM(s.gstCents)}
{s.firstInvoice} → {s.lastInvoice}
))}
>
)}
);
}
window.OFFICE_SCREENS = Object.assign(window.OFFICE_SCREENS || {}, {
salesAnalytics: SalesAnalytics,
inventoryAnalytics: InventoryAnalytics,
financialAnalytics: FinancialAnalytics,
});