/* Metric catalog, column packs & ad-level data ====================== Meta Ads Manager-style: a catalog of metrics grouped into packs the user can switch between or customize, plus derivation of the full metric set + the Ads level (one grain below ad sets). =================================================================== */ /* ---------- Metric catalog ----------------------------------------- */ /* group: performance | delivery | engagement | video | funnel fmt: currency | currency2 | pct | x | int tone: spark / accent color */ const METRIC_CATALOG = [ // Performance { id:'spend', label:'Amount spent', short:'Spend', group:'performance', fmt:'currency', align:'right', tone:'#2f7bff', w:128, spark:true }, { id:'results', label:'Results', short:'Results', group:'performance', fmt:'int', align:'right', tone:'#10b981', w:104, spark:true }, { id:'cpa', label:'Cost / result', short:'CPA', group:'performance', fmt:'currency2', align:'right', tone:'#f59e0b', w:118, spark:true }, { id:'roas', label:'ROAS', short:'ROAS', group:'performance', fmt:'roasci', align:'right', tone:'#8351ff', w:120 }, { id:'revenue', label:'Conv. value', short:'Revenue', group:'performance', fmt:'currency', align:'right', tone:'#10b981', w:128 }, // Delivery { id:'impressions',label:'Impressions', short:'Impr.', group:'delivery', fmt:'int', align:'right', tone:'#06b6d4', w:120 }, { id:'reach', label:'Reach', short:'Reach', group:'delivery', fmt:'int', align:'right', tone:'#06b6d4', w:112 }, { id:'frequency', label:'Frequency', short:'Freq.', group:'delivery', fmt:'freq', align:'right', tone:'#ef4444', w:104 }, { id:'cpm', label:'CPM', short:'CPM', group:'delivery', fmt:'currency2', align:'right', tone:'#8b8b80', w:104 }, // Engagement { id:'clicks', label:'Link clicks', short:'Clicks', group:'engagement', fmt:'int', align:'right', tone:'#2f7bff', w:108 }, { id:'ctr', label:'CTR (link)', short:'CTR', group:'engagement', fmt:'pct', align:'right', tone:'#ec4899', w:104, spark:true }, { id:'cpc', label:'CPC (link)', short:'CPC', group:'engagement', fmt:'currency2', align:'right', tone:'#06b6d4', w:104, spark:true }, { id:'engagement',label:'Engagements', short:'Engmt.', group:'engagement', fmt:'int', align:'right', tone:'#8351ff', w:112 }, // Video { id:'video_views',label:'Video plays', short:'Plays', group:'video', fmt:'int', align:'right', tone:'#06b6d4', w:112 }, { id:'hook_rate', label:'Hook rate (3s)', short:'Hook', group:'video', fmt:'pct', align:'right', tone:'#10b981', w:108 }, { id:'hold_rate', label:'Hold (complete)',short:'Hold', group:'video', fmt:'pct', align:'right', tone:'#f59e0b', w:118 }, { id:'thumb_stop',label:'Thumb-stop', short:'T-stop', group:'video', fmt:'pct', align:'right', tone:'#ec4899', w:108 }, // Funnel { id:'atc', label:'Adds to cart', short:'ATC', group:'funnel', fmt:'int', align:'right', tone:'#f59e0b', w:104 }, { id:'checkout', label:'Checkouts', short:'IC', group:'funnel', fmt:'int', align:'right', tone:'#06b6d4', w:104 }, { id:'purchases', label:'Purchases', short:'Purch.', group:'funnel', fmt:'int', align:'right', tone:'#10b981', w:108 }, { id:'conv_rate', label:'Conv. rate', short:'CVR', group:'funnel', fmt:'pct', align:'right', tone:'#8351ff', w:104 }, ]; const METRIC_BY_ID = Object.fromEntries(METRIC_CATALOG.map(m=>[m.id,m])); const METRIC_GROUPS = [ { id:'performance', label:'Performance' }, { id:'delivery', label:'Delivery' }, { id:'engagement', label:'Engagement' }, { id:'video', label:'Video' }, { id:'funnel', label:'Funnel' }, ]; /* Column packs — the preset views a buyer toggles between (Meta-style) */ const COLUMN_PACKS = { performance: { label:'Performance', cols:['spend','results','cpa','roas','revenue'] }, delivery: { label:'Delivery', cols:['spend','impressions','reach','frequency','cpm'] }, engagement: { label:'Engagement', cols:['spend','clicks','ctr','cpc','engagement'] }, video: { label:'Video', cols:['spend','video_views','hook_rate','hold_rate','thumb_stop'] }, funnel: { label:'Funnel', cols:['spend','atc','checkout','purchases','conv_rate'] }, }; const PACK_ORDER = ['performance','delivery','engagement','video','funnel']; /* ---------- Derive the FULL metric set for any entity --------------- */ /* entity must carry: spend, cpc, ctr, cpa, roas, conv. Returns all metrics. */ function fullMetrics(e, platform){ const seed = strSeed((e.id||'x') + ':' + (platform||'')) ; const r = mulberry(seed); const spend = e.spend; const cpc = e.cpc || 0.8; const ctr = e.ctr || 1.5; const conv = e.conv != null ? e.conv : Math.round(spend/(e.cpa||25)); const roas = e.roas || 2.5; const clicks = Math.round(spend / Math.max(cpc,0.05)); const impressions = Math.round(clicks / Math.max(ctr/100, 0.002)); const frequency = +(1.6 + r()*2.8).toFixed(2); const reach = Math.round(impressions / frequency); const cpm = +(spend / Math.max(impressions,1) * 1000).toFixed(2); const revenue = Math.round(spend * roas); // funnel const atcRate = ({ meta:0.22, facebook:0.21, instagram:0.20, google:0.18, tiktok:0.26, snapchat:0.14, x:0.12 })[platform] || 0.20; const atc = Math.round(clicks * atcRate); const checkout = Math.round(atc * (0.45 + r()*0.2)); const purchases= conv; const conv_rate= +(purchases / Math.max(clicks,1) * 100).toFixed(2); // video (meaningful on social/video platforms) const video_views = Math.round(impressions * (0.55 + r()*0.3)); const hook_rate = +(18 + r()*22).toFixed(1); const hold_rate = +(6 + r()*12).toFixed(1); const thumb_stop= +(22 + r()*18).toFixed(1); const engagement= Math.round(clicks * (1.4 + r()*1.2)); return { spend, results:conv, cpa:e.cpa, roas, revenue, impressions, reach, frequency, cpm, clicks, ctr, cpc, engagement, video_views, hook_rate, hold_rate, thumb_stop, atc, checkout, purchases, conv_rate, }; } /* ---------- Ads level (one grain below ad sets) -------------------- */ const AD_ANGLES = [ { angle:'UGC Testimonial', format:'video', headline:'“I haven’t stopped wearing these.”', body:'Real customer review · 0:21' }, { angle:'Founder Story', format:'video', headline:'Why we built this', body:'Brand POV · 0:34' }, { angle:'Product Hero', format:'image', headline:'Engineered for the long run', body:'Single image · static' }, { angle:'Carousel — Range', format:'carousel', headline:'Find your fit', body:'5-card carousel' }, { angle:'Before / After', format:'image', headline:'See the difference in 14 days', body:'Split image · static' }, { angle:'Offer / Promo', format:'image', headline:'48 hours only — 20% off', body:'Promo card · static' }, { angle:'Creator Collab', format:'video', headline:'Trusted by the people you follow', body:'Spark/UGC · 0:18' }, { angle:'Demo / How-to', format:'video', headline:'Watch it work', body:'Tutorial · 0:27' }, ]; const FORMAT_TONE = { video:'#ec4899', image:'#2f7bff', carousel:'#f59e0b' }; function buildAds(adset, platform){ const r = mulberry(strSeed(adset.id + ':ads')); const n = 2 + Math.floor(r()*3); // 2-4 ads const out = []; for (let i=0;i 0.12 ? 'active' : 'paused'; out.push({ id:`ad_${adset.id}_${i}`, name:`${a.angle} · v${i+1}`, angle:a.angle, format:a.format, headline:a.headline, body:a.body, tone:FORMAT_TONE[a.format], platform, spend, cpc, ctr, cpa, roas, conv: Math.round(spend/Math.max(cpa,1)), status, }); } return out; } /* Flatten a campaign list to the chosen level (campaign | adset | ad) */ function rowsForLevel(campaigns, level){ if (level==='campaign') return campaigns; if (level==='adset') return campaigns.flatMap(c => c.adsets.map(as => ({ ...as, platform:c.platform, _parent:c.name }))); // ad level return campaigns.flatMap(c => c.adsets.flatMap(as => buildAds(as, c.platform).map(ad => ({ ...ad, _parent:as.name, _grandparent:c.name })))); } /* ---------- Per-account metric blocklist ---------------------------- */ /* Product rule: some clients never see certain metrics ANYWHERE — dashboard KPIs, charts, tables, Report Builder, and all exports. Al Munawarah (city/brand account): ROAS is meaningless — no revenue. */ const ACCOUNT_METRIC_BLOCKLIST = { acc_almunawarah: ['roas'] }; function metricHiddenFor(accountId, metricId){ const ids = ACCOUNT_METRIC_BLOCKLIST[accountId]; return !!ids && ids.includes(String(metricId||'').toLowerCase()); } /* Convenience for deep components that don't receive accountId props: uses the globally-selected account (set by GlobalProvider each render, same pattern as ACTIVE_CURRENCY). */ function metricHiddenHere(metricId){ return metricHiddenFor(window.ACTIVE_ACCOUNT_ID || '', metricId); } Object.assign(window, { METRIC_CATALOG, METRIC_BY_ID, METRIC_GROUPS, COLUMN_PACKS, PACK_ORDER, fullMetrics, buildAds, rowsForLevel, AD_ANGLES, FORMAT_TONE, ACCOUNT_METRIC_BLOCKLIST, metricHiddenFor, metricHiddenHere, });