/* Media Plan module ================================================= Account-linked. Two ingestion paths, both previewed in-app: • File upload — PDF (in-app viewer) / CSV / XLSX (rendered as table) • Google Sheets URL — embedded interactive iframe Upload/link tools are RBAC-gated (super_admin + admin only). =================================================================== */ const MEDIA_LS = 'relay_media_plans_v1'; function loadMediaPlans(){ return loadLS(MEDIA_LS, {}); } function saveMediaPlan(accountId, plan){ const all = loadMediaPlans(); if (plan) all[accountId] = plan; else delete all[accountId]; saveLS(MEDIA_LS, all); return all; } /* Convert a Google Sheets edit/view URL into an embeddable preview URL */ function toSheetEmbed(url){ try { if (/\/pubhtml|\/preview|output=embed/.test(url)) return url; return url.replace(/\/edit.*$/, '/preview').replace(/\/view.*$/, '/preview'); } catch(e){ return url; } } /* Parse a CSV/XLSX File into { cols, rows } via SheetJS */ function parseSheetFile(file){ return new Promise((resolve, reject)=>{ const reader = new FileReader(); reader.onload = (e)=>{ try { const data = new Uint8Array(e.target.result); const wb = XLSX.read(data, { type:'array' }); const ws = wb.Sheets[wb.SheetNames[0]]; const arr = XLSX.utils.sheet_to_json(ws, { header:1, blankrows:false, defval:'' }); const cols = (arr[0]||[]).map(c=>String(c)); const rows = arr.slice(1).map(r=>cols.map((_,i)=> r[i]==null?'':r[i])); resolve({ cols, rows, sheetNames: wb.SheetNames }); } catch(err){ reject(err); } }; reader.onerror = reject; reader.readAsArrayBuffer(file); }); } function readAsDataURL(file){ return new Promise((res,rej)=>{ const r=new FileReader(); r.onload=e=>res(e.target.result); r.onerror=rej; r.readAsDataURL(file); }); } /* ---------- Sheet table preview ------------------------------------ */ function SheetTable({ cols, rows }){ if (!cols || !cols.length) return
Empty sheet.
; return (
{cols.map((c,i)=>( ))} {rows.map((r,ri)=>( {r.map((cell,ci)=>{ const num = typeof cell==='number' || (/^[$£€]?[\d,.]+%?$/.test(String(cell)) && String(cell).trim()!==''); return ; })} ))}
{c||{String.fromCharCode(65+i)}}
{ri+1}{String(cell)}
); } /* ---------- Upload / link empty state ------------------------------ */ function MediaPlanEmpty({ canManage, onFile, onLink, busy, error }){ const fileRef = useRef(null); const [url, setUrl] = useState(''); const [drag, setDrag] = useState(false); if (!canManage){ return (
No media plan for this account yet
An Admin or Super Admin can upload a plan or link a Google Sheet here.
); } return (
{e.preventDefault();setDrag(true);}} onDragLeave={()=>setDrag(false)} onDrop={e=>{e.preventDefault();setDrag(false); if(e.dataTransfer.files[0]) onFile(e.dataTransfer.files[0]);}} className={`rounded-2xl border-2 border-dashed p-8 text-center transition ${drag?'border-accent-400 bg-accent-50/60 dark:bg-accent-500/10':'border-ink-200 dark:border-white/15 bg-white dark:bg-ink-900/40'}`}>
Upload a media plan
Drag & drop or browse · PDF, CSV or XLSX
e.target.files[0]&&onFile(e.target.files[0])}/> fileRef.current.click()} disabled={busy}>{busy?'Reading…':'Choose file'}
{['pdf','csv','xlsx'].map(t=>{t})}
Link a Google Sheet
Paste a share URL — it embeds as an interactive sheet.
setUrl(e.target.value)} placeholder="https://docs.google.com/spreadsheets/d/…" className="w-full h-10 px-3 rounded-xl border border-ink-200 dark:border-white/10 bg-white dark:bg-ink-900/60 text-[12.5px] font-mono focus:outline-none focus:border-accent-400 mb-3"/> url.trim()&&onLink(url.trim())} disabled={!url.trim()}>Embed sheet
Tip: set the sheet to “Anyone with the link can view”.
{error &&
{error}
}
); } /* ---------- The Media Plan view ------------------------------------ */ function MediaPlanView({ accountId, currentUser, canManage, pushAudit }){ const [plan, setPlan] = useState(()=>loadMediaPlans()[accountId] || null); const [busy, setBusy] = useState(false); const [error, setError] = useState(''); useEffect(()=>{ setPlan(loadMediaPlans()[accountId]||null); setError(''); }, [accountId]); const acc = ACCOUNTS.find(a=>a.id===accountId); const commit = (p) => { setPlan(p); saveMediaPlan(accountId, p); if (pushAudit) pushAudit(currentUser, 'integration.update', `Media plan · ${acc.name}`, p?`${p.type.toUpperCase()} · ${p.title}`:'removed'); }; const onFile = async (file) => { setError(''); setBusy(true); try { const ext = (file.name.split('.').pop()||'').toLowerCase(); if (ext==='pdf'){ if (file.size > 8*1024*1024){ setError('PDF is over 8MB — too large to persist in the demo store. Use a smaller file or a Google Sheet link.'); setBusy(false); return; } const dataUrl = await readAsDataURL(file); commit({ type:'pdf', title:file.name.replace(/\.pdf$/i,''), fileName:file.name, dataUrl, uploadedBy:currentUser.name, uploadedAt:Date.now() }); } else if (ext==='csv' || ext==='xlsx' || ext==='xls'){ const { cols, rows } = await parseSheetFile(file); commit({ type: ext==='csv'?'csv':'xlsx', title:file.name.replace(/\.(csv|xlsx|xls)$/i,''), fileName:file.name, cols, rows, uploadedBy:currentUser.name, uploadedAt:Date.now() }); } else { setError('Unsupported file type. Upload a PDF, CSV or XLSX.'); } } catch(err){ setError('Could not read that file. '+(err.message||'')); } setBusy(false); }; const onLink = (url) => { if (!/docs\.google\.com\/spreadsheets/.test(url)){ setError('That doesn’t look like a Google Sheets URL.'); return; } commit({ type:'gsheet', title:'Linked Google Sheet', url, embed:toSheetEmbed(url), uploadedBy:currentUser.name, uploadedAt:Date.now() }); }; const header = (

Media Plan

{acc.name} · {plan?`linked ${plan.type.toUpperCase()}`:'not set'}
); if (!plan){ return
{header}
; } return (
{header}
{plan.type}
{plan.title}
{plan.fileName||plan.url} · added by {plan.uploadedBy}
{plan.type==='gsheet' && Open} {plan.type==='pdf' && Download} {canManage && <> }
{plan.type==='pdf' && (