/* =============================================================
CoreIQ Office — Stores expansion
Performance comparison · Stock requests · Regions · Procedures
============================================================= */
const { useState: stExtUseState, useMemo: stExtUseMemo } = React;
const STORE_COL = { "S-001": "var(--teal-700)", "S-002": "var(--navy-600)", "S-003": "var(--violet-600)" };
const ST_REGIONS = [
{ id: "rg-east", name: "Eastern Metro", manager: "Suni Kapoor", stores: ["S-001","S-002"], color: "var(--teal-700)" },
{ id: "rg-outer", name: "Outer Eastern", manager: "James Liu", stores: ["S-003"], color: "var(--violet-600)" },
{ id: "rg-north", name: "Northern (proposed)", manager: "—", stores: [], color: "var(--text-subtle)" },
];
const ST_REQUESTS = [
{ id: "REQ-118", from: "S-002", to: "S-001", productId: "p008", qty: 24, reason: "Stock-out — patient waiting on script", requestedBy: "Rachel Pham", ts: "Today 14:18", priority: "urgent", status: "approved", responseTs: "Today 14:22", responder: "Suni Kapoor" },
{ id: "REQ-117", from: "S-003", to: "S-001", productId: "p012", qty: 4, reason: "Cold-chain Insulin Glargine low", requestedBy: "James Liu", ts: "Today 11:48", priority: "urgent", status: "in-transit",responseTs: "Today 11:52", responder: "Suni Kapoor", linkedTransfer: "TR-2208" },
{ id: "REQ-116", from: "S-001", to: "S-003", productId: "p019", qty: 6, reason: "CGM sensor stock-out — flagged patient", requestedBy: "Suni Kapoor", ts: "Today 10:14", priority: "high", status: "approved", responseTs: "Today 10:18", responder: "James Liu" },
{ id: "REQ-115", from: "S-002", to: "S-003", productId: "p014", qty: 12, reason: "Vitamin run-out · promo demand", requestedBy: "Priya Sharma", ts: "Yesterday", priority: "normal", status: "fulfilled", responseTs: "Yesterday", responder: "James Liu", linkedTransfer: "TR-2205" },
{ id: "REQ-114", from: "S-001", to: "S-002", productId: "p016", qty: 24, reason: "Paracetamol low", requestedBy: "Rachel Pham", ts: "20/05/2026", priority: "normal", status: "declined", responseTs: "20/05/2026", responder: "Rachel Pham", declineReason: "Glen Iris stock also low — ordered fresh PO instead" },
{ id: "REQ-113", from: "S-003", to: "S-001", productId: "p002", qty: 30, reason: "Metformin spike post-clinic referral", requestedBy: "James Liu", ts: "19/05/2026", priority: "high", status: "fulfilled", responseTs: "19/05/2026", responder: "Suni Kapoor", linkedTransfer: "TR-2199" },
{ id: "REQ-112", from: "S-002", to: "S-001", productId: "p025", qty: 6, reason: "Probiotic — local promo demand", requestedBy: "Priya Sharma", ts: "18/05/2026", priority: "normal", status: "fulfilled", responseTs: "18/05/2026", responder: "Suni Kapoor" },
];
const ST_CHECKLIST_OPEN = [
{ id: "o1", task: "Unlock storefront and disable alarm", category: "Security", time: "08:25" },
{ id: "o2", task: "Count opening cash float ($200) — two-person sign-off", category: "Cash", time: "08:30" },
{ id: "o3", task: "Open S8 safe and verify register count", category: "S8", time: "08:32" },
{ id: "o4", task: "Power up registers, scanners, label printers", category: "Hardware", time: "08:35" },
{ id: "o5", task: "Test Eftpos terminal (Tyro/Adyen) with $0 ping", category: "Hardware", time: "08:38" },
{ id: "o6", task: "Check cold-chain fridge temperatures (2–8°C)", category: "Compliance", time: "08:40" },
{ id: "o7", task: "Review overnight scripts (eScript queue)", category: "Dispensary", time: "08:42" },
{ id: "o8", task: "Restock front-counter promo items", category: "Merchandising", time: "08:45" },
{ id: "o9", task: "Brief team on today's pickups, counsel due, flags", category: "Team", time: "08:50" },
{ id: "o10", task: "Unlock doors to public · note opening time", category: "Security", time: "08:30" },
];
const ST_CHECKLIST_CLOSE = [
{ id: "c1", task: "Lock doors to public · finalise queue", category: "Security", time: "19:00" },
{ id: "c2", task: "Close active sales, void held sales", category: "POS", time: "19:02" },
{ id: "c3", task: "Run End-of-day · reconcile registers", category: "Cash", time: "19:05" },
{ id: "c4", task: "Count cash, lodge tomorrow's float, drop excess to safe", category: "Cash", time: "19:15" },
{ id: "c5", task: "S8 register countersign — two pharmacists", category: "S8", time: "19:20" },
{ id: "c6", task: "Lock S8 safe", category: "S8", time: "19:22" },
{ id: "c7", task: "Confirm cold-chain temperatures logged", category: "Compliance", time: "19:25" },
{ id: "c8", task: "Submit PBS Online Claiming batch", category: "Compliance", time: "19:30" },
{ id: "c9", task: "Power down non-essential equipment", category: "Hardware", time: "19:35" },
{ id: "c10", task: "Set alarm, lock storefront", category: "Security", time: "19:40" },
];
// =================================================================
// STORES — expanded landing
// =================================================================
function StoresExpanded() {
const { setScreen, setScreenState, pushToast } = window.useOffice();
const [tab, setTab] = stExtUseState("compare");
return (
setTab("list")}>All stores
setTab("compare")}>Performance comparison
setTab("regions")}>Regions & groups
setTab("requests")}>
Stock requests
{ST_REQUESTS.filter(r => r.status === "approved" || r.status === "in-transit").length}
setTab("procedures")}>Procedures
{tab === "list" &&
}
{tab === "compare" &&
}
{tab === "regions" &&
}
{tab === "requests" &&
}
{tab === "procedures" &&
}
);
}
function StoresListInline() {
const { setScreen, setScreenState } = window.useOffice();
return (
Code
Store
Manager
Registers
Patients
Today
MTD
Stock value
Open
{OD.STORES.map(s => (
{s.manager}
{s.registers}
{s.patients.toLocaleString()}
{oMoney(s.takings.today)}
{oMoney(s.takings.mtd)}
{oMoney(s.stockValue)}
{ setScreenState({ activeStoreId: s.id }); setScreen("storeDetail"); }}>Open
))}
);
}
// =================================================================
// PERFORMANCE COMPARISON
// =================================================================
function PerformanceComparison() {
const [period, setPeriod] = stExtUseState("mtd");
const [metric, setMetric] = stExtUseState("revenue");
const METRICS = [
{ id: "revenue", label: "Revenue", val: s => s.takings[period === "today" ? "today" : period === "ytd" ? "ytd" : "mtd"], fmt: v => oMoney(v) },
{ id: "scripts", label: "Scripts dispensed", val: s => s.scripts[period === "today" ? "today" : period === "ytd" ? "ytd" : "mtd"], fmt: v => v.toLocaleString() },
{ id: "patients", label: "Patient base", val: s => s.patients, fmt: v => v.toLocaleString() },
{ id: "stockValue", label: "Stock value", val: s => s.stockValue, fmt: v => oMoney(v) },
{ id: "perRegister", label: "Revenue per register", val: s => s.takings.mtd / s.registers, fmt: v => oMoney(v) },
{ id: "perPatient", label: "Revenue per patient", val: s => s.takings.mtd / s.patients, fmt: v => oMoney(v) },
];
// Per-metric rows with bar visualisation
const rows = METRICS.map(m => ({
metric: m,
values: OD.STORES.map(s => ({ store: s, value: m.val(s) })),
}));
return (
<>
Period
{[["today","Today"],["mtd","Month to date"],["ytd","Year to date"]].map(([k, label]) => (
setPeriod(k)}>{label}
))}
{/* Hero metric: revenue */}
Revenue · {period === "today" ? "today" : period === "ytd" ? "YTD" : "MTD"}
Export
{OD.REVENUE_30D["S-001"].map((_, i) => (
{OD.STORES.map(s => {
const v = OD.REVENUE_30D[s.id][i];
const max = 6500;
return
;
})}
))}
{OD.STORES.map(s => (
{s.name} · {oMoney(s.takings[period === "today" ? "today" : period === "ytd" ? "ytd" : "mtd"])}
))}
{/* Multi-metric comparison */}
Side-by-side · {period === "today" ? "today" : period === "ytd" ? "YTD" : "MTD"}
{rows.map((row, i) => {
const max = Math.max(...row.values.map(v => v.value));
const leader = row.values.find(v => v.value === max);
return (
{row.metric.label}
Leader · {leader.store.name}
{row.values.map(v => {
const pct = max > 0 ? (v.value / max) * 100 : 0;
return (
{v.store.code}
{row.metric.fmt(v.value)}
);
})}
{row.metric.fmt(row.values.reduce((s, v) => s + v.value, 0))}
Group
);
})}
{/* Index vs group average */}
Index vs group average
100 = group avg
Store
Revenue idx
Scripts idx
$/patient idx
$/register idx
{OD.STORES.map(s => {
const avgRev = OD.STORES.reduce((sum, x) => sum + x.takings.mtd, 0) / OD.STORES.length;
const avgScr = OD.STORES.reduce((sum, x) => sum + x.scripts.mtd, 0) / OD.STORES.length;
const avgPp = OD.STORES.reduce((sum, x) => sum + x.takings.mtd / x.patients, 0) / OD.STORES.length;
const avgPr = OD.STORES.reduce((sum, x) => sum + x.takings.mtd / x.registers, 0) / OD.STORES.length;
const idx = (val, avg) => Math.round((val / avg) * 100);
const cell = (v) => ({color: v >= 110 ? 'var(--paid-text)' : v <= 90 ? 'var(--void-text)' : 'var(--text)'});
return (
{idx(s.takings.mtd, avgRev)}
{idx(s.scripts.mtd, avgScr)}
{idx(s.takings.mtd / s.patients, avgPp)}
{idx(s.takings.mtd / s.registers, avgPr)}
);
})}
>
);
}
// =================================================================
// REGIONS
// =================================================================
function Regions() {
const { pushToast } = window.useOffice();
return (
<>
Group stores into regions to scope reports, manager access, and bulk operations. Useful as the network grows past a handful of stores.
pushToast({kind:'paid', icon:'plus', title:'New region', meta:'Wizard would open here'})}>
New region
{ST_REGIONS.map(r => {
const stores = r.stores.map(id => OD.STORES.find(s => s.id === id)).filter(Boolean);
const rev = stores.reduce((s, x) => s + x.takings.mtd, 0);
const scripts = stores.reduce((s, x) => s + x.scripts.mtd, 0);
const patients = stores.reduce((s, x) => s + x.patients, 0);
return (
{r.name}
{stores.length === 0
?
Empty
:
{stores.length} {stores.length === 1 ? "store" : "stores"}}
Manager · {r.manager}
Edit
{stores.length > 0 ? (
<>
Revenue MTD
{oMoney(rev)}
Scripts MTD
{scripts.toLocaleString()}
Patient base
{patients.toLocaleString()}
Stores
{stores.map(s => (
{s.code} · {s.name}
))}
>
) : (
No stores assigned yet — drag a store here, or assign from the store detail.
)}
);
})}
>
);
}
// =================================================================
// STOCK REQUESTS
// =================================================================
function StockRequests() {
const { pushToast } = window.useOffice();
const [filter, setFilter] = stExtUseState("active");
const [creating, setCreating] = stExtUseState(false);
const FILTERS = [
{ key: "active", label: "Active", pred: r => ["approved","in-transit","pending"].includes(r.status) },
{ key: "pending", label: "Awaiting response", pred: r => r.status === "pending" },
{ key: "fulfilled", label: "Fulfilled", pred: r => r.status === "fulfilled" },
{ key: "declined", label: "Declined", pred: r => r.status === "declined" },
{ key: "all", label: "All", pred: () => true },
];
const list = ST_REQUESTS.filter(FILTERS.find(f => f.key === filter).pred);
return (
<>
Stock requests are how a store asks another for stock — they need approval from the source store before becoming a transfer.
Use these for unplanned needs (stock-out, patient waiting). For routine movements, create a transfer directly.
setCreating(true)}>
New request
["approved","in-transit","pending"].includes(r.status)).length} delta={{dir:"up", text:"Awaiting fulfillment"}}/>
r.priority === "urgent" && r.ts.includes("Today")).length} delta={{dir:"down", text:"Inter-store stock-outs"}}/>
{FILTERS.map(f => {
const c = ST_REQUESTS.filter(f.pred).length;
return (
setFilter(f.key)}>
{f.label} {c}
);
})}
ID
From
To
Product · reason
Qty
Priority
Requested
Response
Status
{list.map(r => {
const product = findP(r.productId);
const from = findS(r.from);
const to = findS(r.to);
return (
{r.id}
{from?.code}
{to?.code}
{product?.name} · {product?.strength}
{r.reason}
{r.declineReason &&
Declined · {r.declineReason}
}
{r.linkedTransfer &&
Linked to transfer {r.linkedTransfer}
}
{r.qty}
{r.priority === "urgent" && Urgent }
{r.priority === "high" && High }
{r.priority === "normal" && Normal }
{r.ts}
{r.requestedBy}
{r.responseTs}
{r.responder}
{r.status === "pending" && Awaiting }
{r.status === "approved" && Approved }
{r.status === "in-transit" && In transit }
{r.status === "fulfilled" && Fulfilled }
{r.status === "declined" && Declined }
);
})}
{creating && setCreating(false)} onSubmit={() => { setCreating(false); pushToast({kind:'paid', icon:'check-circle', title:'Stock request sent', meta:'Source store will respond'}); }}/>}
>
);
}
function StockRequestModal({ onClose, onSubmit }) {
const [from, setFrom] = stExtUseState("S-001");
const [to, setTo] = stExtUseState("S-002");
const [productId, setProductId] = stExtUseState("p008");
const [qty, setQty] = stExtUseState(12);
const [priority, setPriority] = stExtUseState("normal");
const [reason, setReason] = stExtUseState("");
return (
e.stopPropagation()}>
Request stock from another store
Request from
setFrom(e.target.value)}>
{OD.STORES.map(s => {s.name} )}
→
Deliver to
setTo(e.target.value)}>
{OD.STORES.filter(s => s.id !== from).map(s => {s.name} )}
Product
setProductId(e.target.value)}>
{OD.PRODUCTS.map(p => {p.name} · {p.strength} )}
Quantity
setQty(parseInt(e.target.value) || 0)}/>
Priority
setPriority(e.target.value)}>
Normal
High
Urgent · patient waiting
Reason
Cancel
Send request
);
}
// =================================================================
// PROCEDURES — opening / closing checklists
// =================================================================
function Procedures() {
const [side, setSide] = stExtUseState("open");
const list = side === "open" ? ST_CHECKLIST_OPEN : ST_CHECKLIST_CLOSE;
// Local checkmarks
const [checked, setChecked] = stExtUseState(new Set([side === "open" ? "o1" : "c1"]));
const toggle = (id) => setChecked(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
React.useEffect(() => setChecked(new Set([side === "open" ? "o1" : "c1"])), [side]);
const grouped = list.reduce((acc, t) => { (acc[t.category] = acc[t.category] || []).push(t); return acc; }, {});
const progress = Math.round((checked.size / list.length) * 100);
return (
<>
Standard operating procedures for opening & closing. Edit the master here — each store sees this on the POS sign-in & close-register screen. Completion is logged to the audit trail.
Audit log
Edit master
setSide("open")}>
Opening procedure {ST_CHECKLIST_OPEN.length}
setSide("close")}>
Closing procedure {ST_CHECKLIST_CLOSE.length}
{Object.entries(grouped).map(([category, tasks]) => (
{category}
{tasks.filter(t => checked.has(t.id)).length}/{tasks.length} done
{tasks.map(t => {
const done = checked.has(t.id);
return (
toggle(t.id)}/>
{t.task}
{t.time}
);
})}
))}
Progress
{checked.size}/{list.length}
{progress}% complete · est. {side === "open" ? "25 minutes" : "40 minutes"}
Applies to
{OD.STORES.map(s => (
))}
Customise per store
Recent runs
{[
{ store: "S-001", who: "Suni K.", ts: "Today 08:30", dur: "22 min" },
{ store: "S-002", who: "Rachel P.", ts: "Today 08:30", dur: "18 min" },
{ store: "S-003", who: "James L.", ts: "Today 09:00", dur: "24 min" },
{ store: "S-001", who: "Suni K.", ts: "Yesterday 19:40", dur: "38 min" },
].map((r, i) => (
{findS(r.store)?.code} · {r.who}
{r.ts}
{r.dur}
))}
>
);
}
// Overwrite the Stores screen registration
window.OFFICE_SCREENS = Object.assign(window.OFFICE_SCREENS || {}, {
stores: StoresExpanded,
});