import { COLORS, SUBTYPE_COLORS } from "./globals.js";
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) => {
document.querySelectorAll(".report-period")
.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 buildDistributionChart(
id,
type,
rows,
labelKey,
valueKey = 'total',
colorResolver = null
) {
const labels = rows.map(r => r[labelKey]);
buildChart(
id,
type,
labels,
rows.map(r => r[valueKey])
);
if (!charts[id]) return;
charts[id].data.datasets[0].backgroundColor = labels.map(
(label, index) =>
colorResolver
? colorResolver(label, index)
: COLORS[index % COLORS.length]
);
charts[id].update();
}
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 getSubtypeColor(label, index = 0) {
const specialColors = {
'A/H5N1': '#dc2626',
'Influenza': '#b90c00'
};
return specialColors[label]
|| COLORS[index % COLORS.length];
}
function renderSummary(summary = {}) {
const mappings = [
['totalCases', 'casesChange', summary.cases],
['influenzaRate', 'influenzaChange', summary.influenza_rate, '%'],
['covidRate', 'covidChange', summary.covid_rate, '%'],
['hospitalRate', 'hospitalChange', summary.hospital_rate, '%'],
['icuRate', 'icuChange', summary.icu_rate, '%'],
['positivityRate', 'positivityChange', summary.positivity_rate, '%']
];
mappings.forEach(([valueId, changeId, data = {}, suffix = '']) => {
renderTrend(
valueId,
changeId,
data.current || 0,
data.previous || 0,
suffix
);
});
}
function renderDashboard(data = {}) {
renderProgramTrend(data.trend || []);
renderSummary(data.summary);
renderProvinceHeatmap(data.province_distribution || []);
buildDistributionChart(
'pathogenChart',
'doughnut',
(data.pathogen_distribution || [])
.sort((a, b) => b.total - a.total),
'pathogen',
'total',
getSubtypeColor
);
buildDistributionChart(
'ageChart',
'doughnut',
data.age_distribution || [],
'age_group'
);
buildDistributionChart(
'sexChart',
'bar',
data.sex_distribution || [],
'patient_sex'
);
buildDistributionChart(
'subtypeChart',
'bar',
data.subtype_distribution || [],
'subtype',
'total',
getSubtypeColor
);
buildDistributionChart(
'sentinelChart',
'pie',
data.sentinel_sites || [],
'name'
);
}
function renderAFIDashboard(data = {}) {
const trend = data.afi_trend || {};
const positivity = data.afi_case_trend || {};
renderAFITrend(
data.afi_case_trend.section_1,
'afiSection1Trend',
COLORS,
'trend'
);
renderAFITrend(
data.afi_case_trend.section_2,
'afiSection2Trend',
COLORS,
'trend'
);
renderAFITrend(
data.afi_case_trend.section_3,
'afiSection3Trend',
COLORS,
'trend'
);
// DOUGHNUTS
renderAFITrend(
data.afi_case_trend.section_1,
'afiPcrChart',
COLORS,
'donut'
);
renderAFITrend(
data.afi_case_trend.section_2,
'afiMultiplexChart',
COLORS,
'donut'
);
renderAFITrend(
data.afi_case_trend.section_3,
'afiElisaChart',
COLORS,
'donut'
);
renderSummary(data.summary);
renderProvinceHeatmap(data.province_distribution);
renderDemographics(data);
renderPathogenChart(data.pathogen_distribution || []);
renderSentinel(data.sentinel_sites || []);
renderSubtypeChart(data.subtype_distribution || []);
}
function normalizeProvince(name, validSet) {
if (!name || !validSet) return null;
const clean = str =>
str.toLowerCase().replace(/\s+/g, '');
const raw = name.trim();
const provinceMap = {
"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 (provinceMap[key] && validSet.has(provinceMap[key])) {
return provinceMap[key];
}
const match = [...validSet]
.find(p => clean(p) === key);
return match || null;
}
window.normalizeProvince = normalizeProvince;
function renderProvinceHeatmap(rows = []) {
window.latestProvinceData = rows;
if (map) {
map.remove();
}
map = L.map('provinceMap')
.setView([12.7, 104.9], 7);
window.map = map;
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;
totals[province] ??= {
total: 0,
positive: 0
};
totals[province].total += Number(r.total || 0);
totals[province].positive += Number(r.positive || 0);
});
const 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 = `