Files
nrml_dashboard/dashboard/public/js/dashboard/export.js
2026-04-29 09:06:54 +07:00

458 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
document.addEventListener("DOMContentLoaded", () => {
const toggle = document.getElementById("exportToggle");
const items = document.getElementById("exportItems");
const close = document.getElementById("exportClose");
if (!toggle || !items) return;
toggle.addEventListener("click", () => {
items.classList.toggle("show");
toggle.innerText = items.classList.contains("show")
? "Export ▾"
: "Export ▸";
});
if (close) {
close.addEventListener("click", () => {
items.classList.remove("show");
toggle.innerText = "Export ▸";
});
}
});
function openChartSelector() {
if (typeof charts === "undefined" || Object.keys(charts).length === 0) {
alert("Charts are still loading. Please try again.");
return;
}
const modal = document.getElementById("chartModal");
const list = document.getElementById("chartList");
list.innerHTML = "";
Object.keys(charts).forEach(id => {
list.innerHTML += `
<label style="display:block;margin-bottom:6px;">
<input type="checkbox" value="${id}" checked>
${formatChartName(id)}
</label>
`;
});
list.innerHTML += `
<label>
<input type="checkbox" value="provinceMap" checked>
Geographic Distribution
</label>
`;
modal.style.display = "block";
}
function closeChartSelector() {
document.getElementById("chartModal").style.display = "none";
}
function formatChartName(id) {
const map = {
trendChart: "Case Trend & Positivity",
pathogenChart: "Pathogen Distribution",
provinceMap: "Geographic Distribution",
ageChart: "Age Distribution",
sexChart: "Sex Distribution",
subtypeChart: "Influenza Subtypes",
sentinelChart: "Sentinel Site Distribution"
};
return map[id] || id;
}
async function exportSelectedCharts() {
console.log("Exporting selected charts...");
const { jsPDF } = window.jspdf;
const pdf = new jsPDF("l", "mm", "a4");
const margin = 15;
const pageWidth = 297;
const pageHeight = 210;
const contentTop = 35;
const contentHeight = pageHeight - contentTop - 10;
const contentWidth = pageWidth - margin * 2;
const selected = document.querySelectorAll("#chartList input:checked");
const loadingDiv = document.createElement("div");
Object.assign(loadingDiv.style, {
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
background: "rgb(255, 255, 255)",
color: "#fff",
padding: "20px",
borderRadius: "10px",
zIndex: "10000"
});
loadingDiv.innerHTML = "Generating PDF...";
document.body.appendChild(loadingDiv);
function drawHeader() {
pdf.setFillColor(255, 255, 255);
pdf.rect(0, 0, pageWidth, 30, "F");
pdf.setFontSize(18);
pdf.setFont(undefined, "bold");
pdf.setTextColor(0);
pdf.text(getReportTitle(), margin, 14);
pdf.setFontSize(9);
pdf.setTextColor(100);
pdf.text(`Generated: ${new Date().toLocaleString()}`, margin, 22);
pdf.setDrawColor(220);
pdf.line(margin, 28, pageWidth - margin, 28);
}
drawHeader();
const items = [];
for (const cb of selected) {
if (cb.value === "provinceMap") {
loadingDiv.innerHTML = "Processing map...";
const img = await getMapImage();
if (img) {
items.push({
id: "provinceMap",
type: "map",
img
});
}
} else {
const chart = charts[cb.value];
if (!chart) continue;
items.push({
id: cb.value,
type: "chart",
chart
});
}
}
for (let i = 0; i < items.length; i++) {
const item = items[i];
loadingDiv.innerHTML = `Rendering ${formatChartName(item.id)} (${i + 1}/${items.length})`;
if (i !== 0) {
pdf.addPage();
drawHeader();
}
const cardX = margin;
const cardY = contentTop - 5;
const cardWidth = contentWidth;
const cardHeight = contentHeight;
pdf.setFillColor(255, 255, 255);
pdf.roundedRect(cardX, cardY, cardWidth, cardHeight, 3, 3, "F");
pdf.setDrawColor(230);
pdf.roundedRect(cardX, cardY, cardWidth, cardHeight, 3, 3);
pdf.setFontSize(13);
pdf.setFont(undefined, "bold");
pdf.setTextColor(0);
pdf.text(formatChartName(item.id), cardX + 5, cardY + 8);
let img;
let width;
let height;
if (item.type === "map") {
img = item.img;
width = cardWidth - 50;
height = width * 0.65;
} else {
const canvas = item.chart.canvas;
const tempCanvas = document.createElement("canvas");
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
const ctx = tempCanvas.getContext("2d", { willReadFrequently: true });
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(canvas, 0, 0);
img = tempCanvas.toDataURL("image/png");
const ratio = canvas.height / canvas.width;
width = cardWidth - 10;
height = width * ratio;
if (height > cardHeight - 20) {
height = cardHeight - 20;
width = height / ratio;
}
}
const x = cardX + (cardWidth - width) / 2;
const contentTopOffset = 14;
const availableHeight = cardHeight - contentTopOffset;
const y = cardY + contentTopOffset + (availableHeight - height) / 2;
pdf.addImage(img, "PNG", x, y, width, height);
}
document.body.removeChild(loadingDiv);
pdf.save("dashboard_report.pdf");
closeChartSelector();
}
function prepareMapForExport() {
if (!window.map) return;
const bounds = L.latLngBounds([
[10.3, 102.3],
[14.7, 107.6]
]);
map.fitBounds(bounds);
}
async function exportFullDashboard() {
console.log("Exporting full charts...");
const el = document.querySelector(".content-area");
const mapEl = document.getElementById("provinceMap");
let originalMapHTML = null;
if (mapEl) {
originalMapHTML = mapEl.innerHTML;
const mapImg = await getMapImage();
if (mapImg) {
mapEl.innerHTML = `<img src="${mapImg}" style="width:100%;height:100%;object-fit:cover;" />`;
}
}
prepareMapForExport();
await new Promise(resolve => setTimeout(resolve, 800));
const canvas = await html2canvas(el, {
scale: 3,
useCORS: true,
scrollY: -window.scrollY
});
if (mapEl && originalMapHTML !== null) {
mapEl.innerHTML = originalMapHTML;
map.invalidateSize();
}
const img = canvas.toDataURL("image/png");
const { jsPDF } = window.jspdf;
const pdf = new jsPDF("l", "mm", "a4");
const pageWidth = 297;
const pageHeight = 210;
const ratio = Math.min(
pageWidth / canvas.width,
pageHeight / canvas.height
);
const imgWidth = canvas.width * ratio;
const imgHeight = canvas.height * ratio;
const x = (pageWidth - imgWidth) / 2;
const y = (pageHeight - imgHeight) / 2;
pdf.addImage(img, "PNG", x, y, imgWidth, imgHeight);
pdf.save("full_dashboard.pdf");
closeChartSelector();
}
function getColor(value) {
if (value > 50) return "#b91c1c";
if (value >= 10) return "#ef4444";
if (value > 0) return "#fecaca";
return "#f3f4f600";
}
async function getMapImage() {
const original = document.getElementById("provinceMap");
if (!original) return null;
const width = original.offsetWidth;
const height = original.offsetHeight;
const canvas = document.createElement("canvas");
canvas.width = width * 3;
canvas.height = height * 3;
const ctx = canvas.getContext("2d");
ctx.scale(3, 3);
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, width, height);
const zoom = map.getZoom();
const projection = map.options.crs;
const projectedRings = [];
let minX = Infinity, minY = Infinity;
let maxX = -Infinity, maxY = -Infinity;
const totals = {};
const rows = window.latestProvinceData || [];
rows.forEach(r => {
const province = normalizeProvince(r.patient_province, window.validProvinces);
console.log(province, totals[province]);
if (!province) return;
if (!totals[province]) {
totals[province] = { total: 0, positive: 0 };
}
totals[province].total += Number(r.total);
totals[province].positive += Number(r.positive);
});
function getColor(value) {
if (value > 50) return "#b91c1c";
if (value >= 10) return "#ef4444";
if (value > 0) return "#fecaca";
return "#f3f4f600";
}
map.eachLayer(layer => {
if (!layer.toGeoJSON) return;
const geo = layer.toGeoJSON();
const features = geo.type === "FeatureCollection"
? geo.features
: [geo];
features.forEach(f => {
if (!f.geometry) return;
const coords = f.geometry.coordinates;
const polygons = f.geometry.type === "MultiPolygon"
? coords
: [coords];
polygons.forEach(poly => {
poly.forEach(ring => {
const projected = ring.map(([lng, lat]) => {
const latlng = L.latLng(lat, lng);
const point = projection.latLngToPoint(latlng, zoom);
minX = Math.min(minX, point.x);
maxX = Math.max(maxX, point.x);
minY = Math.min(minY, point.y);
maxY = Math.max(maxY, point.y);
return [point.x, point.y];
});
projectedRings.push({
points: projected,
properties: f.properties
});
});
});
});
});
const padding = 10;
const scaleX = (width - padding * 2) / (maxX - minX);
const scaleY = (height - padding * 2) / (maxY - minY);
const scale = Math.min(scaleX, scaleY);
const offsetX = (width - (maxX - minX) * scale) / 2;
const offsetY = (height - (maxY - minY) * scale) / 2;
projectedRings.forEach(({ points, properties }) => {
ctx.beginPath();
points.forEach(([x, y], i) => {
const drawX = (x - minX) * scale + offsetX;
const drawY = (y - minY) * scale + offsetY;
if (i === 0) ctx.moveTo(drawX, drawY);
else ctx.lineTo(drawX, drawY);
});
ctx.closePath();
const province = properties.ADM1_EN;
const value = totals[province]?.total || 0;
ctx.fillStyle = getColor(value);
ctx.fill();
ctx.strokeStyle = "#444";
ctx.lineWidth = 1;
ctx.stroke();
});
const legend = document.querySelector(".map-legend");
if (legend) {
const legendCanvas = await html2canvas(legend, {
scale: 3,
backgroundColor: null
});
const lw = legend.offsetWidth;
const lh = legend.offsetHeight;
ctx.drawImage(
legendCanvas,
width - lw - 10,
height - lh - 10,
lw,
lh
);
}
return canvas.toDataURL("image/png");
}
function getReportTitle() {
const program = window.PROGRAM_CODE || "Overview";
const rangeType = document.getElementById("trend_range")?.value;
if (rangeType === "custom") {
const sy = document.getElementById("start_year")?.value;
const sw = document.getElementById("start_week")?.value;
const ey = document.getElementById("end_year")?.value;
const ew = document.getElementById("end_week")?.value;
return `${program} Report (W${sw}/${sy} W${ew}/${ey})`;
}
return `${program} Report (Last ${rangeType} weeks)`;
}