const standardPrograms = ['SARI', 'ILI', 'LBM', 'AFI', 'NDS']; const programCode = (window.PROGRAM_CODE || '').trim().toUpperCase(); let map; document.addEventListener("DOMContentLoaded", () => { if (!standardPrograms.includes(programCode)) return; new DashboardFilter((startYear, startWeek, endYear, endWeek) => { const elements = document.querySelectorAll(".report-period"); elements.forEach(el => { el.textContent = 'Week ' + startWeek + ' of ' + startYear + ' to ' + 'Week ' + endWeek + ' of ' + endYear }); fetch(`/api/dashboard/program?surveillance_id=${window.SURVEILLANCE_ID}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`) .then(res => res.json()) .then(programCode === 'AFI' ? renderAFIDashboard : renderDashboard) .catch(err => console.error("Dashboard API error:", err)); }); }); function normalizeProvince(name, validSet) { if (!name || !validSet) return null; const clean = str => str.toLowerCase().replace(/\s+/g, ''); const raw = name.trim(); const map = { "kepville": "Kep", "sihanoukville": "Preah Sihanouk", "sihanoukvillecity": "Preah Sihanouk", "krongpailin": "Pailin", "mondulkiri": "Mondulkiri", "odormeanchey": "Oddar Meanchey", "tbongkhmom": "Tboung Khmum", "tboungkhmum": "Tboung Khmum", "rattanakiri": "Ratanak Kiri" }; const key = clean(raw); if (map[key] && validSet.has(map[key])) { return map[key]; } const match = [...validSet].find(p => clean(p) === key); return match || null; } function renderAFIDashboard(data) { const pathogenRows = (data.pathogen_distribution || []) .sort((a, b) => b.total - a.total); const colors = [ '#2563eb', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#14b8a6', '#f97316', '#84cc16', '#e11dba', '#f6f63b' ]; const rows = data.afi_trend || []; const pcr = rows.filter(r => r.test_type === 'PCR'); const serum = rows.filter(r => r.test_type === 'Serum'); renderAFITrend(pcr, 'trendChart', colors); //renderAFITrend(serum, 'pathogenChart', colors); renderSummary(data.summary); renderProvinceHeatmap(data.province_distribution); renderDemographics(data); renderPathogenChart(data.pathogen_distribution || []); renderSentinel(data.sentinel_sites || []); renderSubtypeChart(data.subtype_distribution || []); charts['ageChart'].data.datasets[0].backgroundColor = colors; charts['ageChart'].update(); charts['sexChart'].data.datasets[0].backgroundColor = colors; charts['sexChart'].update(); charts['sentinelChart'].data.datasets[0].backgroundColor = colors; charts['sentinelChart'].update(); charts['subtypeChart'].data.datasets[0].backgroundColor = colors; charts['subtypeChart'].update(); charts['pathogenChart'].data.datasets[0].backgroundColor = colors; charts['pathogenChart'].update(); } function renderProvinceHeatmap(rows) { window.latestProvinceData = rows; if (map) map.remove(); map = L.map('provinceMap').setView([12.7, 104.9], 7); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map); addProvinceLegend(); fetch('/geo/cambodia_provinces.geojson') .then(r => r.json()) .then(geo => { const validProvinces = new Set( geo.features.map(f => f.properties.ADM1_EN) ); window.validProvinces = validProvinces; const totals = {}; rows.forEach(r => { const province = normalizeProvince(r.patient_province, validProvinces); 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"; } window.provinceLayer = L.geoJSON(geo, { style: feature => { const province = feature.properties.ADM1_EN; const value = totals[province]?.total || 0; return { color: "#444", weight: 1.5, fillColor: getColor(value), fillOpacity: 0.8 }; }, onEachFeature: (feature, layer) => { const province = feature.properties.ADM1_EN; const total = totals[province]?.total || 0; const positive = totals[province]?.positive || 0; const percent = total ? ((positive / total) * 100).toFixed(1) : 0; layer.bindTooltip(` ${province}
Total: ${total}
Positivity: ${percent}% `); } }).addTo(map); }); } function addProvinceLegend() { const legend = L.control({ position: "bottomright" }); legend.onAdd = function () { const div = L.DomUtil.create("div", "map-legend"); div.innerHTML = `
Cases
> 50
10 – 50
1 – 9
0
`; return div; }; legend.addTo(map); } function renderTrend(valueId, changeId, current, previous, suffix = '') { const valueEl = document.getElementById(valueId); const changeEl = document.getElementById(changeId); if (!valueEl || !changeEl) return; valueEl.textContent = current + suffix; if (!previous) { changeEl.innerHTML = "— No previous data"; changeEl.className = "text-muted"; return; } const diff = current - previous; const percent = ((diff / previous) * 100).toFixed(1); if (diff > 0) { changeEl.innerHTML = `↑ +${percent}% from previous week`; changeEl.className = "text-success"; } else if (diff < 0) { changeEl.innerHTML = `↓ ${percent}% from previous week`; changeEl.className = "text-danger"; } else { changeEl.innerHTML = "— No significant change"; changeEl.className = "text-muted"; } } function renderProgramTrend(rows) { rows = rows || []; const labels = rows.map(r => `W${r.period}`); const samples = rows.map(r => r.total_samples || 0); const fluRate = rows.map(r => r.influenza_rate || 0); const covidRate = rows.map(r => r.covid_rate || 0); const lines = [ { label: 'Influenza %', data: fluRate, color: '#fa2929' }, { label: 'COVID-19 %', data: covidRate, color: '#1976D2' } ]; // ✅ ONLY NDS gets EV + Mpox if (programCode === 'NDS') { const evRate = rows.map(r => r.ev_rate || 0); const mpoxRate = rows.map(r => r.mpox_rate || 0); lines.push( { label: 'EV %', data: evRate, color: '#f59e0b' }, { label: 'Mpox %', data: mpoxRate, color: '#8b5cf6' } ); } buildMixedTrendChart( 'trendChart', labels, samples, lines ); } function renderSummary(summary) { summary = summary || {}; const cases = summary.cases || {}; const hospital = summary.hospital_rate || {}; const icu = summary.icu_rate || {}; const positivity = summary.positivity_rate || {}; renderTrend( "totalCases", "casesChange", cases.current || 0, cases.previous || 0 ); renderTrend( "influenzaRate", "influenzaChange", summary.influenza_rate.current, summary.influenza_rate.previous, "%" ); renderTrend( "covidRate", "covidChange", summary.covid_rate.current, summary.covid_rate.previous, "%" ); renderTrend( "hospitalRate", "hospitalChange", hospital.current || 0, hospital.previous || 0, "%" ); renderTrend( "icuRate", "icuChange", icu.current || 0, icu.previous || 0, "%" ); renderTrend( "positivityRate", "positivityChange", positivity.current || 0, positivity.previous || 0, "%" ); } function renderDashboard(data) { data = data || {}; let virusRows = data.virus_trend || []; if (!virusRows.length) { virusRows = [ { period: '', influenza: 0, covid: 0, total_samples: 0 } ]; } renderProgramTrend(data.trend || []); renderSummary(data.summary || {}); renderProvinceHeatmap(data.province_distribution || []); const pathogenRows = (data.pathogen_distribution || []) .sort((a, b) => b.total - a.total); const colors = [ '#2563eb', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#14b8a6', '#f97316', '#84cc16', '#e11dba', '#f6f63b' ]; buildChart( 'pathogenChart', 'doughnut', pathogenRows.map(r => r.pathogen), pathogenRows.map(r => r.total) ); charts['pathogenChart'].data.datasets[0].backgroundColor = colors; charts['pathogenChart'].update(); buildChart( 'ageChart', 'doughnut', (data.age_distribution || []).map(r => r.age_group), (data.age_distribution || []).map(r => r.total) ); charts['ageChart'].data.datasets[0].backgroundColor = colors; charts['ageChart'].update(); buildChart( 'sexChart', 'bar', (data.sex_distribution || []).map(r => r.patient_sex), (data.sex_distribution || []).map(r => r.total) ); charts['sexChart'].data.datasets[0].backgroundColor = colors; charts['sexChart'].update(); buildChart( 'subtypeChart', 'bar', (data.subtype_distribution || []).map(r => r.subtype), (data.subtype_distribution || []).map(r => r.total) ); charts['subtypeChart'].data.datasets[0].backgroundColor = colors; charts['subtypeChart'].update(); buildChart( 'sentinelChart', 'pie', (data.sentinel_sites || []).map(r => r.name), (data.sentinel_sites || []).map(r => r.total) ); charts['sentinelChart'].data.datasets[0].backgroundColor = colors; charts['sentinelChart'].update(); } //AFI function renderAFITrend(rows, canvasId, colors) { if (!rows || !rows.length) { buildStackedChart(canvasId, [], []); return; } const cleanRows = rows.filter(r => r.pathogen); const keyFn = r => `${r.year}-${r.period}`; const map = {}; cleanRows.forEach(r => { const key = keyFn(r); if (!map[key]) map[key] = {}; map[key][r.pathogen] = Number(r.total_tests || r.total || 0); }); const keys = Object.keys(map).sort((a, b) => { const [y1, w1] = a.split('-').map(Number); const [y2, w2] = b.split('-').map(Number); return y1 !== y2 ? y1 - y2 : w1 - w2; }); const labels = keys.map(k => `W${k.split('-')[1]}`); const pathogens = [...new Set(cleanRows.map(r => r.pathogen))]; const datasets = pathogens.map((p, i) => ({ label: p, data: keys.map(k => map[k][p] || 0), backgroundColor: colors[i % colors.length] })); buildStackedChart(canvasId, labels, datasets); } function renderPathogenChart(rows) { buildChart( 'pathogenChart', 'doughnut', rows.map(r => r.pathogen), rows.map(r => r.total) ); } function renderSentinel(rows) { buildChart( 'sentinelChart', 'pie', rows.map(r => r.name), rows.map(r => r.total) ); } function renderDemographics(data) { buildChart( 'ageChart', 'doughnut', (data.age_distribution || []).map(r => r.age_group), (data.age_distribution || []).map(r => r.total) ); buildChart( 'sexChart', 'bar', (data.sex_distribution || []).map(r => r.patient_sex), (data.sex_distribution || []).map(r => r.total) ); } function renderSubtypeChart(rows) { buildChart( 'subtypeChart', 'bar', rows.map(r => r.subtype), rows.map(r => r.total) ); }