finshed afi
This commit is contained in:
@@ -13,6 +13,7 @@ function loadSummary() {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
|
const alerts = [];
|
||||||
|
|
||||||
data.forEach(item => {
|
data.forEach(item => {
|
||||||
|
|
||||||
@@ -53,14 +54,19 @@ function loadSummary() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
window._summaryData = data;
|
||||||
|
updateAlerts();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('summary_cards').innerHTML = html;
|
document.getElementById('summary_cards').innerHTML = html;
|
||||||
|
|
||||||
|
renderAlerts(alerts);
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Load Trend Chart
|
| 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
|
| 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) {
|
function normalizeProvince(name, validSet) {
|
||||||
if (!name || !validSet) return null;
|
if (!name || !validSet) return null;
|
||||||
|
|
||||||
@@ -200,8 +421,32 @@ function getRadius(total) {
|
|||||||
const r = Math.sqrt(total);
|
const r = Math.sqrt(total);
|
||||||
return Math.max(4, Math.min(r * 2, 22));
|
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
|
| Province Map
|
||||||
@@ -213,6 +458,7 @@ function loadProvinceMap(startYear, startWeek, endYear, endWeek) {
|
|||||||
if (map) map.remove();
|
if (map) map.remove();
|
||||||
|
|
||||||
map = L.map('provinceMap').setView([12.7, 104.9], 7);
|
map = L.map('provinceMap').setView([12.7, 104.9], 7);
|
||||||
|
addPositivityLegend();
|
||||||
|
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
attribution: '© OpenStreetMap'
|
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())
|
fetch(`/api/dashboard/province-circles?start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`).then(r => r.json())
|
||||||
])
|
])
|
||||||
.then(([geojson, data]) => {
|
.then(([geojson, data]) => {
|
||||||
|
window._provinceData = data;
|
||||||
|
updateAlerts();
|
||||||
|
|
||||||
const validProvinces = new Set(
|
const validProvinces = new Set(
|
||||||
geojson.features.map(f => f.properties.ADM1_EN)
|
geojson.features.map(f => f.properties.ADM1_EN)
|
||||||
@@ -274,8 +522,8 @@ function loadProvinceMap(startYear, startWeek, endYear, endWeek) {
|
|||||||
L.circleMarker([lat, lng], {
|
L.circleMarker([lat, lng], {
|
||||||
radius: getRadius(row.total),
|
radius: getRadius(row.total),
|
||||||
fillColor: colors[row.surveillance_id],
|
fillColor: colors[row.surveillance_id],
|
||||||
color: '#fff',
|
color: getPositivityColor(percent),
|
||||||
weight: 1,
|
weight: 2,
|
||||||
fillOpacity: 0.9
|
fillOpacity: 0.9
|
||||||
})
|
})
|
||||||
.bindTooltip(`
|
.bindTooltip(`
|
||||||
|
|||||||
@@ -66,16 +66,7 @@
|
|||||||
|
|
||||||
<h5 class="fw-bold">Recent Alerts & Notifications</h5>
|
<h5 class="fw-bold">Recent Alerts & Notifications</h5>
|
||||||
|
|
||||||
<ul class="list-group list-group-flush mt-3">
|
<ul id="alertsList" class="list-group list-group-flush mt-3"></ul>
|
||||||
<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>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user