document.body && document.body.classList.add('analytics-modal-open'); function initializeServerReportBuilder() { document.querySelectorAll('.report-builder-modal.is-server-rendered').forEach(function(modal) { if (modal.dataset.reportBuilderReady === '1') return; modal.dataset.reportBuilderReady = '1'; var canvas = modal.querySelector('[data-report-canvas]'); var templates = modal.querySelector('.report-builder-templates'); var activeCell = null; var draggingItem = null; var pageHistory = []; var builderHistory = []; var activePageIndex = 0; var pagination = modal.querySelector('[data-report-pagination]'); function expandAllReportProvinces(root) { if (!root) return; root.querySelectorAll('.call-detail-province-row').forEach(function(row) { row.classList.add('is-open'); }); root.querySelectorAll('[data-province-child]').forEach(function(child) { child.style.setProperty('display', 'table-row', 'important'); }); } expandAllReportProvinces(modal); function reportRows() { return Array.from(canvas ? canvas.querySelectorAll('.report-drop-row') : []); } function showReportPage(index) { var rows = reportRows(); if (!rows.length) { activePageIndex = 0; if (pagination) pagination.innerHTML = 'No rows'; return; } activePageIndex = Math.max(0, Math.min(index, rows.length - 1)); rows.forEach(function(row, i) { row.classList.toggle('is-active-page', i === activePageIndex); }); activeCell = rows[activePageIndex].querySelector('[data-drop-cell]') || null; if (!pagination) return; pagination.innerHTML = rows.map(function(_, i) { return ''; }).join(''); } function updateReportPages() { showReportPage(activePageIndex); } function activeReportRow() { return reportRows()[activePageIndex] || null; } function saveActivePageState() { var row = activeReportRow(); if (!row) return; var stack = pageHistory[activePageIndex] || []; stack.push(row.innerHTML); if (stack.length > 25) stack.shift(); pageHistory[activePageIndex] = stack; } function saveBuilderState() { if (!canvas) return; builderHistory.push(canvas.innerHTML); if (builderHistory.length > 40) builderHistory.shift(); } function undoActivePage() { if (canvas && builderHistory.length) { canvas.innerHTML = builderHistory.pop(); pageHistory = []; activePageIndex = Math.max(0, Math.min(activePageIndex, reportRows().length - 1)); activeCell = null; updateReportPages(); saveReportDraft(false); return; } var row = activeReportRow(); var stack = pageHistory[activePageIndex] || []; if (!row || !stack.length) return; row.innerHTML = stack.pop(); pageHistory[activePageIndex] = stack; activeCell = row.querySelector('[data-drop-cell]') || null; updateReportPages(); } function renderExportCharts(root) { root.querySelectorAll('.call-sector-chart[data-google-stacked-chart]').forEach(function(panel) { var payload; try { payload = JSON.parse(panel.getAttribute('data-google-stacked-chart') || '{}'); } catch (e) { payload = {}; } var groups = Array.isArray(payload.groups) ? payload.groups : []; var categories = Array.isArray(payload.categories) ? payload.categories : []; var matrix = payload.matrix || {}; var colors = Array.isArray(payload.colors) ? payload.colors : []; if (!groups.length || !categories.length) return; // Live Google rendering removes the original fallback bar wrapper. Recreate it // in the export clone before drawing the compact, print-safe SVG. var target = panel.querySelector('.call-bar-chart-wrap'); if (!target) { target = document.createElement('div'); target.className = 'call-bar-chart-wrap export-bar-chart-wrap'; var totalRow = panel.querySelector('.call-category-total-row'); panel.insertBefore(target, totalRow || null); } var width = 700, height = 370, left = 46, right = 46, top = 42, bottom = 104; var plotWidth = width - left - right, plotHeight = height - top - bottom; var totals = groups.map(function(group) { return categories.reduce(function(sum, category) { return sum + Number((matrix[group] || {})[category] || 0); }, 0); }); var maxTotal = Math.max.apply(Math, totals.concat([1])); var slot = plotWidth / groups.length; var barWidth = Math.max(2, Math.min(58, slot * 0.6)); var labelFontSize = 8; var maxLabelWidth = Math.max.apply(Math, groups.map(function(group) { return String(group).length * labelFontSize * 0.6; }).concat([20])); var labelExtent = Math.ceil(maxLabelWidth * Math.SQRT1_2); left = Math.max(46, labelExtent + 12); width = Math.max(700, left + right + groups.length * 26); plotWidth = width - left - right; slot = plotWidth / groups.length; barWidth = Math.max(2, Math.min(58, slot * 0.6)); bottom = 52 + labelExtent; height = top + 240 + bottom; plotHeight = 240; function escSvg(value) { return String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } var svg = ''; for (var tick = 0; tick <= 5; tick++) { var tickY = top + (plotHeight * tick / 5), tickValue = Math.round(maxTotal * (5 - tick) / 5); svg += '' + tickValue + ''; } groups.forEach(function(group, groupIndex) { var x = left + slot * groupIndex + (slot - barWidth) / 2, y = top + plotHeight; categories.forEach(function(category, categoryIndex) { var count = Number((matrix[group] || {})[category] || 0); if (count <= 0) return; var segmentHeight = Math.max(2, plotHeight * count / maxTotal); y -= segmentHeight; svg += ''; var countFontSize = Math.max(2, Math.min(9, segmentHeight * 0.7, barWidth / 2)); svg += '' + count + ''; }); svg += '' + totals[groupIndex] + ''; var labelX = (x + barWidth / 2).toFixed(2), labelY = top + plotHeight + 40; svg += '' + escSvg(group) + ''; }); svg += ''; target.innerHTML = svg; // Match footer cells to the exact SVG plot slots. var alignedTotals = panel.querySelector('.call-category-total-row'); if (alignedTotals) { alignedTotals.style.setProperty('grid-template-columns', (left / width * 100).toFixed(6) + '% repeat(' + groups.length + ', minmax(0, 1fr)) ' + (right / width * 100).toFixed(6) + '%', 'important'); alignedTotals.style.setProperty('padding', '0', 'important'); alignedTotals.style.setProperty('width', '100%', 'important'); alignedTotals.style.setProperty('max-width', 'none', 'important'); alignedTotals.style.setProperty('box-sizing', 'border-box', 'important'); } target.style.setProperty('display', 'block', 'important'); target.style.setProperty('padding', '0', 'important'); target.style.setProperty('background', '#fff', 'important'); panel.querySelectorAll('.export-chart-legend, .call-region-legend, .google-chart-legend').forEach(function(legend) { legend.remove(); }); var legend = document.createElement('div'); legend.className = 'export-chart-legend'; categories.forEach(function(category, categoryIndex) { var item = document.createElement('span'); var swatch = document.createElement('i'); swatch.style.background = String(colors[categoryIndex] || '#2563eb'); item.appendChild(swatch); item.appendChild(document.createTextNode(String(category || '-'))); legend.appendChild(item); }); panel.insertBefore(legend, target); }); root.querySelectorAll('.call-combined-pie[data-pie-segments]').forEach(function(pie) { var segments = []; try { segments = JSON.parse(pie.getAttribute('data-pie-segments') || '[]'); } catch (e) {} var angle = -90; var tinyIndex = 0; var topPaths = []; var depthPaths = []; var labels = []; segments.forEach(function(segment) { var sweep = Math.max(0, parseFloat(segment.percent || 0)) * 3.6; if (sweep <= 0) return; var end = angle + sweep; var middle = angle + sweep / 2; var startRad = angle * Math.PI / 180; var endRad = end * Math.PI / 180; var middleRad = middle * Math.PI / 180; var x1 = 50 + 48 * Math.cos(startRad), y1 = 50 + 48 * Math.sin(startRad); var x2 = 50 + 48 * Math.cos(endRad), y2 = 50 + 48 * Math.sin(endRad); var labelRadius = sweep >= 359.9 ? 0 : (sweep < 12 ? (tinyIndex++ % 2 === 0 ? 32 : 41) : (sweep < 25 ? 34 : 25)); var labelX = 50 + labelRadius * Math.cos(middleRad); var labelY = 50 + labelRadius * Math.sin(middleRad); var large = sweep > 180 ? 1 : 0; var color = segment.color || '#2563eb'; var shape = sweep >= 359.9 ? '' : ''; var hex = String(color).replace('#', ''); var darkColor = /^[0-9a-f]{6}$/i.test(hex) ? '#' + [0,2,4].map(function(offset) { return Math.round(parseInt(hex.slice(offset,offset+2),16)*0.65).toString(16).padStart(2,'0'); }).join('') : color; depthPaths.push(shape.replace(/fill="[^"]*"/, 'fill="' + darkColor + '"')); topPaths.push(shape); labels.push('' + Number(segment.percent || 0).toFixed(1) + '%'); angle = end; }); pie.style.setProperty('background', 'none', 'important'); pie.style.setProperty('box-shadow', 'none', 'important'); pie.style.setProperty('overflow', 'visible', 'important'); pie.innerHTML = ''; var pieCard = pie.closest('.call-combined-pie-card'); if (pieCard) { pieCard.querySelectorAll('.export-pie-legend').forEach(function(legend) { legend.remove(); }); pieCard.classList.add('export-side-pie'); var pieBody = pieCard.querySelector('.call-combined-pie-body'); if (pieBody) { pieBody.querySelectorAll('.export-pie-side-column').forEach(function(column) { column.remove(); }); var leftLabels = document.createElement('div'), rightLabels = document.createElement('div'); leftLabels.className = 'export-pie-side-column is-left'; rightLabels.className = 'export-pie-side-column is-right'; var split = Math.ceil(segments.length / 2); segments.forEach(function(segment,index) { var label = document.createElement('div'); label.textContent = String(segment.label || '-') + ' (' + Number(segment.count || 0).toLocaleString('en-US') + ' calls)'; label.style.color = String(segment.color || '#222'); (index < split ? leftLabels : rightLabels).appendChild(label); }); pieBody.insertBefore(leftLabels, pieBody.firstChild); pieBody.appendChild(rightLabels); } } }); root.querySelectorAll('.call-donut-card').forEach(function(card) { var donut = card.querySelector('.call-donut'); if (!donut) return; var color = card.style.getPropertyValue('--donut-color').trim() || donut.getAttribute('data-google-donut-color') || '#2563eb'; var count = Number(donut.getAttribute('data-google-donut-count') || 0); var total = Math.max(count, Number(donut.getAttribute('data-google-donut-total') || 0)); var pct = total > 0 ? (count / total) * 100 : (parseFloat(card.style.getPropertyValue('--donut-pct')) || 0); var dash = Math.max(0, Math.min(100, pct)); var safeColor = String(color).replace(/&/g, '&').replace(/"/g, '"'); donut.classList.remove('is-google-chart'); donut.style.cssText = 'display:block!important;width:72px!important;height:72px!important;margin:3px auto!important;background:none!important;visibility:visible!important;opacity:1!important;'; donut.innerHTML = ''; }); root.querySelectorAll('.call-stacked-bar span').forEach(function(segment) { var match = (segment.getAttribute('style') || '').match(/#[0-9a-fA-F]{3,8}/); if (match) { segment.style.setProperty('background', match[0], 'important'); segment.style.setProperty('background-image', 'none', 'important'); } }); } function reportHtmlForExport(makeBw) { var rows = reportRows(); var sourceRows = (rows.length ? rows : []).filter(function(row) { return !!row.querySelector('.report-builder-item'); }); // Older saved drafts can contain builder cards without the newer row wrapper. // Treat those cards as export rows too, so added content is never discarded. if (!sourceRows.length && canvas) { sourceRows = Array.from(canvas.querySelectorAll('.report-builder-item')).map(function(item) { var row = document.createElement('div'); row.className = 'report-drop-row is-full-row'; row.appendChild(item.cloneNode(true)); return row; }); } var body = sourceRows.map(function(row, i) { var clone = row.cloneNode(true); clone.querySelectorAll('.is-report-excluded, [data-report-excluded="1"]').forEach(function(excluded) { excluded.remove(); }); clone.querySelectorAll('.report-audit-cell, .report-audit-head, .pie-report-audit, .analytics-full-btn, .analytics-excel-btn, .analytics-image-btn, .analytics-region-excel-proxy, .analytics-modal-full-proxy, .analytics-full-proxy, .call-category-summary-btn, .call-province-exclude-filter, .call-province-exclude-one, .call-expand-all-btn, .report-layout-toggle, .report-text-editor, .drag-handle, .report-builder-item-head').forEach(function(el) { el.remove(); }); clone.querySelectorAll('.call-detail-category-row').forEach(function(groupRow) { var next = groupRow.nextElementSibling, hasData = false; while (next && !next.classList.contains('call-detail-category-row') && !next.classList.contains('call-detail-province-row')) { if (next.classList.contains('call-detail-data-row')) { hasData = true; break; } next = next.nextElementSibling; } if (!hasData) groupRow.remove(); }); clone.querySelectorAll('.call-detail-province-row').forEach(function(groupRow) { var key = groupRow.getAttribute('data-province-group'); if (key && !clone.querySelector('[data-province-child="' + key + '"].call-detail-data-row')) groupRow.remove(); }); clone.querySelectorAll('.call-detail-province-row').forEach(function(provinceRow) { provinceRow.classList.add('is-open'); }); clone.querySelectorAll('[data-province-child]').forEach(function(childRow) { childRow.style.setProperty('display', 'table-row', 'important'); }); clone.querySelectorAll('.call-province-toggle i').forEach(function(icon) { icon.remove(); }); // Export must never reuse the live Google SVG: it carries live-page dimensions // and is the cause of charts being clipped in the printed A4 document. clone.querySelectorAll('.has-google-chart').forEach(function(chartElement) { chartElement.classList.remove('has-google-chart'); chartElement.removeAttribute('data-google-chart-ready'); }); clone.querySelectorAll('.google-stacked-chart-stage').forEach(function(stage) { stage.remove(); }); renderExportCharts(clone); clone.querySelectorAll('[draggable]').forEach(function(el) { el.removeAttribute('draggable'); }); clone.querySelectorAll('[contenteditable]').forEach(function(el) { el.removeAttribute('contenteditable'); }); // Preserve each Report Builder row as one section, but let native print // place consecutive sections in the remaining page space. return '
' + clone.outerHTML + '
'; }).join(''); var styleLinks = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).map(function(link) { return ''; }).join(''); var inlineStyles = ''; // Global app styles may contain color-mix()/color(), unsupported by html2canvas. var css = ''; css += ''; css += ''; css += ''; css += ''; return 'SLIMS Report' + styleLinks + inlineStyles + css + '' + (body || '

No report rows added.

') + ''; } function downloadReport(filename, mime, content) { var blob = new Blob([content], { type: mime }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(function() { URL.revokeObjectURL(url); a.remove(); }, 600); } var reportKind = new URLSearchParams(window.location.search).get('analytics_tab') === 'sms' ? 'sms' : 'calls'; var sheetRecordKey = 'slims.' + reportKind + '.report.sheet.records'; function readSheetRecords() { try { return JSON.parse(localStorage.getItem(sheetRecordKey) || '[]'); } catch (e) { return []; } } function writeSheetRecords(records) { try { localStorage.setItem(sheetRecordKey, JSON.stringify(records.slice(0, 40))); } catch (e) {} } function showReportToast(message) { var toast = modal.querySelector('[data-report-toast]'); if (!toast) return; toast.textContent = message || 'Saved successfully.'; toast.classList.add('is-visible'); clearTimeout(showReportToast.timer); showReportToast.timer = setTimeout(function() { toast.classList.remove('is-visible'); }, 2200); } function showSavedReportModal() { var existing = document.querySelector('.report-saved-modal'); if (existing) existing.remove(); var savedModal = document.createElement('div'); savedModal.className = 'report-saved-modal'; savedModal.innerHTML = '

Your file has been saved successfully.

'; document.body.appendChild(savedModal); savedModal.addEventListener('click', function(event) { if (event.target === savedModal || event.target.closest('button')) savedModal.remove(); }); } function saveSheetRecord(format, filename, html) { if (window.saveSheetRecord) { return window.saveSheetRecord(format, filename, html); } return Promise.resolve({ok:false}); } var reportStateParams = new URLSearchParams(window.location.search); reportStateParams.delete('report_builder'); var reportDraftKey = 'slims.report.builder.draft.v2.' + reportKind + '.' + encodeURIComponent(reportStateParams.toString()); function draftContextKey(query) { var params = new URLSearchParams(String(query || '')); ['report_builder', 'excluded_call_ids', 'excluded_call_ids[]', 'call_tab', 'analytics_tab'].forEach(function(key) { params.delete(key); }); var keys = ['date_from', 'date_to', 'call_sector_categories[]', 'call_sector_categories', 'excluded_provinces[]', 'excluded_provinces']; return keys.map(function(key) { return key + '=' + params.getAll(key).map(String).sort().join(','); }).join('&'); } function parseSavedDraft(key) { try { var raw = localStorage.getItem(key); var draft = raw ? JSON.parse(raw) : null; return draft && draft.html ? draft : null; } catch (e) { return null; } } function readReportDraft() { var current = parseSavedDraft(reportDraftKey); if (current) return current; // Compatibility: selections may change the URL, but an existing Report Builder // draft must remain available. Find the latest same-date/category draft and migrate it. var context = draftContextKey(reportStateParams.toString()); var prefixes = ['slims.report.builder.draft.v2.' + reportKind + '.', 'slims.report.builder.draft.' + reportKind + '.']; var candidates = []; try { for (var i = 0; i < localStorage.length; i++) { var key = localStorage.key(i) || ''; var prefix = prefixes.find(function(item) { return key.indexOf(item) === 0; }); if (!prefix || key === reportDraftKey) continue; var savedQuery = ''; try { savedQuery = decodeURIComponent(key.slice(prefix.length)); } catch (ignored) { continue; } if (draftContextKey(savedQuery) !== context) continue; var draft = parseSavedDraft(key); if (draft) candidates.push(draft); } } catch (e) {} candidates.sort(function(a, b) { return String(b.savedAt || '').localeCompare(String(a.savedAt || '')); }); var recovered = candidates[0] || null; if (recovered) { try { localStorage.setItem(reportDraftKey, JSON.stringify(recovered)); } catch (e) {} } return recovered; } function saveReportDraft(showMessage) { if (!canvas) return; try { localStorage.setItem(reportDraftKey, JSON.stringify({html:canvas.innerHTML,activePageIndex:activePageIndex,savedAt:new Date().toISOString()})); if (showMessage) showReportToast('Draft saved. Continue anytime from Draft.'); } catch (e) { if (showMessage) showReportToast('Draft could not be saved.'); } } function refreshDraftAnalyticsData() { if (!canvas || !templates) return; var selectors = ['.call-detail-report-panel', '.call-percentage-chart', '.call-category-region-chart', '.call-combined-pie-card', '.call-sector-chart']; canvas.querySelectorAll('.report-builder-item[data-builder-key]').forEach(function(item) { var key = item.getAttribute('data-builder-key') || ''; if (!key) return; var source = templates.querySelector('.report-builder-item[data-builder-key="' + key.replace(/"/g, '') + '"]'); if (!source) return; selectors.forEach(function(selector) { var freshParts = Array.from(source.querySelectorAll(selector)); Array.from(item.querySelectorAll(selector)).forEach(function(part, index) { if (freshParts[index]) part.replaceWith(freshParts[index].cloneNode(true)); }); }); }); } function restoreReportDraft() { var draft = readReportDraft(); if (!draft || !draft.html) { showReportToast('No saved draft found.'); return false; } canvas.innerHTML = draft.html; // Keep the saved layout, but always carry the latest Analytics included data. refreshDraftAnalyticsData(); activePageIndex = parseInt(draft.activePageIndex || '0', 10); pageHistory = []; activeCell = null; updateReportPages(); showReportToast('Draft restored from the last saved point.'); return true; } function saveReportOnly() { saveReportDraft(false); var generateMenu = modal.querySelector('.report-generate-menu'); if (generateMenu) generateMenu.classList.remove('is-disabled'); showSavedReportModal(); } function previewReport() { var existing = document.querySelector('.report-preview-modal'); if (existing) existing.remove(); var previewModal = document.createElement('div'); previewModal.className = 'report-preview-modal'; previewModal.innerHTML = '

Report Preview

'; document.body.appendChild(previewModal); var frame = previewModal.querySelector('.report-preview-frame'); frame.srcdoc = reportHtmlForExport(false); previewModal.addEventListener('click', function(event) { if (event.target === previewModal || event.target.closest('.report-preview-close')) previewModal.remove(); }); } function cleanExportDocument(html) { var parsed = new DOMParser().parseFromString(html, 'text/html'); parsed.querySelectorAll('link[rel="stylesheet"]').forEach(function(link) { link.remove(); }); return parsed; } function exportRenderHost(html) { var parsed = cleanExportDocument(html); var host = document.createElement('div'); host.className = 'report-export-render-host'; host.style.cssText = 'position:absolute;left:0;top:0;width:1120px;z-index:2147483646;pointer-events:none;background:#fff;display:block;visibility:visible;overflow:visible;opacity:1;'; host.style.top = (Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) + 100) + 'px'; host.innerHTML = Array.from(parsed.querySelectorAll('style')).map(function(style) { return ''; }).join('') + parsed.body.innerHTML; document.body.appendChild(host); return host; } function triggerReportDownload(blob, filename) { if (!(blob instanceof Blob) || blob.size < 100) throw new Error('Generated file is empty.'); var url = URL.createObjectURL(blob); var link = document.createElement('a'); link.href = url; link.download = filename; link.style.display = 'none'; document.body.appendChild(link); link.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, view:window})); setTimeout(function() { URL.revokeObjectURL(url); link.remove(); }, 15000); } function downloadTextPdfFallback(JsPdf, pages, filename) { var fallback = new JsPdf({orientation:'landscape', unit:'mm', format:'a4', compress:true}); fallback.setFont('helvetica', 'normal'); fallback.setFontSize(10); var first = true; pages.forEach(function(page, pageIndex) { var text = (page.innerText || page.textContent || '').replace(/\s+/g, ' ').trim(); if (!text) return; var lines = fallback.splitTextToSize(text, 270); for (var offset = 0; offset < lines.length; offset += 24) { if (!first) fallback.addPage('a4', 'landscape'); first = false; fallback.setFontSize(14); fallback.text('SLIMS Report - Page ' + (pageIndex + 1), 12, 14); fallback.setFontSize(9); fallback.text(lines.slice(offset, offset + 24), 12, 24, {maxWidth:270, lineHeightFactor:1.35}); } }); if (first) throw new Error('No report content is available for PDF.'); triggerReportDownload(fallback.output('blob'), filename); } var directPdfLibraries = null; function ensureDirectPdfLibraries() { if (typeof window.html2canvas === 'function' && window.jspdf && window.jspdf.jsPDF) { return Promise.resolve({html2canvas: window.html2canvas, JsPdf: window.jspdf.jsPDF}); } if (directPdfLibraries) return directPdfLibraries; var currentScript = Array.from(document.scripts).find(function(script) { return /analytics-report-builder\.js(?:\?|$)/.test(script.src || ''); }); var baseUrl = currentScript ? currentScript.src : window.location.href; function loadLibrary(filename, ready) { if (ready()) return Promise.resolve(); return new Promise(function(resolve, reject) { var script = document.createElement('script'); script.src = new URL(filename, baseUrl).href; script.async = true; script.onload = function() { ready() ? resolve() : reject(new Error(filename + ' did not initialize.')); }; script.onerror = function() { reject(new Error(filename + ' could not be loaded.')); }; document.head.appendChild(script); }); } directPdfLibraries = Promise.all([ loadLibrary('html2canvas.min.js', function() { return typeof window.html2canvas === 'function'; }), loadLibrary('jspdf.umd.min.js', function() { return !!(window.jspdf && window.jspdf.jsPDF); }) ]).then(function() { return {html2canvas: window.html2canvas, JsPdf: window.jspdf.jsPDF}; }).catch(function(error) { directPdfLibraries = null; throw error; }); return directPdfLibraries; } function protectedPdfBlocks(page, canvasScale) { var pageRect = page.getBoundingClientRect(); var selector = '.report-builder-item,.call-sector-chart,.call-percentage-chart,.call-combined-pie-card,.call-category-region-chart'; return Array.from(page.querySelectorAll(selector)).map(function(element) { var rect = element.getBoundingClientRect(); return { top: Math.max(0, Math.round((rect.top - pageRect.top) * canvasScale)), bottom: Math.max(0, Math.round((rect.bottom - pageRect.top) * canvasScale)) }; }).filter(function(block) { return block.bottom > block.top; }); } function safePdfSliceEnd(startY, idealEnd, canvasHeight, protectedBlocks) { var endY = Math.min(idealEnd, canvasHeight); var minimumSlice = Math.max(80, Math.round((idealEnd - startY) * 0.18)); var safeTops = protectedBlocks.filter(function(block) { return block.top > startY + minimumSlice && block.top < endY && block.bottom > endY; }).map(function(block) { return block.top; }); if (safeTops.length) endY = Math.max.apply(Math, safeTops); return endY > startY ? endY : Math.min(idealEnd, canvasHeight); } function openNativePdfPrint(html, filename) { var printWindow = window.open('', '_blank'); if (!printWindow) { showReportToast('Allow pop-ups, then export PDF again.'); return; } var executiveCss = ''; executiveCss += ''; var printDocument = new DOMParser().parseFromString(html, 'text/html'); printDocument.querySelectorAll('.export-section').forEach(function(section) { if (!section.querySelector('.call-detail-report-table') || !section.querySelector('.call-sector-chart,.call-percentage-chart,.call-combined-pie-card')) return; var table = section.querySelector('.call-detail-report-table'); var cell = table.closest('.report-drop-cell') || table.closest('.report-builder-item'); if (cell && !cell.querySelector('.call-sector-chart,.call-percentage-chart,.call-combined-pie-card')) { var tableSection = printDocument.createElement('section'); tableSection.className = 'export-section'; tableSection.appendChild(cell); section.after(tableSection); } }); // Native print cannot paginate long city rowspans reliably. // Repeat the city on each record in the print copy only. printDocument.querySelectorAll('.call-detail-report-table tbody td[rowspan]').forEach(function(cell) { var span = Number(cell.getAttribute('rowspan') || 1); var column = cell.cellIndex; var row = cell.parentElement.nextElementSibling; cell.removeAttribute('rowspan'); cell.classList.remove('call-detail-dist-grouped', 'is-city-group'); for (var remaining = span - 1; remaining > 0 && row; remaining--) { var copy = cell.cloneNode(true); copy.classList.remove('is-city-group-start'); row.insertBefore(copy, row.cells[column] || null); row = row.nextElementSibling; } }); executiveCss += ''; // Each province gets its own table with province name in the repeating THEAD. printDocument.querySelectorAll('.call-detail-report-table').forEach(function(table) { var sourceBody = table.tBodies[0], sourceHead = table.tHead; if (!sourceBody || !sourceHead) return; var panel = table.closest('.call-detail-report-panel'); var titleNode = panel && panel.querySelector('.call-detail-region-title > span'); var fallbackName = titleNode ? titleNode.textContent.trim() : 'Pakistan'; var groups = [], active = null; Array.from(sourceBody.rows).forEach(function(row) { if (row.classList.contains('call-detail-province-row')) { var nameNode = row.querySelector('.call-province-toggle span'); active = {name:nameNode ? nameNode.textContent.trim() : row.textContent.replace(/\d[\d,]*\s*Calls/gi,'').trim(), rows:[]}; groups.push(active); return; } if (!active) { active={name:fallbackName,rows:[]};groups.push(active); } active.rows.push(row); }); var fragment = printDocument.createDocumentFragment(); groups.forEach(function(group) { if (!group.rows.some(function(row) {return row.classList.contains('call-detail-data-row');})) return; var nextTable = table.cloneNode(false); nextTable.classList.add('print-province-table'); Array.from(table.children).filter(function(child) {return child.tagName==='COLGROUP';}).forEach(function(child){nextTable.appendChild(child.cloneNode(true));}); var head = sourceHead.cloneNode(true), provinceRow = printDocument.createElement('tr'), provinceCell = printDocument.createElement('th'); provinceRow.className='print-province-title';provinceCell.colSpan=Array.from(sourceHead.rows[0].cells).reduce(function(sum,cell){return sum+cell.colSpan;},0);provinceCell.textContent=group.name; provinceRow.appendChild(provinceCell);head.insertBefore(provinceRow,head.firstChild);nextTable.appendChild(head); var body = printDocument.createElement('tbody');group.rows.forEach(function(row){body.appendChild(row.cloneNode(true));});nextTable.appendChild(body);fragment.appendChild(nextTable); }); if (fragment.childNodes.length) table.replaceWith(fragment); }); executiveCss += ''; // Pair adjacent single pie sections without changing their internal side labels. var printSections = Array.from(printDocument.querySelectorAll('body > .export-section')); for (var sectionIndex = 0; sectionIndex < printSections.length - 1; sectionIndex++) { var first = printSections[sectionIndex], second = printSections[sectionIndex + 1]; if (first.querySelectorAll('.call-combined-pie-card').length === 1 && second.querySelectorAll('.call-combined-pie-card').length === 1 && !first.querySelector('.call-detail-report-table,.call-sector-chart,.call-percentage-chart') && !second.querySelector('.call-detail-report-table,.call-sector-chart,.call-percentage-chart')) { var pair = printDocument.createElement('div'); pair.className = 'print-pie-pair'; first.before(pair); pair.append(first, second); sectionIndex++; } } // A graph following a completed table starts on a fresh page. var lastContentWasTable = false; Array.from(printDocument.body.children).forEach(function(block) { if (!block.matches('.export-section,.print-pie-pair')) return; var hasTable = !!block.querySelector('.call-detail-report-table'); var hasGraph = !!block.querySelector('.call-sector-chart,.call-percentage-chart,.call-combined-pie-card,.call-category-region-chart'); if (lastContentWasTable && hasGraph && !hasTable) block.classList.add('print-after-table'); lastContentWasTable = hasTable; }); executiveCss += ''; html = '' + printDocument.documentElement.outerHTML; var printableHtml = html.replace(/[\\s\\S]*?<\/title>/i, '<title>').replace('', executiveCss + ''); var ready = false; printWindow.addEventListener('load', function() { if (ready) return; ready = true; var fonts = printWindow.document.fonts ? printWindow.document.fonts.ready : Promise.resolve(); fonts.then(function() { return Promise.all(Array.from(printWindow.document.images).map(function(image) { if (image.complete) return Promise.resolve(); return new Promise(function(resolve) { image.onload = resolve; image.onerror = resolve; }); })); }).then(function() { printWindow.requestAnimationFrame(function() { printWindow.requestAnimationFrame(function() { printWindow.focus(); printWindow.print(); }); }); }).catch(function() { showReportToast('Print preparation failed. Please retry.'); }); }, {once:true}); printWindow.document.open(); printWindow.document.write(printableHtml); printWindow.document.close(); showReportToast('Choose Save as PDF. Use A4, 100% scale, and turn off Headers and footers. Text and charts stay sharp.'); } function downloadPdfReport(html, filename) { showReportToast('Generating PDF for direct download...'); ensureDirectPdfLibraries().then(function(libraries) { var host = exportRenderHost(html); var pages = Array.from(host.querySelectorAll('.export-section')); if (!pages.length) pages = Array.from(host.querySelectorAll('.export-page')); if (!pages.length) { host.remove(); throw new Error('Add report content before generating PDF.'); } // Pack compact report sections into the available A4 space before starting another page. var flow = document.createElement('div'); flow.className = 'export-pdf-flow'; pages.forEach(function(section) { flow.appendChild(section); }); host.appendChild(flow); pages = [flow]; var fontsReady = document.fonts && document.fonts.ready ? document.fonts.ready.catch(function(){}) : Promise.resolve(); return fontsReady.then(function() { var pdf = new libraries.JsPdf({orientation:'portrait', unit:'mm', format:'a4', compress:true}); var pageWidth = 194; var pageHeight = 281; var firstPdfPage = true; var sequence = Promise.resolve(); pages.forEach(function(page) { sequence = sequence.then(function() { var bounds = page.getBoundingClientRect(); var cssHeight = Math.ceil(Math.max(bounds.height, page.scrollHeight)); var cssWidth = Math.ceil(Math.max(bounds.width, page.scrollWidth)); if (cssHeight <= 0 || cssWidth <= 0) throw new Error('Report content has no visible size.'); // Stay within browser canvas dimension and memory limits for long reports. var captureScale = Math.min(2, 16000 / cssHeight, 16000 / cssWidth, Math.sqrt(40000000 / (cssWidth * cssHeight))); return libraries.html2canvas(page, { scale: captureScale, width: cssWidth, height: cssHeight, windowWidth: Math.max(1120, cssWidth), onclone: function(clonedDocument) { // The live host sits below the page. Move only its capture clone to the origin. var clonedHost = clonedDocument.querySelector('.report-export-render-host'); if (!clonedHost) throw new Error('Report capture container is missing.'); clonedHost.style.setProperty('top', '0', 'important'); clonedHost.style.setProperty('left', '0', 'important'); clonedHost.style.setProperty('position', 'absolute', 'important'); clonedHost.style.setProperty('visibility', 'visible', 'important'); clonedHost.style.setProperty('opacity', '1', 'important'); clonedDocument.documentElement.style.setProperty('overflow', 'visible', 'important'); clonedDocument.body.style.setProperty('overflow', 'visible', 'important'); }, useCORS: false, backgroundColor: '#ffffff', logging: false, scrollX: 0, scrollY: 0 }).then(function(canvas) { if (!canvas.width || !canvas.height) throw new Error('PDF capture is empty. Please try a smaller report.'); var probe = document.createElement('canvas'); probe.width = 100; probe.height = 100; var probeContext = probe.getContext('2d'); probeContext.drawImage(canvas, 0, 0, 100, 100); var pixels = probeContext.getImageData(0, 0, 100, 100).data; var hasContent = false; for (var pixel = 0; pixel < pixels.length; pixel += 4) { if (pixels[pixel + 3] > 0 && (pixels[pixel] < 245 || pixels[pixel + 1] < 245 || pixels[pixel + 2] < 245)) { hasContent = true; break; } } if (!hasContent) throw new Error('PDF capture was blank. Please refresh the page and try again.'); var canvasScale = canvas.height / cssHeight; var protectedBlocks = protectedPdfBlocks(page, canvasScale); var idealSliceHeight = Math.max(1, Math.floor(canvas.width * pageHeight / pageWidth)); var y = 0; while (y < canvas.height) { var endY = safePdfSliceEnd(y, y + idealSliceHeight, canvas.height, protectedBlocks); var currentHeight = Math.max(1, endY - y); var slice = document.createElement('canvas'); slice.width = canvas.width; slice.height = currentHeight; var context = slice.getContext('2d'); context.fillStyle = '#ffffff'; context.fillRect(0, 0, slice.width, slice.height); context.drawImage(canvas, 0, y, canvas.width, currentHeight, 0, 0, canvas.width, currentHeight); if (!firstPdfPage) pdf.addPage('a4', 'portrait'); firstPdfPage = false; var renderedHeight = pageWidth * currentHeight / canvas.width; pdf.addImage(slice.toDataURL('image/png'), 'PNG', 8, 8, pageWidth, renderedHeight, undefined, 'FAST'); y = endY; } }); }); }); return sequence.then(function() { pdf.save(filename); host.remove(); showReportToast('PDF downloaded.'); }).catch(function(error) { host.remove(); throw error; }); }); }).catch(function(error) { console.error('SLIMS direct PDF generation failed:', error); showReportToast(error && error.message ? error.message : 'PDF could not be generated.'); }); } function wordTableFromChildren(doc, container, columns) { var children = Array.from(container.children); var table = doc.createElement('table'); table.setAttribute('cellspacing', '8'); table.setAttribute('cellpadding', '0'); table.style.cssText = 'width:100%;table-layout:fixed;border-collapse:separate;'; for (var i = 0; i < children.length; i += columns) { var tr = doc.createElement('tr'); for (var j = 0; j < columns; j++) { var td = doc.createElement('td'); td.style.cssText = 'width:' + (100 / columns) + '%;vertical-align:top;'; if (children[i + j]) td.appendChild(children[i + j]); tr.appendChild(td); } table.appendChild(tr); } container.replaceWith(table); } function makeWordHtml(html) { if (typeof window.html2canvas !== 'function') return Promise.resolve(html); var host = exportRenderHost(html); var graphics = Array.from(host.querySelectorAll('.call-combined-pie, .call-donut, .call-bar-chart-wrap')); var chain = Promise.resolve(); graphics.forEach(function(graphic) { chain = chain.then(function() { return window.html2canvas(graphic, {scale: 2, useCORS: false, backgroundColor: '#ffffff', logging: false}).then(function(canvas) { var image = document.createElement('img'); image.src = canvas.toDataURL('image/png'); image.alt = 'Report graph'; image.style.cssText = 'display:block;max-width:100%;height:auto;margin:0 auto;'; graphic.replaceWith(image); }).catch(function(error) { console.error('SLIMS Word graph capture failed:', error); }); }); }); return chain.then(function() { var doc = cleanExportDocument('' + host.innerHTML + ''); host.remove(); doc.querySelectorAll('.report-drop-row.is-half-row').forEach(function(row) { wordTableFromChildren(doc, row, 2); }); doc.querySelectorAll('.call-combined-pies-grid').forEach(function(grid) { wordTableFromChildren(doc, grid, 2); }); doc.querySelectorAll('.call-donut-grid').forEach(function(grid) { wordTableFromChildren(doc, grid, 4); }); doc.documentElement.setAttribute('xmlns:o', 'urn:schemas-microsoft-com:office:office'); doc.documentElement.setAttribute('xmlns:w', 'urn:schemas-microsoft-com:office:word'); doc.documentElement.setAttribute('xmlns', 'http://www.w3.org/TR/REC-html40'); return '' + doc.documentElement.outerHTML; }); } function reportFileDate(value) { var date = String(value || '').trim(); if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return date || 'All-Dates'; var parts = date.split('-'); var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; return parts[2] + '-' + months[Math.max(0, Number(parts[1]) - 1)] + '-' + parts[0]; } function reportFileScope() { var params = new URLSearchParams(window.location.search); var tab = String(params.get('call_tab') || '').toLowerCase(); var names = {kp:'KP',bln:'BLN',gb:'AJK-GB',punjab:'Punjab',sindh:'Sindh',foreign:'Foreign',unknown:'Unknown'}; if (names[tab]) return names[tab]; var heading = modal.querySelector('.analytics-title-block h2, .call-sector-head h2'); var match = heading && heading.textContent.match(/DISTRICT\s+WISE\s*\(([^)]+)\)/i); return match && match[1] ? match[1].trim().replace(/[^a-z0-9]+/gi, '-') : 'Pakistan'; } function exportReport(type) { var html = reportHtmlForExport(false); var stamp = new Date().toISOString().slice(0, 10); if (type === 'word') { var wordName = 'slims-' + reportKind + '-report-' + stamp + '.doc'; showReportToast('Preparing editable Word report...'); makeWordHtml(html).then(function(wordHtml) { downloadReport(wordName, 'application/msword;charset=utf-8', '' + wordHtml); showReportToast('Editable Word report downloaded.'); saveSheetRecord('WORD', wordName, wordHtml); }); return; } var currentParams = new URLSearchParams(window.location.search); var pdfName = reportFileScope() + '-' + reportFileDate(currentParams.get('date_from')) + '-to-' + reportFileDate(currentParams.get('date_to')) + '.pdf'; openNativePdfPrint(html, pdfName); saveSheetRecord('PDF', pdfName, html); } function reportTextNodes(item) { return Array.from(item.querySelectorAll('.call-combined-pie-row b, .call-region-legend span, .call-bar-item em, .call-donut-card h3')).filter(function(node) { return node.textContent.trim() !== ''; }); } function setReportLabelText(node, value) { if (node.matches('.call-region-legend span')) { var textNode = Array.from(node.childNodes).find(function(child) { return child.nodeType === 3 && child.nodeValue.trim() !== ''; }); if (textNode) { textNode.nodeValue = value; return; } } node.textContent = value; } function openReportTextEditor(item) { var existing = item.querySelector('.report-text-editor'); if (existing) { existing.remove(); return; } var titleNode = item.querySelector('.call-combined-pie-head h2, .analytics-title-block h2, .call-percentage-head h2, .call-category-region-head h2'); var labels = []; reportTextNodes(item).forEach(function(node) { var value = node.textContent.trim(); if (value && labels.indexOf(value) === -1) labels.push(value); }); var panel = document.createElement('div'); panel.className = 'report-text-editor'; panel.innerHTML = '
Labels
'; panel.querySelector('[data-report-title-input]').value = titleNode ? titleNode.textContent.trim() : ''; var list = panel.querySelector('.report-label-editor-list'); labels.forEach(function(label) { var row = document.createElement('label'); row.innerHTML = ''; row.querySelector('span').textContent = label; var input = row.querySelector('input'); input.value = label; input.dataset.originalLabel = label; list.appendChild(row); }); if (!labels.length) panel.querySelector('.report-label-editor-title').style.display = 'none'; item.querySelector('.report-builder-item-head').insertAdjacentElement('afterend', panel); } function applyReportTextEditor(item, panel) { var titleNode = item.querySelector('.call-combined-pie-head h2, .analytics-title-block h2, .call-percentage-head h2, .call-category-region-head h2'); var titleInput = panel.querySelector('[data-report-title-input]'); if (titleNode && titleInput && titleInput.value.trim()) titleNode.textContent = titleInput.value.trim(); panel.querySelectorAll('[data-report-label-input]').forEach(function(input) { var oldValue = input.dataset.originalLabel || ''; var newValue = input.value.trim(); if (!newValue || newValue === oldValue) return; reportTextNodes(item).forEach(function(node) { if (node.textContent.trim() === oldValue) setReportLabelText(node, newValue); }); }); var headTitle = item.querySelector('.report-builder-item-head strong'); if (headTitle && titleInput && titleInput.value.trim()) headTitle.textContent = titleInput.value.trim(); panel.remove(); saveReportDraft(false); } function cloneTemplate(key) { if (!templates || !key) return null; var source = templates.querySelector('.report-builder-item[data-builder-key="' + key + '"]'); if (!source) return null; var clone = source.cloneNode(true); expandAllReportProvinces(clone); clone.classList.add('is-preview-focus'); setTimeout(function(){ clone.classList.remove('is-preview-focus'); }, 900); return clone; } function addReportRow(type) { if (!canvas) return null; saveBuilderState(); var row = document.createElement('div'); row.className = 'report-drop-row ' + (type === 'half' ? 'is-half-row' : 'is-full-row'); row.style.width = '100%'; row.style.maxWidth = 'none'; row.style.justifySelf = 'stretch'; var cells = type === 'half' ? 2 : 1; for (var i = 0; i < cells; i++) { var cell = document.createElement('div'); cell.className = 'report-drop-cell'; cell.setAttribute('data-drop-cell', '1'); cell.style.width = '100%'; cell.style.maxWidth = 'none'; row.appendChild(cell); } canvas.appendChild(row); activeCell = row.querySelector('[data-drop-cell]'); activePageIndex = reportRows().length - 1; updateReportPages(); return row; } function seedDefaultReportPages() { if (!canvas || reportRows().length || readReportDraft()) return; ['server-section-2', 'server-section-3', 'server-section-4'].forEach(function(key) { var row = addReportRow('full'); var cell = row && row.querySelector('[data-drop-cell]'); var item = cloneTemplate(key); if (cell && item) cell.appendChild(item); }); activePageIndex = 0; updateReportPages(); saveReportDraft(false); } seedDefaultReportPages(); modal.querySelectorAll('[data-add-report-row]').forEach(function(btn) { btn.addEventListener('click', function() { addReportRow(btn.dataset.addReportRow || 'full'); }); }); modal.querySelectorAll('[data-add-report-block]').forEach(function(btn) { btn.addEventListener('click', function() { var type = btn.dataset.addReportBlock || 'separator'; var row = addReportRow('full'); if (!row) return; var cell = row.querySelector('[data-drop-cell]'); var item = document.createElement('section'); item.className = 'report-builder-item report-builder-custom-block'; item.dataset.builderKey = 'custom-' + type + '-' + Date.now(); if (type === 'heading') { item.innerHTML = '
Type heading
'; } else { item.innerHTML = '
'; } cell.appendChild(item); if (type === 'heading') { var heading = item.querySelector('.report-builder-custom-heading'); heading.focus(); var selection = window.getSelection(); var range = document.createRange(); range.selectNodeContents(heading); selection.removeAllRanges(); selection.addRange(range); } saveReportDraft(false); }); }); var toolbarUndo = modal.querySelector('[data-undo-report-page]'); if (toolbarUndo) { toolbarUndo.addEventListener('click', function(event) { event.preventDefault(); undoActivePage(); }); } modal.addEventListener('click', function(event) { var editTextBtn = event.target.closest('[data-edit-report-text]'); if (editTextBtn) { event.preventDefault(); event.stopPropagation(); var editItem = editTextBtn.closest('.report-builder-item'); if (editItem) openReportTextEditor(editItem); return; } var applyTextBtn = event.target.closest('[data-apply-report-text]'); if (applyTextBtn) { event.preventDefault(); var editor = applyTextBtn.closest('.report-text-editor'); var editorItem = applyTextBtn.closest('.report-builder-item'); if (editor && editorItem) applyReportTextEditor(editorItem, editor); return; } var cancelTextBtn = event.target.closest('[data-cancel-report-text]'); if (cancelTextBtn) { event.preventDefault(); var cancelEditor = cancelTextBtn.closest('.report-text-editor'); if (cancelEditor) cancelEditor.remove(); return; } var saveDraftBtn = event.target.closest('[data-save-report-draft]'); if (saveDraftBtn) { event.preventDefault(); saveReportDraft(true); return; } var restoreDraftBtn = event.target.closest('[data-restore-report-draft]'); if (restoreDraftBtn) { event.preventDefault(); restoreReportDraft(); return; } var draftBtn = event.target.closest('[data-report-draft]'); if (draftBtn) { event.preventDefault(); if (!reportRows().length && readReportDraft()) { restoreReportDraft(); } else { saveReportDraft(true); } return; } var previewBtn = event.target.closest('[data-preview-report]'); if (previewBtn) { event.preventDefault(); event.stopPropagation(); if (event.stopImmediatePropagation) event.stopImmediatePropagation(); previewReport(); return; } var saveBtn = event.target.closest('[data-save-report]'); if (saveBtn) { event.preventDefault(); event.stopPropagation(); if (event.stopImmediatePropagation) event.stopImmediatePropagation(); saveReportOnly(); return; } var exportBtn = event.target.closest('[data-export-type]'); if (exportBtn) { event.preventDefault(); event.stopPropagation(); if (event.stopImmediatePropagation) event.stopImmediatePropagation(); exportReport(exportBtn.dataset.exportType || 'pdf'); } }); if (pagination) { pagination.addEventListener('click', function(event) { var pageBtn = event.target.closest('[data-report-page]'); if (pageBtn) { event.preventDefault(); showReportPage(parseInt(pageBtn.dataset.reportPage || '0', 10)); } }); } window.addEventListener('beforeunload', function() { saveReportDraft(false); }); if (canvas && window.MutationObserver) { var draftTimer = null; new MutationObserver(function() { clearTimeout(draftTimer); draftTimer = setTimeout(function() { saveReportDraft(false); }, 500); }).observe(canvas, {childList:true,subtree:true,attributes:true,attributeFilter:['class','style']}); } modal.querySelectorAll('.report-builder-palette-item').forEach(function(btn) { btn.addEventListener('dragstart', function(event) { if (!activeCell) addReportRow('full'); event.dataTransfer.setData('text/plain', btn.dataset.builderKey || ''); event.dataTransfer.setData('report-source', 'palette'); event.dataTransfer.effectAllowed = 'copy'; }); btn.addEventListener('click', function() { if (!activeCell) addReportRow('full'); var clone = cloneTemplate(btn.dataset.builderKey || ''); if (clone && activeCell) { saveActivePageState(); activeCell.innerHTML = ''; activeCell.appendChild(clone); updateReportPages(); } }); }); canvas.addEventListener('click', function(event) { var expandAll = event.target.closest('.call-expand-all-btn'); if (expandAll) { event.preventDefault(); event.stopPropagation(); var panel = expandAll.closest('.call-detail-report-panel'); if (!panel) return; var rows = panel.querySelectorAll('.call-detail-province-row'); var shouldOpen = panel.querySelectorAll('.call-detail-province-row.is-open').length !== rows.length; rows.forEach(function(row) { row.classList.toggle('is-open', shouldOpen); var key = row.getAttribute('data-province-group'); panel.querySelectorAll('[data-province-child="' + key + '"]').forEach(function(child) { child.style.display = shouldOpen ? '' : 'none'; }); }); expandAll.textContent = shouldOpen ? (expandAll.dataset.collapseLabel || 'Collapse All') : (expandAll.dataset.expandLabel || 'Expand All'); return; } var provinceToggle = event.target.closest('.call-province-toggle'); if (provinceToggle) { event.preventDefault(); event.stopPropagation(); var row = provinceToggle.closest('.call-detail-province-row'); var panel = provinceToggle.closest('.call-detail-report-panel'); if (!row || !panel) return; var key = row.getAttribute('data-province-group'); var open = row.classList.toggle('is-open'); panel.querySelectorAll('[data-province-child="' + key + '"]').forEach(function(child) { child.style.display = open ? '' : 'none'; }); var allRows = panel.querySelectorAll('.call-detail-province-row'); var openRows = panel.querySelectorAll('.call-detail-province-row.is-open'); var expandBtn = panel.querySelector('.call-expand-all-btn'); if (expandBtn) expandBtn.textContent = (allRows.length && openRows.length === allRows.length) ? (expandBtn.dataset.collapseLabel || 'Collapse All') : (expandBtn.dataset.expandLabel || 'Expand All'); return; } }, true); canvas.addEventListener('click', function(event) { var cell = event.target.closest('[data-drop-cell]'); if (cell) { activeCell = cell; var row = cell.closest('.report-drop-row'); var rows = reportRows(); var index = rows.indexOf(row); if (index >= 0 && index !== activePageIndex) showReportPage(index); } }); canvas.addEventListener('dragstart', function(event) { var item = event.target.closest('.report-builder-item'); if (!item) return; draggingItem = item; item.classList.add('is-dragging'); event.dataTransfer.setData('text/plain', item.dataset.builderKey || ''); event.dataTransfer.setData('report-source', 'canvas'); event.dataTransfer.effectAllowed = 'move'; }); canvas.addEventListener('dragend', function() { if (draggingItem) draggingItem.classList.remove('is-dragging'); draggingItem = null; canvas.querySelectorAll('.is-drop-hover').forEach(function(cell) { cell.classList.remove('is-drop-hover'); }); }); canvas.addEventListener('dragover', function(event) { var cell = event.target.closest('[data-drop-cell]'); if (!cell) return; event.preventDefault(); cell.classList.add('is-drop-hover'); }, true); canvas.addEventListener('dragleave', function(event) { var cell = event.target.closest('[data-drop-cell]'); if (cell) cell.classList.remove('is-drop-hover'); }, true); canvas.addEventListener('drop', function(event) { var cell = event.target.closest('[data-drop-cell]'); if (!cell) return; event.preventDefault(); event.stopPropagation(); cell.classList.remove('is-drop-hover'); activeCell = cell; var source = event.dataTransfer.getData('report-source'); var key = event.dataTransfer.getData('text/plain'); var item = source === 'canvas' && draggingItem ? draggingItem : cloneTemplate(key); if (!item) return; saveActivePageState(); cell.innerHTML = ''; cell.appendChild(item); updateReportPages(); saveReportDraft(false); }, true); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeServerReportBuilder, {once:true}); } else { initializeServerReportBuilder(); }