/* 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)=>(
{c||{String.fromCharCode(65+i)}} |
))}
{rows.map((r,ri)=>(
| {ri+1} |
{r.map((cell,ci)=>{
const num = typeof cell==='number' || (/^[$£€]?[\d,.]+%?$/.test(String(cell)) && String(cell).trim()!=='');
return {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' && (
)}
{plan.type==='gsheet' && (
)}
{(plan.type==='csv' || plan.type==='xlsx') && (
)}
{error &&
{error}
}
);
}
Object.assign(window, { MediaPlanView, loadMediaPlans, saveMediaPlan, toSheetEmbed, parseSheetFile, SheetTable });