/* Report Builder — exporters: PPTX (native/editable), PDF (16:9 snapshot), XLSX (multi-sheet) + optional ZIP-of-CSVs. All three ship together (v1). Heavy libraries lazy-load on first export so the dashboard stays light. Everything derives from (reportDef, dataset, brandKit) — same source as the live preview. PDF page = 13.333×7.5in landscape, never A4. ======================================================================== */ /* ---------- Lazy CDN loader ------------------------------------------- */ const R_LIBS = { pptx: { url:'https://cdn.jsdelivr.net/npm/pptxgenjs@3.12.0/dist/pptxgen.bundle.js', test:()=>window.PptxGenJS }, jspdf: { url:'https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js', test:()=>window.jspdf }, html2canvas: { url:'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js',test:()=>window.html2canvas }, jszip: { url:'https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js', test:()=>window.JSZip }, }; const _rLibP = {}; function rLoadLib(name){ const lib = R_LIBS[name]; if (lib.test()) return Promise.resolve(); if (_rLibP[name]) return _rLibP[name]; _rLibP[name] = new Promise((res, rej)=>{ const s = document.createElement('script'); s.src = lib.url; s.async = true; s.onload = ()=> lib.test() ? res() : rej(new Error(name+' failed to initialise')); s.onerror = ()=> rej(new Error('Could not load '+name+' (network)')); document.head.appendChild(s); }); return _rLibP[name]; } /* ---------- Shared helpers --------------------------------------------- */ function rExportFilename(def, ds, ext){ const brand = (def.brandName || (ds.account && ds.account.name) || 'Report').trim(); const period = def.scope.start ? (def.scope.start + ' to ' + def.scope.end) : ''; const base = `${brand} — ${period} — Performance Report`.replace(/[\\/:*?"<>|]/g,'-'); return base + '.' + ext; } function rDownloadBlob(blob, filename){ const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); setTimeout(()=>{ URL.revokeObjectURL(a.href); a.remove(); }, 2000); } const _imgCache = {}; async function rImgData(path){ if (!path) return null; if (_imgCache[path]) return _imgCache[path]; try { const res = await fetch(path, { credentials:'include' }); if (!res.ok) return null; const blob = await res.blob(); const data = await new Promise((r)=>{ const fr = new FileReader(); fr.onload=()=>r(fr.result); fr.readAsDataURL(blob); }); const dims = await new Promise((r)=>{ const im = new Image(); im.onload=()=>r({w:im.naturalWidth,h:im.naturalHeight}); im.onerror=()=>r(null); im.src=data; }); _imgCache[path] = { data, dims }; return _imgCache[path]; } catch(e){ return null; } } /* Platform brand icons for PPTX: render the React SVG offscreen, rasterize to PNG. color resolves `currentColor` (use #fff on branded slides). */ const _iconPngCache = {}; async function rBrandIconPng(icon, color){ const key = icon + '|' + color; if (_iconPngCache[key] !== undefined) return _iconPngCache[key]; let out = null; const host = document.createElement('div'); host.style.cssText = 'position:fixed;left:-9999px;top:0;color:' + color + ';'; document.body.appendChild(host); const root = ReactDOM.createRoot(host); try { await new Promise(res=>{ root.render(React.createElement(BrandIcon, { name:icon, size:256 })); setTimeout(res, 80); }); const svg = host.querySelector('svg'); if (svg){ svg.setAttribute('xmlns','http://www.w3.org/2000/svg'); const ser = new XMLSerializer().serializeToString(svg).replace(/currentColor/g, color); out = await new Promise(res=>{ const im = new Image(); im.onload = ()=>{ try { const c = document.createElement('canvas'); c.width = 256; c.height = 256; c.getContext('2d').drawImage(im, 0, 0, 256, 256); res(c.toDataURL('image/png')); } catch(e){ res(null); } }; im.onerror = ()=>res(null); im.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(ser); }); } } catch(e){ out = null; } root.unmount(); host.remove(); _iconPngCache[key] = out; return out; } /* Platform mark for exports: official PNG first, rasterized SVG fallback. */ async function rPlatformImg(platform, fallbackColor){ const p = typeof rPlatformLogo==='function' ? rPlatformLogo(platform) : null; if (p){ const im = await rImgData(p); if (im) return im.data; } return await rBrandIconPng(rPlatform(platform).icon, fallbackColor || '#1A1A17'); } /* Preflight: slides referencing platforms with no rows in range. */ function rPreflight(def, ds){ const have = new Set(rDatasetPlatforms(ds)); const warned = new Set(); const out = []; def.slides.forEach((s,i)=>{ const plats = new Set(); if (s.platform) plats.add(s.platform); (s.blocks||[]).forEach(b=>{ if (b.binding && b.binding.platform) plats.add(b.binding.platform); }); plats.forEach(p=>{ if (!have.has(p) && !warned.has(p+':'+i)){ warned.add(p+':'+i); out.push({ slide:i+1, title:s.title, platform:p, msg:`Slide ${i+1} “${s.title}” references ${rPlatform(p).label}, which has no data in this range.` }); } }); }); return out; } /* ============================ XLSX / CSV ============================ */ const R_XLSX_METRICS = ['spend','impressions','reach','clicks','link_clicks','ctr','cpc','cpm', 'conversions','cpa','cvr','revenue','roas','video_views','engagements','leads']; function _sheetName(s){ return s.replace(/[\\/*?:\[\]]/g,'').slice(0,31); } function _entityTable(ds, level, platform, currency){ const XM = R_XLSX_METRICS.filter(k=>!rMetricBlockedFor(ds.account.id, k)); const { rows } = rEntityRows(ds, level, { platform, sortKey:'spend', sortDir:'desc' }); const head = [ level==='campaign'?'Campaign':level==='adset'?'Ad Set':'Ad', ...(level!=='campaign' ? ['Parent'] : []), 'Status', ...XM.map(k=>rMetric(k).label + (rMetric(k).fmt.startsWith('currency')?` (${currency})`:'')) ]; const campaignsById = Object.fromEntries((ds.entities.campaigns||[]).map(c=>[c.id,c.name])); const adsetsById = Object.fromEntries((ds.entities.adsets||[]).map(a=>[a.id,a.name])); const body = rows.map(r=>[ r.name, ...(level==='adset' ? [campaignsById[r.campaign_id]||''] : level==='ad' ? [adsetsById[r.adset_id]||''] : []), r.status||'', ...XM.map(k=>{ const v = rMetricValue(r.cur,k); return rMetric(k).fmt==='pct' || rMetric(k).fmt==='x' || rMetric(k).fmt==='currency2' ? +v.toFixed(3) : Math.round(v*100)/100; }), ]); return [head, ...body]; } function rBuildWorkbookSheets(def, ds){ const currency = ds.account.currency || 'USD'; /* per-account blocklist (e.g. Al Munawarah: no ROAS in any export) */ const XM = R_XLSX_METRICS.filter(k=>!rMetricBlockedFor(ds.account.id, k)); const sheets = []; // [name, aoa] /* Summary */ const plats = rByPlatform(ds); const tot = rTotals(ds); const summary = [ ['Account', ds.account.name], ['Brand', def.brandName || ds.account.name], ['Currency', currency], ['Period', ds.range.start + ' → ' + ds.range.end], ['Comparison period', ds.compare.start + ' → ' + ds.compare.end], ['Generated', new Date().toISOString().slice(0,16).replace('T',' ')], [], ['Platform', ...XM.map(k=>rMetric(k).label), 'Δ Spend %', 'Δ Results %'], ...plats.map(p=>{ const dS = rDelta(rMetricValue(p.cur,'spend'), rMetricValue(p.prev,'spend'),'spend'); const dC = rDelta(rMetricValue(p.cur,'conversions'), rMetricValue(p.prev,'conversions'),'conversions'); return [ rPlatform(p.platform).label, ...XM.map(k=>+rMetricValue(p.cur,k).toFixed(3)), dS?+dS.pct.toFixed(1):'', dC?+dC.pct.toFixed(1):'' ]; }), [ 'TOTAL', ...XM.map(k=>+rMetricValue(tot.cur,k).toFixed(3)), (()=>{ const d=rDelta(rMetricValue(tot.cur,'spend'),rMetricValue(tot.prev,'spend'),'spend'); return d?+d.pct.toFixed(1):''; })(), (()=>{ const d=rDelta(rMetricValue(tot.cur,'conversions'),rMetricValue(tot.prev,'conversions'),'conversions'); return d?+d.pct.toFixed(1):''; })() ], ]; const brand = rBrand(ds); if (brand.hasData){ summary.push([], ['Followers gained (period)']); brand.platforms.forEach(p=>summary.push([rPlatform(p).label, brand.gained[p]||0, brand.latest[p]!=null?('total '+brand.latest[p]):'' ])); summary.push(['Combined', brand.totalGained, brand.totalFollowers||'']); } sheets.push(['Summary', summary]); /* Per-platform entity sheets in locked order */ rDatasetPlatforms(ds).forEach(p=>{ const P = rPlatform(p).label; [['campaign','Campaigns'],['adset','Ad Sets'],['ad','Ads']].forEach(([lvl,lbl])=>{ const table = _entityTable(ds, lvl, p, currency); if (table.length > 1) sheets.push([_sheetName(`${P} ${lbl}`), table]); }); }); /* Daily data */ const daily = [ ['Date','Platform',...XM.filter(k=>rMetric(k).agg==='sum').map(k=>rMetric(k).label)], ...(ds.daily||[]).map(r=>[ r.date, rPlatform(r.platform).label, ...XM.filter(k=>rMetric(k).agg==='sum').map(k=>Number(r[k])||0) ]) ]; sheets.push(['Daily Data', daily]); /* Definitions */ const defs = [ ['Metric','Definition / formula'], ...rAllMetrics().map(m=>{ let f = 'Reported by platform (summed daily)'; if (m.agg && m.agg.num) f = `${rMetric(m.agg.num).label} ÷ ${rMetric(m.agg.den).label}` + (m.agg.mult?` × ${m.agg.mult}`:''); if (m.agg && m.agg.formula) f = 'Custom: ' + m.agg.formula; if (m.agg === 'last') f = 'Latest reported value (not summed)'; return [m.label, f + (m.note ? ' — ' + m.note : '')]; }) ]; sheets.push(['Definitions', defs]); return sheets; } async function rExportXLSX(def, ds, opts){ const sheets = rBuildWorkbookSheets(def, ds); if (opts && opts.csvZip){ await rLoadLib('jszip'); const zip = new JSZip(); sheets.forEach(([name, aoa])=>{ const ws = XLSX.utils.aoa_to_sheet(aoa); zip.file(name.replace(/[^\w؀-ۿ -]/g,'') + '.csv', '' + XLSX.utils.sheet_to_csv(ws)); }); const blob = await zip.generateAsync({ type:'blob' }); rDownloadBlob(blob, rExportFilename(def, ds, 'zip')); return; } const wb = XLSX.utils.book_new(); sheets.forEach(([name, aoa])=>{ const ws = XLSX.utils.aoa_to_sheet(aoa); ws['!cols'] = (aoa[0]||[]).map((_,i)=>({ wch: i===0?34:14 })); XLSX.utils.book_append_sheet(wb, ws, name); }); const out = XLSX.write(wb, { bookType:'xlsx', type:'array' }); rDownloadBlob(new Blob([out], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}), rExportFilename(def, ds, 'xlsx')); } /* ============================ PDF ============================ */ /* Mounts selected slides offscreen at full 1280×720, snapshots each at 2x. */ async function rExportPDF(def, ds, kit, slideIds, onProgress){ await Promise.all([rLoadLib('jspdf'), rLoadLib('html2canvas')]); const theme = rTheme(kit, def.lang); const currency = ds.account.currency || 'USD'; const slides = def.slides.filter(s=>slideIds.includes(s.id)); const host = document.createElement('div'); host.style.cssText = 'position:fixed;left:-99999px;top:0;z-index:-1;'; document.body.appendChild(host); const root = ReactDOM.createRoot(host); const renderAll = ()=> new Promise(res=>{ root.render(
{slides.map(s=>(
))}
); setTimeout(res, 650); // charts render synchronously (animations off); settle fonts/images }); try { await renderAll(); try { await (document.fonts && document.fonts.ready); } catch(e){} const { jsPDF } = window.jspdf; const pdf = new jsPDF({ orientation:'landscape', unit:'in', format:[13.333, 7.5] }); for (let i=0; i{ doc.querySelectorAll('.r-slide, .r-slide *').forEach(el=>{ el.style.letterSpacing = 'normal'; }); } }); const img = canvas.toDataURL('image/jpeg', 0.92); if (i > 0) pdf.addPage([13.333,7.5], 'landscape'); pdf.addImage(img, 'JPEG', 0, 0, 13.333, 7.5); } pdf.save(rExportFilename(def, ds, 'pdf')); } finally { root.unmount(); host.remove(); } } /* ============================ PPTX ============================ */ const IN_W = 13.333, IN_H = 7.5, MARGIN = 0.55; const PX = (px)=> px / R_SLIDE_W * IN_W; // logical px → inches function _hex(c){ return String(c||'#000000').replace('#',''); } async function rExportPPTX(def, ds, kit, slideIds, onProgress){ await rLoadLib('pptx'); const theme = rTheme(kit, def.lang); const lang = def.lang, rtl = theme.rtl; const currency = ds.account.currency || 'USD'; const pptx = new PptxGenJS(); pptx.defineLayout({ name:'WIDE', width:IN_W, height:IN_H }); pptx.layout = 'WIDE'; pptx.author = 'Relay'; pptx.title = def.name; const T = (o)=> Object.assign({ fontFace: rtl ? 'Sakkal Majalla' : 'Calibri', lang: rtl ? 'ar-SA' : 'en-US', rtlMode: rtl }, o); const alignStart = rtl ? 'right' : 'left'; const bgImg = rBrandedBg(theme) ? await rImgData(rBrandedBg(theme)) : null; const logo = theme.logo ? await rImgData(theme.logo) : null; const logoDark= theme.logoDark ? await rImgData(theme.logoDark) : null; const logoRatio = (img)=> img && img.dims ? img.dims.w/img.dims.h : 3; const periodStr = def.scope.start ? rDateLabel(def.scope.start,lang)+' – '+rDateLabel(def.scope.end,lang) : ''; const slides = def.slides.filter(s=>slideIds.includes(s.id)); const middle = def.slides.filter(s=>!['cover','agenda','thank_you'].includes(s.type)); const brandedBase = (s)=>{ if (bgImg){ s.background = { data: bgImg.data }; return; } s.background = { color:_hex(theme.main) }; /* brand-derived geometry when the kit has no cover artwork */ const ex = rtl ? -2.2 : IN_W - 4.3; s.addShape(pptx.ShapeType.ellipse, { x:ex+1.5, y:-2.4, w:6.5, h:6.5, fill:{ color:_hex(theme.secondary), transparency:86 }, line:{ type:'none' } }); s.addShape(pptx.ShapeType.ellipse, { x:ex+2.2, y:4.6, w:4.5, h:4.5, fill:{ color:_hex(theme.main), transparency:100 }, line:{ color:_hex(rMix(theme.tertiary, theme.main, 0.35)), width:2 } }); s.addShape(pptx.ShapeType.ellipse, { x:ex, y:1.15, w:1.9, h:1.9, fill:{ color:_hex(theme.tertiary), transparency:82 }, line:{ type:'none' } }); }; const onBrand = _hex(theme.onMain); /* agency credit ("Prepared by FLNT") — separate from the client brand */ const agency = def.agency && def.agency.enabled ? def.agency : null; const agencyLogo = agency && agency.logo ? await rImgData(agency.logo) : null; const addAgency = (s, x, y, align)=>{ if (!agency) return; let ax = x; if (agencyLogo){ const w = Math.min(0.24 * (agencyLogo.dims ? agencyLogo.dims.w/agencyLogo.dims.h : 3), 0.9); s.addImage({ data:agencyLogo.data, x: align==='center' ? ax - w/2 - 0.9 : ax + 1.05, y:y-0.015, h:0.24, w }); } s.addText([ { text:'Prepared by ', options:{ transparency:30 } }, { text: agency.name || 'FLNT', options:{ bold:true } }, ], T({ x: align==='center' ? ax-2 : ax, y, w:4, h:0.3, fontSize:10.5, color:onBrand, align: align || 'left', rtlMode:false, lang:'en-US' })); }; for (let i=0; i 3.8 ? 3.8 : 1.15*logoRatio(lg) }); if (def.brandName) s.addText(def.brandName.toUpperCase(), T({ x:MARGIN, y:2.6, w:IN_W-MARGIN*2, h:0.45, fontSize:15, color:onBrand, transparency:20, charSpacing:4, bold:true, align:alignStart })); s.addText(sl.title || rStr('performance_report',lang), T({ x:MARGIN, y:3.0, w:IN_W-MARGIN*2, h:1.7, fontSize:40, bold:true, color:onBrand, align:alignStart })); if (periodStr) s.addText(rStr('exec_summary',lang)+' — '+periodStr, T({ x:MARGIN, y:4.75, w:IN_W-MARGIN*2, h:0.55, fontSize:16, color:onBrand, transparency:10, align:alignStart })); s.addShape(pptx.ShapeType.roundRect, { x: rtl ? IN_W-MARGIN-1.0 : MARGIN, y:5.45, w:1.0, h:0.08, fill:{ color:_hex(theme.secondary) }, line:{ type:'none' }, rectRadius:0.04 }); addAgency(s, MARGIN, IN_H-0.62, 'left'); } else if (sl.type === 'thank_you'){ brandedBase(s); const lg = logoDark || logo; if (lg){ const w = Math.min(1.55*logoRatio(lg), 4.2); s.addImage({ data:lg.data, x:(IN_W-w)/2, y:1.75, h:1.55, w }); } s.addText(sl.title || rStr('thank_you',lang), T({ x:1, y:3.65, w:IN_W-2, h:1.1, fontSize:40, bold:true, color:onBrand, align:'center' })); if (def.brandName) s.addText(def.brandName, T({ x:1, y:4.85, w:IN_W-2, h:0.55, fontSize:16, color:onBrand, transparency:15, align:'center' })); addAgency(s, IN_W/2, 5.55, 'center'); } else if (sl.type === 'divider'){ brandedBase(s); const dividers = def.slides.filter(x=>x.type==='divider'); const secNum = String(dividers.indexOf(sl)+1).padStart(2,'0'); /* oversized section-number watermark */ s.addText(secNum, T({ x: rtl ? 0.1 : IN_W-4.7, y:IN_H-3.3, w:4.6, h:3.3, fontSize:190, bold:true, color:onBrand, transparency:91, align: rtl?'left':'right', rtlMode:false, lang:'en-US' })); /* platform logo (official PNG in a white chip) */ if (sl.platform){ const ic = await rPlatformImg(sl.platform, '#ffffff'); if (ic){ s.addShape(pptx.ShapeType.roundRect, { x: rtl ? IN_W-MARGIN-1.05 : MARGIN, y:1.5, w:1.05, h:1.05, fill:{ color:'FFFFFF', transparency:8 }, line:{ type:'none' }, rectRadius:0.18, shadow:{ type:'outer', blur:8, offset:2, angle:90, color:'000000', opacity:0.3 } }); s.addImage({ data:ic, x: (rtl ? IN_W-MARGIN-1.05 : MARGIN)+0.17, y:1.67, w:0.71, h:0.71 }); } } s.addText((rtl ? '' : 'SECTION ') + secNum, T({ x:MARGIN, y:2.72, w:IN_W-MARGIN*2, h:0.35, fontSize:12, bold:true, color:onBrand, transparency:35, charSpacing:3, align:alignStart, rtlMode:false, lang:'en-US' })); s.addText(sl.title, T({ x:MARGIN, y:3.05, w:IN_W-MARGIN*2, h:1.4, fontSize:40, bold:true, color:onBrand, align:alignStart })); s.addShape(pptx.ShapeType.roundRect, { x: rtl ? IN_W-MARGIN-1.2 : MARGIN, y:4.6, w:1.2, h:0.1, fill:{ color:_hex(theme.secondary) }, line:{ type:'none' }, rectRadius:0.05 }); } else if (sl.type === 'agenda'){ await _pptxHeader(pptx, s, sl.title || rStr('agenda',lang), theme, T, alignStart, periodStr, logo, null); const rowsPerCol = Math.ceil(middle.length/2); const rowH = Math.min(0.72, 5.4/Math.max(rowsPerCol,1)); // spread down the slide middle.forEach((m, idx)=>{ const col = Math.floor(idx/rowsPerCol), row = idx % rowsPerCol; let x = MARGIN + col * ((IN_W-MARGIN*2)/2 + 0.15); const y = 1.7 + row * rowH; s.addShape(pptx.ShapeType.roundRect, { x: rtl ? IN_W - x - 0.42 : x, y:y+0.03, w:0.42, h:0.42, fill:{ color:_hex(theme.main) }, line:{ type:'none' }, rectRadius:0.08 }); s.addText(String(idx+1).padStart(2,'0'), T({ x: rtl ? IN_W - x - 0.42 : x, y:y+0.03, w:0.42, h:0.42, fontSize:12, bold:true, color:onBrand, align:'center', valign:'middle' })); s.addText((m.type==='divider' ? (lang==='ar'?'قسم: ':'Section: ') : '') + m.title, T({ x: rtl ? IN_W - x - ((IN_W-MARGIN*2)/2 - 0.3) : x+0.58, y, w:(IN_W-MARGIN*2)/2 - 0.65, h:0.48, fontSize:15, bold:true, color:'33332E', align:alignStart, valign:'middle' })); }); } else { await _pptxHeader(pptx, s, sl.title, theme, T, alignStart, periodStr, logo, sl.platform || null); /* block layout cursor mirroring the DOM 12-col grid; a single block gets the whole content area (tall) */ let y = 1.6; const blocks = sl.blocks || []; const tall = blocks.length === 1; let bi = 0; while (bi < blocks.length && y < IN_H - 0.6){ const blk = blocks[bi]; const meta = R_BLOCKS[blk.type] || { span:12 }; const span = (blk.options && blk.options.span) || meta.span; const next = blocks[bi+1]; const nextSpan = next ? ((next.options && next.options.span) || (R_BLOCKS[next.type]||{span:12}).span) : 12; if (span <= 6 && nextSpan <= 6 && next){ const wHalf = (IN_W - MARGIN*2 - 0.3)/2; const h1 = await _pptxBlock(pptx, s, blk, def, ds, theme, T, { x:MARGIN, y, w:wHalf }, currency, alignStart); const h2 = await _pptxBlock(pptx, s, next, def, ds, theme, T, { x:MARGIN+wHalf+0.3, y, w:wHalf }, currency, alignStart); y += Math.max(h1, h2) + 0.25; bi += 2; } else { const h = await _pptxBlock(pptx, s, blk, def, ds, theme, T, { x:MARGIN, y, w:IN_W-MARGIN*2, tall }, currency, alignStart); y += h + 0.25; bi += 1; } } } /* light-slide chrome: top brand strip + footer (brand · agency · page) */ if (!['cover','thank_you','divider'].includes(sl.type)){ s.addShape(pptx.ShapeType.rect, { x:0, y:0, w:IN_W, h:0.055, fill:{ color:_hex(theme.main) }, line:{ type:'none' } }); s.addShape(pptx.ShapeType.rect, { x:IN_W*0.55, y:0, w:IN_W*0.45, h:0.055, fill:{ color:_hex(theme.tertiary) }, line:{ type:'none' } }); s.addShape(pptx.ShapeType.rect, { x:MARGIN, y:IN_H-0.42, w:IN_W-MARGIN*2, h:0.008, fill:{ color:'ECECE6' }, line:{ type:'none' } }); s.addText(def.brandName || '', T({ x:MARGIN, y:IN_H-0.38, w:4, h:0.28, fontSize:9.5, bold:true, color:'A3A39A', align:'left', rtlMode:false })); const pageTxt = agency ? [{ text:'Prepared by ', options:{ color:'A3A39A' } }, { text:(agency.name||'FLNT'), options:{ bold:true, color:'8B8B80' } }, { text:' · ' + (def.slides.indexOf(sl)+1) + ' / ' + def.slides.length, options:{ color:'A3A39A' } }] : [{ text:(def.slides.indexOf(sl)+1) + ' / ' + def.slides.length, options:{ color:'A3A39A' } }]; s.addText(pageTxt, T({ x:IN_W-5-MARGIN, y:IN_H-0.38, w:5, h:0.28, fontSize:9.5, align:'right', rtlMode:false, lang:'en-US' })); } } onProgress && onProgress('Writing file…'); await pptx.writeFile({ fileName: rExportFilename(def, ds, 'pptx') }); } async function _pptxHeader(pptx, s, title, theme, T, alignStart, periodStr, logo, platform){ s.background = { color:'FBFBF9' }; const rtl = theme.rtl; s.addShape(pptx.ShapeType.roundRect, { x: rtl ? IN_W-MARGIN-0.07 : MARGIN, y:0.45, w:0.07, h:0.55, fill:{ color:_hex(theme.main) }, line:{ type:'none' }, rectRadius:0.035 }); s.addText(title, T({ x: rtl ? MARGIN : MARGIN+0.2, y:0.38, w:IN_W-MARGIN*2-1.8, h:0.6, fontSize:24, bold:true, color:'22221E', align:alignStart })); if (periodStr) s.addText(periodStr, T({ x: rtl ? MARGIN : MARGIN+0.2, y:0.95, w:IN_W-MARGIN*2-1.8, h:0.32, fontSize:11, color:'9A9A90', align:alignStart })); /* platform logo (before the kit logo, on the header's end side) */ let endX = rtl ? MARGIN : IN_W - MARGIN; if (logo){ const w = Math.min(0.5 * (logo.dims ? logo.dims.w/logo.dims.h : 3), 1.8); s.addImage({ data:logo.data, x: rtl ? endX : endX - w, y:0.42, h:0.5, w }); endX = rtl ? endX + w + 0.18 : endX - w - 0.18; } if (platform){ const ic = await rPlatformImg(platform, '#1A1A17'); if (ic) s.addImage({ data:ic, x: rtl ? endX : endX - 0.46, y:0.44, w:0.46, h:0.46 }); } } /* Render one block natively; returns consumed height in inches. */ async function _pptxBlock(pptx, s, blk, def, ds, theme, T, box, currency, alignStart){ const lang = def.lang; const b = blk.binding || {}; const type = blk.type; if (type === 'scorecard' || type === 'metric_grid'){ const t = b.platform ? rTotals(ds, b.platform) : rTotals(ds); const metrics = (b.metrics && b.metrics.length ? b.metrics : ['spend','conversions','cpa','roas']); const cols = metrics.length <= 4 ? metrics.length : Math.ceil(metrics.length/2); const rows = Math.ceil(metrics.length/cols); const big = box.tall && metrics.length <= 4; // solo scorecards fill the slide const gap = big ? 0.26 : 0.2; const cardW = (box.w - gap*(cols-1))/cols; const cardH = big ? 3.4 : 1.45; const cy0 = big ? box.y + 0.6 : box.y; metrics.forEach((k, i)=>{ const m = rMetric(k); if (!m) return; const cx = box.x + (i%cols)*(cardW+gap), cy = cy0 + Math.floor(i/cols)*(cardH+gap); /* Total Followers = brand metric (brand_metrics_daily), not ad sums */ let cur, prev, d, followersSub = null; if (k === 'followers'){ const br = rBrand(ds); cur = br.totalFollowers || br.totalGained || 0; prev = null; d = null; followersSub = br.totalGained ? '+' + br.totalGained.toLocaleString('en-US') + ' ' + rStr('followers_by_platform',lang) : null; } else { cur = rMetricValue(t.cur,k); prev = rMetricValue(t.prev,k); d = rDelta(cur, prev, k); } const padTop = big ? 0.85 : 0.1; s.addShape(pptx.ShapeType.roundRect, { x:cx, y:cy, w:cardW, h:cardH, fill:{ color:'FFFFFF' }, line:{ color:'E7E7E2', width:1 }, rectRadius:0.1 }); s.addShape(pptx.ShapeType.roundRect, { x:cx+0.16, y:cy+(big?0.55:0.09), w:big?0.55:0.42, h:0.05, fill:{ color:_hex(theme.ramp[i % theme.ramp.length]) }, line:{ type:'none' }, rectRadius:0.025 }); s.addText(rMetricLabel(k,lang), T({ x:cx+0.16, y:cy+padTop, w:cardW-0.32, h:0.34, fontSize: big?13:11, bold:true, color:'6D6D64', align:alignStart })); s.addText(rCompact(cur, k==='followers' ? 'int' : m.fmt, currency, lang), T({ x:cx+0.16, y:cy+padTop+0.32, w:cardW-0.32, h:big?0.7:0.5, fontSize: big?30:23, bold:true, color:_hex(theme.main), align:alignStart })); if (d) s.addText((d.up?'▲ ':'▼ ') + Math.abs(d.pct).toFixed(1) + '% ' + rStr('vs_prev',lang), T({ x:cx+0.16, y:cy+padTop+(big?1.1:0.86), w:cardW-0.32, h:0.3, fontSize: big?11:9.5, bold:true, color: d.tone==='good' ? '0C7A43' : d.tone==='bad' ? 'B42323' : '5E5E55', align:alignStart })); else if (followersSub) s.addText(followersSub, T({ x:cx+0.16, y:cy+padTop+(big?1.1:0.86), w:cardW-0.32, h:0.3, fontSize: big?11:9.5, bold:true, color:'0C7A43', align:alignStart })); }); return big ? 5.2 : rows*(cardH+gap); } if (type === 'trend'){ const keys = (b.metrics && b.metrics.length ? b.metrics : ['spend']); const data = rSeries(ds, keys, b.platform); if (!data.length) return _pptxEmpty(pptx, s, box, T, lang); const labels = data.map(r=>r.label); const series = keys.map((k,i)=>({ name: rMetricLabel(k,lang), labels, values: data.map(r=>+(r[k]||0).toFixed(3)) })); const showCompare = blk.options && blk.options.showCompare !== false; if (showCompare) keys.forEach((k)=>{ series.push({ name: rMetricLabel(k,lang)+' (prev)', labels, values: data.map(r=>+(r['_cmp_'+k]||0).toFixed(3)) }); }); const colors = keys.map((k,i)=>_hex(theme.ramp[i%theme.ramp.length])); const h = box.tall ? 5.3 : 3.2; s.addChart(pptx.ChartType.line, series, { x:box.x, y:box.y, w:box.w, h, chartColors: showCompare ? [...colors, ...colors] : colors, lineSize: 2.25, lineSmooth: true, lineDataSymbol:'none', catAxisLabelFontSize: 10, valAxisLabelFontSize: 10, legendFontSize: 11, showLegend: true, legendPos:'b', valGridLine:{ color:'E5E5E0', style:'solid', size:1 }, catGridLine:{ style:'none' } }); return h; } if (type === 'bar'){ const key = b.metric || 'spend'; let items; if ((b.dimension||'platform')==='platform'){ items = rByPlatform(ds).map(p=>({ name:(lang==='ar'?rPlatform(p.platform).ar:rPlatform(p.platform).label), v:rMetricValue(p.cur,key) })); } else { const { rows } = rEntityRows(ds, b.level||'campaign', { platform:b.platform, sortKey:key, topN:b.topN||8 }); items = rows.map(r=>({ name:r.name, v:rMetricValue(r.cur,key) })); } items = items.filter(d=>d.v>0); if (!items.length) return _pptxEmpty(pptx, s, box, T, lang); const h = box.tall ? 5.3 : 3.0; s.addChart(pptx.ChartType.bar, [{ name:rMetricLabel(key,lang), labels:items.map(d=>d.name), values:items.map(d=>+d.v.toFixed(2)) }], { x:box.x, y:box.y, w:box.w, h, chartColors: items.map((_,i)=>_hex(theme.ramp[i%theme.ramp.length])), barDir:'col', showLegend:false, catAxisLabelFontSize:10, valAxisLabelFontSize:10, valGridLine:{ color:'E5E5E0', style:'solid', size:1 } }); return h; } if (type === 'donut'){ const key = b.metric || 'spend'; const items = rByPlatform(ds).map(p=>({ name:(lang==='ar'?rPlatform(p.platform).ar:rPlatform(p.platform).label), v:rMetricValue(p.cur,key) })).filter(d=>d.v>0); if (!items.length) return _pptxEmpty(pptx, s, box, T, lang); const h = box.tall ? 5.2 : 3.0; s.addText(rStr('spend_share',lang), T({ x:box.x, y:box.y-0.05, w:box.w, h:0.32, fontSize:12.5, bold:true, color:'4B4B44', align:alignStart })); s.addChart(pptx.ChartType.doughnut, [{ name:rMetricLabel(key,lang), labels:items.map(d=>d.name), values:items.map(d=>+d.v.toFixed(2)) }], { x:box.x, y:box.y+0.3, w:box.w, h:h-0.35, chartColors: items.map((_,i)=>_hex(theme.ramp[i%theme.ramp.length])), holeSize: 60, showLegend:true, legendPos: theme.rtl?'l':'r', legendFontSize:11, showPercent:true, dataLabelFontSize:10 }); return h; } if (type === 'table'){ const metrics = b.metrics && b.metrics.length ? b.metrics : ['spend','impressions','clicks','ctr','conversions','cpa']; const res = rEntityRows(ds, b.level||'campaign', { platform:b.platform, sortKey:b.sortKey||'spend', sortDir:b.sortDir||'desc', topN:Math.min(b.topN||10, 12), filters:b.filters||[] }); const rows = res.others ? [...res.rows, res.others] : res.rows; if (!rows.length) return _pptxEmpty(pptx, s, box, T, lang); const heat = (blk.options && blk.options.heatmap) || []; const maxOf = {}; heat.forEach(k=>{ maxOf[k] = Math.max(...rows.map(r=>rMetricValue(r.cur,k)), 0.0001); }); const nameHead = b.level==='ad'?rStr('ads',lang):b.level==='adset'?rStr('adsets',lang):rStr('campaigns',lang); const headFill = _hex(rMix('#FFFFFF', theme.main, 0.08)); const headRow = [ { text:nameHead, options:{ bold:true, color:'55554E', fill:{color:headFill}, align:alignStart } }, ...metrics.map(k=>({ text:rMetricLabel(k,lang), options:{ bold:true, color:'55554E', fill:{color:headFill}, align: theme.rtl?'left':'right' } })) ]; const bodyRows = rows.map((r,ri)=>[ { text:String(r.name).slice(0,48), options:{ bold:true, color:'33332E', align:alignStart, italic:!!r._others, fill: ri%2===1 ? { color:'FAFAF8' } : undefined } }, ...metrics.map(k=>{ const m = rMetric(k), v = rMetricValue(r.cur,k); let fill = ri%2===1 ? { color:'FAFAF8' } : undefined; if (heat.includes(k) && !r._others){ fill = { color:_hex(rMix('#FFFFFF', theme.tertiary, 0.14 + 0.5*Math.min(1, v/maxOf[k]))) }; } return { text:rFmt(v, m?m.fmt:'int', currency, lang), options:{ align: theme.rtl?'left':'right', fill, color:'3D3D37' } }; }), ]); const rowH = box.tall ? Math.min(0.44, 5.3/(rows.length+1)) : 0.32; const h = (rows.length+1)*rowH + 0.12; s.addTable([headRow, ...bodyRows], { x:box.x, y:box.y, w:box.w, colW: [box.w*0.3, ...metrics.map(()=> box.w*0.7/metrics.length)], fontSize: box.tall ? 11 : 10, fontFace: T({}).fontFace, border:{ type:'solid', color:'EFEFE9', pt:0.5 }, rowH, valign:'middle', margin:0.05 }); return h; } if (type === 'ranked_list'){ const key = b.metric || 'impressions', sec = b.secondary || 'ctr'; const mk = rMetric(key), ms = rMetric(sec); const { rows } = rEntityRows(ds, b.level||'campaign', { platform:b.platform, sortKey:key, sortDir:'desc', topN:b.topN||5 }); if (!rows.length) return _pptxEmpty(pptx, s, box, T, lang); const total = rows.reduce((a,r)=>a+rMetricValue(r.cur,key),0); const rowH = box.tall ? Math.min(0.95, (5.3 - 0.14*rows.length)/rows.length) : 0.72; rows.forEach((r,i)=>{ const y = box.y + i*(rowH+0.14); const v = rMetricValue(r.cur,key), share = total>0? v/total*100 : 0; s.addShape(pptx.ShapeType.roundRect, { x:box.x, y, w:box.w, h:rowH, fill:{color:'FFFFFF'}, line:{color:'E7E7E2', width:1}, rectRadius:0.08 }); const badge = Math.min(0.52, rowH-0.2); const bx = theme.rtl ? box.x + box.w - badge - 0.16 : box.x + 0.16; s.addShape(pptx.ShapeType.ellipse, { x:bx, y:y+(rowH-badge)/2, w:badge, h:badge, fill:{color:_hex(theme.secondary)}, line:{type:'none'} }); s.addText(String(i+1), T({ x:bx, y:y+(rowH-badge)/2, w:badge, h:badge, fontSize:15, bold:true, color:_hex(rContrastText(theme.secondary)), align:'center', valign:'middle' })); const tx = theme.rtl ? box.x + 0.18 : box.x + badge + 0.34; const tw = box.w - badge - 0.6; s.addText(String(r.name).slice(0,60), T({ x:tx, y:y+0.07, w:tw, h:rowH*0.45, fontSize:14, bold:true, color:'2B2B26', align:alignStart })); s.addText(`${rCompact(v, mk?mk.fmt:'int', currency, lang)} — ${share.toFixed(1)}% • ${rMetricLabel(sec,lang)} ${rFmt(rMetricValue(r.cur,sec), ms?ms.fmt:'pct', currency, lang)}`, T({ x:tx, y:y+rowH*0.5, w:tw, h:rowH*0.4, fontSize:10.5, color:'7C7C72', align:alignStart })); }); return rows.length*(rowH+0.14); } if (type === 'brand'){ const br = rBrand(ds); if (!br.hasData) return _pptxEmpty(pptx, s, box, T, lang); s.addText(rStr('followers_by_platform',lang), T({ x:box.x, y:box.y, w:box.w, h:0.28, fontSize:10.5, bold:true, color:'4B4B44', align:alignStart })); let y = box.y + 0.32; br.platforms.forEach(p=>{ s.addText((lang==='ar'?rPlatform(p).ar:rPlatform(p).label), T({ x:box.x, y, w:box.w*0.55, h:0.26, fontSize:10, bold:true, color:'3D3D37', align:alignStart })); s.addText((br.gained[p]?'+'+br.gained[p].toLocaleString('en-US'):'—') + (br.latest[p]!=null ? ` (${Number(br.latest[p]).toLocaleString('en-US')})` : ''), T({ x: theme.rtl ? box.x : box.x+box.w*0.5, y, w:box.w*0.5, h:0.26, fontSize:10, bold:true, color:_hex(theme.main), align: theme.rtl?'left':'right' })); y += 0.28; }); s.addText(rStr('total_followers',lang) + ' ' + (br.totalFollowers ? br.totalFollowers.toLocaleString('en-US') : '+'+br.totalGained.toLocaleString('en-US')), T({ x:box.x, y:y+0.05, w:box.w, h:0.28, fontSize:10.5, bold:true, color:_hex(theme.main), align:alignStart })); return (y + 0.4) - box.y; } if (type === 'heatmap'){ /* approximate: compact table of weeks × days with fills */ const key = b.metric || 'spend'; const series = rSeries(ds, [key], b.platform); if (!series.length) return _pptxEmpty(pptx, s, box, T, lang); const weeks = {}; series.forEach(r=>{ const d = new Date(r.date+'T00:00:00'); const dow = d.getDay(); const ws = new Date(d); ws.setDate(d.getDate()-dow); const wk = ws.toISOString().slice(0,10); (weeks[wk] = weeks[wk] || Array(7).fill(null))[dow] = r[key]; }); const wkeys = Object.keys(weeks).sort(); const max = Math.max(...series.map(r=>r[key]||0), 0.0001); const days = lang==='ar' ? ['أحد','إثن','ثلا','أرب','خمي','جمع','سبت'] : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; const tRows = [ [ {text:'', options:{fill:{color:'FFFFFF'}}}, ...wkeys.map(w=>({ text:new Date(w+'T00:00:00').toLocaleDateString('en-US',{month:'short',day:'numeric'}), options:{ fontSize:7, color:'9A9A90', align:'center' } })) ], ...days.map((dl,dow)=>[ { text:dl, options:{ fontSize:7.5, color:'8B8B80' } }, ...wkeys.map(w=>{ const v = weeks[w][dow]; const fill = v==null ? 'F4F4F0' : _hex(rMix('#FFFFFF', theme.main, 0.12+0.72*Math.min(1,v/max))); return { text: v!=null && v/max>0.02 ? rCompact(v,'int',currency,'en') : '', options:{ fill:{color:fill}, fontSize:6.5, align:'center', color:_hex(rContrastText('#'+fill)) } }; }), ]), ]; const hmRowH = box.tall ? 0.6 : 0.3; const h = 8*hmRowH + 0.1; s.addTable(tRows, { x:box.x, y:box.y, w:box.w, rowH:hmRowH, border:{ type:'solid', color:'FFFFFF', pt:1 }, fontFace:T({}).fontFace, margin:0.02, valign:'middle' }); return h; } if (type === 'perf_flags'){ const perf = rPerfInsights(ds, b.level||'ad', { platform:b.platform, lang, currency }); if (!perf) return _pptxEmpty(pptx, s, box, T, lang); const half = (box.w - 0.25) / 2; const card = (x, tone, label, name, reasons)=>{ s.addShape(pptx.ShapeType.roundRect, { x, y:box.y, w:half, h:1.55, fill:{ color:tone.bg }, line:{ color:tone.border, width:1 }, rectRadius:0.1 }); s.addText(label, T({ x:x+0.15, y:box.y+0.08, w:half-0.3, h:0.3, fontSize:10, bold:true, color:tone.fg, charSpacing:2, align:alignStart })); s.addText(String(name).slice(0,60), T({ x:x+0.15, y:box.y+0.38, w:half-0.3, h:0.34, fontSize:13, bold:true, color:'26261F', align:alignStart })); s.addText(reasons.slice(0,3).join(' • '), T({ x:x+0.15, y:box.y+0.74, w:half-0.3, h:0.72, fontSize:9.5, color:tone.fg, align:alignStart, valign:'top' })); }; const bestLbl = lang==='ar' ? '👑 الأفضل أداءً' : (lang==='both' ? '👑 الأفضل أداءً · TOP PERFORMER' : '👑 TOP PERFORMER'); const worstLbl = lang==='ar' ? '⚠️ يحتاج معالجة' : (lang==='both' ? '⚠️ يحتاج معالجة · NEEDS ATTENTION' : '⚠️ NEEDS ATTENTION'); card(box.x, { bg:'F2F9F2', border:'BFE3BF', fg:'1D7A34' }, bestLbl, perf.best.name, perf.best.reasons); card(box.x+half+0.25, { bg:'FDF5F0', border:'F0CDB8', fg:'B4560F' }, worstLbl, perf.worst.name, perf.worst.reasons); return 1.7; } if (type === 'priorities'){ const items = (blk.options && blk.options.items && blk.options.items.length) ? blk.options.items : rRecommendations(ds, lang, currency); if (!items.length) return _pptxEmpty(pptx, s, box, T, lang); const PRI = { high:{ bar:'D9482B', label: lang==='ar'?'عالية':'HIGH' }, med: { bar:'E2A615', label: lang==='ar'?'متوسطة':'MED' }, low: { bar:'4F9A67', label: lang==='ar'?'منخفضة':'LOW' } }; const cols = 2, gap = 0.22; const cardW = (box.w - gap)/cols; const rowsN = Math.ceil(items.length/cols); const cardH = Math.min(1.65, (5.3 - gap*(rowsN-1)) / rowsN); items.forEach((it, i)=>{ const p = PRI[it.pri] || PRI.med; const cx = box.x + (i%cols)*(cardW+gap), cy = box.y + Math.floor(i/cols)*(cardH+gap); s.addShape(pptx.ShapeType.roundRect, { x:cx, y:cy, w:cardW, h:cardH, fill:{ color:'FFFFFF' }, line:{ color:'E7E7E2', width:1 }, rectRadius:0.09 }); s.addShape(pptx.ShapeType.rect, { x: theme.rtl ? cx+cardW-0.07 : cx, y:cy, w:0.07, h:cardH, fill:{ color:p.bar }, line:{ type:'none' } }); s.addText(String(it.title), T({ x:cx+0.18, y:cy+0.06, w:cardW-1.0, h:0.3, fontSize:12, bold:true, color:'26261F', align:alignStart })); s.addText(p.label, T({ x: theme.rtl ? cx+0.14 : cx+cardW-0.85, y:cy+0.08, w:0.68, h:0.24, fontSize:8, bold:true, color:p.bar, align:'center', rtlMode:false })); s.addText([ { text: String(it.why).replace(/\n/g,' '), options:{ color:'55554E', breakLine:true } }, { text: '→ ' + String(it.action).replace(/\n/g,' '), options:{ bold:true, color:_hex(theme.main) } }, ], T({ x:cx+0.18, y:cy+0.38, w:cardW-0.36, h:cardH-0.46, fontSize:9, align:alignStart, valign:'top' })); }); return rowsN*(cardH+gap); } if (type === 'text'){ const o = blk.options || {}; let bullets = o.bullets && o.bullets.length ? o.bullets : (o.autoInsights ? rAutoInsights(ds, lang, currency) : []); const items = []; if (o.text) items.push({ text:o.text, options:T({ fontSize:15.5, color:'3A3A34', align:alignStart, breakLine:true }) }); bullets.forEach(t=>items.push({ text:t, options:T({ fontSize:15, color:'3A3A34', align:alignStart, bullet:{ code:'2022', indent:16 }, breakLine:true, paraSpaceAfter:12 }) })); if (!items.length) return 0.1; const h = Math.min(0.6 + items.length*(box.tall?0.9:0.65), 5.3); s.addText(items, T({ x:box.x, y:box.y, w:box.w, h, valign:'top' })); return h; } if (type === 'image'){ const o = blk.options || {}; if (!o.src) return 0.1; const img = await rImgData(o.src); if (!img) return 0.1; const ratio = img.dims ? img.dims.w/img.dims.h : 1.6; const h = Math.min(3.4, box.w/ratio); s.addImage({ data:img.data, x:box.x + (box.w - h*ratio)/2, y:box.y, w:h*ratio, h }); return h; } return 0.1; } function _pptxEmpty(pptx, s, box, T, lang){ s.addShape(pptx.ShapeType.roundRect, { x:box.x, y:box.y, w:box.w, h:0.8, fill:{color:'FAFAF7'}, line:{ color:'D9D9D4', width:1, dashType:'dash' }, rectRadius:0.09 }); s.addText(rStr('no_data',lang), T({ x:box.x, y:box.y, w:box.w, h:0.8, fontSize:10.5, color:'8B8B80', align:'center', valign:'middle' })); return 0.9; } /* ============================ Export modal ============================ */ function ReportExportModal({ open, onClose, def, ds, kit }){ const lang = def ? def.lang : 'en'; const [format, setFormat] = useState('pptx'); const [csvZip, setCsvZip] = useState(false); const [sel, setSel] = useState({}); const [busy, setBusy] = useState(false); const [progress, setProgress] = useState(''); const [error, setError] = useState(''); useEffect(()=>{ if (open && def) setSel(Object.fromEntries(def.slides.map(s=>[s.id, true]))); setError(''); setProgress(''); }, [open]); if (!open || !def || !ds) return null; const warnings = rPreflight(def, ds); const selIds = def.slides.filter(s=>sel[s.id]).map(s=>s.id); const allOn = selIds.length === def.slides.length; const typeIcon = { cover:'file', agenda:'columns', exec_summary:'sparkles', divider:'chev-right', content:'columns', thank_you:'check' }; const doExport = async ()=>{ setBusy(true); setError(''); try { if (format==='xlsx') await rExportXLSX(def, ds, { csvZip }); else if (format==='pdf') await rExportPDF(def, ds, kit, selIds, setProgress); else await rExportPPTX(def, ds, kit, selIds, setProgress); onClose(); } catch(e){ console.error('[report export]', e); setError(e.message || 'Export failed'); } setBusy(false); setProgress(''); }; const fmts = [ { id:'pptx', label:'PowerPoint (.pptx)', desc:'Native editable slides & charts' }, { id:'pdf', label:'PDF (16:9)', desc:'Pixel-identical to the preview' }, { id:'xlsx', label:'Excel (.xlsx)', desc:'Multi-sheet data workbook' }, ]; return ( {}:onClose} width={640}>
Export report
{def.name}
{fmts.map(f=>( ))}
{format==='xlsx' && ( )} {format!=='xlsx' && (
Slides ({selIds.length}/{def.slides.length})
{def.slides.map((s,i)=>( ))}
)} {warnings.length > 0 && (
{warnings.slice(0,3).map((w,i)=>(
{w.msg}
))}
)} {error &&
{error}
}
{busy ? progress || 'Working…' : rExportFilename(def, ds, format==='xlsx' ? (csvZip?'zip':'xlsx') : format)}
Cancel {busy ? 'Exporting…' : 'Export'}
); } Object.assign(window, { rExportXLSX, rExportPDF, rExportPPTX, rPreflight, rExportFilename, rLoadLib, ReportExportModal, });