finalize detail page except NDS and squencing

This commit is contained in:
2026-03-20 15:50:30 +07:00
parent aab4bd25dc
commit d4a8c9ded6
8 changed files with 212665 additions and 105432 deletions

View File

@@ -1,36 +1,84 @@
const standardPrograms = ['SARI', 'ILI', 'LBM'];
const standardPrograms = ['SARI', 'ILI', 'LBM', 'AFI'];
const programCode = (window.PROGRAM_CODE || '').trim().toUpperCase();
let map;
let provinceLayer;
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;
}
document.addEventListener("DOMContentLoaded", () => {
if (!standardPrograms.includes(programCode)) return;
new DashboardFilter((startYear, startWeek, endYear, endWeek) => {
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(renderDashboard)
.then(programCode === 'AFI' ? renderAFIDashboard : renderDashboard)
.catch(err => console.error("Dashboard API error:", err));
});
});
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'
];
renderSummary(data.summary);
renderAFITrend(data.afi_trend);
renderProvinceHeatmap(data.province_distribution);
renderPathogenChart(data.pathogen_distribution);
renderDemographics(data);
renderSentinel(data.sentinel_sites || []);
renderSubtypeChart(data.subtype_distribution || []);
charts['pathogenChart'].data.datasets[0].backgroundColor = colors;
charts['pathogenChart'].update();
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();
}
function renderProvinceHeatmap(rows) {
const totals = {};
rows.forEach(r => {
totals[r.site_province_name] = {
total: Number(r.total),
positive: Number(r.positive)
};
});
if (map) {
map.remove();
}
if (map) map.remove();
map = L.map('provinceMap').setView([12.7, 104.9], 7);
@@ -44,6 +92,28 @@ function renderProvinceHeatmap(rows) {
.then(r => r.json())
.then(geo => {
const validProvinces = new Set(
geo.features.map(f => f.properties.ADM1_EN)
);
const totals = {};
rows.forEach(r => {
// ✅ FIX: use patient_province + validSet
const province = normalizeProvince(r.patient_province, validProvinces);
if (!province) return;
// ✅ FIX: accumulate instead of overwrite
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";
@@ -65,11 +135,15 @@ function renderProvinceHeatmap(rows) {
};
},
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;
console.log(province, total, positive, percent);
// ✅ positivity kept
const percent = total
? ((positive / total) * 100).toFixed(1)
: 0;
layer.bindTooltip(`
${province}<br>
@@ -159,6 +233,7 @@ function renderTrend(valueId, changeId, current, previous, suffix = '') {
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);
@@ -230,32 +305,22 @@ function renderSummary(summary) {
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 || []);
// buildStackedChart(
// "pathogenChart",
// labels,
// [
// {
// label: "Influenza",
// data: influenza,
// backgroundColor: "#2E7D32"
// },
// {
// label: "SARS-CoV-2",
// data: covid,
// backgroundColor: "#A5D6A7"
// }
// ]
// );
const pathogenRows = (data.pathogen_distribution || [])
.sort((a, b) => b.total - a.total);
const colors = [
'#2563eb', '#10b981', '#f59e0b', '#ef4444',
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16'
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16', '#e11dba', '#f6f63b'
];
@@ -305,3 +370,114 @@ function renderDashboard(data) {
charts['sentinelChart'].update();
}
//AFI
function renderAFITrend(rows) {
if (!rows.length) {
buildStackedChart('trendChart', [], []);
return;
}
const { labels, datasets } = transformAFIData(rows);
buildStackedChart('trendChart', labels, datasets);
}
function transformAFIData(rows) {
const grouped = {};
const pathogensSet = new Set();
rows.forEach(r => {
const key = `${r.year}-W${r.period}`;
if (!grouped[key]) {
grouped[key] = {};
}
grouped[key][r.pathogen] = r.total;
pathogensSet.add(r.pathogen);
});
const labels = Object.keys(grouped).sort((a, b) => {
const [yA, wA] = a.split('-W').map(Number);
const [yB, wB] = b.split('-W').map(Number);
return yA === yB ? wA - wB : yA - yB;
});
const pathogenTotals = {};
rows.forEach(r => {
pathogenTotals[r.pathogen] = (pathogenTotals[r.pathogen] || 0) + r.total;
});
const pathogens = Object.keys(pathogenTotals)
.sort((a, b) => pathogenTotals[b] - pathogenTotals[a]);
const datasets = pathogens.map(p => ({
label: p,
data: labels.map(l => grouped[l][p] || 0),
backgroundColor: getColorForPathogen(p)
}));
return { labels: labels.map(l => l.split('-')[1]), datasets };
}
function getColorForPathogen(name) {
const colors = {
Dengue: '#2563eb',
Chikungunya: '#10b981',
Zika: '#f59e0b',
Leptospira: '#ef4444',
Rickettsia: '#8b5cf6',
Salmonella: '#f97316',
Plasmodium: '#14b8a6',
Influenza: '#84cc16'
};
if (colors[name]) return colors[name];
// fallback random color (for future pathogens)
return `hsl(${Math.floor(Math.random() * 360)}, 70%, 60%)`;
}
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)
);
}