/* =============================================================
CoreIQ Office — Legacy archive retrieval (LIVE · legacy archive)
Read-only back-office views over the Z (APSS) 7-year retention archive
(z_archive.*, non-synced). Lets staff / an auditor pull up a historical
receipt, a supplier tax invoice, an account statement, or a stock movement
trail. Every figure is queried live from Aurora via /archive/*.
============================================================= */
const { useState: aUseState, useEffect: aUseEffect, useCallback: aUseCallback } = React;
const aMoney = (c) => "$" + (Number(c || 0) / 100).toLocaleString("en-AU", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const aNum = (n) => Number(n || 0).toLocaleString("en-AU");
const aDate = (s) => (s ? String(s).slice(0, 10) : "—");
const aDateTime = (s) => (s ? String(s).replace("T", " ").slice(0, 16) : "—");
const aHuman = (s) => String(s || "").replace(/_/g, " ");
// Generic list fetch hook: re-runs when `key` changes, exposes data/loading/err.
function useArchive(fetcher, key) {
const [data, setData] = aUseState(null);
const [loading, setLoading] = aUseState(true);
const [err, setErr] = aUseState(null);
aUseEffect(() => {
let alive = true;
setLoading(true); setErr(null);
Promise.resolve(fetcher())
.then((d) => { if (alive) { setData(d); setLoading(false); } })
.catch((e) => { if (alive) { setErr(String((e && e.message) || e)); setLoading(false); } });
return () => { alive = false; };
}, [key]);
return { data, loading, err };
}
function AHeader({ eyebrow, title, actions, back }) {
return (
);
}
function ABackBtn({ onClick }) {
return ;
}
function AErr({ err }) {
return
{err}
;
}
function AEmpty({ children }) {
return {children}
;
}
function APager({ total, limit, offset, onPage }) {
if (!total) return null;
const from = offset + 1, to = Math.min(offset + limit, total);
return (
Showing {aNum(from)}–{aNum(to)} of {aNum(total)}
);
}
function ASearch({ value, onChange, placeholder, extra }) {
return (
onChange(e.target.value)} placeholder={placeholder} style={{flex:1, height:34, border:"none", background:"transparent", padding:0}}/>
{extra}
);
}
// Clickable column header → server-side sort. `num` right-aligns + defaults desc.
function SortHead({ label, col, sort, onSort, num, def }) {
const active = sort.col === col;
return (
onSort(col, def || (num ? "desc" : "asc"))} title="Sort"
style={Object.assign({ cursor: "pointer", userSelect: "none" }, num ? { justifyContent: "flex-end" } : {})}>
{label}{active ? (sort.dir === "asc" ? " ↑" : " ↓") : ""}
);
}
const nextSort = (sort, col, def) => (sort.col === col ? { col, dir: sort.dir === "asc" ? "desc" : "asc" } : { col, dir: def || "asc" });
// -----------------------------------------------------------------
// 1) SALES HISTORY — receipts → lines + tenders + EFTPOS detail
// -----------------------------------------------------------------
function ArchiveSales() {
const [q, setQ] = aUseState("");
const [from, setFrom] = aUseState("");
const [to, setTo] = aUseState("");
const [offset, setOffset] = aUseState(0);
const [applied, setApplied] = aUseState({ q: "", from: "", to: "", offset: 0 });
const [sel, setSel] = aUseState(null);
const [sort, setSort] = aUseState({ col: "date", dir: "desc" });
const onSort = (c, d) => { setOffset(0); setSort((s) => nextSort(s, c, d)); };
aUseEffect(() => { setApplied({ q, from, to, offset }); }, [offset]);
const apply = () => { setOffset(0); setApplied({ q, from, to, offset: 0 }); };
const key = JSON.stringify(applied) + "|" + sort.col + sort.dir;
const { data, loading, err } = useArchive(
() => window.OfficeAPI.archiveSales({ limit: 50, offset: applied.offset, sort: sort.col, dir: sort.dir, ...(applied.q ? { q: applied.q } : {}), ...(applied.from ? { from: applied.from } : {}), ...(applied.to ? { to: applied.to } : {}) }),
key,
);
if (sel) return setSel(null)}/>;
return (
setFrom(e.target.value)} style={{height:34, width:150}} title="From"/>
→
setTo(e.target.value)} style={{height:34, width:150}} title="To"/>
>}/>
{err && }
Receipts
{loading ? "loading…" : aNum(data?.total) + " matching · newest first"}
{!loading && data && data.items.length === 0 &&
No receipts match.}
{data && data.items.length > 0 && (
{data.items.map((r) => (
setSel(r.id)}>
{aDateTime(r.date)}
{r.number}
{r.customerName || "—"}
{r.status === "voided" ? voided : r.status}
{aMoney(r.totalCents)}
))}
)}
{data &&
}
);
}
function ArchiveSaleDetail({ id, onBack }) {
const { data, loading, err } = useArchive(() => window.OfficeAPI.archiveSale(id), id);
const [showCard, setShowCard] = aUseState(false);
if (loading) return } eyebrow="loading…" title="Receipt"/> ;
if (err || !data) return } eyebrow="error" title="Receipt"/> ;
const t = data.transaction;
return (
} eyebrow={`${aDateTime(t.date)}${t.customerName ? " · " + t.customerName : ""}`} title={"Receipt #" + t.number}/>
Lines
Item
Qty
Unit
PBS subsidy
Line total
{data.lines.map((l) => (
{l.name || l.sku || "—"}{l.status === "C" ? cancelled : null}
{l.quantity}
{aMoney(l.unitPriceCents)}
{l.govtContributionCents ? aMoney(l.govtContributionCents) : "—"}
{aMoney(l.lineTotalCents)}
))}
Tenders
{data.tenders.length === 0 &&
No tender lines.}
{data.tenders.map((t2) => (
{aHuman(t2.type)}{t2.accountId ? · on account : null}
{aMoney(t2.amountCents)}
))}
Card / EFTPOS
{data.cards.length ? : null}
{data.cards.length === 0 &&
No card-present auth on this sale.}
{data.cards.map((c) => (
Sale ref
{c.saleReference}
{showCard && c.merchantReceipt && (
{c.merchantReceipt}
)}
))}
);
}
// -----------------------------------------------------------------
// 2) PURCHASING — wholesaler tax invoices + purchase orders
// -----------------------------------------------------------------
function ArchivePurchasing() {
const [tab, setTab] = aUseState("invoices");
const [q, setQ] = aUseState("");
const [applied, setApplied] = aUseState("");
const [offset, setOffset] = aUseState(0);
const [sel, setSel] = aUseState(null);
const [sort, setSort] = aUseState({ col: "date", dir: "desc" });
const onSort = (c, d) => { setOffset(0); setSort((s) => nextSort(s, c, d)); };
aUseEffect(() => { setOffset(0); setSel(null); setSort({ col: "date", dir: "desc" }); }, [tab]);
const key = tab + "|" + applied + "|" + offset + "|" + sort.col + sort.dir;
const { data, loading, err } = useArchive(
() => (tab === "invoices"
? window.OfficeAPI.archiveInvoices({ limit: 50, offset, sort: sort.col, dir: sort.dir, ...(applied ? { q: applied } : {}) })
: window.OfficeAPI.archivePurchaseOrders({ limit: 50, offset, sort: sort.col, dir: sort.dir, ...(applied ? { q: applied } : {}) })),
key,
);
if (sel) return setSel(null)}/>;
const supName = (it) => it.supplierName || (it.notes ? String(it.notes).split(" / ")[0] : null) || "—";
return (
{ setOffset(0); setApplied(q); }}>Search}/>
{err && }
{tab === "invoices" ? "Supplier tax invoices" : "Purchase orders"}
{loading ? "loading…" : aNum(data?.total) + " total · newest first"}
{!loading && data && data.items.length === 0 &&
Nothing matches.}
{tab === "invoices" && data && data.items.length > 0 && (
Ex-GST
GST
{data.items.map((r) => (
setSel(r.id)}>
{aDate(r.date)}
{r.number || "—"}
{supName(r)}
{aMoney(r.subtotalCents)}
{aMoney(r.gstCents)}
{aMoney(r.totalCents)}
))}
)}
{tab === "orders" && data && data.items.length > 0 && (
Source
{data.items.map((r) => (
{aDate(r.date)}
{r.number}
{r.supplierName || "—"}
{r.sourceType === "pharmx" ? PharmX : aHuman(r.sourceType)}
{aHuman(r.status)}
))}
)}
{data &&
}
);
}
function ArchiveInvoiceDetail({ id, onBack }) {
const { data, loading, err } = useArchive(() => window.OfficeAPI.archiveInvoice(id), id);
if (loading) return } eyebrow="loading…" title="Invoice"/> ;
if (err || !data) return } eyebrow="error" title="Invoice"/> ;
const i = data.invoice;
const sup = i.supplierName || (i.notes ? String(i.notes).split(" / ")[0] : "—");
return (
} eyebrow={`${aDate(i.date)} · ${sup}`} title={"Invoice #" + (i.number || "—")}/>
Description
Supplier code
Qty
Unit cost
Line ex-GST
{data.lines.map((l) => (
{l.name || l.sku || "—"}
{l.supplierCode || "—"}
{l.quantity}
{aMoney(l.unitCostCents)}
{aMoney(l.lineTotalCents)}
))}
);
}
// -----------------------------------------------------------------
// 3) ACCOUNT STATEMENTS — A/R ledger per debtor
// -----------------------------------------------------------------
const AR_TYPE = { I: "Charge", P: "Payment", C: "Credit", A: "Adjustment", S: "Statement", D: "Discount", Z: "Migrated" };
function ArchiveAccounts() {
const [q, setQ] = aUseState("");
const [applied, setApplied] = aUseState("");
const [offset, setOffset] = aUseState(0);
const [sel, setSel] = aUseState(null);
const [sort, setSort] = aUseState({ col: "entries", dir: "desc" });
const onSort = (c, d) => { setOffset(0); setSort((s) => nextSort(s, c, d)); };
const key = applied + "|" + offset + "|" + sort.col + sort.dir;
const { data, loading, err } = useArchive(() => window.OfficeAPI.archiveAccounts({ limit: 50, offset, sort: sort.col, dir: sort.dir, ...(applied ? { q: applied } : {}) }), key);
if (sel) return setSel(null)}/>;
return (
{ setOffset(0); setApplied(q); }}>Search}/>
{err && }
Debtor accounts
{loading ? "loading…" : aNum(data?.total) + " accounts · most active first"}
{!loading && data && data.items.length === 0 &&
No accounts match.}
{data && data.items.length > 0 && (
{data.items.map((r) => (
{ const o = new String(r.id); o._name = r.customerName; setSel(o); }}>
{r.accountNumber || "—"}
{r.customerName || "—"}
0 ? "var(--void-text)" : "inherit"}}>{aMoney(r.balanceCents)}
{aNum(r.entries)}
{aDate(r.lastActivity)}
))}
)}
{data &&
}
);
}
function ArchiveStatement({ id, name, onBack }) {
const [from, setFrom] = aUseState("");
const [to, setTo] = aUseState("");
const [applied, setApplied] = aUseState({ from: "", to: "" });
const key = String(id) + "|" + JSON.stringify(applied);
const { data, loading, err } = useArchive(
() => window.OfficeAPI.archiveAccountLedger(String(id), { ...(applied.from ? { from: applied.from } : {}), ...(applied.to ? { to: applied.to } : {}) }),
key,
);
if (loading) return } eyebrow="loading…" title={String(name || "Account")}/> ;
if (err || !data) return } eyebrow="error" title="Account"/> ;
const a = data.account;
return (
} eyebrow={`account ${a.accountNumber || ""}`} title={a.customerName || "Account"}/>
0 ? "owing" : "clear" }}/>
{data.entries.length === 0 &&
No ledger entries in this window.}
{data.entries.length > 0 && (
Date
Type
Narration
Amount
Balance
{data.entries.map((e) => (
{aDate(e.date)}
{AR_TYPE[e.type] || e.type}
{e.narration || "—"}
{aMoney(e.amountCents)}
{aMoney(e.runningBalanceCents)}
))}
)}
);
}
// -----------------------------------------------------------------
// 4) STOCK MOVEMENT HISTORY — full legacy trail incl. S8 controlled-drug
// -----------------------------------------------------------------
const MOVE_TYPES = ["", "sale", "dispense", "receive_invoice", "adjustment", "refund", "transfer", "order"];
function ArchiveStock() {
const { setScreenState, setScreen } = window.useOffice();
const [q, setQ] = aUseState("");
const [type, setType] = aUseState("");
const [applied, setApplied] = aUseState({ q: "", type: "" });
const [offset, setOffset] = aUseState(0);
const [sort, setSort] = aUseState({ col: "date", dir: "desc" });
const onSort = (c, d) => { setOffset(0); setSort((s) => nextSort(s, c, d)); };
const key = JSON.stringify(applied) + "|" + offset + "|" + sort.col + sort.dir;
const { data, loading, err } = useArchive(
() => window.OfficeAPI.archiveStockMovements({ limit: 50, offset, sort: sort.col, dir: sort.dir, ...(applied.q ? { q: applied.q } : {}), ...(applied.type ? { type: applied.type } : {}) }),
key,
);
const openProduct = (pid) => { setScreenState({ activeProductId: pid, productFrom: "stockHistory" }); setScreen("productDetail"); };
return (
>}/>
{err && }
Movements
{loading ? "loading…" : aNum(data?.total) + " in trail · newest first"}
{!loading && data && data.items.length === 0 &&
No movements match.}
{data && data.items.length > 0 && (
On hand
{data.items.map((m) => {
const up = m.quantityDelta >= 0;
return (
{aDateTime(m.occurredAt)}
openProduct(m.productId) : undefined}>
{m.productName || m.sku || "—"}{m.schedule === "S8" ? S8 : null}
{aHuman(m.movementType)}
{up ? "+" : ""}{m.quantityDelta}
{m.quantityBefore}→{m.quantityAfter}
);
})}
)}
{data &&
}
);
}
// -----------------------------------------------------------------
// Register
// -----------------------------------------------------------------
window.OFFICE_SCREENS = Object.assign(window.OFFICE_SCREENS || {}, {
salesHistory: ArchiveSales,
invoices: ArchivePurchasing,
accounts: ArchiveAccounts,
stockHistory: ArchiveStock,
});