/* Campaign Table with nested Campaign → AdSet → Offer drilldown ===== */ const DEFAULT_COLS = [ { id:'name', label:'Campaign', w:'minmax(280px,1.6fr)', align:'left', sortable:true }, { id:'status', label:'Status', w:'110px', align:'left', sortable:true }, { id:'platform',label:'Platform', w:'90px', align:'left', sortable:true }, { id:'budget', label:'Budget', w:'120px', align:'right', sortable:true, fmt:'currency' }, { id:'spend', label:'Spend · 7d', w:'140px', align:'right', sortable:true, fmt:'currency' }, { id:'cpa', label:'CPA · 7d', w:'110px', align:'right', sortable:true, fmt:'currency2' }, { id:'roas', label:'ROAS · 95% CI', w:'110px', align:'right', sortable:true, fmt:'x' }, { id:'cpc', label:'CPC · 7d', w:'100px', align:'right', sortable:true, fmt:'currency2' }, { id:'ctr', label:'CTR · 7d', w:'100px', align:'right', sortable:true, fmt:'pct' }, { id:'conv', label:'Conv · 7d', w:'110px', align:'right', sortable:true, fmt:'int' }, { id:'pacing', label:'Pacing', w:'130px', align:'left', sortable:true }, ]; /* Grid template — ROAS column drops entirely for blocklisted accounts (e.g. Al Munawarah). Header + campaign row + ad-set row share this. */ const _GRID_FULL = 'minmax(220px,1.6fr) 104px 86px 112px 132px 104px 104px 96px 96px 104px 120px'; const _GRID_NOROAS = 'minmax(220px,1.6fr) 104px 86px 112px 132px 104px 96px 96px 104px 120px'; const roasHidden = ()=> typeof metricHiddenHere === 'function' && metricHiddenHere('roas'); const campaignGrid = ()=> roasHidden() ? _GRID_NOROAS : _GRID_FULL; /* Inline tiny sparkline (used inside table cells) */ function MicroSpark({ data, tone, width=36, height=14 }){ if (!data || !data.length) return null; const vals = data.map(d=>d.v); const min = Math.min(...vals), max = Math.max(...vals); const x = i => (i*(width-2)/(data.length-1)) + 1; const y = v => height - 1 - ((v-min)/((max-min)||1))*(height-2); const path = data.map((d,i)=>(i===0?'M':'L')+x(i).toFixed(2)+','+y(d.v).toFixed(2)).join(' '); return ( ); } /* Numeric cell with optional sparkline */ function NumCell({ value, sparkData, tone='#8b8b80', className='', children }){ return (
{children ?? value} {sparkData && }
); } /* ROAS cell with CI bar (small low-confidence indicator) */ function RoasCell({ roas, conv, target=1.5 }){ const ci = roasCI(roas || 0, conv || 0); const r = roas || 0; return (
=3?'text-emerald-600 dark:text-emerald-400':r<2?'text-rose-600 dark:text-rose-400':''}`}> {r.toFixed(2)}× {ci.wide && ( ! )} {/* hover CI bar */}
95% CI: {ci.lo.toFixed(2)}× – {ci.hi.toFixed(2)}× · n={conv}
); } function PacingBar({ value }){ // value: ratio of pacing (1.0 = on pace); null → no budget set if (value == null || Number.isNaN(value)) { return ; } const pct = Math.min(1.5, value); const ok = value <= 1.05 && value >= 0.85; const over = value > 1.10; const tone = over ? '#ef4444' : ok ? '#10b981' : '#f59e0b'; return (
{(value*100).toFixed(0)}%
); } function SortHeader({ col, sort, setSort }){ const active = sort.id===col.id; return ( ); } function AdSetRow({ as, accent, toggleStatus, readOnly }){ const paused = as.status === 'paused'; return (
AS {as.name}
toggleStatus(as.id)} size="sm"/>
{fmt(as.spend,'currency')}
{fmt(as.cpa,'currency2')}
{!roasHidden() && }
{fmt(as.cpc,'currency2')}
{(as.ctr||0).toFixed(2)}%
{(as.conv||0).toLocaleString()}
); } function CampaignRow({ c, expanded, toggleExpand, toggleStatus, readOnly }){ const paused = c.status === 'paused'; const accent = (PLATFORMS.find(p=>p.id===c.platform)||PLATFORMS[0]).tone; /* No synthetic sparklines — cells show real range values only. */ const funnel = useMemo(()=>campaignFunnel(c), [c.id, c.spend, c.conv]); return (
{!paused && } {paused && } {c.name} {(()=>{ const parts = String(c.id||'').split('_'); return parts.length >= 2 ? parts[parts.length-2] + '-' + parts[parts.length-1] : (c.id || ''); })()}
toggleStatus(c.id)} size="sm"/> {paused?'Paused':'Active'}
{fmt(c.budget,'currency')}
{fmt(c.spend,'currency')} {fmt(c.cpa,'currency2')} {!roasHidden() && } {fmt(c.cpc,'currency2')} {(c.ctr||0).toFixed(2)}% {(c.conv||0).toLocaleString()}
{expanded && (
{/* Funnel strip */} {/* Ad sets */}
Ad sets · {(c.adsets||[]).length}
{(c.adsets||[]).map(as => )}
)}
); } function CampaignTable({ accountId, platformId, search='', onOpenColumns, canEdit=true }){ const readOnly = !canEdit; const baseCampaigns = useMemo(()=>getCampaigns(accountId, platformId), [accountId, platformId]); /* Local state: status & expansion */ const [statusMap, setStatusMap] = useState({}); // id -> 'active' | 'paused' useEffect(()=>{ setStatusMap({}); setExpanded({}); }, [accountId, platformId]); const [expanded, setExpanded] = useState({}); const toggleExpand = (id)=> setExpanded(s => ({...s, [id]: !s[id]})); const toggleStatus = (id) => { if(readOnly) return; setStatusMap(s => { const current = s[id]; const all = [...baseCampaigns, ...baseCampaigns.flatMap(c=>c.adsets||[])]; const it = all.find(x => x.id===id); const base = it?.status; const isActive = current ? current === 'active' : base === 'active'; return { ...s, [id]: isActive ? 'paused' : 'active' }; }); }; const applyStatus = (it) => { const ov = statusMap[it.id]; return ov ? { ...it, status: ov } : it; }; const campaigns = useMemo(()=>baseCampaigns.map(c=>{ const cc = applyStatus(c); cc.adsets = (c.adsets||[]).map(applyStatus); return cc; }), [baseCampaigns, statusMap]); /* Sort */ const [sort, setSort] = useState({ id:'spend', dir:'desc' }); const sorted = useMemo(()=>{ const arr = [...campaigns]; arr.sort((a,b)=>{ const va = a[sort.id], vb = b[sort.id]; if (typeof va === 'string') return sort.dir==='asc' ? va.localeCompare(vb) : vb.localeCompare(va); return sort.dir==='asc' ? (va||0) - (vb||0) : (vb||0) - (va||0); }); return arr; }, [campaigns, sort]); /* Search filter */ const filtered = useMemo(()=>{ if (!search.trim()) return sorted; const q = search.toLowerCase(); return sorted.filter(c => (c.name||'').toLowerCase().includes(q) || (c.adsets||[]).some(a => (a.name||'').toLowerCase().includes(q))); }, [sorted, search]); /* Totals */ const totals = useMemo(()=>{ return filtered.reduce((t,c)=>({ spend: t.spend + (c.spend||0), conv: t.conv + (c.conv||0), budget:t.budget+ (c.budget||0), }), { spend:0, conv:0, budget:0 }); }, [filtered]); return (
{/* Header bar */}
Campaigns
{filtered.length} of {baseCampaigns.length} Spend {fmt(totals.spend,'currency')} · Conv. {totals.conv.toLocaleString()}
Filters Columns New campaign
{/* Column headers + body share ONE horizontal scroll container so columns stay aligned */}
{DEFAULT_COLS.filter(col => !(col.id==='roas' && roasHidden())).map(col => (
))}
{/* Body */}
{filtered.length === 0 && (
{baseCampaigns.length === 0 ? <>No campaigns yet — connect a data source or import data in Admin → Integrations. : <>No campaigns match "{search}"}
)} {filtered.map(c => ( ))}
); } Object.assign(window, { CampaignTable, MicroSpark, RoasCell, NumCell });