/* ============================================================= CoreIQ Office — Customers / patients (this pharmacy) Real, tenant-scoped customer directory served by /customers. Searchable list → click a row for the full 360 profile: contact + clinical (NDSS/HENS) detail, the linked A/R account balance, lifetime spend, and recent purchase history. No mock data. ============================================================= */ const { useState: cuUseState, useEffect: cuUseEffect, useRef: cuUseRef } = React; function CustomersScreen() { const [q, setQ] = cuUseState(""); const [data, setData] = cuUseState({ total: 0, customers: [] }); const [loading, setLoading] = cuUseState(true); const [err, setErr] = cuUseState(null); const [sort, setSort] = cuUseState({ col: "name", dir: "asc" }); const [selId, setSelId] = cuUseState(null); const [detail, setDetail] = cuUseState(null); const [detailLoading, setDetailLoading] = cuUseState(false); const seq = cuUseRef(0); // Debounced live search (empty query = alphabetical browse). cuUseEffect(() => { const s = ++seq.current; setLoading(true); const t = setTimeout(() => { window.OfficeAPI.listCustomers(q.trim(), 60, sort.col, sort.dir) .then((d) => { if (s !== seq.current) return; setData(d); setErr(null); setLoading(false); }) .catch((e) => { if (s !== seq.current) return; setErr(String(e.message || e)); setLoading(false); }); }, 220); return () => clearTimeout(t); }, [q, sort.col, sort.dir]); const open = (id) => { setSelId(id); setDetail(null); setDetailLoading(true); window.OfficeAPI.getCustomer(id) .then((d) => { setDetail(d); setDetailLoading(false); }) .catch((e) => { setErr(String(e.message || e)); setDetailLoading(false); }); }; const money = (c) => (c == null ? "—" : window.officeMoney(c / 100)); const day = (s) => (s ? String(s).slice(0, 10) : "—"); const COLS = [ { label: "Customer", key: "name" }, { label: "Code", key: "code" }, { label: "Contact", key: "contact" }, { label: "Suburb", key: "suburb" }, { label: "Account", key: "balance", num: true }, ]; // Click a header to sort server-side (whole dataset, not just the page). // Same column toggles direction; balance defaults to descending (biggest first). const toggleSort = (key) => setSort((s) => (s.col === key ? { col: key, dir: s.dir === "asc" ? "desc" : "asc" } : { col: key, dir: key === "balance" ? "desc" : "asc" })); const arrow = (key) => (sort.col === key ? (sort.dir === "asc" ? " ↑" : " ↓") : ""); return (
Customers · this pharmacy

Customers

{data.total.toLocaleString()} patient / account records
{/* ---- list (scrolls independently of the detail panel) ---- */}
setQ(e.target.value)} autoFocus />
{loading ? "Searching…" : `${data.customers.length} shown`}
{err &&
Couldn’t reach the customers API — {err}
}
{COLS.map((col) => (
toggleSort(col.key)} title="Sort" style={Object.assign({ cursor: "pointer", userSelect: "none" }, col.num ? { justifyContent: "flex-end" } : {})} > {col.label}{arrow(col.key)}
))} {data.customers.map((c) => (
open(c.id)} style={{ cursor: "pointer", background: c.id === selId ? "var(--surface-2, rgba(0,0,0,0.04))" : undefined }} >
{c.name}{c.hasNdss ? NDSS : null}
{c.code || "—"}
{c.phone || c.email || "—"}
{c.locality || "—"}
{c.accountBalanceCents == null ? "—" : money(c.accountBalanceCents)}
))}
{!loading && data.customers.length === 0 && !err && (
No customers match “{q}”.
)}
{/* ---- 360 detail ---- */} {selId && ( )}
); } window.OFFICE_SCREENS = Object.assign(window.OFFICE_SCREENS || {}, { customers: CustomersScreen, });