/* What-if budget Reallocator ======================================= */ function ReallocatorModal({ open, onClose, accountId }){ /* The reallocator models revenue via ROAS — meaningless for accounts where ROAS is blocklisted (e.g. Al Munawarah). Guard applied AFTER hooks (below) to keep the hook order stable across renders. */ const roasBlocked = typeof metricHiddenFor==='function' && metricHiddenFor(accountId, 'roas'); /* Build base allocations from current platform spend */ const base = useMemo(()=>{ const arr = platformBreakdown(accountId).map(m => { const p = PLATFORMS.find(x=>x.id===m.platform) || {}; return { id:m.platform, label:(p.short||m.platform), tone:(p.tone||'#8351ff'), spend:m.spend, roas:m.roas, base:m.spend }; }).filter(p => p.spend > 0); const total = arr.reduce((s,p)=>s+p.spend, 0); return { arr, total }; }, [accountId, open]); const [alloc, setAlloc] = useState(() => Object.fromEntries(base.arr.map(p=>[p.id, p.spend]))); const [horizon, setHorizon] = useState(7); // days useEffect(() => { if (open) setAlloc(Object.fromEntries(base.arr.map(p=>[p.id, p.spend]))); }, [open, base]); const totalNow = useMemo(()=>Object.values(alloc).reduce((s,v)=>s+v,0), [alloc]); /* Update a single platform — redistribute delta across others proportionally */ const setPlatform = (id, value) => { setAlloc(prev => { const newVal = Math.max(0, Math.round(value)); const oldVal = prev[id]; const diff = newVal - oldVal; const others = base.arr.filter(p => p.id !== id); const othersSum = others.reduce((s,p)=>s+prev[p.id], 0); const next = { ...prev, [id]: newVal }; // Distribute -diff proportionally to others if (othersSum > 0){ others.forEach(p => { const share = prev[p.id]/othersSum; next[p.id] = Math.max(0, Math.round(prev[p.id] - diff*share)); }); } return next; }); }; /* Projected revenue with diminishing returns */ const projection = useMemo(()=>{ const rows = base.arr.map(p => { const baseRev = p.spend * p.roas; const newSpend = alloc[p.id] || 0; const newRev = projectedRevenueFrom(p.spend, p.roas, newSpend); const baseRoas = p.roas; const newRoas = newSpend > 0 ? newRev / newSpend : 0; return { ...p, newSpend, newRev, deltaRev: newRev - baseRev, deltaSpend: newSpend - p.spend, newRoas, roasDelta: newRoas - baseRoas, }; }); const baseTotal = rows.reduce((s,r)=>s + r.spend * r.roas, 0); const newTotal = rows.reduce((s,r)=>s + r.newRev, 0); return { rows, baseTotal, newTotal, gain: newTotal - baseTotal, ratio: newTotal / baseTotal }; }, [base, alloc, accountId]); const reset = () => setAlloc(Object.fromEntries(base.arr.map(p=>[p.id, p.spend]))); if (roasBlocked) return (
Reallocation unavailable
Budget reallocation is ROAS-driven, and ROAS is disabled for this account (no revenue tracking). Pick another account to use this tool.
Close
); return (
Budget reallocator
Shift spend across platforms — projected revenue updates instantly.
{/* Top summary */}
Total spend
{fmt(totalNow,'currency')}
{totalNow===base.total?'unchanged':`${totalNow>base.total?'+':''}${(totalNow-base.total).toLocaleString()} vs base`}
Projected revenue
{fmt(Math.round(projection.newTotal),'currency')}
=0?'text-emerald-600 dark:text-emerald-400':'text-rose-600 dark:text-rose-400'}`}> {projection.gain>=0?'+':''}{fmt(Math.round(projection.gain),'currency')} ({((projection.ratio-1)*100).toFixed(1)}%)
Implied ROAS
{(projection.newTotal/Math.max(totalNow,1)).toFixed(2)}×
{horizon}d horizon
{/* Sliders */}
{projection.rows.length===0 && (
No platform spend in this range yet — connect or import data first.
)} {projection.rows.map(r => { const sliderMax = Math.round(r.spend * 2.5); const pctChange = ((r.newSpend - r.spend)/r.spend)*100; const efficiency = r.newRoas / r.roas; // 1.0 = same as baseline return (
{r.label}
base {fmt(r.spend,'currency')} · base ROAS {r.roas.toFixed(2)}×
{fmt(r.newSpend,'currency')}
0?'text-emerald-600 dark:text-emerald-400':pctChange<0?'text-rose-600 dark:text-rose-400':'text-ink-400'}`}> {pctChange>=0?'+':''}{pctChange.toFixed(1)}%
{/* Slider */}
setPlatform(r.id, +e.target.value)} className="w-full accent-accent-500" style={{ accentColor: r.tone }}/> {/* Base marker */}
Proj. revenue {fmt(Math.round(r.newRev),'currency')}
Implied ROAS =0?'text-emerald-600 dark:text-emerald-400':'text-rose-600 dark:text-rose-400'}`}> {r.newRoas.toFixed(2)}×
Efficiency {(efficiency*100).toFixed(0)}%
); })}
Diminishing-returns model
Projections derive from each platform's historical efficiency curve (revenue ∝ spend0.78). Treat as directional, not predictive.
Reset to current
Discard Apply allocation
); } Object.assign(window, { ReallocatorModal });