/* =============================================================
CoreIQ Office — Head-office modules:
Payment accounts · Promotions · Loyalty
============================================================= */
const { useState: hoUseState, useMemo: hoUseMemo } = React;
// -----------------------------------------------------------------
// Sample data for accounts, promos, loyalty
// -----------------------------------------------------------------
const HO_ACCOUNTS = [
{ id: "ACC-1042", type: "business", name: "Camberwell Aged Care Facility", abn: "49 122 884 207", store: "S-001", terms: "Net 30", creditLimit: 15000, balance: 8420.65, status: "active", overdueDays: 0, monthlyAvg: 9840, contact: "Joanne Pereira", authorised: 24, lastCharge: "Today 14:12" },
{ id: "ACC-1108", type: "business", name: "Whitehorse Family Medical Centre", abn: "62 008 117 003", store: "S-001", terms: "Net 14", creditLimit: 5000, balance: 1280.50, status: "active", overdueDays: 0, monthlyAvg: 2240, contact: "Dr S. Kapoor", authorised: 0, lastCharge: "Yesterday" },
{ id: "ACC-1214", type: "business", name: "Bridgewater NDIS Services", abn: "98 642 119 220", store: "S-002", terms: "Net 30", creditLimit: 10000, balance: 4152.00, status: "active", overdueDays: 0, monthlyAvg: 4200, contact: "Marta Voss", authorised: 7, lastCharge: "Today 11:30" },
{ id: "ACC-1018", type: "person", name: "Whitlock, Henry", abn: "—", store: "S-001", terms: "Monthly", creditLimit: 1500, balance: 890.40, status: "overdue", overdueDays: 12, monthlyAvg: 410, contact: "Henry Whitlock", authorised: 1, lastCharge: "3 days ago" },
{ id: "ACC-1320", type: "person", name: "Costa, Ana", abn: "—", store: "S-003", terms: "Net 30", creditLimit: 300, balance: 145.20, status: "active", overdueDays: 0, monthlyAvg: 160, contact: "Ana Costa", authorised: 1, lastCharge: "Yesterday" },
{ id: "ACC-1190", type: "business", name: "Burke Road Community House", abn: "11 220 401 558", store: "S-001", terms: "Net 30", creditLimit: 2000, balance: 1980.00, status: "over-limit", overdueDays: 0, monthlyAvg: 1840, contact: "Linh Tran", authorised: 8, lastCharge: "Today 13:02" },
{ id: "ACC-1488", type: "business", name: "Yarra River Disability Support", abn: "76 119 882 044", store: "S-002", terms: "Net 30", creditLimit: 4000, balance: 0, status: "hold", overdueDays: 0, monthlyAvg: 0, contact: "Aleksei Markov", authorised: 0, lastCharge: "—" },
{ id: "ACC-1611", type: "business", name: "Eastland Medical Suites", abn: "33 991 002 117", store: "S-003", terms: "Net 14", creditLimit: 7500, balance: 3210.40, status: "active", overdueDays: 0, monthlyAvg: 3680, contact: "Dr R. Patel", authorised: 12, lastCharge: "Today 10:48" },
{ id: "ACC-1612", type: "business", name: "Glen Iris Day Centre", abn: "55 880 117 022", store: "S-002", terms: "Net 30", creditLimit: 6000, balance: 5418.20, status: "active", overdueDays: 0, monthlyAvg: 5240, contact: "Helen Yeo", authorised: 14, lastCharge: "Today 11:48" },
{ id: "ACC-1718", type: "person", name: "Sharma, Priya", abn: "—", store: "S-001", terms: "Net 30", creditLimit: 500, balance: 88.10, status: "active", overdueDays: 0, monthlyAvg: 120, contact: "Priya Sharma", authorised: 1, lastCharge: "Yesterday" },
];
const HO_PROMOS = [
{ id: "PR-2026-001", name: "Summer Saver · Skincare", type: "percent", value: 30, target: "category", targetLabel: "Skincare", stores: ["S-001","S-002","S-003"], starts: "20/05/2026", ends: "31/05/2026", redemptions: 142, revenue: 2480.40, status: "active", priority: 1, requiresLoyalty: false },
{ id: "PR-2026-002", name: "Loyalty $5 off · Spend > $30", type: "amount", value: 5, target: "minSpend", targetLabel: "Spend ≥ $30", stores: ["S-001","S-002","S-003"], starts: "01/05/2026", ends: "30/06/2026", redemptions: 280, revenue: 1400.00, status: "active", priority: 2, requiresLoyalty: true },
{ id: "PR-2026-003", name: "Vitamins · Buy 2 save $4", type: "bogo", value: 4, target: "category", targetLabel: "Vitamins · 2+", stores: ["S-001","S-002","S-003"], starts: "01/05/2026", ends: "31/05/2026", redemptions: 88, revenue: 1742.30, status: "active", priority: 3, requiresLoyalty: false },
{ id: "PR-2026-004", name: "Hayfever bundle", type: "amount", value: 3, target: "bundle", targetLabel: "Loratadine + Cetirizine", stores: ["S-001","S-002"], starts: "12/05/2026", ends: "10/06/2026", redemptions: 24, revenue: 412.20, status: "active", priority: 4, requiresLoyalty: false },
{ id: "PR-2026-005", name: "Mother's Day skincare", type: "percent", value: 20, target: "category", targetLabel: "Skincare", stores: ["S-001","S-002","S-003"], starts: "01/05/2026", ends: "12/05/2026", redemptions: 312, revenue: 6420.80, status: "ended", priority: 1, requiresLoyalty: false },
{ id: "PR-2026-006", name: "Winter Wellness · Coming", type: "percent", value: 15, target: "category", targetLabel: "Cold & flu", stores: ["S-001","S-002","S-003"], starts: "01/06/2026", ends: "31/07/2026", redemptions: 0, revenue: 0, status: "scheduled", priority: 1, requiresLoyalty: false },
{ id: "PR-2026-007", name: "NDSS pen-needle saver", type: "amount", value: 1, target: "product", targetLabel: "BD Pen Needles", stores: ["S-001","S-002","S-003"], starts: "01/04/2026", ends: "31/12/2026", redemptions: 188, revenue: 188.00, status: "active", priority: 5, requiresLoyalty: false },
{ id: "PR-2026-008", name: "Camberwell only · Spacers", type: "percent", value: 10, target: "product", targetLabel: "Salbutamol Spacer", stores: ["S-001"], starts: "15/05/2026", ends: "31/05/2026", redemptions: 12, revenue: 168.00, status: "active", priority: 6, requiresLoyalty: false },
];
const HO_LOYALTY_TIERS = [
{ id: "bronze", name: "Bronze", members: 1842, color: "#a0683e", spendThreshold: 0, discount: 2, perks: ["2% off non-PBS", "Birthday treat"] },
{ id: "silver", name: "Silver", members: 642, color: "#7d9097", spendThreshold: 400, discount: 5, perks: ["5% off non-PBS", "Free delivery on $50+", "Birthday treat"] },
{ id: "gold", name: "Gold", members: 118, color: "#b8893e", spendThreshold: 1200, discount: 5, perks: ["5% off non-PBS", "Free delivery", "Priority counsel", "Bonus 2× points"] },
];
const HO_LOYALTY_KPI = {
totalMembers: 2602,
newThisMonth: 184,
activeRate: 0.62,
avgBasketLift: 14,
monthlyRedemption: 2840,
};
// =================================================================
// PAYMENT ACCOUNTS — list
// =================================================================
function OfficeAccounts() {
const { setScreen, setScreenState, pushToast } = window.useOffice();
const [filter, setFilter] = hoUseState("all");
const [q, setQ] = hoUseState("");
const FILTERS = [
{ key: "all", label: "All", pred: () => true },
{ key: "business", label: "Business", pred: a => a.type === "business" },
{ key: "person", label: "Person", pred: a => a.type === "person" },
{ key: "overdue", label: "Overdue", pred: a => a.status === "overdue" },
{ key: "over-limit", label: "Over limit", pred: a => a.status === "over-limit" },
{ key: "hold", label: "On hold", pred: a => a.status === "hold" },
];
const list = HO_ACCOUNTS.filter(a => {
if (!FILTERS.find(f => f.key === filter).pred(a)) return false;
if (!q) return true;
return (a.name + " " + a.id + " " + (a.abn || "")).toLowerCase().includes(q.toLowerCase());
});
const totals = {
receivable: HO_ACCOUNTS.reduce((s, a) => s + a.balance, 0),
overdue: HO_ACCOUNTS.filter(a => a.status === "overdue").reduce((s, a) => s + a.balance, 0),
overLimit: HO_ACCOUNTS.filter(a => a.status === "over-limit").length,
business: HO_ACCOUNTS.filter(a => a.type === "business").length,
person: HO_ACCOUNTS.filter(a => a.type === "person").length,
};
return (
Head-office function
Payment accounts
Statement run
Aged receivables
pushToast({kind:'sched', icon:'plus', title:'New account', meta:'Wizard would open here'})}>
New account
setQ(e.target.value)} style={{height:34, fontSize:13}}/>
{FILTERS.map(f => {
const c = HO_ACCOUNTS.filter(f.pred).length;
return (
setFilter(f.key)}>
{f.label} {c}
);
})}
{list.length} of {HO_ACCOUNTS.length}
Account
Holder
Type
Home store
Terms
Available · Limit
Balance
Status
{list.map(a => {
const used = a.creditLimit > 0 ? Math.min(1, a.balance / a.creditLimit) : 0;
const usedPct = Math.round(used * 100);
const store = findS(a.store);
return (
{ setScreenState({ activeAccountId: a.id }); setScreen("accountDetail"); }}>
{a.id}
{a.name}
{a.type === "business" ? `ABN ${a.abn}` : a.contact}
{a.type === "business" ? "Business" : "Person"}
{store?.code} · {store?.name}
{a.terms}
{oMoney(Math.max(0, a.creditLimit - a.balance))} of {oMoney(a.creditLimit)}
95 ? " inv-stockbar--bad" : usedPct > 75 ? " inv-stockbar--warn" : "")}>
{oMoney(a.balance)}
{a.status === "active" && Active }
{a.status === "overdue" && Overdue · {a.overdueDays}d }
{a.status === "over-limit" && Over limit }
{a.status === "hold" && On hold }
);
})}
);
}
// =================================================================
// PAYMENT ACCOUNT — detail (compact admin view)
// =================================================================
function OfficeAccountDetail() {
const { setScreen, screenState, pushToast } = window.useOffice();
const a = HO_ACCOUNTS.find(x => x.id === screenState.activeAccountId) || HO_ACCOUNTS[0];
const usedPct = a.creditLimit > 0 ? Math.round((a.balance / a.creditLimit) * 100) : 0;
return (
Account · {a.id}
{a.name}
setScreen("accounts")}>
Accounts
Statement
pushToast({kind:'paid', icon:'check-circle', title:`Payment recorded · ${a.name}`})}>
Receive payment
95 ? "down" : "up", text: usedPct > 95 ? "Over limit" : "Within terms"}}/>
Recent activity
View full ledger
When
Type
Description
By
Amount
Balance
{[
{ ts:"Today 14:12", kind:"charge", who:"SK", desc:"Metformin XR · M. Tan (room 14)", amt: 31.40, bal: a.balance },
{ ts:"Today 11:48", kind:"charge", who:"RP", desc:"Dispense ×3 · Ana Costa (room 09)", amt: 92.20, bal: a.balance - 31.40 },
{ ts:"Today 10:14", kind:"charge", who:"JL", desc:"DAA refill week 22 · 12 residents", amt: 144.00, bal: a.balance - 123.60 },
{ ts:"Yesterday", kind:"charge", who:"SK", desc:"Webster pack · H. Whitlock (room 22)", amt: 84.00, bal: a.balance - 267.60 },
{ ts:"20/05/2026", kind:"charge", who:"JL", desc:"Dispense ×6 · weekly batch", amt: 412.65, bal: a.balance - 351.60 },
{ ts:"18/05/2026", kind:"credit", who:"SK", desc:"Damaged stock credit · S. Wilson", amt: -28.20, bal: a.balance - 764.25 },
{ ts:"12/05/2026", kind:"payment", who:"—", desc:"Payment received · BPay", amt: -4800.00, bal: a.balance - 736.05 },
].map((t, i) => (
{t.ts}
{t.kind === "charge" && Charge }
{t.kind === "payment" && Payment }
{t.kind === "credit" && Credit }
{t.desc}
{t.who}
{oMoney(t.amt)}
{oMoney(t.bal)}
))}
Account details
{a.abn !== "—" &&
}
{a.status === "overdue" && (
Overdue · {a.overdueDays} days
Charges blocked until payment received. SMS reminder sent — no response.
Reminder
Receive payment
)}
Quick actions
Print statement
Manage authorised
Change credit limit
Suspend charges
);
}
function Row({ label, value, mono }) {
return (
{label}
{value}
);
}
// =================================================================
// PROMOTIONS — list + designer
// =================================================================
function Promotions() {
const { setScreen, setScreenState, pushToast } = window.useOffice();
const [tab, setTab] = hoUseState("active");
const FILTER = {
active: p => p.status === "active",
scheduled: p => p.status === "scheduled",
ended: p => p.status === "ended",
all: () => true,
};
const list = HO_PROMOS.filter(FILTER[tab]);
const totals = {
redemptionsMTD: HO_PROMOS.reduce((s, p) => s + p.redemptions, 0),
revenue: HO_PROMOS.reduce((s, p) => s + p.revenue, 0),
active: HO_PROMOS.filter(p => p.status === "active").length,
scheduled: HO_PROMOS.filter(p => p.status === "scheduled").length,
};
return (
setTab("active")}>
Active {totals.active}
setTab("scheduled")}>
Scheduled {totals.scheduled}
setTab("ended")}>Ended
setTab("all")}>All
Promotion
Discount
Applies to
Stores
Dates
Redeems
Revenue
Status
{list.map(p => (
{ setScreenState({ activePromoId: p.id }); setScreen("promoNew"); }}>
{p.type === "percent" ? `${p.value}%` : p.type === "amount" ? `$${p.value} off` : p.type === "bogo" ? `$${p.value} bundle` : `$${p.value}`}
{p.targetLabel}{p.requiresLoyalty && Loyalty }
{p.stores.length === 3 ? "All stores" : p.stores.map(id => findS(id)?.code).join(", ")}
{p.starts} → {p.ends.split("/").slice(0,2).join("/")}
{p.redemptions}
{p.revenue > 0 ? oMoney(p.revenue) : "—"}
{p.status === "active" && Active }
{p.status === "scheduled" && Scheduled }
{p.status === "ended" && Ended }
))}
);
}
function PromoNew() {
const { setScreen, pushToast, screenState } = window.useOffice();
const editing = screenState.activePromoId ? HO_PROMOS.find(p => p.id === screenState.activePromoId) : null;
const [form, setForm] = hoUseState(editing || {
name: "", type: "percent", value: 10, target: "category", targetLabel: "Vitamins",
stores: ["S-001","S-002","S-003"], starts: "27/05/2026", ends: "30/06/2026",
requiresLoyalty: false, priority: 5,
});
function set(k, v) { setForm(f => ({ ...f, [k]: v })); }
function toggleStore(id) { setForm(f => ({ ...f, stores: f.stores.includes(id) ? f.stores.filter(x => x !== id) : [...f.stores, id] })); }
function submit() {
pushToast({ kind:'paid', icon:'check-circle', title:`Promotion ${editing ? 'updated' : 'created'} · ${form.name}`, meta: `${form.type === "percent" ? form.value + "%" : "$" + form.value + " off"} · ${form.stores.length} stores` });
setScreen("promotions");
}
return (
Promotions
{editing ? `Edit · ${editing.name}` : "New promotion"}
setScreen("promotions")}>Cancel
Save as draft
{editing ? "Save changes" : "Launch promotion"}
{/* Right — preview */}
Preview · shelf flag
Special
{form.type === "percent" ? `${form.value}%` : `$${form.value}`}
{form.type === "percent" ? "off" : "off"} · {form.targetLabel}
Until {form.ends}{form.requiresLoyalty && " · Loyalty members"}
When it goes live
{form.name || "This promotion"} will activate at {form.stores.length} {form.stores.length === 1 ? "store" : "stores"} from {form.starts} to {form.ends} .
POS will auto-apply at checkout. Receipts show the line-item discount.
);
}
// =================================================================
// LOYALTY
// =================================================================
function Loyalty() {
const { pushToast } = window.useOffice();
const [tab, setTab] = hoUseState("tiers");
return (
Head-office function
Loyalty programme
Export members
pushToast({kind:'paid', icon:'check', title:'Programme settings saved'})}>
Save changes
setTab("tiers")}>Tiers & perks
setTab("earn")}>Earn rules
setTab("rewards")}>Rewards
setTab("members")}>Members
setTab("campaigns")}>Campaigns
{tab === "tiers" && (
{HO_LOYALTY_TIERS.map(t => (
{t.name}
{t.members.toLocaleString()} members
Spend threshold
${t.spendThreshold}/year
Standard discount
{t.discount}%
Perks
{t.perks.map((p, i) => {p} )}
Edit tier
))}
)}
{tab === "earn" && (
Points & earn rules
PBS items
Whether PBS prescriptions earn points.
Don't earn Earn at half rate
Points expiry
After how long unused points expire.
6 months 12 months 24 months Never
)}
{tab === "rewards" && (
Reward
Points cost
Tier required
Type
Redeems
Status
{[
{ name: "$5 off voucher", cost: 100, tier: "Any", type: "Voucher", redeems: 482, status: "active" },
{ name: "$10 off voucher", cost: 200, tier: "Any", type: "Voucher", redeems: 312, status: "active" },
{ name: "Free delivery", cost: 150, tier: "Any", type: "Service", redeems: 188, status: "active" },
{ name: "Multivitamin · free", cost: 400, tier: "Silver+", type: "Product", redeems: 22, status: "active" },
{ name: "MedsCheck consult", cost: 600, tier: "Gold", type: "Service", redeems: 8, status: "active" },
{ name: "Sunscreen bundle", cost: 300, tier: "Any", type: "Product", redeems: 0, status: "scheduled" },
].map((r, i) => (
{r.name}
{r.cost}
{r.tier}
{r.type}
{r.redeems}
{r.status === "active" ? Active : Scheduled }
))}
)}
{tab === "members" && (
Member
Home store
Tier
Points
YTD spend
Last visit
{[
{ id:"u01", first:"Mei", last:"Tan", tier:"Silver", store:"S-001", points: 432, ytd: 1240, lastVisit:"3 days ago" },
{ id:"u02", first:"Jordan", last:"Riley", tier:"Bronze", store:"S-001", points: 88, ytd: 320, lastVisit:"Today" },
{ id:"u03", first:"Ana", last:"Costa", tier:"Gold", store:"S-003", points: 1840,ytd: 2410, lastVisit:"Yesterday" },
{ id:"u05", first:"Priya", last:"Sharma", tier:"Silver", store:"S-001", points: 318, ytd: 680, lastVisit:"5 days ago" },
{ id:"u06", first:"Nadia", last:"Khoury", tier:"Bronze", store:"S-001", points: 142, ytd: 220, lastVisit:"Today" },
{ id:"u09", first:"Henry", last:"Whitlock", tier:"Gold", store:"S-001", points: 2210,ytd: 3120, lastVisit:"2 days ago" },
{ id:"u10", first:"Beatrix", last:"Lim", tier:"Bronze", store:"S-002", points: 64, ytd: 140, lastVisit:"Today" },
].map(m => {
const tier = HO_LOYALTY_TIERS.find(t => t.name === m.tier);
return (
{(m.first[0]+m.last[0]).toUpperCase()}
{m.first} {m.last}
{findS(m.store)?.code} · {findS(m.store)?.name}
{m.tier}
{m.points.toLocaleString()}
${m.ytd.toLocaleString()}
{m.lastVisit}
);
})}
)}
{tab === "campaigns" && (
Campaign
Channel
Audience
Sent
Redeems
Status
{[
{ name: "Mother's Day · Gold members", ch: "SMS", aud: "Gold", sent: "08/05/2026", reds: 64, status: "complete" },
{ name: "Bronze re-engage · no visit 60d", ch: "Email", aud: "Bronze · inactive", sent: "15/05/2026", reds: 38, status: "complete" },
{ name: "Hayfever bundle · all members", ch: "SMS", aud: "All", sent: "12/05/2026", reds: 24, status: "complete" },
{ name: "Winter Wellness · launch", ch: "Email",aud: "All", sent: "Scheduled 01/06", reds: 0, status: "scheduled" },
{ name: "Birthday bonus · May", ch: "SMS", aud: "Birthdays", sent: "01/05/2026", reds: 122, status: "complete" },
].map((c, i) => (
{c.name}
{c.ch}
{c.aud}
{c.sent}
{c.reds}
{c.status === "complete" ? Complete : Scheduled }
))}
)}
);
}
window.OFFICE_SCREENS = Object.assign(window.OFFICE_SCREENS || {}, {
accounts: OfficeAccounts,
accountDetail: OfficeAccountDetail,
promotions: Promotions,
promoNew: PromoNew,
loyalty: Loyalty,
});