From e80cb128bfe181d555eb13e969246b016fcfacd8 Mon Sep 17 00:00:00 2001 From: Khantey Long Date: Thu, 19 Mar 2026 09:20:42 +0700 Subject: [PATCH] finalized overview page --- .../Controllers/Api/DashboardController.php | 1 + dashboard/app/Services/DashboardService.php | 318 +++++++++++---- dashboard/public/js/dashboard/charts.js | 371 +++++++++++++++--- dashboard/public/js/dashboard/filter.js | 1 - dashboard/public/js/overview.js | 2 +- dashboard/public/js/program.js | 182 ++++++++- .../views/dashboard/detail.blade.php | 159 ++++---- .../views/dashboard/overview.blade.php | 2 +- .../resources/views/layouts/app.blade.php | 13 +- 9 files changed, 822 insertions(+), 227 deletions(-) diff --git a/dashboard/app/Http/Controllers/Api/DashboardController.php b/dashboard/app/Http/Controllers/Api/DashboardController.php index 3b960a3..bc07fb9 100644 --- a/dashboard/app/Http/Controllers/Api/DashboardController.php +++ b/dashboard/app/Http/Controllers/Api/DashboardController.php @@ -74,6 +74,7 @@ class DashboardController extends Controller } $data = $this->service->aggregateAllPrograms( + $range['startYear'], $range['startWeek'], $range['endYear'], diff --git a/dashboard/app/Services/DashboardService.php b/dashboard/app/Services/DashboardService.php index c00720b..bcd085b 100644 --- a/dashboard/app/Services/DashboardService.php +++ b/dashboard/app/Services/DashboardService.php @@ -8,6 +8,16 @@ use App\Models\CaseLabResult; class DashboardService { + private function totalTested($surveillanceId, $startYear, $startWeek, $endYear, $endWeek) + { + return SurveillanceCase::where('surveillance_id', $surveillanceId) + ->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) { + $q->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek]) + ->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]); + }) + ->distinct('lab_code') + ->count('lab_code'); + } /* |-------------------------------------------------------------------------- @@ -56,9 +66,8 @@ class DashboardService |-------------------------------------------------------------------------- */ - public function sariSummaryFast($surveillanceId, $year, $week) + public function programSummaryFast($surveillanceId, $year, $week) { - $row = SurveillanceCase::leftJoin( 'case_lab_results', 'surveillance_cases.lab_code', @@ -79,14 +88,20 @@ class DashboardService END) as overall_positive, COUNT(DISTINCT CASE - WHEN case_lab_results.indicator = 'SARI Influenza Test' - AND case_lab_results.is_positive = 1 + WHEN case_lab_results.is_positive = 1 + AND ( + LOWER(case_lab_results.pathogen_name) LIKE '%influenza%' + OR LOWER(case_lab_results.pathogen_name) LIKE '%influzena%' + ) THEN surveillance_cases.lab_code END) as influenza_positive, COUNT(DISTINCT CASE - WHEN case_lab_results.indicator = 'SARI Covid Test' - AND case_lab_results.is_positive = 1 + WHEN case_lab_results.is_positive = 1 + AND ( + LOWER(case_lab_results.pathogen_name) LIKE '%covid%' + OR LOWER(case_lab_results.pathogen_name) LIKE '%sars%' + ) THEN surveillance_cases.lab_code END) as covid_positive ") @@ -103,29 +118,14 @@ class DashboardService ]; } - return [ - 'cases' => $row->total_cases, - 'overall_rate' => round( - ($row->overall_positive / $row->total_cases) * 100 - , - 1 - ), + 'overall_rate' => round(($row->overall_positive / $row->total_cases) * 100, 1), - 'influenza_rate' => round( - ($row->influenza_positive / $row->total_cases) * 100 - , - 1 - ), - - 'covid_rate' => round( - ($row->covid_positive / $row->total_cases) * 100 - , - 1 - ), + 'influenza_rate' => round(($row->influenza_positive / $row->total_cases) * 100, 1), + 'covid_rate' => round(($row->covid_positive / $row->total_cases) * 100, 1), ]; } @@ -148,9 +148,25 @@ class DashboardService $prevYear--; } - $current = $this->sariSummaryFast($surveillanceId, $endYear, $endWeek); - $previous = $this->sariSummaryFast($surveillanceId, $prevYear, $prevWeek); + $latest = SurveillanceCase::where('surveillance_id', $surveillanceId) + ->selectRaw("year_data, week_data") + ->orderByDesc('year_data') + ->orderByDesc('week_data') + ->first(); + $year = $latest->year_data; + $week = $latest->week_data; + $current = $this->programSummaryFast($surveillanceId, $year, $week); + $previous = $this->programSummaryFast($surveillanceId, $prevYear, $prevWeek); + $prevWeek = $week - 1; + $prevYear = $year; + + if ($prevWeek <= 0) { + $prevWeek = 52; + $prevYear--; + } + + $previous = $this->programSummaryFast($surveillanceId, $prevYear, $prevWeek); return [ @@ -195,7 +211,7 @@ class DashboardService |-------------------------------------------------------------------------- */ - public function aggregateAllPrograms($periodType, $startYear, $startWeek, $endYear, $endWeek) + public function aggregateAllPrograms($startYear, $startWeek, $endYear, $endWeek) { $data = SurveillanceCase::selectRaw(" @@ -293,13 +309,34 @@ class DashboardService $endWeek ), - 'province_distribution' => $this->provinceCirclesProgram( + 'province_distribution' => $this->provinceProgram( $surveillanceId, $startYear, $startWeek, $endYear, $endWeek - ) + ), + 'virus_trend' => $this->virusTrend( + $surveillanceId, + $startYear, + $startWeek, + $endYear, + $endWeek + ), + 'subtype_distribution' => $this->subtypeDistribution( + $surveillanceId, + $startYear, + $startWeek, + $endYear, + $endWeek + ), + 'sentinel_sites' => $this->sentinelSites( + $surveillanceId, + $startYear, + $startWeek, + $endYear, + $endWeek + ), ]; @@ -340,14 +377,46 @@ class DashboardService }) ->selectRaw(" - surveillance_cases.year_data as year, - surveillance_cases.week_data as period, - COUNT(DISTINCT surveillance_cases.lab_code) as total_samples, + surveillance_cases.year_data as year, + surveillance_cases.week_data as period, - ROUND( - SUM(CASE WHEN case_lab_results.is_positive = 1 THEN 1 ELSE 0 END) - / NULLIF(COUNT(*),0) * 100,1 - ) as positivity_rate + COUNT(DISTINCT surveillance_cases.lab_code) as total_samples, + + -- Overall positivity rate + ROUND( + COUNT(DISTINCT CASE + WHEN case_lab_results.is_positive = 1 + THEN surveillance_cases.lab_code + END) + / NULLIF(COUNT(DISTINCT surveillance_cases.lab_code), 0) * 100 + ,1) as positivity_rate, + + -- Influenza positivity rate + ROUND( + COUNT(DISTINCT CASE + WHEN case_lab_results.is_positive = 1 + AND ( + LOWER(case_lab_results.pathogen_name) LIKE '%influenza%' + OR LOWER(case_lab_results.pathogen_name) LIKE '%influzena%' + ) + THEN surveillance_cases.lab_code + END) + / NULLIF(COUNT(DISTINCT surveillance_cases.lab_code), 0) * 100 + ,1) as influenza_rate, + + -- COVID positivity rate + ROUND( + COUNT(DISTINCT CASE + WHEN case_lab_results.is_positive = 1 + AND ( + case_lab_results.pathogen_name = 'Positive' + OR case_lab_results.pathogen_name = 'SARS-CoV-2' + ) + AND case_lab_results.indicator LIKE '%Covid%' + THEN surveillance_cases.lab_code + END) + / NULLIF(COUNT(DISTINCT surveillance_cases.lab_code), 0) * 100 + ,1) as covid_rate ") ->groupBy( @@ -399,7 +468,49 @@ class DashboardService return $results; } + public function virusTrend($surveillanceId, $startYear, $startWeek, $endYear, $endWeek) + { + return CaseLabResult::join( + 'surveillance_cases', + 'case_lab_results.lab_code', + '=', + 'surveillance_cases.lab_code' + ) + ->where('surveillance_cases.surveillance_id', $surveillanceId) + + ->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) { + $q->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek]) + ->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]); + }) + + ->selectRaw(" + surveillance_cases.week_data as period, + + COUNT(DISTINCT CASE + WHEN case_lab_results.is_positive = 1 + AND ( + LOWER(case_lab_results.pathogen_name) LIKE '%influenza%' + OR LOWER(case_lab_results.pathogen_name) LIKE '%influzena%' + ) + THEN surveillance_cases.lab_code + END) as influenza, + + COUNT(DISTINCT CASE + WHEN case_lab_results.is_positive = 1 + AND ( + case_lab_results.pathogen_name = 'Positive' + OR case_lab_results.pathogen_name = 'SARS-CoV-2' + ) + AND case_lab_results.indicator LIKE '%Covid%' + THEN surveillance_cases.lab_code + END) as covid + ") + + ->groupBy('surveillance_cases.week_data') + ->orderBy('surveillance_cases.week_data') + ->get(); + } /* @@ -407,14 +518,24 @@ class DashboardService | Province Distribution (Program) |-------------------------------------------------------------------------- */ - - public function provinceCirclesProgram($surveillanceId, $startYear, $startWeek, $endYear, $endWeek) + public function provinceCircles($startYear, $startWeek, $endYear, $endWeek) + { + return SurveillanceCase::selectRaw(" surveillance_cases.site_province_name, surveillance_cases.surveillance_id, COUNT(*) as total ")->join('case_lab_results', 'surveillance_cases.lab_code', '=', 'case_lab_results.lab_code')->where('case_lab_results.is_positive', 1)->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) { + $q->whereRaw("(surveillance_cases.year_data > ? OR (surveillance_cases.year_data = ? AND surveillance_cases.week_data >= ?))", [$startYear, $startYear, $startWeek])->whereRaw("(surveillance_cases.year_data < ? OR (surveillance_cases.year_data = ? AND surveillance_cases.week_data <= ?))", [$endYear, $endYear, $endWeek]); + })->groupBy('surveillance_cases.site_province_name', 'surveillance_cases.surveillance_id')->get(); + } + public function provinceProgram($surveillanceId, $startYear, $startWeek, $endYear, $endWeek) { - return SurveillanceCase::selectRaw(" - surveillance_cases.site_province_name, - COUNT(DISTINCT surveillance_cases.lab_code) as total - ") + surveillance_cases.site_province_name, + + COUNT(DISTINCT surveillance_cases.lab_code) as total, + + COUNT(DISTINCT CASE + WHEN case_lab_results.is_positive = 1 + THEN surveillance_cases.lab_code + END) as positive + ") ->join( 'case_lab_results', @@ -424,7 +545,6 @@ class DashboardService ) ->where('surveillance_cases.surveillance_id', $surveillanceId) - ->where('case_lab_results.is_positive', 1) ->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) { @@ -442,10 +562,30 @@ class DashboardService ->groupBy('surveillance_cases.site_province_name') ->get(); - } + /* + |-------------------------------------------------------------------------- + | sentinel sites + |-------------------------------------------------------------------------- + */ + public function sentinelSites($surveillanceId, $startYear, $startWeek, $endYear, $endWeek) + { + return SurveillanceCase::selectRaw(" + sentinel_site_name as name, + COUNT(DISTINCT lab_code) as total + ") + ->where('surveillance_id', $surveillanceId) + ->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) { + $q->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek]) + ->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]); + }) + + ->groupBy('sentinel_site_name') + ->orderByDesc('total') // nice for chart + ->get(); + } /* |-------------------------------------------------------------------------- @@ -455,44 +595,84 @@ class DashboardService public function pathogenDistribution($surveillanceId, $startYear, $startWeek, $endYear, $endWeek) { + $total = $this->totalTested($surveillanceId, $startYear, $startWeek, $endYear, $endWeek); - return CaseLabResult::selectRaw(" - pathogen_name, - COUNT(*) as total - ") + $rows = CaseLabResult::join( + 'surveillance_cases', + 'case_lab_results.lab_code', + '=', + 'surveillance_cases.lab_code' + ) + ->where('surveillance_cases.surveillance_id', $surveillanceId) - ->join( - 'surveillance_cases', - 'case_lab_results.lab_code', - '=', - 'surveillance_cases.lab_code' - ) + ->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) { + $q->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek]) + ->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]); + }) + ->where('case_lab_results.is_positive', 1) + + ->selectRaw(" + CASE + WHEN LOWER(case_lab_results.pathogen_name) LIKE '%influenza%' + OR LOWER(case_lab_results.pathogen_name) LIKE '%influzena%' + THEN 'Influenza' + + WHEN case_lab_results.pathogen_name = 'Positive' + AND case_lab_results.indicator LIKE '%Covid%' + THEN 'SARS-CoV-2' + + ELSE case_lab_results.pathogen_name + END as pathogen, + + COUNT(DISTINCT surveillance_cases.lab_code) as total + ") + + ->groupBy('pathogen') + ->havingRaw("pathogen IS NOT NULL AND pathogen != ''") + ->orderByDesc('total') + ->get(); + + return $rows->map(function ($r) use ($total) { + $r->rate = $total > 0 ? round(($r->total / $total) * 100, 1) : 0; + return $r; + }); + } + public function subtypeDistribution($surveillanceId, $startYear, $startWeek, $endYear, $endWeek) + { + $total = $this->totalTested($surveillanceId, $startYear, $startWeek, $endYear, $endWeek); + + $rows = CaseLabResult::join( + 'surveillance_cases', + 'case_lab_results.lab_code', + '=', + 'surveillance_cases.lab_code' + ) ->where('surveillance_cases.surveillance_id', $surveillanceId) ->where('case_lab_results.is_positive', 1) ->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) { - - $q->whereRaw( - "(year_data > ? OR (year_data = ? AND week_data >= ?))", - [$startYear, $startYear, $startWeek] - ) - - ->whereRaw( - "(year_data < ? OR (year_data = ? AND week_data <= ?))", - [$endYear, $endYear, $endWeek] - ); - + $q->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek]) + ->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]); }) - ->groupBy('pathogen_name') - ->orderByDesc('total') - ->get(); + ->selectRaw(" + subtype, + COUNT(DISTINCT surveillance_cases.lab_code) as total + ") + ->groupBy('subtype') + ->havingRaw("subtype IS NOT NULL AND subtype != 'Positive' AND subtype != ''") + ->orderByDesc('total') + ->get(); + + return $rows->map(function ($r) use ($total) { + $r->rate = $total > 0 ? round(($r->total / $total) * 100, 1) : 0; + return $r; + }); } - /* |-------------------------------------------------------------------------- | Age Distribution diff --git a/dashboard/public/js/dashboard/charts.js b/dashboard/public/js/dashboard/charts.js index ce7b4fd..706b45f 100644 --- a/dashboard/public/js/dashboard/charts.js +++ b/dashboard/public/js/dashboard/charts.js @@ -1,6 +1,83 @@ +Chart.register(ChartDataLabels); const charts = {}; -function buildChart(id, type, labels, data, label = 'Cases') { + +function buildStackedChart(canvasId, labels, datasets) { + const ctx = document.getElementById(canvasId); + + if (!ctx) return; + + if (charts[canvasId]) { + charts[canvasId].destroy(); + } + + charts[canvasId] = new Chart(ctx, { + + type: "bar", + + data: { + labels: labels, + datasets: datasets, + datalabels: { + display: true + } + }, + + plugins: [ChartDataLabels], + options: { + responsive: true, + maintainAspectRatio: false, + + layout: { + padding: { + top: 20, + bottom: 30 + } + }, + + + plugins: { + legend: { + position: 'bottom', + align: 'center', + + labels: { + padding: 20, + boxWidth: 10, + boxHeight: 10, + usePointStyle: true, + pointStyle: 'circle' + } + }, + datalabels: { + color: "#000", + anchor: "end", + align: "top", + clamp: true, + clip: false, + font: { + weight: "bold", + size: 10 + }, + formatter: function (value) { + return value > 0 ? value : null; + } + } + }, + + scales: { + x: { + stacked: true + }, + y: { + stacked: true, + beginAtZero: true + } + } + } + }); +} +function buildChart(id, type, labels, data) { const ctx = document.getElementById(id); @@ -13,96 +90,276 @@ function buildChart(id, type, labels, data, label = 'Cases') { data: { labels: labels, datasets: [{ - label: label, data: data, borderWidth: 2, - tension: 0.3 + tension: 0.3, + barPercentage: 0.8, + categoryPercentage: 0.6, + maxBarThickness: 50 }] }, - options: { - responsive: true, - maintainAspectRatio: false - } - }); - -} -function buildMixedTrendChart(canvasId, labels, samples, positivity) { - const ctx = document.getElementById(canvasId); - if (!ctx) return; - if (charts[canvasId]) charts[canvasId].destroy(); - charts[canvasId] = new Chart(ctx, { - data: { - labels: labels, - datasets: [ - { - type: 'line', - label: '% Positive', - data: positivity, - borderColor: '#1e6ef2', - borderWidth: 2, - tension: 0.4, - fill: false, - pointRadius: 4, - pointStyle: 'line', - yAxisID: 'y1' - }, - { - type: 'bar', - label: 'Total sample', - data: samples, - backgroundColor: '#2ecc71', - borderRadius: 6, - barPercentage: 0.6, - pointStyle: 'rect', - categoryPercentage: 0.7, - yAxisID: 'y', - }, - ] - }, options: { responsive: true, maintainAspectRatio: false, + + layout: { + padding: { + top: 30, + bottom: 30 + } + }, + plugins: { legend: { position: 'bottom', align: 'center', + display: type === 'pie' || type === 'doughnut', + labels: { + padding: 10, + boxWidth: 14, + boxHeight: 14, + usePointStyle: true, + pointStyle: 'circle' + } + }, + datalabels: { + color: "#282626", + anchor: type === "bar" ? "end" : "center", + align: type === "bar" ? "top" : "center", + font: { + size: 10 + }, + formatter: function(value, ctx) { + const data = ctx.chart.data.datasets[0].data; + const total = data.reduce((a, b) => a + b, 0); + const percent = total ? (value / total * 100).toFixed(1) : 0; + return percent + '%'; + } + } + } + } + }); + +} +function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) { + + const ctx = document.getElementById(canvasId); + if (!ctx) return; + + if (charts[canvasId]) charts[canvasId].destroy(); + + charts[canvasId] = new Chart(ctx, { + + data: { + labels: labels, + datasets: [ + + { + type: 'line', + label: 'Influenza %', + data: fluRate, + borderColor: '#fa2929', + backgroundColor: '#fa2929', + tension: 0.4, + yAxisID: 'y1', + pointStyle: 'line', + + }, + + + { + type: 'line', + label: 'COVID-19 %', + data: covidRate, + borderColor: '#1976D2', + backgroundColor: '#1976D2', + tension: 0.4, + yAxisID: 'y1', + pointStyle: 'line', + }, + + + { + type: 'bar', + label: 'Total Cases', + data: samples, + backgroundColor: '#0B8F3C', + borderRadius: 2, + barPercentage: 0.8, + categoryPercentage: 0.7, + yAxisID: 'y', + } + + ] + }, + + plugins: [ChartDataLabels], + + options: { + responsive: true, + maintainAspectRatio: false, + + plugins: { + legend: { + position: 'bottom', labels: { usePointStyle: true, - padding: 20, - boxWidth: 30, - font: { - size: 12 + padding: 20 + } + }, + datalabels: { + align: "top", + anchor: "end", + color: "#555", + font: { + size: 10 + }, + formatter: function (value, context) { + if (Number(value) === 0) return null; + + if (context.dataset.type === 'line') { + return value + '%'; } + + return value; } } }, layout: { padding: { - bottom: 50 + top: 30, + bottom: 20 } }, + scales: { y: { position: 'left', title: { display: true, - text: 'Total sample' + text: 'Cases' } }, y1: { position: 'right', - grid: { - drawOnChartArea: false - }, + grid: { drawOnChartArea: false }, title: { display: true, - text: '% Positive' + text: '% Positivity' }, - ticks: { - callback: value => value + '%' - } + } } } }); -} \ No newline at end of file +} +// function buildMixedTrendChart(canvasId, labels, samples, positivity) { + +// const ctx = document.getElementById(canvasId); +// if (!ctx) return; + +// if (charts[canvasId]) charts[canvasId].destroy(); + +// charts[canvasId] = new Chart(ctx, { +// data: { +// labels: labels, +// datasets: [ +// { +// type: 'line', +// label: '% Positive', +// data: positivity, +// borderColor: '#1e6ef2', +// borderWidth: 2, +// tension: 0.4, +// fill: false, +// pointRadius: 2, +// pointStyle: 'line', +// yAxisID: 'y1', + + +// }, + +// { +// type: 'bar', +// label: 'Total sample ', +// data: samples, +// backgroundColor: '#2ecc71', +// borderRadius: 2, +// barPercentage: 0.8, +// categoryPercentage: 0.7, +// yAxisID: 'y', + +// } +// ] +// }, + +// plugins: [ChartDataLabels], + +// options: { +// responsive: true, +// maintainAspectRatio: false, + +// plugins: { +// legend: { +// position: 'bottom', +// align: 'center', +// labels: { +// usePointStyle: true, +// padding: 20, +// boxWidth: 30, +// font: { size: 12 } +// } +// }, +// datalabels: { +// align: "top", +// anchor: "end", +// color: "#555", +// font: { +// size: 10 +// }, +// formatter: function (value, context) { + +// if (Number(value) === 0) return null; +// if (context.dataset.type === 'line') { +// console.log(value); +// return value + '%'; +// } + +// return value; +// } +// } +// }, + +// layout: { +// padding: { +// top: 20, +// bottom: 50 +// } +// }, + +// scales: { +// y: { +// position: 'left', +// title: { +// display: true, +// text: 'Total sample' +// } +// }, + +// y1: { +// position: 'right', +// grid: { +// drawOnChartArea: false +// }, +// title: { +// display: true, +// text: '% Positive' +// }, +// ticks: { +// callback: value => value + '%' +// } +// } +// } +// } +// }); +// } \ No newline at end of file diff --git a/dashboard/public/js/dashboard/filter.js b/dashboard/public/js/dashboard/filter.js index 6951a9a..2d12fa3 100644 --- a/dashboard/public/js/dashboard/filter.js +++ b/dashboard/public/js/dashboard/filter.js @@ -1,7 +1,6 @@ class DashboardFilter { constructor(onChange) { - this.onChange = onChange; this.rangeSelect = document.getElementById("trend_range"); diff --git a/dashboard/public/js/overview.js b/dashboard/public/js/overview.js index ceb29db..8dd0d57 100644 --- a/dashboard/public/js/overview.js +++ b/dashboard/public/js/overview.js @@ -238,7 +238,7 @@ function loadProvinceMap(startYear, startWeek, endYear, endWeek) { }) .bindTooltip(` ${province}
- ${programName}
+ ${programName}
Total: ${row.total} `) .addTo(map); diff --git a/dashboard/public/js/program.js b/dashboard/public/js/program.js index d815dcb..9627be9 100644 --- a/dashboard/public/js/program.js +++ b/dashboard/public/js/program.js @@ -1,6 +1,7 @@ const standardPrograms = ['SARI', 'ILI', 'LBM']; const programCode = (window.PROGRAM_CODE || '').trim().toUpperCase(); - +let map; +let provinceLayer; document.addEventListener("DOMContentLoaded", () => { @@ -15,6 +16,115 @@ document.addEventListener("DOMContentLoaded", () => { }); }); + + +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(); + } + + map = L.map('provinceMap').setView([12.7, 104.9], 7); + + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }).addTo(map); + + addProvinceLegend(); + + fetch('/geo/cambodia_provinces.geojson') + .then(r => r.json()) + .then(geo => { + + function getColor(value) { + if (value > 50) return "#b91c1c"; + if (value >= 10) return "#ef4444"; + if (value > 0) return "#fecaca"; + return "#f3f4f600"; + } + + provinceLayer = L.geoJSON(geo, { + style: feature => { + + const province = feature.properties.ADM1_EN; + const value = totals[province]?.total || 0; + + return { + color: "#444", + weight: 1, + fillColor: getColor(value), + fillOpacity: 0.7 + }; + }, + 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); + + layer.bindTooltip(` + ${province}
+ Total: ${total}
+ Positivity: ${percent}% + `); + } + }).addTo(map); + + }); +} +function addProvinceLegend() { + + const legend = L.control({ position: "bottomright" }); + + legend.onAdd = function () { + + const div = L.DomUtil.create("div", "map-legend"); + + div.innerHTML = ` +
+
Cases
+ +
+ + > 50 +
+ +
+ + 10 – 50 +
+ +
+ + 1 – 9 +
+ +
+ + 0 +
+
+ `; + + return div; + }; + + legend.addTo(map); +} function renderTrend(valueId, changeId, current, previous, suffix = '') { const valueEl = document.getElementById(valueId); @@ -49,16 +159,17 @@ 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 positivity = rows.map(r => r.positivity_rate || 0); + const fluRate = rows.map(r => r.influenza_rate || 0); + const covidRate = rows.map(r => r.covid_rate || 0); buildMixedTrendChart( 'trendChart', labels, samples, - positivity + fluRate, + covidRate ); } function renderSummary(summary) { @@ -117,26 +228,46 @@ function renderSummary(summary) { ); } function renderDashboard(data) { - console.log("SUMMARY:", data.summary); + data = data || {}; renderProgramTrend(data.trend || []); renderSummary(data.summary || {}); - buildChart( - 'provinceChart', - 'bar', - (data.province_distribution || []).map(r => r.site_province_name), - (data.province_distribution || []).map(r => r.total) - ); + 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' + ]; + + buildChart( 'pathogenChart', - 'bar', - (data.pathogen_distribution || []).map(r => r.pathogen_name), - (data.pathogen_distribution || []).map(r => r.total), - 'Positive' + 'doughnut', + pathogenRows.map(r => r.pathogen), + pathogenRows.map(r => r.total) ); + charts['pathogenChart'].data.datasets[0].backgroundColor = colors; + charts['pathogenChart'].update(); buildChart( 'ageChart', @@ -144,6 +275,8 @@ function renderDashboard(data) { (data.age_distribution || []).map(r => r.age_group), (data.age_distribution || []).map(r => r.total) ); + charts['ageChart'].data.datasets[0].backgroundColor = colors; + charts['ageChart'].update(); buildChart( 'sexChart', @@ -151,5 +284,24 @@ function renderDashboard(data) { (data.sex_distribution || []).map(r => r.patient_sex), (data.sex_distribution || []).map(r => r.total) ); + charts['sexChart'].data.datasets[0].backgroundColor = colors; + charts['sexChart'].update(); + + buildChart( + 'subtypeChart', + 'bar', + (data.subtype_distribution || []).map(r => r.subtype), + (data.subtype_distribution || []).map(r => r.total) + ); + charts['subtypeChart'].data.datasets[0].backgroundColor = colors; + charts['subtypeChart'].update(); + buildChart( + 'sentinelChart', + 'pie', + (data.sentinel_sites || []).map(r => r.name), + (data.sentinel_sites || []).map(r => r.total) + ); + charts['sentinelChart'].data.datasets[0].backgroundColor = colors; + charts['sentinelChart'].update(); } diff --git a/dashboard/resources/views/dashboard/detail.blade.php b/dashboard/resources/views/dashboard/detail.blade.php index 461fbb7..7f747da 100644 --- a/dashboard/resources/views/dashboard/detail.blade.php +++ b/dashboard/resources/views/dashboard/detail.blade.php @@ -50,14 +50,9 @@
- - Total Cases Reported (Last 7 Days) + Total Cases Reported (latest epiweek)

0

- - - — No change - - + — No change
@@ -65,14 +60,9 @@
- Overall Positivity Rate

0%

- - - — No change - - + — No change
@@ -80,139 +70,152 @@
- Influenza Rate

0%

- - - — No change - - + — No change
-
- - SARS-Cov-2 Rate + SARS-CoV-2 Rate

0%

+ — No change +
+
+
- - — No change - + + +
+
+
+
+ +
+ Case Trends & Positivity Rate by Epiweek +
+ +
+ +
+ +
+
+
+
Pathogen Distribution
+
+ +
-
- - - - -
-
- -
-
Case Trends and Positivity Rate by Epiweek
- +
- - -
- - - - +
-
- + +
-
+
-
- Cases by Province -
+
Cases by Province
- +
-
- - -
- +
-
+
-
- Pathogen Distribution -
+
Sentinel Sites & Influenza Subtypes
- +
+ + +
+ Cases by Sentinel Site +
+ +
+
+ + +
+ Influenza Subtypes +
+ +
+
+ +
-
-
- - - +
-
-
-
+
+
+
+
Age Distribution
-
Age Distribution
- - +
+ +
+
+
+
Sex Distribution
+
+ +
+
+
-
-
-
-
+
-
Sex Distribution
-
-
+ @endsection + @section('scripts') + + @endsection \ No newline at end of file diff --git a/dashboard/resources/views/dashboard/overview.blade.php b/dashboard/resources/views/dashboard/overview.blade.php index 397fe2e..f722c40 100644 --- a/dashboard/resources/views/dashboard/overview.blade.php +++ b/dashboard/resources/views/dashboard/overview.blade.php @@ -51,7 +51,7 @@
Epidemic Trend

- Weekly reported cases by surveillance program + (based on selected epiweek range)

diff --git a/dashboard/resources/views/layouts/app.blade.php b/dashboard/resources/views/layouts/app.blade.php index 998ec9a..e5e5662 100644 --- a/dashboard/resources/views/layouts/app.blade.php +++ b/dashboard/resources/views/layouts/app.blade.php @@ -4,13 +4,16 @@ NRML Dashboard - - + + + + + + - - +