/* App shell ========================================================== Global context provider (Account + Date Range + scope + sidebar) → collapsible Sidebar with the fixed nav hierarchy → master controls → Workspace router. Every view reads useGlobal() so a change to the Account or Date Range instantly re-derives all data. =================================================================== */ /* ---- Date helpers + Meta-style presets ---- */ const TODAY_ANCHOR = new Date(); // live: tracks the real current date const _d = (d)=>{ const x=new Date(d); x.setHours(0,0,0,0); return x; }; function addDays(d,n){ const x=_d(d); x.setDate(x.getDate()+n); return x; } function sameDay(a,b){ return !!(a&&b) && _d(a).getTime()===_d(b).getTime(); } function fmtMD(d){ return _d(d).toLocaleDateString('en-US',{month:'short',day:'numeric'}); } function fmtMDY(d){ return _d(d).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}); } function diffDays(a,b){ return Math.round((_d(b)-_d(a))/86400000); } const DATE_PRESETS = [ { id:'today', label:'Today', range:()=>[TODAY_ANCHOR, TODAY_ANCHOR] }, { id:'yest', label:'Yesterday', range:()=>{ const y=addDays(TODAY_ANCHOR,-1); return [y,y]; } }, { id:'7d', label:'Last 7 days', range:()=>[addDays(TODAY_ANCHOR,-6), TODAY_ANCHOR] }, { id:'14d', label:'Last 14 days', range:()=>[addDays(TODAY_ANCHOR,-13), TODAY_ANCHOR] }, { id:'28d', label:'Last 28 days', range:()=>[addDays(TODAY_ANCHOR,-27), TODAY_ANCHOR] }, { id:'30d', label:'Last 30 days', range:()=>[addDays(TODAY_ANCHOR,-29), TODAY_ANCHOR] }, { id:'thisweek', label:'This week', range:()=>{ const s=addDays(TODAY_ANCHOR,-TODAY_ANCHOR.getDay()); return [s, TODAY_ANCHOR]; } }, { id:'lastweek', label:'Last week', range:()=>{ const e=addDays(TODAY_ANCHOR,-TODAY_ANCHOR.getDay()-1); return [addDays(e,-6), e]; } }, { id:'thismonth', label:'This month', range:()=>[new Date(TODAY_ANCHOR.getFullYear(),TODAY_ANCHOR.getMonth(),1), TODAY_ANCHOR] }, { id:'lastmonth', label:'Last month', range:()=>[new Date(TODAY_ANCHOR.getFullYear(),TODAY_ANCHOR.getMonth()-1,1), new Date(TODAY_ANCHOR.getFullYear(),TODAY_ANCHOR.getMonth(),0)] }, { id:'maximum', label:'Maximum', range:()=>[new Date(2025,7,1), TODAY_ANCHOR] }, ]; function presetRange(id){ const p=DATE_PRESETS.find(x=>x.id===id); return p?p.range():DATE_PRESETS[5].range(); } function matchPreset(s,e){ const p=DATE_PRESETS.find(x=>{ const [a,b]=x.range(); return sameDay(a,s)&&sameDay(b,e); }); return p?p.id:'custom'; } function presetLabel(dr){ const p=DATE_PRESETS.find(x=>x.id===dr.preset); return p?p.label:'Custom'; } function formatRange(dr){ if(!dr) return 'Last 30 days'; const s=new Date(dr.start), e=new Date(dr.end); if(sameDay(s,e)) return fmtMD(s); return (s.getFullYear()!==e.getFullYear()) ? `${fmtMDY(s)} – ${fmtMDY(e)}` : `${fmtMD(s)} – ${fmtMD(e)}`; } function chartRangeFor(dr){ const n=diffDays(new Date(dr.start),new Date(dr.end))+1; if(n<=7)return '7d'; if(n<=14)return '14d'; return '30d'; } /* ===================== Global context ============================= */ const GlobalContext = React.createContext(null); function useGlobal(){ return React.useContext(GlobalContext); } function GlobalProvider({ currentUser, integrations, brand, children }){ const myAccounts = useMemo(()=>accessibleAccounts(currentUser), [currentUser]); const [accountId, setAccountId] = useState(()=>(myAccounts[0]||ACCOUNTS[0]||{id:null}).id); const [dateRange, setDateRange] = useState(()=>{ const [s,e]=presetRange('30d'); return { preset:'30d', start:+s, end:+e, compare:false }; }); const range = dateRange.preset; const setRange = (preset)=>{ const [s,e]=presetRange(preset); setDateRange(d=>({ preset, start:+s, end:+e, compare:d.compare })); }; /* Reporter role: Report Builder is the ONLY workspace — start there and route-guard every other scope. */ const isReporter = !!currentUser && currentUser.role === 'reporter'; const [scope, setScope] = useState(isReporter ? 'reports' : 'overview'); useEffect(()=>{ if (isReporter && scope !== 'reports') setScope('reports'); }, [isReporter, scope]); const [search, setSearch] = useState(''); const [collapsed, setCollapsed] = useState(true); const [mobileNav, setMobileNav] = useState(false); const [moduleAll, setModuleAll] = useState(loadModuleSettings); /* keep account valid if access changes */ useEffect(()=>{ const fb=(myAccounts[0]||ACCOUNTS[0]); if(fb && !myAccounts.find(a=>a.id===accountId)) setAccountId(fb.id); }, [myAccounts]); const moduleSettings = moduleSettingsFor(moduleAll, accountId); const setModuleSettings = (s)=> setModuleAll(prev=>{ const next={...prev,[accountId]:s}; saveLS(MODULE_LS, next); return next; }); /* Render money in the selected account's real currency (SAR/EGP/USD…). */ const activeAccount = myAccounts.find(a=>a.id===accountId) || ACCOUNTS.find(a=>a.id===accountId); setActiveCurrency(activeAccount?.currency || 'USD'); window.ACTIVE_ACCOUNT_ID = accountId; // drives per-account metric blocklist (metricHiddenHere) const feeds = connectedPlatforms(integrations); /* Load the live data bundles the current view needs (overview + scoped platform). */ const platformScope = scope.startsWith('platform:') ? scope.split(':')[1] : 'overview'; useEnsureData(accountId, platformScope, dateRange); const value = { currentUser, myAccounts, feeds, isReporter, accountId, setAccountId, range, setRange, dateRange, setDateRange, scope, setScope, search, setSearch, collapsed, setCollapsed, mobileNav, setMobileNav, moduleSettings, setModuleSettings, brand: brand || loadBranding(), }; return {children}; } /* ===================== Master controls =========================== */ const _initials = (name)=> String(name||'?').trim().split(/\s+/).map(s=>s[0]||'').slice(0,2).join('').toUpperCase() || '?'; function AccountSwitcher(){ const { accountId, setAccountId, myAccounts } = useGlobal(); const [q, setQ] = useState(''); const list = (myAccounts && myAccounts.length) ? myAccounts : ACCOUNTS; const acc = list.find(a=>a.id===accountId) || list[0] || { name:'—', handle:'', tier:'' }; const initial = _initials(acc.name); const shown = q.trim() ? list.filter(a => (String(a.name||'')+' '+String(a.handle||'')+' '+String(a.tier||'')).toLowerCase().includes(q.trim().toLowerCase())) : list; return ( {initial} {acc.name} {acc.handle} · {acc.tier} }> {(close)=>(
Ad Accounts
setQ(e.target.value)} onKeyDown={e=>{ if(e.key==='Enter' && shown.length){ setAccountId(shown[0].id); setQ(''); close(); } }} placeholder="Search accounts…" className="w-full bg-transparent outline-none text-[12.5px] placeholder:text-ink-400"/> {q && }
{shown.length===0 &&
No accounts match “{q}”
} {shown.map(a => { const init = _initials(a.name); const active = a.id === accountId; return ( ); })}
{shown.length} of {ACCOUNTS.length} accounts · access set in Admin
)} ); } /* Meta Ads Manager-style calendar month */ function CalMonth({ view, sel, hover, onPick, onHover }){ const y=view.getFullYear(), m=view.getMonth(); const lead=new Date(y,m,1).getDay(); // 0=Sun const dim=new Date(y,m+1,0).getDate(); const cells=[]; for(let i=0;i a&&b && _d(d)>=_d(a) && _d(d)<=_d(b); return (
{view.toLocaleDateString('en-US',{month:'long',year:'numeric'})}
{['S','M','T','W','T','F','S'].map((w,i)=>
{w}
)} {cells.map((d,i)=>{ if(!d) return
; const disabled = _d(d)>_d(TODAY_ANCHOR); const isStart=sameDay(d,a), isEnd=sameDay(d,b), inb=within(d), single=isStart&&isEnd; const isToday=sameDay(d,TODAY_ANCHOR); return (
); })}
); } function DateRangePicker(){ const { dateRange, setDateRange } = useGlobal(); const [open,setOpen] = useState(false); const [sel,setSel] = useState({ start:new Date(dateRange.start), end:new Date(dateRange.end) }); const [hover,setHover] = useState(null); const [compare,setCompare] = useState(!!dateRange.compare); const [view,setView] = useState(()=>{ const e=new Date(dateRange.end); return new Date(e.getFullYear(), e.getMonth()-1, 1); }); const ref = useRef(null); const onReset = ()=>{ setSel({ start:new Date(dateRange.start), end:new Date(dateRange.end) }); setCompare(!!dateRange.compare); const e=new Date(dateRange.end); setView(new Date(e.getFullYear(), e.getMonth()-1, 1)); setHover(null); }; useEffect(()=>{ if(open) onReset(); }, [open]); useEffect(()=>{ if(!open) return; const h=(e)=>{ if(ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown',h); return ()=>document.removeEventListener('mousedown',h); }, [open]); const activePreset = (sel.start&&sel.end) ? matchPreset(sel.start, sel.end) : 'custom'; const pickPreset = (p)=>{ const [s,e]=p.range(); setSel({ start:s, end:e }); setHover(null); setView(new Date(e.getFullYear(), e.getMonth()-1, 1)); }; const pickDay = (d)=>{ if(!sel.start || sel.end){ setSel({ start:d, end:null }); } // begin a new range else if(_d(d) < _d(sel.start)){ setSel({ start:d, end:null }); } else { setSel({ start:sel.start, end:d }); } }; const apply = ()=>{ const s=sel.start, e=sel.end||sel.start; setDateRange({ preset: matchPreset(s,e), start:+_d(s), end:+_d(e), compare }); setOpen(false); }; const navMax = new Date(TODAY_ANCHOR.getFullYear(), TODAY_ANCHOR.getMonth(), 1); const canFwd = +new Date(view.getFullYear(), view.getMonth()+1, 1) < +navMax; return (
{open && (
{/* preset rail */}
{DATE_PRESETS.map(p=>( ))}
{/* calendar side */}
{/* date inputs */}
{sel.start?fmtMDY(sel.start):'—'}
{sel.end?fmtMDY(sel.end):(sel.start?'Select end':'—')}
{/* months */}
setHover(null)}>
{/* footer */}
Reset Update
)}
); } function GlobalSearch(){ const { search, setSearch } = useGlobal(); return (
setSearch(e.target.value)} placeholder="Search…" className="w-full h-full pl-8 pr-3 rounded-lg border border-ink-200 dark:border-white/10 bg-white dark:bg-ink-900/40 text-[12.5px] focus:outline-none focus:border-accent-400"/>
); } function WorkspaceUserMenu({ user, onOpenAdmin, onSignOut, canAdmin }){ return ( }> {(close)=>(
{user.name}
{user.email}
{canAdmin && ( )}
)} ); } function ConnectionBanner({ integrations, canAdmin, onOpenAdmin }){ const problems = integrations.filter(i => (i.kind!=='db' && i.kind!=='custom') && (i.status!=='connected' || i.health==='degraded')); if (problems.length===0) return null; const offline = problems.filter(i=>i.status!=='connected'); const tone = offline.length>0 ? { bg:'bg-rose-50 dark:bg-rose-500/10', border:'border-rose-200 dark:border-rose-500/25', text:'text-rose-700 dark:text-rose-300' } : { bg:'bg-amber-50 dark:bg-amber-500/10', border:'border-amber-200 dark:border-amber-500/25', text:'text-amber-700 dark:text-amber-300' }; return (
{offline.length>0?`${offline.length} connection${offline.length>1?'s':''} offline`:''}{offline.length>0&&problems.length>offline.length?' · ':''}{problems.length>offline.length?`${problems.length-offline.length} degraded`:''}
{problems.map(i=>({i.name}{i.status!=='connected'?'offline':'degraded'}))}
{canAdmin && Fix in Admin}
); } /* ===================== Sidebar =================================== */ function SidebarItem({ icon, label, active, collapsed, onClick, indent, brandIcon, tone }){ return ( ); } function Sidebar({ onOpenAdmin, canAdmin }){ const { scope, setScope, collapsed, setCollapsed, mobileNav, setMobileNav, moduleSettings, feeds, accountId, currentUser, brand, isReporter } = useGlobal(); const [platOpen, setPlatOpen] = useState(true); const canSettings = can(currentUser, 'settings.manage'); const dataVersion = useDataVersion(); /* re-render the rail when live data lands (this