PageSourceSearch

https://border-bi.netlify.app/board-print.js?v=1789596018

js border-bi.netlify.app collected 2026-10-03 10:31:44 UTC 12,934 bytes, 279 lines download raw bytes

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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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.