finshed afi

This commit is contained in:
2026-03-24 09:45:13 +07:00
parent d4a8c9ded6
commit b22457f344
2 changed files with 299 additions and 60 deletions

View File

@@ -13,6 +13,7 @@ function loadSummary() {
.then(data => {
let html = '';
const alerts = [];
data.forEach(item => {
@@ -53,14 +54,19 @@ function loadSummary() {
</div>
</div>
`;
window._summaryData = data;
updateAlerts();
});
document.getElementById('summary_cards').innerHTML = html;
renderAlerts(alerts);
});
}
/*
|--------------------------------------------------------------------------
| Load Trend Chart
@@ -157,6 +163,215 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
});
}
function updateAlerts() {
if (!window._summaryData || !window._provinceData) return;
const raw = buildAlerts(window._summaryData, window._provinceData);
const finalAlerts = processAlerts(raw);
renderAlerts(finalAlerts);
}
function generateAlerts(data) {
const alerts = [];
// -------------------------
// 1. Program-level alerts
// -------------------------
const summary = data.summary || {};
const programs = [
{ key: 'influenza_rate', label: 'Influenza' },
{ key: 'covid_rate', label: 'COVID-19' },
{ key: 'positivity_rate', label: 'Overall positivity' }
];
programs.forEach(p => {
const current = summary[p.key]?.current || 0;
const previous = summary[p.key]?.previous || 0;
const diff = previous ? ((current - previous) / previous) * 100 : 0;
if (current >= 15) {
alerts.push(`🔴 High ${p.label} (${current}%)`);
} else if (current >= 10) {
alerts.push(`🟠 Moderate ${p.label} (${current}%)`);
}
if (diff >= 10) {
alerts.push(`🟡 Increasing ${p.label} (+${diff.toFixed(1)}%)`);
}
});
// -------------------------
// 2. Province-level alerts
// -------------------------
const provinces = data.province_distribution || [];
const top = [...provinces]
.sort((a, b) => b.total - a.total)
.slice(0, 3);
top.forEach(p => {
const percent = p.total
? ((p.positive / p.total) * 100)
: 0;
if (percent >= 15) {
alerts.push(`🔴 High positivity in ${p.patient_province} (${percent.toFixed(1)}%)`);
} else if (percent >= 10) {
alerts.push(`🟠 Moderate positivity in ${p.patient_province}`);
}
if (p.total >= 50) {
alerts.push(`🟡 High case volume in ${p.patient_province} (${p.total})`);
}
});
// -------------------------
// fallback
// -------------------------
if (!alerts.length) {
alerts.push("🟢 No unusual activity detected");
}
return alerts;
}
function createAlert(type, message, priority) {
return { type, message, priority };
}
function buildAlerts(summaryData, provinceData) {
const alerts = [];
// -------------------------
// 1. Program alerts
// -------------------------
summaryData.forEach(item => {
// 🔴 High activity
if (item.current_total >= 80) {
alerts.push(createAlert(
'high',
`High ${item.code} activity (${item.current_total} cases)`,
1
));
}
// 🟠 Moderate
else if (item.current_total >= 40) {
alerts.push(createAlert(
'moderate',
`${item.code} activity elevated (${item.current_total})`,
2
));
}
// 🟡 Increasing trend
if (item.percent_change >= 10) {
alerts.push(createAlert(
'trend',
`Increasing ${item.code} (+${item.percent_change}%)`,
3
));
}
});
// -------------------------
// 2. Province alerts
// -------------------------
const top = [...provinceData]
.sort((a, b) => b.total - a.total)
.slice(0, 5);
top.forEach(p => {
const percent = p.total
? ((p.positive / p.total) * 100)
: 0;
// 🔴 High positivity
if (percent >= 15) {
alerts.push(createAlert(
'high',
`High positivity in ${p.patient_province} (${percent.toFixed(1)}%)`,
1
));
}
// 🟠 Moderate positivity
else if (percent >= 10) {
alerts.push(createAlert(
'moderate',
`Moderate positivity in ${p.patient_province}`,
2
));
}
// 🟡 High volume
if (p.total >= 50) {
alerts.push(createAlert(
'volume',
`High case volume in ${p.patient_province} (${p.total})`,
3
));
}
});
return alerts;
}
function processAlerts(alerts) {
const seen = new Set();
const unique = alerts.filter(a => {
if (seen.has(a.message)) return false;
seen.add(a.message);
return true;
});
// sort by priority
unique.sort((a, b) => a.priority - b.priority);
// limit to top 5
return unique.slice(0, 5);
}
function renderAlerts(alerts) {
const container = document.getElementById('alertsList');
if (!container) return;
if (!alerts.length) {
container.innerHTML = `
<li class="list-group-item text-success">
🟢 No unusual activity detected
</li>
`;
return;
}
const colorMap = {
high: 'text-danger',
moderate: 'text-warning',
trend: 'text-primary',
volume: 'text-secondary'
};
container.innerHTML = alerts.map(a => `
<li class="list-group-item ${colorMap[a.type] || ''}">
${a.type === 'high' ? '🔴' :
a.type === 'moderate' ? '🟠' :
a.type === 'trend' ? '🟡' :
'🔵'}
${a.message}
</li>
`).join('');
}
/*
@@ -164,7 +379,13 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
| Province Map Helpers
|--------------------------------------------------------------------------
*/
function getPositivityColor(p) {
if (p > 20) return "#b91c1c";
if (p > 10) return "#ef4444";
if (p > 5) return "#f59e0b";
if (p > 0) return "#84cc16";
return "#9ca3af";
}
function normalizeProvince(name, validSet) {
if (!name || !validSet) return null;
@@ -200,8 +421,32 @@ function getRadius(total) {
const r = Math.sqrt(total);
return Math.max(4, Math.min(r * 2, 22));
}
function addPositivityLegend() {
const legend = L.control({ position: "bottomleft" });
legend.onAdd = function () {
const div = L.DomUtil.create("div", "map-legend");
div.innerHTML = `
<div style="background:white;padding:10px 12px;border-radius:6px;
box-shadow:0 2px 6px rgba(0,0,0,0.2);font-size:12px;">
<div style="font-weight:600;margin-bottom:6px;">Positivity</div>
<div><span style="border:3px solid #b91c1c;width:12px;height:12px;display:inline-block;margin-right:6px;"></span>> 20%</div>
<div><span style="border:3px solid #ef4444;width:12px;height:12px;display:inline-block;margin-right:6px;"></span>1020%</div>
<div><span style="border:3px solid #f59e0b;width:12px;height:12px;display:inline-block;margin-right:6px;"></span>510%</div>
<div><span style="border:3px solid #84cc16;width:12px;height:12px;display:inline-block;margin-right:6px;"></span>05%</div>
<div><span style="border:3px solid #9ca3af;width:12px;height:12px;display:inline-block;margin-right:6px;"></span>0%</div>
</div>
`;
return div;
};
legend.addTo(map);
}
/*
|--------------------------------------------------------------------------
| Province Map
@@ -213,6 +458,7 @@ function loadProvinceMap(startYear, startWeek, endYear, endWeek) {
if (map) map.remove();
map = L.map('provinceMap').setView([12.7, 104.9], 7);
addPositivityLegend();
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
@@ -223,6 +469,8 @@ function loadProvinceMap(startYear, startWeek, endYear, endWeek) {
fetch(`/api/dashboard/province-circles?start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`).then(r => r.json())
])
.then(([geojson, data]) => {
window._provinceData = data;
updateAlerts();
const validProvinces = new Set(
geojson.features.map(f => f.properties.ADM1_EN)
@@ -274,8 +522,8 @@ function loadProvinceMap(startYear, startWeek, endYear, endWeek) {
L.circleMarker([lat, lng], {
radius: getRadius(row.total),
fillColor: colors[row.surveillance_id],
color: '#fff',
weight: 1,
color: getPositivityColor(percent),
weight: 2,
fillOpacity: 0.9
})
.bindTooltip(`

View File

@@ -66,16 +66,7 @@
<h5 class="fw-bold">Recent Alerts & Notifications</h5>
<ul class="list-group list-group-flush mt-3">
<li class="list-group-item">
Monitoring influenza increase in selected provinces.
</li>
<li class="list-group-item">
🔔 SARS-CoV-2 positivity rate under review.
</li>
</ul>
<ul id="alertsList" class="list-group list-group-flush mt-3"></ul>
</div>
</div>