/* ============================================================= CoreIQ Office — Price Position (LIVE · this pharmacy) Where our shelf price sits against the latest OBSERVED competitor price. Source: public.competitor_prices (migrated CWH observations + Office captures), compared within-tenant only (market research, not cross-pharmacy coordination). Honest by construction: the "reduce to sell faster" recommendation runs on FRONT-OF-SHOP RETAIL lines only. Prescription/ethical lines are shown as a monitor — for a script the competitor's captured price is the patient co-pay, not a like-for-like retail price, so the gap there is informational only. ============================================================= */ const { useState: ppState, useEffect: ppEffect, useRef: ppRef } = React; const ppM = (c) => "$" + (Number(c || 0) / 100).toLocaleString("en-AU", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const ppPct = (v) => (v > 0 ? "+" : "") + Number(v).toFixed(1) + "%"; // A small typeahead that resolves one of OUR products to feed a capture. function ProductPicker({ value, onPick }) { const [q, setQ] = ppState(""); const [matches, setMatches] = ppState([]); const [open, setOpen] = ppState(false); const t = ppRef(null); ppEffect(() => () => { if (t.current) clearTimeout(t.current); }, []); // clear pending debounce on unmount const search = (text) => { setQ(text); onPick(null); if (t.current) clearTimeout(t.current); if (!text.trim()) { setMatches([]); setOpen(false); return; } t.current = setTimeout(() => { window.OfficeAPI.listProducts({ search: text.trim(), limit: 8 }) .then((ps) => { setMatches(ps || []); setOpen(true); }) .catch(() => { setMatches([]); setOpen(false); }); }, 220); }; if (value) { return (
{value.name}
); } return (
search(e.target.value)} onFocus={() => matches.length && setOpen(true)}/> {open && matches.length > 0 && (
{matches.map((p) => (
{ onPick(p); setOpen(false); }}>
{p.name}
{p.sku} · our {ppM(p.sellPriceCents)}
))}
)}
); } // Capture a competitor price. `fixedProduct` pins the product (product-page use); // otherwise the form shows a product picker (price-position screen use). function CompetitorCaptureForm({ fixedProduct, onSaved, onCancel }) { const [product, setProduct] = ppState(fixedProduct || null); const [competitor, setCompetitor] = ppState("Chemist Warehouse"); const [price, setPrice] = ppState(""); const [url, setUrl] = ppState(""); const [busy, setBusy] = ppState(false); const [msg, setMsg] = ppState(null); const submit = async () => { setMsg(null); const pid = product ? product.id : null; if (!pid) { setMsg({ ok: false, t: "Pick a product first." }); return; } if (!competitor.trim()) { setMsg({ ok: false, t: "Competitor name is required." }); return; } const dollars = parseFloat(price); if (!(dollars >= 0)) { setMsg({ ok: false, t: "Enter a valid price." }); return; } setBusy(true); try { await window.OfficeAPI.recordCompetitorPrice({ productId: pid, competitor: competitor.trim(), priceCents: Math.round(dollars * 100), priceBasis: "manual", url: url.trim() || null, }); setMsg({ ok: true, t: "Saved." }); setPrice(""); setUrl(""); if (!fixedProduct) setProduct(null); if (onSaved) onSaved(); } catch (e) { setMsg({ ok: false, t: String((e && e.message) || e) }); } finally { setBusy(false); } }; return (

Add a competitor price

{onCancel && }
{!fixedProduct && ( )}
{msg && {msg.t}}
); } // A single price-position row's gap chip (above = dearer than market = red-ish). function GapChip({ deltaPct, isRx }) { if (isRx) return {ppPct(deltaPct)} · monitor; const above = deltaPct > 0.05; const below = deltaPct < -0.05; const c = above ? "var(--void-text)" : below ? "var(--paid)" : "var(--text-subtle)"; const bg = above ? "var(--void-bg)" : below ? "var(--paid-bg)" : "var(--bg-subtle)"; return {ppPct(deltaPct)}; } function PricePosition() { const { setScreen, setScreenState } = window.useOffice(); const [scope, setScope] = ppState("retail"); const [position, setPosition] = ppState("all"); const [search, setSearch] = ppState(""); const [data, setData] = ppState(null); const [err, setErr] = ppState(null); const [loading, setLoading] = ppState(true); const [showAdd, setShowAdd] = ppState(false); const [reloadKey, setReloadKey] = ppState(0); ppEffect(() => { let alive = true; setLoading(true); const q = { scope, position, limit: 200 }; if (search.trim()) q.search = search.trim(); window.OfficeAPI.pricePosition(q) .then((d) => { if (alive) { setData(d); setErr(null); setLoading(false); } }) .catch((e) => { if (alive) { setErr(String((e && e.message) || e)); setLoading(false); } }); return () => { alive = false; }; }, [scope, position, search, reloadKey]); const openProduct = (pid) => { setScreenState((st) => ({ ...st, activeProductId: pid, productFrom: "pricePosition" })); setScreen("productDetail"); }; const s = data && data.summary; const rows = (data && data.rows) || []; const Kpi = window.KpiCard; const SCOPES = [ { k: "retail", label: "Retail (front of shop)" }, { k: "rx", label: "Prescription" }, { k: "all", label: "All" }, ]; const POS = [ { k: "all", label: "All" }, { k: "above", label: "Above market" }, { k: "below", label: "Below market" }, ]; return (
live · this pharmacy · competitor observations

Price position

{showAdd &&
setReloadKey((k) => k + 1)} onCancel={() => setShowAdd(false)}/>
} {/* KPIs — counts are honest: above/below are over the retail subset only */} {s && Kpi && (
0 ? "down" : undefined }}/>
)} {/* controls */}
{SCOPES.map((t) => ( ))}
{POS.map((t) => ( ))}
setSearch(e.target.value)}/>
{err &&
{err}
} {scope === "rx" && (
Prescription lines — the competitor's captured price is the patient co-pay, not a like-for-like retail price. Shown for monitoring; the gap is informational.
)} {!loading && !err && rows.length === 0 && (
{scope === "retail" ? "No front-of-shop retail comparisons yet" : "No competitor observations match"}
{scope === "retail" ? "Your observed competitor prices so far are prescription items — switch to Prescription to monitor those. Capture a retail competitor price above, and new comparisons also arrive from the national feed as it grows." : "Try a different scope or clear the filter."}
)} {rows.length > 0 && (

{position === "above" ? "Priced above market" : position === "below" ? "Priced below market" : "Our price vs latest competitor"}

{rows.length} line{rows.length === 1 ? "" : "s"} · sorted by gap
Product
Competitor
Our price
Theirs
Gap
{rows.map((r, i) => (
r.productId && openProduct(r.productId)}>
{r.productName || r.productId} {r.isRx && SCRIPT}
{ppM(r.ourSellCents)}
{ppM(r.competitorPriceCents)}
))}
)}
); } window.CompetitorCaptureForm = CompetitorCaptureForm; window.OFFICE_SCREENS = Object.assign(window.OFFICE_SCREENS || {}, { pricePosition: PricePosition });