/* =============================================================
CoreIQ Office — App shell, sign-in, state
============================================================= */
const { useState: oUseState, useEffect: oUseEffect, useMemo: oUseMemo, useCallback: oUseCallback, useRef: oUseRef, createContext: oCC, useContext: oUC } = React;
const OD = window.OFFICE_DATA;
const oMoney = window.officeMoney;
const findP = window.findOfficeProduct;
const findS = window.findOfficeStore;
const findSup = window.findOfficeSupplier;
const findU = window.findOfficeUser;
// -----------------------------------------------------------------
// Office context — shared state
// -----------------------------------------------------------------
const OfficeContext = oCC(null);
window.useOffice = () => oUC(OfficeContext);
function OfficeProvider({ children }) {
// Sign-in — persisted so switching store (which reloads the page) doesn't log you out.
// In Cognito mode the session is only valid while the stored ID token is unexpired;
// an expired/missing token forces a fresh sign-in.
const [signedIn, setSignedIn] = oUseState(() => {
try {
if (window.OFFICE_COGNITO) {
const t = sessionStorage.getItem("coreiq-office-idtoken");
if (!t) return false;
const body = JSON.parse(atob(t.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
if (!body.exp || body.exp * 1000 < Date.now() + 60000) return false;
window.OfficeAPI.setToken(t);
return true;
}
return localStorage.getItem("coreiq-office-signedin") === "1";
} catch (_) { return false; }
});
oUseEffect(() => {
try { localStorage.setItem("coreiq-office-signedin", signedIn ? "1" : "0"); } catch (_) {}
}, [signedIn]);
const [activeUser, setActiveUser] = oUseState(OD.USERS[0]);
// Multi-store (group) switcher: every store the owner controls ACROSS their orgs,
// loaded LIVE from /stores. Each store is one (org, site). The active store sets
// which org/site all screens query (sent as headers; persisted across the reload
// we do on switch). Empty until loaded.
const [stores, setStores] = oUseState([]);
const [storesLoading, setStoresLoading] = oUseState(true);
const [orgError, setOrgError] = oUseState(null);
const [activeStoreId, setActiveStoreId] = oUseState(null); // the active store's site id
oUseEffect(() => {
let alive = true;
window.OfficeAPI.listStores()
.then(res => {
if (!alive) return;
const list = (res.stores || []).map(s => ({ id: s.siteId, orgId: s.orgId, code: s.code, name: s.siteName, orgName: s.orgName }));
setStores(list);
// Reconcile the persisted store against the LIVE list. The saved org can go
// stale (e.g. a re-migration moved a site under a different org), and since
// every request is scoped by that org, a stale org silently returns ZEROS
// across the whole dashboard. Match by site (authoritative), else the org the
// API resolved to, else the first store — then re-persist the CURRENT org so
// the headers hit the right tenant.
const active = window.OfficeAPI.getActiveStore();
const chosen =
list.find(s => s.id === active.siteId) ||
list.find(s => s.orgId === res.defaultOrgId) ||
list[0] || null;
if (chosen && (active.orgId !== chosen.orgId || active.siteId !== chosen.id)) {
window.OfficeAPI.setActiveStore(chosen.orgId, chosen.id);
// Reload once so every screen re-queries the corrected org (same mechanism
// as switchStore). Guard against a reload loop if it can't converge.
if (!sessionStorage.getItem("coreiq-store-reconciled")) {
sessionStorage.setItem("coreiq-store-reconciled", "1");
window.location.reload();
return;
}
}
setActiveStoreId(chosen ? chosen.id : null);
setStoresLoading(false);
})
.catch(err => { if (alive) { setOrgError(String(err && err.message || err)); setStoresLoading(false); } });
return () => { alive = false; };
}, []);
// Switch the active store → persist + reload so every screen re-queries that org.
const switchStore = oUseCallback((store) => {
if (!store || store.id === activeStoreId) return;
window.OfficeAPI.setActiveStore(store.orgId, store.id);
window.location.reload();
}, [activeStoreId]);
// Active store as the "scope" the rest of the shell already understands.
const scope = activeStoreId || "all";
const setScope = () => {}; // legacy no-op; switching goes through switchStore
const scopedStores = stores.filter(s => s.id === activeStoreId);
const scopedStoreIds = scopedStores.map(s => s.id);
// Navigation
const [screen, setScreen] = oUseState("dashboard");
const [screenState, setScreenState] = oUseState({});
// Toasts
const [toasts, setToasts] = oUseState([]);
const pushToast = oUseCallback((t) => {
const id = "t" + Date.now() + Math.random();
setToasts(ts => [...ts, { ...t, id }]);
setTimeout(() => setToasts(ts => ts.filter(x => x.id !== id)), 4200);
}, []);
const dismissToast = (id) => setToasts(ts => ts.filter(t => t.id !== id));
// Right-side drawer (used by store-switcher etc)
const [drawer, setDrawer] = oUseState(null); // null | "stores"
const value = {
signedIn, setSignedIn,
activeUser, setActiveUser,
scope, setScope, scopedStores, scopedStoreIds,
activeStoreId, switchStore,
stores, storesLoading, orgError,
screen, setScreen, screenState, setScreenState,
toasts, pushToast, dismissToast,
drawer, setDrawer,
};
return {children};
}
// -----------------------------------------------------------------
// Icon helper — defaults 16×16
// -----------------------------------------------------------------
function OIcon({ name, width = 16, height = 16, ...rest }) {
return ;
}
window.OIcon = OIcon;
// -----------------------------------------------------------------
// Nav definitions
// -----------------------------------------------------------------
// MVP menu — only the features wired to the live Office API are shown. The rest
// of the back-office (Stores, Buying, Customer, Clinical, Government, Staff,
// Finance, Reports, Operations, and the Admin extras) is built in the prototype
// but NOT yet wired to the API, so it is hidden until each vertical is wired.
// The complete menu is preserved as OFF_NAV_FUTURE below for phased re-enable.
const OFF_NAV = [
{ section: "Overview", items: [
{ key: "dashboard", label: "Dashboard", icon: "dashboard" },
{ key: "customers", label: "Customers", icon: "users" },
]},
{ section: "Catalogue & stock", items: [
{ key: "catalogue", label: "Product catalogue", icon: "pill" },
{ key: "stockOverview", label: "Stock overview", icon: "stock" },
{ key: "manageStock", label: "Manage stock", icon: "list" },
{ key: "newStock", label: "New stock line", icon: "plus" },
{ key: "suppliers", label: "Suppliers", icon: "truck" },
]},
{ section: "History", items: [
{ key: "salesHistory", label: "Sales history", icon: "doc" },
{ key: "invoices", label: "Wholesaler invoices", icon: "doc" },
{ key: "accounts", label: "Account statements", icon: "card" },
{ key: "stockHistory", label: "Stock movements", icon: "transfer" },
]},
{ section: "Analytics", items: [
{ key: "salesAnalytics", label: "Sales analytics", icon: "chart" },
{ key: "inventoryAnalytics", label: "Inventory intelligence", icon: "stock" },
{ key: "financialAnalytics", label: "Financial analytics", icon: "card" },
{ key: "pricePosition", label: "Price position", icon: "tag" },
{ key: "shortageRadar", label: "Supply & shortages", icon: "truck" },
]},
{ section: "Administration", items: [
{ key: "storeSetup", label: "Store setup", icon: "settings" },
]},
];
// Full back-office menu — future work, re-enable per vertical once wired.
const OFF_NAV_FUTURE = [
{ section: "Overview", items: [
{ key: "dashboard", label: "Dashboard", icon: "dashboard" },
]},
{ section: "Stock", items: [
{ key: "stockOverview", label: "Stock overview", icon: "stock" },
{ key: "manageStock", label: "Manage stock", icon: "list" },
{ key: "newStock", label: "New stock line", icon: "plus" },
{ key: "labels", label: "Print labels", icon: "printer" },
{ key: "esls", label: "Electronic shelf labels", icon: "tag" },
{ key: "stocktake", label: "Stocktake", icon: "clipboard" },
{ key: "planner", label: "Floor & shelf planner", icon: "shelf" },
{ key: "shelfMarket", label: "Shelf marketplace", icon: "tag" },
]},
{ section: "Stores", items: [
{ key: "stores", label: "Stores", icon: "store" },
{ key: "transfers", label: "Stock transfers", icon: "transfer" },
]},
{ section: "Buying", items: [
{ key: "purchaseOrders", label: "Purchase orders", icon: "doc" },
{ key: "receiving", label: "Receiving", icon: "download" },
{ key: "ordering", label: "Ordering channels", icon: "truck" },
{ key: "priceLists", label: "Price lists", icon: "list" },
{ key: "backorders", label: "Backorders", icon: "clock" },
{ key: "buyingGroups", label: "Groups & formulary", icon: "users" },
{ key: "ediStatus", label: "EDI status", icon: "transfer" },
{ key: "invoices", label: "Wholesaler invoices", icon: "doc" },
]},
{ section: "Customer", items: [
{ key: "customerProfiles", label: "Customer profiles", icon: "users" },
{ key: "scriptsOnFile", label: "Scripts on file", icon: "rx" },
{ key: "commsPrefs", label: "Consent & comms", icon: "phone" },
{ key: "accounts", label: "Payment accounts", icon: "card" },
{ key: "promotions", label: "Promotions", icon: "tag" },
{ key: "loyalty", label: "Loyalty", icon: "loyalty" },
{ key: "vouchers", label: "Corporate vouchers", icon: "loyalty" },
]},
{ section: "Clinical", items: [
{ key: "ironbark", label: "Ironbark · AI", icon: "pill" },
{ key: "vaccinations", label: "Vaccinations · AIR", icon: "check-circle" },
{ key: "servicesLog", label: "MedsCheck & services", icon: "doc" },
{ key: "medProfiles", label: "Patient med profiles", icon: "user" },
]},
{ section: "Government programs", items: [
{ key: "govPrograms", label: "Federal · State · PPA", icon: "doc" },
]},
{ section: "Staff", items: [
{ key: "staff", label: "Profiles, roster, training", icon: "users" },
]},
{ section: "Finance", items: [
{ key: "payments", label: "Payments · Adyen", icon: "card" },
{ key: "pbsClaims", label: "PBS claims", icon: "doc" },
{ key: "eodRecon", label: "End of day", icon: "clock" },
{ key: "invoiceMatching",label: "Invoice matching", icon: "transfer" },
{ key: "pnl", label: "P&L by store", icon: "chart" },
{ key: "tax", label: "Tax & BAS", icon: "doc" },
]},
{ section: "Reports", items: [
{ key: "reports", label: "Reports", icon: "chart" },
{ key: "schemas", label: "Report columns", icon: "columns" },
]},
{ section: "Operations", items: [
{ key: "operations", label: "Incidents & SLA", icon: "alert" },
]},
{ section: "Admin", items: [
{ key: "users", label: "Users & roles", icon: "users" },
{ key: "suppliers", label: "Suppliers", icon: "truck" },
{ key: "auditLogs", label: "Audit logs", icon: "doc" },
{ key: "integrations", label: "Integrations", icon: "transfer" },
{ key: "settings", label: "Settings", icon: "settings" },
]},
];
// -----------------------------------------------------------------
// Sign-in
// -----------------------------------------------------------------
function SignIn() {
const { setSignedIn, setActiveUser } = window.useOffice();
const [email, setEmail] = oUseState(window.OFFICE_COGNITO ? "" : "suni.k@coreiq.au");
const [password, setPassword] = oUseState(window.OFFICE_COGNITO ? "" : "•••••••••");
const [error, setError] = oUseState(null);
const [loading, setLoading] = oUseState(false);
// Real Cognito sign-in when the page carries pool config (production);
// the demo-user path remains for local dev where no pool is configured.
async function cognitoAuth() {
const cfg = window.OFFICE_COGNITO;
const res = await fetch(`https://cognito-idp.${cfg.region}.amazonaws.com/`, {
method: "POST",
headers: {
"Content-Type": "application/x-amz-json-1.1",
"X-Amz-Target": "AWSCognitoIdentityProviderService.InitiateAuth",
},
body: JSON.stringify({
ClientId: cfg.clientId,
AuthFlow: "USER_PASSWORD_AUTH",
AuthParameters: { USERNAME: email, PASSWORD: password },
}),
});
const data = await res.json();
if (!res.ok || !data.AuthenticationResult || !data.AuthenticationResult.IdToken) {
throw new Error(data.message || "Sign-in failed — check your email and password.");
}
const idToken = data.AuthenticationResult.IdToken;
window.OfficeAPI.setToken(idToken);
try { sessionStorage.setItem("coreiq-office-idtoken", idToken); } catch (_) {}
const first = (email.split("@")[0].split(/[._-]/)[0] || "User");
return {
id: "cognito", email,
first: first[0].toUpperCase() + first.slice(1), last: "",
role: "Owner", status: "active",
};
}
function tryAuth(e) {
e.preventDefault();
setError(null);
if (window.OFFICE_COGNITO) {
setLoading(true);
cognitoAuth()
.then((user) => { setActiveUser(user); setSignedIn(true); setLoading(false); })
.catch((err) => { setError(String(err.message || err)); setLoading(false); });
return;
}
const user = OD.USERS.find(u => u.email === email && u.status === "active");
if (!user) {
setError("No account matches that email — or it's been suspended.");
return;
}
setLoading(true);
setTimeout(() => {
setActiveUser(user);
setSignedIn(true);
setLoading(false);
}, 350);
}
function pickDemoUser(u) {
setEmail(u.email);
setError(null);
}
return (
C
CoreIQ
Office · Admin web
Sign in to Office
Multi-store admin, stock and reporting.
);
}
// -----------------------------------------------------------------
// Sidebar
// -----------------------------------------------------------------
function OffSidebar() {
const { screen, setScreen, activeUser, setSignedIn, setDrawer } = window.useOffice();
const badgeFor = (key) => {
switch (key) {
case "labels": return OD.LABEL_QUEUE.filter(l => l.status === "queued").length;
case "stocktake": return OD.STOCKTAKES.filter(s => s.status === "in-progress").length;
case "transfers": return OD.TRANSFERS.filter(t => t.status === "pending" || t.status === "in-transit").length;
case "accounts": return null;
case "promotions": return null;
case "users": return OD.USERS.filter(u => u.status === "pending").length;
default: return null;
}
};
return (
);
}
// -----------------------------------------------------------------
// Data-source badge — tells you, per screen, whether what you're looking at is
// REAL (this org's live Aurora data), real-but-shared (the national reference
// catalogue), or still mock prototype data. Keyed by screen so it can never
// drift from the truth: a screen is only "live" once it's listed here.
// -----------------------------------------------------------------
const SCREEN_SOURCE = {
dashboard: "live-org", // real org KPIs from /overview
stockOverview: "live-org", // real org inventory from /stock + /overview
manageStock: "live-org", // real products; edits PATCH to Aurora → POS
suppliers: "live-org", // real suppliers from /suppliers
supplierDetail: "live-org", // real supplier profile + its products
productDetail: "live-org", // full real product page; edits PATCH to Aurora → POS
catalogue: "live-ref", // real, but the NATIONAL shared reference catalogue (not this org's products)
// This pharmacy's own real data — to the user it's a seamless continuation, not an "archive".
salesHistory: "live-org",
invoices: "live-org",
accounts: "live-org",
stockHistory: "live-org",
salesAnalytics: "live-org",
inventoryAnalytics: "live-org",
financialAnalytics: "live-org",
pricePosition: "live-org",
shortageRadar: "live-org",
};
const DATA_BADGE = {
"live-org": { text: "LIVE · this pharmacy", bg: "var(--paid-bg)", fg: "var(--paid-text)", dot: "var(--paid)",
title: "Real data for the signed-in organisation, queried live from Aurora." },
"live-ref": { text: "LIVE · national reference", bg: "var(--navy-100)", fg: "var(--navy-800)", dot: "var(--navy-600)",
title: "Real data, but the shared national ARTG/PBS reference catalogue — not this organisation's own products." },
"live-archive": { text: "LIVE · legacy archive", bg: "var(--hold-bg)", fg: "var(--hold-text)", dot: "var(--hold)",
title: "Real retained data from the legacy Z system (7-year retention archive), queried live from Aurora. Read-only history." },
"demo": { text: "DEMO DATA", bg: "var(--hold-bg)", fg: "var(--hold-text)", dot: "var(--hold)",
title: "Mock prototype data. NOT real — this screen is not wired to the database yet." },
};
function DataBadge({ screen }) {
const cfg = DATA_BADGE[SCREEN_SOURCE[screen] || "demo"];
return (
{cfg.text}
);
}
// -----------------------------------------------------------------
// Topbar
// -----------------------------------------------------------------
function OffTopbar() {
const { scope, stores, storesLoading, screen, setDrawer, drawer } = window.useOffice();
const cur = stores.find(s => s.id === scope);
const label = storesLoading ? "Loading stores…" : (cur ? cur.name : "Select store");
const avLabel = cur?.code || "··";
return (
⌘ K
);
}
// -----------------------------------------------------------------
// Store switcher drawer
// -----------------------------------------------------------------
function StoreDrawer() {
const { scope, switchStore, setDrawer, stores, storesLoading, orgError } = window.useOffice();
return (
Your stores · live from Aurora
Switch store
{storesLoading &&
Loading stores…
}
{orgError &&
Couldn't load stores: {orgError}
}
{!storesLoading && !orgError && stores.length === 0 &&
No stores found.
}
{stores.map(s => (
{ setDrawer(null); switchStore(s); }}
>
{s.code || "··"}
{s.name}
{s.orgName} · live
{scope === s.id &&
}
))}
);
}
// -----------------------------------------------------------------
// Toast stack
// -----------------------------------------------------------------
function OffToastStack() {
const { toasts, dismissToast } = window.useOffice();
return (
{toasts.map(t => (
{t.title}
{t.meta ?
{t.meta}
: null}
))}
);
}
// -----------------------------------------------------------------
// Screen host
// -----------------------------------------------------------------
function OffScreenHost() {
const { screen } = window.useOffice();
const Comp = (window.OFFICE_SCREENS || {})[screen];
if (!Comp) {
return (
Screen not registered yet
);
}
return ;
}
// -----------------------------------------------------------------
// App entry
// -----------------------------------------------------------------
function OfficeApp() {
const [theme, setTheme] = oUseState(() => {
try { return localStorage.getItem("coreiq-office-theme") || "light"; } catch (_) { return "light"; }
});
oUseEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
try { localStorage.setItem("coreiq-office-theme", theme); } catch (_) {}
}, [theme]);
return (
);
}
function OfficeAppInner({ theme, setTheme }) {
const { signedIn, drawer } = window.useOffice();
if (!signedIn) return ;
return (
<>
{drawer === "stores" && }
>
);
}
// Mount
(function mount() {
const node = document.getElementById("office-root");
if (!node) { window.addEventListener("DOMContentLoaded", mount); return; }
const root = ReactDOM.createRoot(node);
root.render();
})();