/* Live pricing reports and configuration. Scoped API saves never write a retail price. */
(function () {
const {useState,useEffect,useMemo} = React;
const money = c => c == null ? "—" : "$" + (c/100).toLocaleString("en-AU",{minimumFractionDigits:2,maximumFractionDigits:2});
const percent = n => n == null ? "—" : Number(n).toFixed(1) + "%";
const signed = c => c == null ? "—" : (c>0 ? "+" : c<0 ? "−" : "") + money(Math.abs(c));
const date = v => v ? new Date(v).toLocaleDateString("en-AU",{day:"numeric",month:"short",year:"numeric"}) : "Never observed";
const clone = v => JSON.parse(JSON.stringify(v));
function useLoad(loader,deps=[]) {
const [data,setData]=useState(null),[error,setError]=useState(null),[busy,setBusy]=useState(true),[reload,setReload]=useState(0);
useEffect(()=>{const refresh=()=>setReload(k=>k+1);window.addEventListener("office-pricing-refresh",refresh);return()=>window.removeEventListener("office-pricing-refresh",refresh);},[]);
useEffect(()=>{let alive=true;setBusy(true);setError(null);loader().then(d=>{if(alive)setData(d);}).catch(e=>{if(alive)setError(e.message||"Couldn't load pricing data.");}).finally(()=>{if(alive)setBusy(false);});return()=>{alive=false;};},[reload,...deps]);
return {data,setData,error,setError,busy,refresh:()=>setReload(k=>k+1)};
}
function Message({error,children}) {return
{children}
;}
function Frame({children,actions}) {return
{actions}
{children}
;}
function LoadState({r}) {return <>{r.error&&{r.error}}{r.busy&&Loading live pricing data…}>;}
function useOpen(from) {const {setScreen,setScreenState}=window.useOffice();return productId=>{setScreenState(st=>({...st,priceProductId:productId,priceFrom:from,priceMethod:undefined,priceStrategy:undefined}));setScreen("priceDetail");};}
function Pager({page,setPage,total,size=50}) {return total>size?
{page*size+1}–{Math.min((page+1)*size,total)} of {total}
r.enabled).length+1} delta={{text:"Most specific assignment wins"}}/>
A rule set at group level covers everything. Anything set at category, brand or line level overrides it, and the most specific assignment always wins. The detail screen names which one governed the price and which ones lost.
{d.basisNote}
Sales: {date(d.period?.from)} to {date(d.period?.to)}. Margin comparisons use the same costed unit basket.
Ordered from general to specific. The working names the rule that wins.
>}
;
}
function Checks() {
const r=useLoad(()=>window.OfficeAPI.pricingChecks()),d=r.data,s=d?.summary,open=useOpen("pricingChecks"),{setScreen}=window.useOffice(),Kpi=window.KpiCard;
return Recheck}>{d&&<>
Pack ladders
A bigger pack should cost less per unit. Confirmed pack units establish the comparison; a price ladder also needs a recorded selling price for each pack.
{d.packLadders.length?
{d.packLadders.map(p=>
{p.broken?"Broken":"Confirmed units"}
{p.levels.map((l,i)=>
{l.name}
{l.unitsInBase==null?"—":l.unitsInBase}
base units{l.isSell?" · sold at till":""}
{l.priceCents!=null&&
{money(l.priceCents)}
}
)}
{p.note}
)}
:
No confirmed pack group has enough comparable selling-price evidence to assess a price ladder.
}
Only confirmed levels within the same product are grouped. Missing pack prices remain unassessed.
You may be chasing a promotion
Where the cheapest observed price is inferred to be on sale, matching it may follow a temporary price. No end date or return price is assumed.
{d.promos.length?
{d.promos.map((p,i)=>
Inferred sale
Observed{money(p.priceCents)}
Retailer{p.retailerName}
Last seen{date(p.observedAt)}
Return price—
{p.evidence}
)}
:
No cheapest-observed prices currently meet the sale-history threshold.
}
Same line, different price by store
Independent shelf prices at each store are needed to distinguish deliberate local pricing from drift.
{d.crossStore.reason}
Pricing assignments can vary by site; recorded shelf prices currently apply across the organisation.
{date(p.lastObservedAt)}{p.enabled&&p.due&&Due for recheck}
;})}
{!d.rows.length&&
No eligible products match this search.
}
Turning a line on enables its KVI second pass after saving. {Object.keys(changes).length} unsaved changes.
>};
}
function Methods() {
const r=useLoad(()=>window.OfficeAPI.pricingConfig());
const reports=useLoad(()=>Promise.all([window.OfficeAPI.pricingBreakdown(),window.OfficeAPI.pricingChecks()]).then(([breakdown,checks])=>({breakdown,checks})));
const [draft,setDraft]=useState(null),[methodId,setMethodId]=useState("hlprice"),[dirty,setDirty]=useState(false),[saving,setSaving]=useState(false),[notice,setNotice]=useState(null);
const {screenState={},setScreenState}=window.useOffice();
const [showRule,setShowRule]=useState(!!screenState.priceEditRule),[ruleScope,setRuleScope]=useState("category"),[ruleValue,setRuleValue]=useState(""),[ruleName,setRuleName]=useState(""),[ruleMethod,setRuleMethod]=useState("hlprice"),[picked,setPicked]=useState(null),[lock,setLock]=useState(false),[lockPrice,setLockPrice]=useState(""),[ruleError,setRuleError]=useState(null);
useEffect(()=>{if(r.data&&!dirty){setDraft(clone(r.data));}},[r.data,dirty]);
useEffect(()=>{if(screenState.priceEditRule)setScreenState(st=>({...st,priceEditRule:false}));},[]);
const edit=fn=>{setDraft(prev=>{const next=clone(prev);fn(next);return next;});setDirty(true);setNotice(null);};
const method=draft?.methods.find(m=>m.id===methodId);
const editMethod=(key,value)=>edit(d=>{d.methods.find(m=>m.id===methodId)[key]=value;});
const editBand=(i,key,value)=>edit(d=>{d.methods.find(m=>m.id===methodId).bands[i][key]=Math.round(Number(value)*100);});
const save=async()=>{setSaving(true);r.setError(null);try{const result=await window.OfficeAPI.savePricingConfig(draft);setNotice("Configuration saved. Recommendations will use these rules on the next read. Shelf prices are unchanged.");if(result.methods){r.setData(result);}else{r.refresh();}reports.refresh();setDirty(false);}catch(e){r.setError(e.message||"Couldn't save pricing configuration.");}finally{setSaving(false);}};
const addRule=()=>{
const value=ruleScope==="group"?null:ruleScope==="product"?picked?.id:ruleValue.trim();
if(ruleScope!=="group"&&!value){setRuleError("Choose a target for this rule.");return;}
if(lock&&ruleScope==="product"&&(!Number.isFinite(Number(lockPrice))||Number(lockPrice)<=0||!ruleName.trim())){setRuleError("A lock needs a positive price and a reason.");return;}
if(draft.rules.some(r=>r.enabled&&r.scope===ruleScope&&r.value===value)){setRuleError("An enabled rule already targets this level and value. Remove it before adding a replacement.");return;}
edit(d=>{d.rules.push({id:"rule-"+Date.now().toString(36),name:ruleName.trim()||picked?.name||value||"Everything",scope:ruleScope,value,methodId:ruleMethod,enabled:true});if(lock&&ruleScope==="product"){d.productLocks=(d.productLocks||[]).filter(l=>l.productId!==picked.id).concat({productId:picked.id,priceCents:Math.round(Number(lockPrice)*100),reason:ruleName.trim()});}});
setShowRule(false);setRuleError(null);setRuleName("");setRuleValue("");setPicked(null);setLock(false);
};
return {saving?"Saving…":"Save changes"}}>{notice&&{notice}}{draft&&method&&};
}
window.OFFICE_SCREENS=Object.assign(window.OFFICE_SCREENS||{},{pricingBreakdown:Breakdown,pricingChecks:Checks,pricingRegister:Register,pricingMethods:Methods});
})();