let trendChart; let map; /* |-------------------------------------------------------------------------- | Load Summary Cards |-------------------------------------------------------------------------- */ function loadSummary() { fetch('/api/dashboard/summary') .then(res => res.json()) .then(data => { let html = ''; data.forEach(item => { let trendColor = 'text-secondary'; if (item.percent_change > 0) trendColor = 'text-danger'; if (item.percent_change < 0) trendColor = 'text-success'; html += `
${item.code}

${item.current_total}

Last 7 days
${item.percent_change > 0 ? '▲' : item.percent_change < 0 ? '▼' : '–'} ${Math.abs(item.percent_change)}%
${item.previous_total ?? 0} last week
`; }); document.getElementById('summary_cards').innerHTML = html; }); } /* |-------------------------------------------------------------------------- | Load Trend Chart |-------------------------------------------------------------------------- */ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) { fetch(`/api/dashboard/trend?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`) .then(res => res.json()) .then(data => { if (trendChart) trendChart.destroy(); const labelsSet = new Set(); Object.values(data).forEach(program => { program.forEach(row => { labelsSet.add(`${row.year}-${row.period}`); }); }); const labels = Array.from(labelsSet).sort((a, b) => { const [yearA, weekA] = a.split('-').map(Number); const [yearB, weekB] = b.split('-').map(Number); if (yearA !== yearB) return yearA - yearB; return weekA - weekB; }); const colors = { SARI: '#2563eb', ILI: '#10b981', LBM: '#9333ea' }; const datasets = []; const allowedPrograms = ['SARI', 'ILI', 'LBM']; Object.keys(data).forEach(code => { if (!allowedPrograms.includes(code)) return; const values = labels.map(label => { const found = data[code].find(row => `${row.year}-${row.period}` === label); return found ? found.total : 0; }); datasets.push({ label: code, data: values, borderColor: colors[code], backgroundColor: colors[code], borderWidth: 3, pointRadius: 4, fill: false, tension: 0.3 }); }); const displayLabels = labels.map(l => { const [year, week] = l.split('-'); return `W${String(week).padStart(2, '0')}`; }); trendChart = new Chart(document.getElementById('trendChart'), { type: 'line', data: { labels: displayLabels, datasets: datasets }, options: { responsive: true, plugins: { legend: { position: 'bottom' } }, interaction: { mode: 'index', intersect: false }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } }, x: { grid: { display: false } } } } }); }); } /* |-------------------------------------------------------------------------- | Province Map |-------------------------------------------------------------------------- */ function loadProvinceMap(startYear, startWeek, endYear, endWeek) { 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' }).addTo(map); Promise.all([ fetch('/geo/cambodia_provinces.geojson').then(r => r.json()), fetch(`/api/dashboard/province-circles?start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`).then(r => r.json()) ]) .then(([geojson, data]) => { L.geoJSON(geojson, { style: { fillOpacity: 0, color: '#ccc', weight: 1, interactive: false }, onEachFeature: function (feature, layer) { const province = feature.properties.ADM1_EN; const center = layer.getBounds().getCenter(); const rows = data.filter(d => d.site_province_name === province); const offsets = { 1: -0.15, 2: 0, 3: 0.15 }; rows.forEach(row => { const lat = center.lat; const lng = center.lng + offsets[row.surveillance_id]; const programName = row.surveillance_id === 1 ? 'SARI' : row.surveillance_id === 2 ? 'ILI' : 'LBM'; const colors = { 1: '#2563eb', 2: '#10b981', 3: '#9333ea' }; L.circleMarker([lat, lng], { radius: 9, fillColor: colors[row.surveillance_id], color: '#fff', weight: 1, fillOpacity: 0.9 }) .bindTooltip(` ${province}
${programName}
Total: ${row.total} `) .addTo(map); }); } }).addTo(map); }); } /* |-------------------------------------------------------------------------- | Initialize Dashboard |-------------------------------------------------------------------------- */ document.addEventListener("DOMContentLoaded", () => { loadSummary(); new DashboardFilter((startYear, startWeek, endYear, endWeek) => { loadTrend('week', startYear, startWeek, endYear, endWeek); loadProvinceMap(startYear, startWeek, endYear, endWeek); }); });