p.where==='basket').length+' are register rules, not shelf prices'}}/>days(d.today,p.endsOn)<=7).length} delta={{text:'The recorded base price resumes when the offer ends'}}/>
Six weeks out
Dates and overlaps across recorded promotions. Open a bar to inspect its offer.
{Array.from({length:6},(_,i)=>
{date(addDays(start,i*7)).slice(0,5)}
)}
today
{visible.map(p=>{const a=Math.max(0,days(start,p.startsOn)),b=Math.min(42,days(start,p.endsOn)+1);return ;})}{!visible.length&&Nothing scheduled or live in this window.}
{filters.map(([id,label,pred])=>)}
{shown.map(p=>
open(p)}>{p.name}{p.funded&&funded}{p.id}{p.mechanicLabel}{p.say}{p.scopeKind==='all'?'Group':p.scopeKind}{p.scopeLabel}{p.siteName||d.siteName||'Organisation'}{date(p.startsOn)} – {date(p.endsOn)}{p.state==='live'&&{days(d.today,p.endsOn)} days left}
)}
{!shown.length&&{all.length?'Nothing matches that filter.':'No promotions yet. Create a draft to begin.'}}
>;
}
function Register({d,open}) {
const [lines,setLines]=useState([]),[options,setOptions]=useState([]),[query,setQuery]=useState(''),[result,setResult]=useState(null),[error,setError]=useState(null),[busy,setBusy]=useState(false);
useEffect(()=>{let alive=true;window.OfficeAPI.promotionScopeOptions().then(o=>{if(alive)setOptions(o.products||[]);}).catch(e=>{if(alive)setError(e.message);});return()=>{alive=false;};},[]);
useEffect(()=>{let alive=true;if(!lines.length){setResult(null);setBusy(false);return;}setBusy(true);setResult(null);const t=setTimeout(()=>window.OfficeAPI.simulatePromotions({siteId:d.siteId,lines}).then(x=>{if(alive){setResult(x);setError(null);}}).catch(e=>{if(alive)setError(e.message);}).finally(()=>{if(alive)setBusy(false);}),150);return()=>{alive=false;clearTimeout(t);};},[JSON.stringify(lines),d.siteId]);
const live=(d.promotions||[]).filter(p=>p.state==='live'),prices=live.filter(p=>p.where==='price'),rules=live.filter(p=>p.where==='basket');
const add=id=>{setLines(prev=>prev.some(l=>l.productId===id)?prev.map(l=>l.productId===id?{...l,qty:l.qty+1}:l):[...prev,{productId:id,qty:1}]);setQuery('');};
const change=(id,delta)=>setLines(prev=>prev.map(l=>l.productId===id?{...l,qty:l.qty+delta}:l).filter(l=>l.qty>0));
return <>Price offers are stored as promotion prices; supported basket offers are stored as rules. This view reads the publication records and simulates the supported register calculations.
{prices.map(p=>
{p.lineCount} lines · {p.say}
)}{!prices.length&&No live price promotions.}
{rules.map(p=>
{p.say}
{p.prompt&&
{p.prompt}
}
)}{!rules.length&&No live basket rules.}
{d.till?.note||'Publication records are available; individual register delivery acknowledgements are not.'}
;})}{!lines.length&&Add a product to start a basket.}
{(result?.appliedRules||[]).map((r,i)=>
{r.name||r.description||'Basket rule'}
{r.note||r.ruleId}
{cash(r.discountCents??r.savedCents)}
)}
Before promotions{cash(result?.subtotalCents)}
Saved{cash(result?.discountCents)}
To pay{cash(result?.totalCents)}
{(result?.limitations||[]).map((n,i)=>
{n}
)}
>;
}
function Overlaps({d,open}) {
const c=d.conflicts||{},s=c.counts||{},rows=c.rows||[];
return <>
{c.policy||'Overlaps use the current publication model.'}
{rows.map((r,i)=>
{(r.candidates||r.promotions||[]).map(p=>
{p.where==='basket'?'basket':'snapshot'}
{p.say||p.where}
{cash(p.priceCents??p.promoCents)}
)}
Current stored unit price: {cash(r.effectiveCents)}{r.belowCost ? " · below recorded cost floor " + cash(r.floorCents) : ""}. {(r.trace||[]).join(" ")}{r.note||""}
)}
{!rows.length&&No overlapping publication records in this scope.}>;
}
function Group({d,open}) {
const [tab,setTab]=useState('stores'),g=d.group||{},sites=g.sites||[];
const tabs=[['stores','By store'],['diverge','Price divergence'],['risk','Funding at risk'],['patchy','Uneven coverage']];
const active=(d.promotions||[]).filter(p=>['live','scheduled'].includes(p.state));
return <>
{g.note||'Promotion ownership and the price-delivery scope are shown separately.'}
{tabs.map(([k,l])=>)}
{tab==='stores'?
{sites.map(s=>
{s.siteName}{s.liveCount??0}{s.scheduledCount??0}{s.fundedCount??0}Promotion owner · price profiles apply across the organisation
)}
:tab==='patchy'?<>
{active.map(p=>
{p.name}{p.siteName||p.siteId}
)}
{!active.length&&No live or scheduled promotions.}>:{tab==='diverge'?'Independent store promotion prices are not recorded by the current organisation-wide profile model.':'Recorded sales do not identify which funded promotion won, so realised funding loss cannot be calculated.'}}
>;
}
function Performance({d,open}) {
const p=d.performance||{},rows=p.rows||[],s=p.summary||{},measured=rows.filter(r=>r.gpDeltaCents!=null),best=measured.slice().sort((a,b)=>b.gpDeltaCents-a.gpDeltaCents)[0];
return <>
r.gpDeltaCents>0).length+' of '+measured.length:'—'} delta={{text:'Measured scope GP versus the stated baseline'}}/>
{p.basisNote}
Against trailing eight weeks
{rows.map(r=>
{date(r.period?.from)} – {date(r.period?.to)}{r.scopeUnits??'—'}{r.baselineUnits==null?'—':Number(r.baselineUnits).toFixed(1)}{pct(r.liftPct)}{cash(r.recordedGpCents)}{cash(r.gpDeltaCents)}{r.note||'Sales in scope during the offer; not verified promotion redemptions.'}
)}
{!rows.length&&No promotion period has recorded sales to compare yet.}
>;
}
function Funding({d,refresh}) {
const f=d.funding||{},s=f.summary||{},rows=f.rows||[];
const [selected,setSelected]=useState(null),[status,setStatus]=useState('open'),[amount,setAmount]=useState(''),[units,setUnits]=useState(''),[reference,setReference]=useState(''),[note,setNote]=useState(''),[busy,setBusy]=useState(false),[error,setError]=useState(null);
const edit=r=>{setSelected(r);setStatus(r.record?.status||'open');setAmount(r.record?.amountCents==null?'':String(r.record.amountCents/100));setUnits(r.record?.units==null?'':String(r.record.units));setReference(r.record?.reference||'');setNote(r.record?.note||'');setError(null);};
const save=async()=>{if(!amount.trim()||!Number.isFinite(Number(amount))||Number(amount)<0){setError('Enter the documented claim amount.');return;}if(!note.trim()){setError('Add a note identifying the supporting evidence.');return;}setBusy(true);setError(null);try{await window.OfficeAPI.savePromotionFundingRecord(selected.promotionId,{revision:selected.record?.revision??selected.revision??'default',status,amountCents:Math.round(Number(amount)*100),units:units===''?null:Number(units),reference:reference||null,note:note||null});setSelected(null);refresh();}catch(e){setError(e.message);}finally{setBusy(false);}};
return <>
Authored here, run at the register. Some promotions are a price, others are a rule the till evaluates against the whole basket.
{error&&
{error}
}{busy&&
Loading recorded promotions…
}{delivery&&{d?.till?.note||'Published records sync through the existing register feed. Individual register acknowledgements are unavailable.'} Drafts are not published by this check.}{d&&setReload(k=>k+1)}/>}