finshed afi
This commit is contained in:
@@ -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>10–20%</div>
|
||||
<div><span style="border:3px solid #f59e0b;width:12px;height:12px;display:inline-block;margin-right:6px;"></span>5–10%</div>
|
||||
<div><span style="border:3px solid #84cc16;width:12px;height:12px;display:inline-block;margin-right:6px;"></span>0–5%</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(`
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user