1/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2 board-print.js â Relatório de impressão / PDF do board de BI 3 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 4 Porta o modelo do site financeiro (imprimirRelatorio): mini-capa 100vh, 5 Ãndice, folha-capa de capÃtulo com numeral grande, cabeçalho/rodapé 6 corrido, thead repetido, toggle Cor/P&B â tudo via window.print() nativo, 7 sem lib externa. O andaime é montado em runtime e desfeito no afterprint; 8 a tela nunca muda. O visual de papel vive no @media print do styles.css. 9 10 Dois modos: 11 · 'pagina' â painel ATUAL (já carregado na tela). Caminho rápido. 12 · 'completo' â só a seção 1 (Dados ao Vivo): capa + Ãndice + os 8 painéis 13 como capÃtulos. Aguarda os load() async (Meta/GSC) antes 14 de imprimir. 15 16 Gráficos: o Chart.js vive em <canvas> e reflui/borra no @media print. Antes 17 de imprimir, cada chart vivo vira um <img> PNG de alta resolução (dpr 3, 18 sem animação); o CSS de print esconde o canvas e mostra a imagem. Tudo é 19 removido no afterprint. 20 21 Toggle de dev: ?print=pagina|completo emula o layout na tela; &cor=cor|bw 22 força o modo de cor sem mexer no localStorage. 23 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 24(function () { 25 'use strict'; 26 27 // Painéis da seção 1 (Dados ao Vivo) que entram no relatório completo, em 28 // ordem. Cruzamentos fica de fora (pesado/condicional); o mapa de 29 // concorrentes (canvas 2D nativo) não pertence a esta seção. 30 const REPORT_PANELS = ['painel-diario', 'insights', 'metrics', 'pago', 'organico', 'mensais', 'conteudo', 'publico']; 31 32 // key do painel â objeto Bi* global com load() async + flag _loaded 33 const PANEL_LOADERS = { 34 'painel-diario': 'BiPainelDiario', 35 'insights': 'BiInsightsLive', 36 'metrics': 'BiMetricas', 37 'pago': 'BiPago', 38 'organico': 'BiOrganicoLive', 39 'mensais': 'BiCrescimento', 40 'conteudo': 'BiConteudo', 41 'publico': 'BiPublico', 42 }; 43 44 // Objetos que mantêm instâncias vivas de Chart.js em .charts{} 45 const CHART_OWNERS = ['BiPainelDiario', 'BiPago', 'BiCrescimento']; 46 47 const esc = (s) => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); 48 49 function hoje() { 50 return new Date().toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' }); 51 } 52 53 /* ââ Preferência de cor (Cor padrão; lembra a última escolha) âââââââââââ */ 54 function lerPrefCor() { 55 try { return localStorage.getItem('board-print-color') === 'bw' ? 'bw' : 'cor'; } catch (e) { return 'cor'; } 56 } 57 function setPrefCor(v) { 58 const val = v === 'bw' ? 'bw' : 'cor'; 59 try { localStorage.setItem('board-print-color', val); } catch (e) {} 60 document.querySelectorAll('[data-printcolor]').forEach(b => b.classList.toggle('is-active', b.dataset.printcolor === val)); 61 } 62 63 /* ââ Painel ativo (board usa .panel.active, id = p-<key>) âââââââââââââââ */ 64 function activeKey() { 65 const p = document.querySelector('.panel.active'); 66 return p ? p.id.replace(/^p-/, '') : 'home'; 67 } 68 function tituloDoPainel(key) { 69 const t = document.querySelector('#p-' + key + ' .panel-title'); 70 return t ? t.textContent.trim() : 'Relatório'; 71 } 72 function leadDoPainel(key) { 73 const s = document.querySelector('#p-' + key + ' .panel-sub'); 74 return s ? s.textContent.trim() : ''; 75 } 76 77 /* ââ HTML da capa (espelha a do financeiro, copy do BI) âââââââââââââââââ */ 78 function miniCapaPrintHTML(titulo, sub, dataStr) { 79 return `<section class="print-cover"> 80 <div class="pc-top">
81 <span class="pc-kicker">border ltd. · BI</span> 82 <span class="pc-kicker">Business Intelligence</span> 83 </div> 84 <div class="pc-mid"> 85 <div class="pc-brand"> 86 <img class="pc-logo" src="IMG/borderltd-white.png" alt="border ltd."> 87 <div class="pc-signature">Unbound Garments</div> 88 </div> 89 <div class="pc-titleblock"> 90 <div class="pc-eyebrow">Relatório de BI</div> 91 <h1 class="pc-title">${esc(titulo)}</h1> 92 ${sub ? `<p class="pc-sub">${esc(sub)}</p>` : ''} 93 </div> 94 </div> 95 <div class="pc-foot"> 96 <span>street alfaiataria brasileira</span> 97 <span>Santos · litoral · SP</span> 98 <span>Gerado em ${esc(dataStr)}</span> 99 </div> 100 </section>`; 101 } 102 103 // Painéis do relatório na ORDEM em que aparecem no DOM â o numeral da 104 // folha-capa (counter CSS) segue o DOM, então o Ãndice precisa casar. 105 function orderedReport() { 106 const dom = Array.from(document.querySelectorAll('.panel')).map(p => p.id.replace(/^p-/, '')); 107 return dom.filter(k => REPORT_PANELS.includes(k)); 108 } 109 110 /* ââ Ãndice (modo completo): linha por painel da seção Dados ao Vivo âââââ */ 111 function indicePrintHTML() { 112 const linhas = orderedReport().map((key, i) => { 113 const n = String(i + 1).padStart(2, '0'); 114 return `<li><span class="n">${n}</span><span class="t">${esc(tituloDoPainel(key))}</span><span class="d">${esc(leadDoPainel(key))}</span></li>`; 115 }).join(''); 116 return `<section class="print-index"> 117 <span class="sec-label">Conteúdo</span> 118 <h2 class="toc-title">Sumário</h2> 119 <ul class="toc">${linhas}</ul> 120 </section>`; 121 } 122 123 /* ââ Snapshot dos gráficos: canvas vivo â <img> PNG de alta resolução âââ */ 124 function snapshotCharts() { 125 CHART_OWNERS.forEach(name => { 126 const bi = window[name]; 127 if (!bi || !bi.charts) return; 128 Object.values(bi.charts).forEach(chart => { 129 if (!chart || !chart.canvas) return; 130 let url = ''; 131 const prevDpr = chart.options && chart.options.devicePixelRatio; 132 try { 133 chart.options.animation = false; 134 chart.options.devicePixelRatio = 3; // ~nitidez de A4 135 chart.resize(); 136 chart.update('none'); 137 url = chart.toBase64Image('image/png', 1); 138 } catch (e) { 139 try { url = chart.toBase64Image(); } catch (_) { return; } 140 } finally { 141 try { chart.options.devicePixelRatio = prevDpr; chart.resize(); } catch (_) {} 142 } 143 if (!url) return; 144 const cv = chart.canvas; 145 const wrap = cv.parentNode; 146 if (!wrap) return; 147 const old = wrap.querySelector('.print-chart-img'); 148 if (old) old.remove(); 149 const img = document.createElement('img'); 150 img.className = 'print-chart-img'; 151 img.src = url; 152 img.alt = ''; 153 img.setAttribute('aria-hidden', 'true'); 154 cv.insertAdjacentElement('afterend', img); 155 }); 156 }); 157 } 158 159 /* ââ Limpeza: desfaz todo o andaime (chamada no afterprint) âââââââââââââ */ 160 function limparPrint() { 161 document.querySelectorAll('#print-cover-host, #print-emu, .ch-top, .ch-foot, .print-chart-img, #print-overlay').forEach(n => n.remove()); 162 delete document.body.dataset.print; 163 delete document.body.dataset.printColor; 164 } 165 166 /* ââ Overlay "Montando relatórioâ¦" (modo completo, enquanto espera APIs) â */ 167 function showOverlay() { 168 if (document.getElementById('print-overlay')) return; 169 const o = document.createElement('div'); 170 o.id = 'print-overlay'; 171 o.innerHTML = `<div class="print-overlay-box"><span class="bi-spinner"></span><span>Montando relatórioâ¦</span></div>`; 172 document.body.appendChild(o); 173 } 174 function hideOverlay() { 175 const o = document.getElementById('print-overlay'); 176 if (o) o.remove(); 177 } 178 179 /* ââ Garante que os 8 painéis de Dados ao Vivo carregaram (async) âââââââ */ 180 async function ensureReportLoaded() { 181 const jobs = REPORT_PANELS.map(key => { 182 const bi = window[PANEL_LOADERS[key]]; 183 if (!bi || bi._loaded) return Promise.resolve();
184 try { return Promise.resolve(bi.load()); } catch (e) { return Promise.resolve(); } 185 }); 186 await Promise.allSettled(jobs); 187 // os charts são criados num setTimeout(80) DEPOIS do render â dá folga 188 await new Promise(r => setTimeout(r, 300)); 189 } 190 191 /* ââ Monta o andaime (capa/Ãndice/folha-capa) sem disparar a impressão ââ */ 192 function montarRelatorio(modo) { 193 const dataStr = hoje(); 194 limparPrint(); 195 document.body.dataset.print = modo; 196 document.body.dataset.printColor = lerPrefCor(); // o chrome é sempre mono; isto colore só os dados 197 198 const host = document.createElement('div'); 199 host.id = 'print-cover-host'; 200 host.setAttribute('aria-hidden', 'true'); 201 202 if (modo === 'pagina') { 203 const key = activeKey(); 204 host.innerHTML = miniCapaPrintHTML(tituloDoPainel(key), leadDoPainel(key), dataStr); 205 } else { 206 host.innerHTML = miniCapaPrintHTML('Board de BI', 'Dados ao Vivo â visão completa', dataStr) + indicePrintHTML(); 207 } 208 const main = document.querySelector('.main'); 209 main.insertBefore(host, main.firstChild); 210 211 // modo completo: cada folha-capa de capÃtulo ganha o MESMO topo + rodapé da capa 212 if (modo === 'completo') { 213 const chTop = `<div class="ch-top" aria-hidden="true"><span>border ltd. · BI</span><span>Business Intelligence</span></div>`; 214 const chFoot = `<div class="ch-foot" aria-hidden="true"><span>street alfaiataria brasileira</span><span>Santos · litoral · SP</span><span>Gerado em ${esc(dataStr)}</span></div>`; 215 REPORT_PANELS.forEach(key => { 216 const head = document.querySelector('#p-' + key + ' .panel-header'); 217 if (!head) return; 218 head.querySelectorAll('.ch-top, .ch-foot').forEach(n => n.remove()); // idempotente 219 head.insertAdjacentHTML('afterbegin', chTop); 220 head.insertAdjacentHTML('beforeend', chFoot); 221 }); 222 } 223 } 224 225 /* ââ Impressão real âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 226 async function imprimirRelatorio(modo) { 227 const m = modo === 'completo' ? 'completo' : 'pagina'; 228 if (m === 'completo') { 229 showOverlay(); 230 try { await ensureReportLoaded(); } catch (e) {} 231 montarRelatorio('completo'); 232 snapshotCharts(); 233 hideOverlay(); 234 } else { 235 montarRelatorio('pagina'); 236 snapshotCharts(); 237 } 238 window.print(); 239 } 240 241 /* ââ Dev: emula o layout de impressão na tela (sem o diálogo nativo) âââââ */ 242 async function emularPrintNaTela(modo) { 243 const m = modo === 'completo' ? 'completo' : 'pagina'; 244 if (m === 'completo') { try { await ensureReportLoaded(); } catch (e) {} } 245 montarRelatorio(m); 246 snapshotCharts(); 247 const out = []; 248 for (const ss of document.styleSheets) { 249 let rules; try { rules = ss.cssRules; } catch (e) { continue; } 250 if (!rules) continue; 251 for (const r of rules) { 252 if (r.type === CSSRule.MEDIA_RULE && /\bprint\b/.test(r.media.mediaText) && !/screen/.test(r.media.mediaText)) { 253 for (const ir of r.cssRules) out.push(ir.cssText); 254 } 255 } 256 } 257 const st = document.createElement('style'); 258 st.id = 'print-emu'; 259 st.textContent = out.join('\n'); 260 document.head.appendChild(st); 261 window.scrollTo(0, 0); 262 } 263 264 /* ââ Wire-up ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 265 document.addEventListener('DOMContentLoaded', () => { 266 document.querySelectorAll('[data-print]').forEach(b => b.addEventListener('click', () => imprimirRelatorio(b.dataset.print || 'pagina'))); 267 document.querySelectorAll('[data-printcolor]').forEach(b => b.addEventListener('click', () => setPrefCor(b.dataset.printcolor))); 268 setPrefCor(lerPrefCor()); 269 window.addEventListener('afterprint', limparPrint); 270 271 const sp = new URLSearchParams(location.search); 272 const pp = sp.get('print'), pc = sp.get('cor'); 273 if (pc === 'bw' || pc === 'cor') setPrefCor(pc); 274 if (pp === 'pagina' || pp === 'completo') setTimeout(() => emularPrintNaTela(pp), 80); 275 }); 276 277 // expõe pro console/depuração 278 window.BoardPrint = { imprimirRelatorio, emularPrintNaTela, montarRelatorio, limparPrint, snapshotCharts }; 279})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.