Merge pull request #2 from khantey1998/feature/overview_page
Feature/overview page
This commit is contained in:
217138
dashboard.sql
217138
dashboard.sql
File diff suppressed because it is too large
Load Diff
@@ -50,12 +50,7 @@ class DashboardController extends Controller
|
|||||||
|
|
||||||
public function summary()
|
public function summary()
|
||||||
{
|
{
|
||||||
$dateFrom = Carbon::now()->subDays(7)->toDateString();
|
return response()->json($this->service->summaryCards());
|
||||||
$dateTo = Carbon::now()->toDateString();
|
|
||||||
|
|
||||||
$data = $this->service->summaryCards($dateFrom, $dateTo);
|
|
||||||
|
|
||||||
return response()->json($data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -148,21 +143,22 @@ class DashboardController extends Controller
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public function sentinelMap(Request $request)
|
// public function sentinelMap(Request $request)
|
||||||
{
|
|
||||||
$range = $this->getEpiRange($request);
|
|
||||||
|
|
||||||
if (!$range) {
|
// {
|
||||||
return response()->json(['error' => 'Missing epiweek range'], 400);
|
// $range = $this->getEpiRange($request);
|
||||||
}
|
|
||||||
|
|
||||||
$data = $this->service->sentinelMap(
|
// if (!$range) {
|
||||||
$range['startYear'],
|
// return response()->json(['error' => 'Missing epiweek range'], 400);
|
||||||
$range['startWeek'],
|
// }
|
||||||
$range['endYear'],
|
|
||||||
$range['endWeek']
|
|
||||||
);
|
|
||||||
|
|
||||||
return response()->json($data);
|
// $data = $this->service->sentinelSites(
|
||||||
}
|
// $range['startYear'],
|
||||||
|
// $range['startWeek'],
|
||||||
|
// $range['endYear'],
|
||||||
|
// $range['endWeek']
|
||||||
|
// );
|
||||||
|
|
||||||
|
// return response()->json($data);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
@@ -25,27 +25,32 @@ class DashboardService
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public function summaryCards($dateFrom, $dateTo)
|
public function summaryCards()
|
||||||
{
|
{
|
||||||
$programs = Surveillance::orderBy('id')->get();
|
$programs = Surveillance::orderBy('id')->get();
|
||||||
$results = [];
|
$results = [];
|
||||||
|
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
|
||||||
|
$currentFrom = date('Y-m-d', strtotime('-6 days'));
|
||||||
|
$currentTo = $today;
|
||||||
|
|
||||||
|
$prevFrom = date('Y-m-d', strtotime('-13 days'));
|
||||||
|
$prevTo = date('Y-m-d', strtotime('-7 days'));
|
||||||
|
|
||||||
foreach ($programs as $program) {
|
foreach ($programs as $program) {
|
||||||
|
|
||||||
$current = SurveillanceCase::where('surveillance_id', $program->id)
|
$current = SurveillanceCase::where('surveillance_id', $program->id)
|
||||||
->whereBetween('case_date', [$dateFrom, $dateTo])
|
->whereBetween('case_date', [$currentFrom, $currentTo])
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
$previous = SurveillanceCase::where('surveillance_id', $program->id)
|
$previous = SurveillanceCase::where('surveillance_id', $program->id)
|
||||||
->whereBetween('case_date', [
|
->whereBetween('case_date', [$prevFrom, $prevTo])
|
||||||
date('Y-m-d', strtotime($dateFrom . ' -7 days')),
|
|
||||||
date('Y-m-d', strtotime($dateFrom . ' -1 day'))
|
|
||||||
])
|
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
$percentChange = $previous > 0
|
$percentChange = $previous > 0
|
||||||
? round((($current - $previous) / $previous) * 100, 1)
|
? round((($current - $previous) / $previous) * 100, 1)
|
||||||
: 0;
|
: ($current > 0 ? 100 : 0);
|
||||||
|
|
||||||
$results[] = [
|
$results[] = [
|
||||||
'surveillance_id' => $program->id,
|
'surveillance_id' => $program->id,
|
||||||
@@ -59,7 +64,6 @@ class DashboardService
|
|||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Fast SARI Summary (single query)
|
| Fast SARI Summary (single query)
|
||||||
@@ -80,31 +84,31 @@ class DashboardService
|
|||||||
->where('surveillance_cases.week_data', $week)
|
->where('surveillance_cases.week_data', $week)
|
||||||
|
|
||||||
->selectRaw("
|
->selectRaw("
|
||||||
COUNT(DISTINCT surveillance_cases.lab_code) as total_cases,
|
COUNT(DISTINCT surveillance_cases.lab_code) as total_cases,
|
||||||
|
|
||||||
COUNT(DISTINCT CASE
|
COUNT(DISTINCT CASE
|
||||||
WHEN case_lab_results.is_positive = 1
|
WHEN case_lab_results.is_positive = 1
|
||||||
THEN surveillance_cases.lab_code
|
THEN surveillance_cases.lab_code
|
||||||
END) as overall_positive,
|
END) as overall_positive,
|
||||||
|
|
||||||
COUNT(DISTINCT CASE
|
COUNT(DISTINCT CASE
|
||||||
WHEN case_lab_results.is_positive = 1
|
WHEN case_lab_results.is_positive = 1
|
||||||
AND (
|
AND (
|
||||||
LOWER(case_lab_results.pathogen_name) LIKE '%influenza%'
|
LOWER(case_lab_results.pathogen_name) LIKE '%influenza%'
|
||||||
OR LOWER(case_lab_results.pathogen_name) LIKE '%influzena%'
|
OR LOWER(case_lab_results.pathogen_name) LIKE '%influzena%'
|
||||||
)
|
)
|
||||||
THEN surveillance_cases.lab_code
|
THEN surveillance_cases.lab_code
|
||||||
END) as influenza_positive,
|
END) as influenza_positive,
|
||||||
|
|
||||||
COUNT(DISTINCT CASE
|
COUNT(DISTINCT CASE
|
||||||
WHEN case_lab_results.is_positive = 1
|
WHEN case_lab_results.is_positive = 1
|
||||||
AND (
|
AND (
|
||||||
LOWER(case_lab_results.pathogen_name) LIKE '%covid%'
|
LOWER(case_lab_results.pathogen_name) LIKE '%covid%'
|
||||||
OR LOWER(case_lab_results.pathogen_name) LIKE '%sars%'
|
OR LOWER(case_lab_results.pathogen_name) LIKE '%sars%'
|
||||||
)
|
)
|
||||||
THEN surveillance_cases.lab_code
|
THEN surveillance_cases.lab_code
|
||||||
END) as covid_positive
|
END) as covid_positive
|
||||||
")
|
")
|
||||||
|
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
@@ -136,7 +140,51 @@ class DashboardService
|
|||||||
| Program Summary
|
| Program Summary
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
public function afiTrend($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]);
|
||||||
|
})
|
||||||
|
|
||||||
|
->where('case_lab_results.is_positive', 1)
|
||||||
|
|
||||||
|
->whereNotNull('case_lab_results.pathogen_name')
|
||||||
|
->where('case_lab_results.pathogen_name', '!=', '')
|
||||||
|
->where('case_lab_results.pathogen_name', '!=', 'Positive')
|
||||||
|
|
||||||
|
->selectRaw("
|
||||||
|
surveillance_cases.year_data as year,
|
||||||
|
surveillance_cases.week_data as period,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN LOWER(case_lab_results.pathogen_name) LIKE '%influenza%' THEN 'Influenza'
|
||||||
|
ELSE case_lab_results.pathogen_name
|
||||||
|
END as pathogen,
|
||||||
|
|
||||||
|
COUNT(DISTINCT surveillance_cases.lab_code) as total
|
||||||
|
")
|
||||||
|
|
||||||
|
->groupBy(
|
||||||
|
'surveillance_cases.year_data',
|
||||||
|
'surveillance_cases.week_data',
|
||||||
|
'pathogen'
|
||||||
|
)
|
||||||
|
|
||||||
|
->orderBy('surveillance_cases.year_data')
|
||||||
|
->orderBy('surveillance_cases.week_data')
|
||||||
|
|
||||||
|
->get();
|
||||||
|
}
|
||||||
public function programSummary($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
|
public function programSummary($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -308,6 +356,13 @@ class DashboardService
|
|||||||
$endYear,
|
$endYear,
|
||||||
$endWeek
|
$endWeek
|
||||||
),
|
),
|
||||||
|
'afi_trend' => $this->afiTrend(
|
||||||
|
$surveillanceId,
|
||||||
|
$startYear,
|
||||||
|
$startWeek,
|
||||||
|
$endYear,
|
||||||
|
$endWeek
|
||||||
|
),
|
||||||
|
|
||||||
'province_distribution' => $this->provinceProgram(
|
'province_distribution' => $this->provinceProgram(
|
||||||
$surveillanceId,
|
$surveillanceId,
|
||||||
@@ -520,22 +575,55 @@ class DashboardService
|
|||||||
*/
|
*/
|
||||||
public function provinceCircles($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) {
|
return SurveillanceCase::leftJoin(
|
||||||
$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]);
|
'case_lab_results',
|
||||||
})->groupBy('surveillance_cases.site_province_name', 'surveillance_cases.surveillance_id')->get();
|
'surveillance_cases.lab_code',
|
||||||
|
'=',
|
||||||
|
'case_lab_results.lab_code'
|
||||||
|
)
|
||||||
|
|
||||||
|
->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]
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
->selectRaw("
|
||||||
|
surveillance_cases.patient_province,
|
||||||
|
surveillance_cases.surveillance_id,
|
||||||
|
|
||||||
|
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
|
||||||
|
")
|
||||||
|
|
||||||
|
->groupBy(
|
||||||
|
'surveillance_cases.patient_province',
|
||||||
|
'surveillance_cases.surveillance_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
->get();
|
||||||
}
|
}
|
||||||
public function provinceProgram($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
|
public function provinceProgram($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
|
||||||
{
|
{
|
||||||
return SurveillanceCase::selectRaw("
|
return SurveillanceCase::selectRaw("
|
||||||
surveillance_cases.site_province_name,
|
surveillance_cases.patient_province,
|
||||||
|
|
||||||
COUNT(DISTINCT surveillance_cases.lab_code) as total,
|
COUNT(DISTINCT surveillance_cases.lab_code) as total,
|
||||||
|
|
||||||
COUNT(DISTINCT CASE
|
COUNT(DISTINCT CASE
|
||||||
WHEN case_lab_results.is_positive = 1
|
WHEN case_lab_results.is_positive = 1
|
||||||
THEN surveillance_cases.lab_code
|
THEN surveillance_cases.lab_code
|
||||||
END) as positive
|
END) as positive
|
||||||
")
|
")
|
||||||
|
|
||||||
->join(
|
->join(
|
||||||
'case_lab_results',
|
'case_lab_results',
|
||||||
@@ -560,7 +648,7 @@ class DashboardService
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
->groupBy('surveillance_cases.site_province_name')
|
->groupBy('surveillance_cases.patient_province')
|
||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,7 +671,7 @@ class DashboardService
|
|||||||
})
|
})
|
||||||
|
|
||||||
->groupBy('sentinel_site_name')
|
->groupBy('sentinel_site_name')
|
||||||
->orderByDesc('total') // nice for chart
|
->orderByDesc('total')
|
||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,16 +685,13 @@ class DashboardService
|
|||||||
{
|
{
|
||||||
$total = $this->totalTested($surveillanceId, $startYear, $startWeek, $endYear, $endWeek);
|
$total = $this->totalTested($surveillanceId, $startYear, $startWeek, $endYear, $endWeek);
|
||||||
|
|
||||||
$rows = CaseLabResult::join(
|
$rows = CaseLabResult::where('case_lab_results.surveillance_id', $surveillanceId)
|
||||||
'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) {
|
->whereIn('lab_code', function ($q) use ($surveillanceId, $startYear, $startWeek, $endYear, $endWeek) {
|
||||||
$q->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek])
|
$q->select('lab_code')
|
||||||
|
->from('surveillance_cases')
|
||||||
|
->where('surveillance_id', $surveillanceId)
|
||||||
|
->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek])
|
||||||
->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]);
|
->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]);
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -615,17 +700,11 @@ class DashboardService
|
|||||||
->selectRaw("
|
->selectRaw("
|
||||||
CASE
|
CASE
|
||||||
WHEN LOWER(case_lab_results.pathogen_name) LIKE '%influenza%'
|
WHEN LOWER(case_lab_results.pathogen_name) LIKE '%influenza%'
|
||||||
OR LOWER(case_lab_results.pathogen_name) LIKE '%influzena%'
|
|
||||||
THEN 'Influenza'
|
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
|
ELSE case_lab_results.pathogen_name
|
||||||
END as pathogen,
|
END as pathogen,
|
||||||
|
|
||||||
COUNT(DISTINCT surveillance_cases.lab_code) as total
|
COUNT(DISTINCT case_lab_results.lab_code) as total
|
||||||
")
|
")
|
||||||
|
|
||||||
->groupBy('pathogen')
|
->groupBy('pathogen')
|
||||||
@@ -642,29 +721,36 @@ class DashboardService
|
|||||||
{
|
{
|
||||||
$total = $this->totalTested($surveillanceId, $startYear, $startWeek, $endYear, $endWeek);
|
$total = $this->totalTested($surveillanceId, $startYear, $startWeek, $endYear, $endWeek);
|
||||||
|
|
||||||
$rows = CaseLabResult::join(
|
$rows = CaseLabResult::where('case_lab_results.surveillance_id', $surveillanceId)
|
||||||
'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) {
|
->whereIn('case_lab_results.lab_code', function ($q) use ($surveillanceId, $startYear, $startWeek, $endYear, $endWeek) {
|
||||||
$q->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek])
|
$q->select('lab_code')
|
||||||
->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]);
|
->from('surveillance_cases')
|
||||||
|
->where('surveillance_id', $surveillanceId)
|
||||||
|
->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("
|
->selectRaw("
|
||||||
subtype,
|
case_lab_results.subtype,
|
||||||
COUNT(DISTINCT surveillance_cases.lab_code) as total
|
COUNT(DISTINCT case_lab_results.lab_code) as total
|
||||||
")
|
")
|
||||||
|
|
||||||
->groupBy('subtype')
|
->whereNotNull('case_lab_results.subtype')
|
||||||
->havingRaw("subtype IS NOT NULL AND subtype != 'Positive' AND subtype != ''")
|
->where('case_lab_results.subtype', '!=', '')
|
||||||
->orderByDesc('total')
|
->where('case_lab_results.subtype', '!=', 'Positive')
|
||||||
->get();
|
|
||||||
|
->groupBy('case_lab_results.subtype')
|
||||||
|
->orderByDesc('total')
|
||||||
|
->get();
|
||||||
|
|
||||||
return $rows->map(function ($r) use ($total) {
|
return $rows->map(function ($r) use ($total) {
|
||||||
$r->rate = $total > 0 ? round(($r->total / $total) * 100, 1) : 0;
|
$r->rate = $total > 0 ? round(($r->total / $total) * 100, 1) : 0;
|
||||||
@@ -672,7 +758,6 @@ class DashboardService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Age Distribution
|
| Age Distribution
|
||||||
@@ -681,17 +766,31 @@ class DashboardService
|
|||||||
|
|
||||||
public function ageDistribution($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
|
public function ageDistribution($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
|
||||||
{
|
{
|
||||||
|
|
||||||
return SurveillanceCase::selectRaw("
|
return SurveillanceCase::selectRaw("
|
||||||
CASE
|
CASE
|
||||||
WHEN patient_age_inday < 365 THEN '0-1y'
|
WHEN patient_age_inday <= 28 THEN '0–28 days'
|
||||||
WHEN patient_age_inday < 1825 THEN '1-5y'
|
WHEN patient_age_inday <= 364 THEN '29 days–11 months'
|
||||||
WHEN patient_age_inday < 6570 THEN '5-18y'
|
WHEN patient_age_inday <= 1460 THEN '1–4 years'
|
||||||
WHEN patient_age_inday < 21900 THEN '18-60y'
|
WHEN patient_age_inday <= 5110 THEN '5–14 years'
|
||||||
ELSE '60+y'
|
WHEN patient_age_inday <= 8765 THEN '15–24 years'
|
||||||
|
WHEN patient_age_inday <= 18250 THEN '25–49 years'
|
||||||
|
WHEN patient_age_inday <= 23725 THEN '50–64 years'
|
||||||
|
ELSE '65+ years'
|
||||||
END as age_group,
|
END as age_group,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN patient_age_inday <= 28 THEN 1
|
||||||
|
WHEN patient_age_inday <= 364 THEN 2
|
||||||
|
WHEN patient_age_inday <= 1460 THEN 3
|
||||||
|
WHEN patient_age_inday <= 5110 THEN 4
|
||||||
|
WHEN patient_age_inday <= 8765 THEN 5
|
||||||
|
WHEN patient_age_inday <= 18250 THEN 6
|
||||||
|
WHEN patient_age_inday <= 23725 THEN 7
|
||||||
|
ELSE 8
|
||||||
|
END as age_order,
|
||||||
|
|
||||||
COUNT(*) as total
|
COUNT(*) as total
|
||||||
")
|
")
|
||||||
|
|
||||||
->where('surveillance_id', $surveillanceId)
|
->where('surveillance_id', $surveillanceId)
|
||||||
|
|
||||||
@@ -701,7 +800,6 @@ class DashboardService
|
|||||||
"(year_data > ? OR (year_data = ? AND week_data >= ?))",
|
"(year_data > ? OR (year_data = ? AND week_data >= ?))",
|
||||||
[$startYear, $startYear, $startWeek]
|
[$startYear, $startYear, $startWeek]
|
||||||
)
|
)
|
||||||
|
|
||||||
->whereRaw(
|
->whereRaw(
|
||||||
"(year_data < ? OR (year_data = ? AND week_data <= ?))",
|
"(year_data < ? OR (year_data = ? AND week_data <= ?))",
|
||||||
[$endYear, $endYear, $endWeek]
|
[$endYear, $endYear, $endWeek]
|
||||||
@@ -709,13 +807,11 @@ class DashboardService
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
->groupBy('age_group')
|
->groupBy('age_group', 'age_order')
|
||||||
|
->orderBy('age_order')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Sex Distribution
|
| Sex Distribution
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,58 @@
|
|||||||
|
Chart.register({
|
||||||
|
id: 'noDataText',
|
||||||
|
afterDraw(chart) {
|
||||||
|
|
||||||
|
const datasets = chart.data.datasets || [];
|
||||||
|
|
||||||
|
const hasData = datasets.some(ds =>
|
||||||
|
(ds.data || []).some(v => Number(v) > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
chart.$noData = !hasData;
|
||||||
|
|
||||||
|
if (hasData) return;
|
||||||
|
|
||||||
|
const { ctx, width, height } = chart;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.font = '14px sans-serif';
|
||||||
|
ctx.fillStyle = '#9ca3af';
|
||||||
|
|
||||||
|
ctx.fillText('No data available', width / 2, height / 2);
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Chart.register({
|
||||||
|
id: 'centerText',
|
||||||
|
afterDraw(chart) {
|
||||||
|
|
||||||
|
if (chart.config.type !== 'doughnut') return;
|
||||||
|
if (chart.$noData) return;
|
||||||
|
|
||||||
|
const { ctx, width, height } = chart;
|
||||||
|
|
||||||
|
const data = chart.data.datasets[0].data;
|
||||||
|
const total = data.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
|
||||||
|
ctx.font = 'bold 18px sans-serif';
|
||||||
|
ctx.fillStyle = '#111827';
|
||||||
|
ctx.fillText(total, width / 2, height / 2 - 8);
|
||||||
|
|
||||||
|
ctx.font = '12px sans-serif';
|
||||||
|
ctx.fillStyle = '#6b7280';
|
||||||
|
ctx.fillText('Total cases', width / 2, height / 2 + 12);
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
Chart.register(ChartDataLabels);
|
Chart.register(ChartDataLabels);
|
||||||
const charts = {};
|
const charts = {};
|
||||||
|
|
||||||
@@ -80,10 +135,126 @@ function buildStackedChart(canvasId, labels, datasets) {
|
|||||||
function buildChart(id, type, labels, data) {
|
function buildChart(id, type, labels, data) {
|
||||||
|
|
||||||
const ctx = document.getElementById(id);
|
const ctx = document.getElementById(id);
|
||||||
|
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
if (charts[id]) charts[id].destroy();
|
Chart.getChart(id)?.destroy();
|
||||||
|
|
||||||
|
const hasData = data && data.some(v => Number(v) > 0);
|
||||||
|
|
||||||
|
if (!hasData) {
|
||||||
|
labels = [];
|
||||||
|
data = [];
|
||||||
|
}
|
||||||
|
const isHorizontal = id === 'sexChart';
|
||||||
|
const isAgeChart = id === 'ageChart';
|
||||||
|
const options = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
|
||||||
|
layout: {
|
||||||
|
padding: 30
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
indexAxis: isHorizontal ? 'y' : 'x',
|
||||||
|
plugins: {
|
||||||
|
|
||||||
|
legend: {
|
||||||
|
position: isAgeChart ? 'left' : 'bottom',
|
||||||
|
align: 'center',
|
||||||
|
display: (ctx) => {
|
||||||
|
const chart = ctx.chart;
|
||||||
|
|
||||||
|
if (!(chart.config.type === 'pie' || chart.config.type === 'doughnut')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !chart.$noData;
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
padding: 14,
|
||||||
|
boxWidth: 10,
|
||||||
|
boxHeight: 10,
|
||||||
|
usePointStyle: true,
|
||||||
|
pointStyle: 'circle',
|
||||||
|
font: {
|
||||||
|
size: 11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
datalabels: {
|
||||||
|
color: "#282626",
|
||||||
|
offset: 6,
|
||||||
|
clip: false,
|
||||||
|
display: (ctx) => {
|
||||||
|
const chart = ctx.chart;
|
||||||
|
if (chart.$noData) return false;
|
||||||
|
if (chart.config.type === 'bar') return true;
|
||||||
|
return !chart.$noData;
|
||||||
|
},
|
||||||
|
anchor: (ctx) => {
|
||||||
|
const type = ctx.chart.config.type;
|
||||||
|
|
||||||
|
if (type === 'doughnut' || type === 'pie') {
|
||||||
|
return 'center';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'end';
|
||||||
|
},
|
||||||
|
|
||||||
|
align: (ctx) => {
|
||||||
|
const type = ctx.chart.config.type;
|
||||||
|
if (type === 'doughnut' || type === 'pie') {
|
||||||
|
return 'center';
|
||||||
|
}
|
||||||
|
if (type === 'bar') {
|
||||||
|
return ctx.chart.options.indexAxis === 'y' ? 'right' : 'end';
|
||||||
|
}
|
||||||
|
return 'center';
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
size: 10,
|
||||||
|
weight: '600'
|
||||||
|
},
|
||||||
|
|
||||||
|
formatter: (value, ctx) => {
|
||||||
|
if (ctx.chart.$noData) return '';
|
||||||
|
|
||||||
|
const data = ctx.chart.data.datasets[0].data;
|
||||||
|
const total = data.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
if (!total) return '';
|
||||||
|
|
||||||
|
return ((value / total) * 100).toFixed(1) + '%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (type === 'bar') {
|
||||||
|
options.scales = {
|
||||||
|
x: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
display: false // cleaner look
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
grid: {
|
||||||
|
color: '#f3f4f6' // subtle grid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === 'doughnut') {
|
||||||
|
options.cutout = '70%';
|
||||||
|
|
||||||
|
options.elements = {
|
||||||
|
arc: {
|
||||||
|
borderWidth: (ctx) => ctx.chart.$noData ? 0 : 1
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
charts[id] = new Chart(ctx, {
|
charts[id] = new Chart(ctx, {
|
||||||
type: type,
|
type: type,
|
||||||
@@ -98,54 +269,18 @@ function buildChart(id, type, labels, data) {
|
|||||||
maxBarThickness: 50
|
maxBarThickness: 50
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: 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) {
|
function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) {
|
||||||
|
|
||||||
const ctx = document.getElementById(canvasId);
|
const ctx = document.getElementById(canvasId);
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
if (!labels.length) labels = [''];
|
||||||
|
|
||||||
|
if (!fluRate.length) fluRate = [0];
|
||||||
|
if (!covidRate.length) covidRate = [0];
|
||||||
|
if (!samples.length) samples = [0];
|
||||||
if (charts[canvasId]) charts[canvasId].destroy();
|
if (charts[canvasId]) charts[canvasId].destroy();
|
||||||
|
|
||||||
charts[canvasId] = new Chart(ctx, {
|
charts[canvasId] = new Chart(ctx, {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -89,7 +95,6 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
|
|||||||
const [yearB, weekB] = b.split('-').map(Number);
|
const [yearB, weekB] = b.split('-').map(Number);
|
||||||
|
|
||||||
if (yearA !== yearB) return yearA - yearB;
|
if (yearA !== yearB) return yearA - yearB;
|
||||||
|
|
||||||
return weekA - weekB;
|
return weekA - weekB;
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -109,11 +114,8 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
|
|||||||
if (!allowedPrograms.includes(code)) return;
|
if (!allowedPrograms.includes(code)) return;
|
||||||
|
|
||||||
const values = labels.map(label => {
|
const values = labels.map(label => {
|
||||||
|
|
||||||
const found = data[code].find(row => `${row.year}-${row.period}` === label);
|
const found = data[code].find(row => `${row.year}-${row.period}` === label);
|
||||||
|
|
||||||
return found ? found.total : 0;
|
return found ? found.total : 0;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
datasets.push({
|
datasets.push({
|
||||||
@@ -135,14 +137,11 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
trendChart = new Chart(document.getElementById('trendChart'), {
|
trendChart = new Chart(document.getElementById('trendChart'), {
|
||||||
|
|
||||||
type: 'line',
|
type: 'line',
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
labels: displayLabels,
|
labels: displayLabels,
|
||||||
datasets: datasets
|
datasets: datasets
|
||||||
},
|
},
|
||||||
|
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
plugins: {
|
plugins: {
|
||||||
@@ -160,14 +159,294 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
|
|||||||
x: { grid: { display: false } }
|
x: { grid: { display: false } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
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('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 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;
|
||||||
|
|
||||||
|
const clean = str =>
|
||||||
|
str.toLowerCase().replace(/\s+/g, '');
|
||||||
|
|
||||||
|
const raw = name.trim();
|
||||||
|
|
||||||
|
const map = {
|
||||||
|
"kepville": "Kep",
|
||||||
|
"sihanoukville": "Preah Sihanouk",
|
||||||
|
"sihanoukvillecity": "Preah Sihanouk",
|
||||||
|
"krongpailin": "Pailin",
|
||||||
|
"mondulkiri": "Mondulkiri",
|
||||||
|
"odormeanchey": "Oddar Meanchey",
|
||||||
|
"tbongkhmom": "Tboung Khmum",
|
||||||
|
"tboungkhmum": "Tboung Khmum",
|
||||||
|
"rattanakiri": "Ratanak Kiri"
|
||||||
|
};
|
||||||
|
|
||||||
|
const key = clean(raw);
|
||||||
|
|
||||||
|
if (map[key] && validSet.has(map[key])) {
|
||||||
|
return map[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = [...validSet].find(p => clean(p) === key);
|
||||||
|
|
||||||
|
return match || null;
|
||||||
|
}
|
||||||
|
function getRadius(total) {
|
||||||
|
if (!total) return 0;
|
||||||
|
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
|
| Province Map
|
||||||
@@ -179,17 +458,23 @@ 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'
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch('/geo/cambodia_provinces.geojson').then(r => r.json()),
|
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())
|
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(
|
||||||
|
geojson.features.map(f => f.properties.ADM1_EN)
|
||||||
|
);
|
||||||
|
|
||||||
L.geoJSON(geojson, {
|
L.geoJSON(geojson, {
|
||||||
style: {
|
style: {
|
||||||
@@ -204,43 +489,49 @@ function loadProvinceMap(startYear, startWeek, endYear, endWeek) {
|
|||||||
const province = feature.properties.ADM1_EN;
|
const province = feature.properties.ADM1_EN;
|
||||||
const center = layer.getBounds().getCenter();
|
const center = layer.getBounds().getCenter();
|
||||||
|
|
||||||
const rows = data.filter(d => d.site_province_name === province);
|
const rows = data.filter(d => {
|
||||||
|
if (![1, 2, 3].includes(d.surveillance_id)) return false;
|
||||||
|
|
||||||
const offsets = {
|
const name = normalizeProvince(d.patient_province, validProvinces);
|
||||||
1: -0.15,
|
return name === province;
|
||||||
2: 0,
|
});
|
||||||
3: 0.15
|
|
||||||
|
const offsets = { 1: -0.15, 2: 0, 3: 0.15 };
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
1: '#2563eb',
|
||||||
|
2: '#10b981',
|
||||||
|
3: '#9333ea'
|
||||||
};
|
};
|
||||||
|
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
|
|
||||||
|
const percent = row.total
|
||||||
|
? ((row.positive / row.total) * 100).toFixed(1)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const offset = offsets[row.surveillance_id] ?? 0;
|
||||||
|
|
||||||
const lat = center.lat;
|
const lat = center.lat;
|
||||||
const lng = center.lng + offsets[row.surveillance_id];
|
const lng = center.lng + offset;
|
||||||
|
|
||||||
const programName =
|
const programName =
|
||||||
row.surveillance_id === 1 ? 'SARI' :
|
row.surveillance_id === 1 ? 'SARI' :
|
||||||
row.surveillance_id === 2 ? 'ILI' : 'LBM';
|
row.surveillance_id === 2 ? 'ILI' : 'LBM';
|
||||||
|
|
||||||
const colors = {
|
|
||||||
1: '#2563eb',
|
|
||||||
2: '#10b981',
|
|
||||||
3: '#9333ea'
|
|
||||||
};
|
|
||||||
|
|
||||||
L.circleMarker([lat, lng], {
|
L.circleMarker([lat, lng], {
|
||||||
|
radius: getRadius(row.total),
|
||||||
radius: 9,
|
|
||||||
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(`
|
||||||
<strong>${province}</strong><br>
|
<strong>${province}</strong><br>
|
||||||
${programName}<br>
|
${programName}<br>
|
||||||
Total: ${row.total}
|
Cases: ${row.total}<br>
|
||||||
`)
|
Positivity: ${percent}%
|
||||||
|
`)
|
||||||
.addTo(map);
|
.addTo(map);
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -267,7 +558,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
new DashboardFilter((startYear, startWeek, endYear, endWeek) => {
|
new DashboardFilter((startYear, startWeek, endYear, endWeek) => {
|
||||||
|
|
||||||
loadTrend('week', startYear, startWeek, endYear, endWeek);
|
loadTrend('week', startYear, startWeek, endYear, endWeek);
|
||||||
|
|
||||||
loadProvinceMap(startYear, startWeek, endYear, endWeek);
|
loadProvinceMap(startYear, startWeek, endYear, endWeek);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,36 +1,84 @@
|
|||||||
const standardPrograms = ['SARI', 'ILI', 'LBM'];
|
const standardPrograms = ['SARI', 'ILI', 'LBM', 'AFI'];
|
||||||
const programCode = (window.PROGRAM_CODE || '').trim().toUpperCase();
|
const programCode = (window.PROGRAM_CODE || '').trim().toUpperCase();
|
||||||
let map;
|
let map;
|
||||||
let provinceLayer;
|
let provinceLayer;
|
||||||
|
|
||||||
|
function normalizeProvince(name, validSet) {
|
||||||
|
if (!name || !validSet) return null;
|
||||||
|
|
||||||
|
const clean = str =>
|
||||||
|
str.toLowerCase().replace(/\s+/g, '');
|
||||||
|
|
||||||
|
const raw = name.trim();
|
||||||
|
|
||||||
|
const map = {
|
||||||
|
"kepville": "Kep",
|
||||||
|
"sihanoukville": "Preah Sihanouk",
|
||||||
|
"sihanoukvillecity": "Preah Sihanouk",
|
||||||
|
"krongpailin": "Pailin",
|
||||||
|
"mondulkiri": "Mondulkiri",
|
||||||
|
"odormeanchey": "Oddar Meanchey",
|
||||||
|
"tbongkhmom": "Tboung Khmum",
|
||||||
|
"tboungkhmum": "Tboung Khmum",
|
||||||
|
"rattanakiri": "Ratanak Kiri"
|
||||||
|
};
|
||||||
|
|
||||||
|
const key = clean(raw);
|
||||||
|
|
||||||
|
if (map[key] && validSet.has(map[key])) {
|
||||||
|
return map[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = [...validSet].find(p => clean(p) === key);
|
||||||
|
|
||||||
|
return match || null;
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
|
|
||||||
if (!standardPrograms.includes(programCode)) return;
|
if (!standardPrograms.includes(programCode)) return;
|
||||||
|
|
||||||
new DashboardFilter((startYear, startWeek, endYear, endWeek) => {
|
new DashboardFilter((startYear, startWeek, endYear, endWeek) => {
|
||||||
|
|
||||||
fetch(`/api/dashboard/program?surveillance_id=${window.SURVEILLANCE_ID}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
|
fetch(`/api/dashboard/program?surveillance_id=${window.SURVEILLANCE_ID}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(renderDashboard)
|
.then(programCode === 'AFI' ? renderAFIDashboard : renderDashboard)
|
||||||
.catch(err => console.error("Dashboard API error:", err));
|
.catch(err => console.error("Dashboard API error:", err));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function renderAFIDashboard(data) {
|
||||||
|
const pathogenRows = (data.pathogen_distribution || [])
|
||||||
|
.sort((a, b) => b.total - a.total);
|
||||||
|
const colors = [
|
||||||
|
'#2563eb', '#10b981', '#f59e0b', '#ef4444',
|
||||||
|
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16', '#e11dba', '#f6f63b'
|
||||||
|
];
|
||||||
|
|
||||||
|
renderSummary(data.summary);
|
||||||
|
renderAFITrend(data.afi_trend);
|
||||||
|
renderProvinceHeatmap(data.province_distribution);
|
||||||
|
renderPathogenChart(data.pathogen_distribution);
|
||||||
|
renderDemographics(data);
|
||||||
|
renderSentinel(data.sentinel_sites || []);
|
||||||
|
renderSubtypeChart(data.subtype_distribution || []);
|
||||||
|
|
||||||
|
charts['pathogenChart'].data.datasets[0].backgroundColor = colors;
|
||||||
|
charts['pathogenChart'].update();
|
||||||
|
charts['ageChart'].data.datasets[0].backgroundColor = colors;
|
||||||
|
charts['ageChart'].update();
|
||||||
|
charts['sexChart'].data.datasets[0].backgroundColor = colors;
|
||||||
|
charts['sexChart'].update();
|
||||||
|
charts['sentinelChart'].data.datasets[0].backgroundColor = colors;
|
||||||
|
charts['sentinelChart'].update();
|
||||||
|
charts['subtypeChart'].data.datasets[0].backgroundColor = colors;
|
||||||
|
charts['subtypeChart'].update();
|
||||||
|
}
|
||||||
function renderProvinceHeatmap(rows) {
|
function renderProvinceHeatmap(rows) {
|
||||||
|
|
||||||
const totals = {};
|
if (map) map.remove();
|
||||||
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);
|
map = L.map('provinceMap').setView([12.7, 104.9], 7);
|
||||||
|
|
||||||
@@ -44,6 +92,28 @@ function renderProvinceHeatmap(rows) {
|
|||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(geo => {
|
.then(geo => {
|
||||||
|
|
||||||
|
const validProvinces = new Set(
|
||||||
|
geo.features.map(f => f.properties.ADM1_EN)
|
||||||
|
);
|
||||||
|
|
||||||
|
const totals = {};
|
||||||
|
|
||||||
|
rows.forEach(r => {
|
||||||
|
|
||||||
|
// ✅ FIX: use patient_province + validSet
|
||||||
|
const province = normalizeProvince(r.patient_province, validProvinces);
|
||||||
|
if (!province) return;
|
||||||
|
|
||||||
|
// ✅ FIX: accumulate instead of overwrite
|
||||||
|
if (!totals[province]) {
|
||||||
|
totals[province] = { total: 0, positive: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
totals[province].total += Number(r.total);
|
||||||
|
totals[province].positive += Number(r.positive);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
function getColor(value) {
|
function getColor(value) {
|
||||||
if (value > 50) return "#b91c1c";
|
if (value > 50) return "#b91c1c";
|
||||||
if (value >= 10) return "#ef4444";
|
if (value >= 10) return "#ef4444";
|
||||||
@@ -65,11 +135,15 @@ function renderProvinceHeatmap(rows) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
onEachFeature: (feature, layer) => {
|
onEachFeature: (feature, layer) => {
|
||||||
|
|
||||||
const province = feature.properties.ADM1_EN;
|
const province = feature.properties.ADM1_EN;
|
||||||
const total = totals[province]?.total || 0;
|
const total = totals[province]?.total || 0;
|
||||||
const positive = totals[province]?.positive || 0;
|
const positive = totals[province]?.positive || 0;
|
||||||
const percent = total ? ((positive / total) * 100).toFixed(1) : 0;
|
|
||||||
console.log(province, total, positive, percent);
|
// ✅ positivity kept
|
||||||
|
const percent = total
|
||||||
|
? ((positive / total) * 100).toFixed(1)
|
||||||
|
: 0;
|
||||||
|
|
||||||
layer.bindTooltip(`
|
layer.bindTooltip(`
|
||||||
${province}<br>
|
${province}<br>
|
||||||
@@ -159,6 +233,7 @@ function renderTrend(valueId, changeId, current, previous, suffix = '') {
|
|||||||
function renderProgramTrend(rows) {
|
function renderProgramTrend(rows) {
|
||||||
|
|
||||||
rows = rows || [];
|
rows = rows || [];
|
||||||
|
|
||||||
const labels = rows.map(r => `W${r.period}`);
|
const labels = rows.map(r => `W${r.period}`);
|
||||||
const samples = rows.map(r => r.total_samples || 0);
|
const samples = rows.map(r => r.total_samples || 0);
|
||||||
const fluRate = rows.map(r => r.influenza_rate || 0);
|
const fluRate = rows.map(r => r.influenza_rate || 0);
|
||||||
@@ -230,32 +305,22 @@ function renderSummary(summary) {
|
|||||||
function renderDashboard(data) {
|
function renderDashboard(data) {
|
||||||
|
|
||||||
data = data || {};
|
data = data || {};
|
||||||
|
let virusRows = data.virus_trend || [];
|
||||||
|
if (!virusRows.length) {
|
||||||
|
virusRows = [
|
||||||
|
{ period: '', influenza: 0, covid: 0, total_samples: 0 }
|
||||||
|
];
|
||||||
|
}
|
||||||
renderProgramTrend(data.trend || []);
|
renderProgramTrend(data.trend || []);
|
||||||
renderSummary(data.summary || {});
|
renderSummary(data.summary || {});
|
||||||
renderProvinceHeatmap(data.province_distribution || []);
|
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 || [])
|
const pathogenRows = (data.pathogen_distribution || [])
|
||||||
.sort((a, b) => b.total - a.total);
|
.sort((a, b) => b.total - a.total);
|
||||||
const colors = [
|
const colors = [
|
||||||
'#2563eb', '#10b981', '#f59e0b', '#ef4444',
|
'#2563eb', '#10b981', '#f59e0b', '#ef4444',
|
||||||
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16'
|
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16', '#e11dba', '#f6f63b'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
@@ -305,3 +370,114 @@ function renderDashboard(data) {
|
|||||||
charts['sentinelChart'].update();
|
charts['sentinelChart'].update();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//AFI
|
||||||
|
function renderAFITrend(rows) {
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
buildStackedChart('trendChart', [], []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { labels, datasets } = transformAFIData(rows);
|
||||||
|
|
||||||
|
buildStackedChart('trendChart', labels, datasets);
|
||||||
|
}
|
||||||
|
function transformAFIData(rows) {
|
||||||
|
|
||||||
|
const grouped = {};
|
||||||
|
const pathogensSet = new Set();
|
||||||
|
|
||||||
|
rows.forEach(r => {
|
||||||
|
|
||||||
|
const key = `${r.year}-W${r.period}`;
|
||||||
|
|
||||||
|
if (!grouped[key]) {
|
||||||
|
grouped[key] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
grouped[key][r.pathogen] = r.total;
|
||||||
|
pathogensSet.add(r.pathogen);
|
||||||
|
});
|
||||||
|
|
||||||
|
const labels = Object.keys(grouped).sort((a, b) => {
|
||||||
|
const [yA, wA] = a.split('-W').map(Number);
|
||||||
|
const [yB, wB] = b.split('-W').map(Number);
|
||||||
|
return yA === yB ? wA - wB : yA - yB;
|
||||||
|
});
|
||||||
|
|
||||||
|
const pathogenTotals = {};
|
||||||
|
|
||||||
|
rows.forEach(r => {
|
||||||
|
pathogenTotals[r.pathogen] = (pathogenTotals[r.pathogen] || 0) + r.total;
|
||||||
|
});
|
||||||
|
|
||||||
|
const pathogens = Object.keys(pathogenTotals)
|
||||||
|
.sort((a, b) => pathogenTotals[b] - pathogenTotals[a]);
|
||||||
|
const datasets = pathogens.map(p => ({
|
||||||
|
label: p,
|
||||||
|
data: labels.map(l => grouped[l][p] || 0),
|
||||||
|
backgroundColor: getColorForPathogen(p)
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { labels: labels.map(l => l.split('-')[1]), datasets };
|
||||||
|
}
|
||||||
|
function getColorForPathogen(name) {
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
Dengue: '#2563eb',
|
||||||
|
Chikungunya: '#10b981',
|
||||||
|
Zika: '#f59e0b',
|
||||||
|
Leptospira: '#ef4444',
|
||||||
|
Rickettsia: '#8b5cf6',
|
||||||
|
Salmonella: '#f97316',
|
||||||
|
Plasmodium: '#14b8a6',
|
||||||
|
Influenza: '#84cc16'
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (colors[name]) return colors[name];
|
||||||
|
|
||||||
|
// fallback random color (for future pathogens)
|
||||||
|
return `hsl(${Math.floor(Math.random() * 360)}, 70%, 60%)`;
|
||||||
|
}
|
||||||
|
function renderPathogenChart(rows) {
|
||||||
|
buildChart(
|
||||||
|
'pathogenChart',
|
||||||
|
'doughnut',
|
||||||
|
rows.map(r => r.pathogen),
|
||||||
|
rows.map(r => r.total)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function renderSentinel(rows) {
|
||||||
|
buildChart(
|
||||||
|
'sentinelChart',
|
||||||
|
'pie',
|
||||||
|
rows.map(r => r.name),
|
||||||
|
rows.map(r => r.total)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function renderDemographics(data) {
|
||||||
|
buildChart(
|
||||||
|
'ageChart',
|
||||||
|
'doughnut',
|
||||||
|
(data.age_distribution || []).map(r => r.age_group),
|
||||||
|
(data.age_distribution || []).map(r => r.total)
|
||||||
|
);
|
||||||
|
|
||||||
|
buildChart(
|
||||||
|
'sexChart',
|
||||||
|
'bar',
|
||||||
|
(data.sex_distribution || []).map(r => r.patient_sex),
|
||||||
|
(data.sex_distribution || []).map(r => r.total)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function renderSubtypeChart(rows) {
|
||||||
|
buildChart(
|
||||||
|
'subtypeChart',
|
||||||
|
'bar',
|
||||||
|
rows.map(r => r.subtype),
|
||||||
|
rows.map(r => r.total)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -172,21 +172,9 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body" style="height:400px">
|
<div class="card-body" style="height:400px">
|
||||||
<div class="row h-100">
|
<h6 class="fw-bold mb-3">Sex Distribution</h6>
|
||||||
<div class="col-md-6 d-flex flex-column">
|
<div style="height:360px; position:relative;">
|
||||||
<h6 class="fw-bold mb-3">Age Distribution</h6>
|
<canvas id="sexChart"></canvas>
|
||||||
|
|
||||||
<div style="height:360px; position:relative;">
|
|
||||||
<canvas id="ageChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 d-flex flex-column">
|
|
||||||
<h6 class="fw-bold mb-3">Sex Distribution</h6>
|
|
||||||
|
|
||||||
<div style="height:360px; position:relative;">
|
|
||||||
<canvas id="sexChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,16 +183,14 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body" style="height:400px">
|
<div class="card-body" style="height:400px">
|
||||||
|
<h6 class="fw-bold mb-3">Age Distribution</h6>
|
||||||
|
<div style="height:360px; position:relative;">
|
||||||
|
<canvas id="ageChart"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,11 @@
|
|||||||
background: white;
|
background: white;
|
||||||
border-bottom: 1px solid #dcdcdc;
|
border-bottom: 1px solid #dcdcdc;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border-bottom: 1px solid #dcdcdc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* NAV ITEMS */
|
/* NAV ITEMS */
|
||||||
@@ -57,14 +62,12 @@
|
|||||||
background: #cce0d4;
|
background: #cce0d4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ACTIVE TAB */
|
|
||||||
.active-tab {
|
.active-tab {
|
||||||
color: #0B8F3C;
|
color: #0B8F3C;
|
||||||
border-bottom: 3px solid #0B8F3C;
|
border-bottom: 3px solid #0B8F3C;
|
||||||
background: #e5efe8;
|
background: #e5efe8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CONTENT */
|
|
||||||
.content-area {
|
.content-area {
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user