finalize export function
This commit is contained in:
@@ -64,82 +64,13 @@ class DashboardService
|
|||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Fast SARI Summary (single query)
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
public function programSummaryFast($surveillanceId, $year, $week)
|
|
||||||
{
|
|
||||||
$row = SurveillanceCase::leftJoin(
|
|
||||||
'case_lab_results',
|
|
||||||
'surveillance_cases.lab_code',
|
|
||||||
'=',
|
|
||||||
'case_lab_results.lab_code'
|
|
||||||
)
|
|
||||||
|
|
||||||
->where('surveillance_cases.surveillance_id', $surveillanceId)
|
|
||||||
->where('surveillance_cases.year_data', $year)
|
|
||||||
->where('surveillance_cases.week_data', $week)
|
|
||||||
|
|
||||||
->selectRaw("
|
|
||||||
COUNT(DISTINCT surveillance_cases.lab_code) as total_cases,
|
|
||||||
|
|
||||||
COUNT(DISTINCT CASE
|
|
||||||
WHEN case_lab_results.is_positive = 1
|
|
||||||
THEN surveillance_cases.lab_code
|
|
||||||
END) as overall_positive,
|
|
||||||
|
|
||||||
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_positive,
|
|
||||||
|
|
||||||
COUNT(DISTINCT CASE
|
|
||||||
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
|
|
||||||
")
|
|
||||||
|
|
||||||
->first();
|
|
||||||
|
|
||||||
|
|
||||||
if (!$row || $row->total_cases == 0) {
|
|
||||||
return [
|
|
||||||
'cases' => 0,
|
|
||||||
'overall_rate' => 0,
|
|
||||||
'influenza_rate' => 0,
|
|
||||||
'covid_rate' => 0
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'cases' => $row->total_cases,
|
|
||||||
|
|
||||||
'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),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Program Summary
|
| Program Summary
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public function afiTrend($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
|
public function afiTrend($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
|
||||||
{
|
{
|
||||||
return CaseLabResult::join(
|
return CaseLabResult::join(
|
||||||
@@ -152,107 +83,177 @@ class DashboardService
|
|||||||
->where('surveillance_cases.surveillance_id', $surveillanceId)
|
->where('surveillance_cases.surveillance_id', $surveillanceId)
|
||||||
|
|
||||||
->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) {
|
->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) {
|
||||||
$q->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek])
|
$q->whereRaw(
|
||||||
->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]);
|
"(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)
|
->where(function ($q) {
|
||||||
|
$q->whereNotNull('case_lab_results.pathogen_name')
|
||||||
->whereNotNull('case_lab_results.pathogen_name')
|
->orWhereRaw("LOWER(case_lab_results.indicator) LIKE '%serum%'");
|
||||||
->where('case_lab_results.pathogen_name', '!=', '')
|
})
|
||||||
->where('case_lab_results.pathogen_name', '!=', 'Positive')
|
|
||||||
|
|
||||||
->selectRaw("
|
->selectRaw("
|
||||||
surveillance_cases.year_data as year,
|
surveillance_cases.year_data as year,
|
||||||
surveillance_cases.week_data as period,
|
surveillance_cases.week_data as period,
|
||||||
|
|
||||||
CASE
|
CASE
|
||||||
WHEN LOWER(case_lab_results.pathogen_name) LIKE '%influenza%' THEN 'Influenza'
|
WHEN LOWER(case_lab_results.pathogen_name) LIKE '%influenza%'
|
||||||
ELSE case_lab_results.pathogen_name
|
OR LOWER(case_lab_results.pathogen_name) LIKE '%influzena%'
|
||||||
END as pathogen,
|
THEN 'Influenza'
|
||||||
|
ELSE case_lab_results.pathogen_name
|
||||||
|
END as pathogen,
|
||||||
|
|
||||||
COUNT(DISTINCT surveillance_cases.lab_code) as total
|
CASE
|
||||||
")
|
WHEN LOWER(case_lab_results.indicator) LIKE '%serum%' THEN 'Serum'
|
||||||
|
ELSE 'PCR'
|
||||||
|
END as test_type,
|
||||||
|
COUNT(case_lab_results.id) as total_tests,
|
||||||
|
COUNT(DISTINCT surveillance_cases.lab_code) as total_tested,
|
||||||
|
|
||||||
|
COUNT(CASE
|
||||||
|
WHEN case_lab_results.is_positive = 1
|
||||||
|
THEN surveillance_cases.lab_code
|
||||||
|
END) as total_positive
|
||||||
|
")
|
||||||
|
|
||||||
->groupBy(
|
->groupBy(
|
||||||
'surveillance_cases.year_data',
|
'surveillance_cases.year_data',
|
||||||
'surveillance_cases.week_data',
|
'surveillance_cases.week_data',
|
||||||
'pathogen'
|
'pathogen',
|
||||||
|
'test_type'
|
||||||
)
|
)
|
||||||
|
//->havingRaw("pathogen IS NOT NULL AND pathogen != ''")
|
||||||
->orderBy('surveillance_cases.year_data')
|
->orderBy('surveillance_cases.year_data')
|
||||||
->orderBy('surveillance_cases.week_data')
|
->orderBy('surveillance_cases.week_data')
|
||||||
|
|
||||||
->get();
|
->get()
|
||||||
|
->map(function ($r) {
|
||||||
|
|
||||||
|
$r->positivity_rate = $r->total_tested > 0
|
||||||
|
? round(($r->total_positive / $r->total_tested) * 100, 1)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return $r;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public function programSummaryFast($surveillanceId, $year = null, $week = null, $dateFrom = null, $dateTo = null)
|
||||||
|
{
|
||||||
|
$query = SurveillanceCase::leftJoin(
|
||||||
|
'case_lab_results',
|
||||||
|
'surveillance_cases.lab_code',
|
||||||
|
'=',
|
||||||
|
'case_lab_results.lab_code'
|
||||||
|
)
|
||||||
|
->where('surveillance_cases.surveillance_id', $surveillanceId);
|
||||||
|
|
||||||
|
if ($dateFrom && $dateTo) {
|
||||||
|
$query->whereBetween('surveillance_cases.case_date', [$dateFrom, $dateTo]);
|
||||||
|
} else {
|
||||||
|
$query->where('surveillance_cases.year_data', $year)
|
||||||
|
->where('surveillance_cases.week_data', $week);
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $query->selectRaw("
|
||||||
|
COUNT(DISTINCT surveillance_cases.lab_code) as total_cases,
|
||||||
|
|
||||||
|
COUNT(DISTINCT CASE
|
||||||
|
WHEN case_lab_results.is_positive = 1
|
||||||
|
THEN surveillance_cases.lab_code
|
||||||
|
END) as overall_positive,
|
||||||
|
|
||||||
|
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_positive,
|
||||||
|
|
||||||
|
COUNT(DISTINCT CASE
|
||||||
|
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
|
||||||
|
")->first();
|
||||||
|
|
||||||
|
if (!$row || $row->total_cases == 0) {
|
||||||
|
return [
|
||||||
|
'cases' => 0,
|
||||||
|
'overall_rate' => 0,
|
||||||
|
'influenza_rate' => 0,
|
||||||
|
'covid_rate' => 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'cases' => $row->total_cases,
|
||||||
|
'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),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
public function programSummary($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
|
public function programSummary($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
|
||||||
{
|
{
|
||||||
|
$dateTo = now()->toDateString();
|
||||||
|
$dateFrom = now()->subDays(7)->toDateString();
|
||||||
|
|
||||||
$prevWeek = $endWeek - 1;
|
$current = $this->programSummaryFast(
|
||||||
$prevYear = $endYear;
|
$surveillanceId,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$dateFrom,
|
||||||
|
$dateTo
|
||||||
|
);
|
||||||
|
|
||||||
if ($prevWeek <= 0) {
|
$prevFrom = date('Y-m-d', strtotime($dateFrom . ' -7 days'));
|
||||||
$prevWeek = 52;
|
$prevTo = date('Y-m-d', strtotime($dateFrom . ' -1 day'));
|
||||||
$prevYear--;
|
|
||||||
}
|
|
||||||
|
|
||||||
$latest = SurveillanceCase::where('surveillance_id', $surveillanceId)
|
$previous = $this->programSummaryFast(
|
||||||
->selectRaw("year_data, week_data")
|
$surveillanceId,
|
||||||
->orderByDesc('year_data')
|
null,
|
||||||
->orderByDesc('week_data')
|
null,
|
||||||
->first();
|
$prevFrom,
|
||||||
|
$prevTo
|
||||||
$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 [
|
return [
|
||||||
|
|
||||||
'cases' => [
|
'cases' => [
|
||||||
'current' => $current['cases'],
|
'current' => $current['cases'],
|
||||||
'previous' => $previous['cases']
|
'previous' => $previous['cases']
|
||||||
],
|
],
|
||||||
|
|
||||||
'hospital_rate' => [
|
'hospital_rate' => [
|
||||||
'current' => 0,
|
'current' => 0,
|
||||||
'previous' => 0
|
'previous' => 0
|
||||||
],
|
],
|
||||||
|
|
||||||
'icu_rate' => [
|
'icu_rate' => [
|
||||||
'current' => 0,
|
'current' => 0,
|
||||||
'previous' => 0
|
'previous' => 0
|
||||||
],
|
],
|
||||||
|
|
||||||
'positivity_rate' => [
|
'positivity_rate' => [
|
||||||
'current' => $current['overall_rate'],
|
'current' => $current['overall_rate'],
|
||||||
'previous' => $previous['overall_rate']
|
'previous' => $previous['overall_rate']
|
||||||
],
|
],
|
||||||
|
|
||||||
'influenza_rate' => [
|
'influenza_rate' => [
|
||||||
'current' => $current['influenza_rate'],
|
'current' => $current['influenza_rate'],
|
||||||
'previous' => $previous['influenza_rate']
|
'previous' => $previous['influenza_rate']
|
||||||
],
|
],
|
||||||
|
|
||||||
'covid_rate' => [
|
'covid_rate' => [
|
||||||
'current' => $current['covid_rate'],
|
'current' => $current['covid_rate'],
|
||||||
'previous' => $previous['covid_rate']
|
'previous' => $previous['covid_rate']
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Overview Trend
|
| Overview Trend
|
||||||
@@ -437,7 +438,7 @@ class DashboardService
|
|||||||
|
|
||||||
COUNT(DISTINCT surveillance_cases.lab_code) as total_samples,
|
COUNT(DISTINCT surveillance_cases.lab_code) as total_samples,
|
||||||
|
|
||||||
-- Overall positivity rate
|
-- Overall positivity
|
||||||
ROUND(
|
ROUND(
|
||||||
COUNT(DISTINCT CASE
|
COUNT(DISTINCT CASE
|
||||||
WHEN case_lab_results.is_positive = 1
|
WHEN case_lab_results.is_positive = 1
|
||||||
@@ -446,33 +447,52 @@ class DashboardService
|
|||||||
/ NULLIF(COUNT(DISTINCT surveillance_cases.lab_code), 0) * 100
|
/ NULLIF(COUNT(DISTINCT surveillance_cases.lab_code), 0) * 100
|
||||||
,1) as positivity_rate,
|
,1) as positivity_rate,
|
||||||
|
|
||||||
-- Influenza positivity rate
|
-- Influenza %
|
||||||
ROUND(
|
ROUND(
|
||||||
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%'
|
|
||||||
)
|
)
|
||||||
THEN surveillance_cases.lab_code
|
THEN surveillance_cases.lab_code
|
||||||
END)
|
END)
|
||||||
/ NULLIF(COUNT(DISTINCT surveillance_cases.lab_code), 0) * 100
|
/ NULLIF(COUNT(DISTINCT surveillance_cases.lab_code), 0) * 100
|
||||||
,1) as influenza_rate,
|
,1) as influenza_rate,
|
||||||
|
|
||||||
-- COVID positivity rate
|
-- COVID %
|
||||||
ROUND(
|
ROUND(
|
||||||
COUNT(DISTINCT CASE
|
COUNT(DISTINCT CASE
|
||||||
WHEN case_lab_results.is_positive = 1
|
WHEN case_lab_results.is_positive = 1
|
||||||
AND (
|
AND (
|
||||||
case_lab_results.pathogen_name = 'Positive'
|
case_lab_results.pathogen_name = 'COVID-19'
|
||||||
OR case_lab_results.pathogen_name = 'SARS-CoV-2'
|
OR case_lab_results.pathogen_name = 'SARS-CoV-2'
|
||||||
)
|
)
|
||||||
AND case_lab_results.indicator LIKE '%Covid%'
|
AND case_lab_results.indicator LIKE '%COVID%'
|
||||||
THEN surveillance_cases.lab_code
|
THEN surveillance_cases.lab_code
|
||||||
END)
|
END)
|
||||||
/ NULLIF(COUNT(DISTINCT surveillance_cases.lab_code), 0) * 100
|
/ NULLIF(COUNT(DISTINCT surveillance_cases.lab_code), 0) * 100
|
||||||
,1) as covid_rate
|
,1) as covid_rate,
|
||||||
")
|
|
||||||
|
-- EV %
|
||||||
|
ROUND(
|
||||||
|
COUNT(DISTINCT CASE
|
||||||
|
WHEN case_lab_results.is_positive = 1
|
||||||
|
AND LOWER(case_lab_results.pathogen_name) LIKE '%enterovirus%'
|
||||||
|
THEN surveillance_cases.lab_code
|
||||||
|
END)
|
||||||
|
/ NULLIF(COUNT(DISTINCT surveillance_cases.lab_code), 0) * 100
|
||||||
|
,1) as ev_rate,
|
||||||
|
|
||||||
|
-- MPOX %
|
||||||
|
ROUND(
|
||||||
|
COUNT(DISTINCT CASE
|
||||||
|
WHEN case_lab_results.is_positive = 1
|
||||||
|
AND LOWER(case_lab_results.pathogen_name) LIKE '%mpox%'
|
||||||
|
THEN surveillance_cases.lab_code
|
||||||
|
END)
|
||||||
|
/ NULLIF(COUNT(DISTINCT surveillance_cases.lab_code), 0) * 100
|
||||||
|
,1) as mpox_rate
|
||||||
|
")
|
||||||
|
|
||||||
->groupBy(
|
->groupBy(
|
||||||
'surveillance_cases.year_data',
|
'surveillance_cases.year_data',
|
||||||
@@ -546,7 +566,6 @@ class DashboardService
|
|||||||
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%'
|
|
||||||
)
|
)
|
||||||
THEN surveillance_cases.lab_code
|
THEN surveillance_cases.lab_code
|
||||||
END) as influenza,
|
END) as influenza,
|
||||||
@@ -559,7 +578,19 @@ class DashboardService
|
|||||||
)
|
)
|
||||||
AND case_lab_results.indicator LIKE '%Covid%'
|
AND case_lab_results.indicator LIKE '%Covid%'
|
||||||
THEN surveillance_cases.lab_code
|
THEN surveillance_cases.lab_code
|
||||||
END) as covid
|
END) as covid,
|
||||||
|
|
||||||
|
COUNT(DISTINCT CASE
|
||||||
|
WHEN case_lab_results.is_positive = 1
|
||||||
|
AND LOWER(case_lab_results.pathogen_name) LIKE '%enterovirus%'
|
||||||
|
THEN surveillance_cases.lab_code
|
||||||
|
END) as ev,
|
||||||
|
|
||||||
|
COUNT(DISTINCT CASE
|
||||||
|
WHEN case_lab_results.is_positive = 1
|
||||||
|
AND LOWER(case_lab_results.pathogen_name) LIKE '%mpox%'
|
||||||
|
THEN surveillance_cases.lab_code
|
||||||
|
END) as mpox
|
||||||
")
|
")
|
||||||
|
|
||||||
->groupBy('surveillance_cases.week_data')
|
->groupBy('surveillance_cases.week_data')
|
||||||
@@ -643,7 +674,7 @@ class DashboardService
|
|||||||
return SurveillanceCase::join('case_lab_results', function ($join) {
|
return SurveillanceCase::join('case_lab_results', function ($join) {
|
||||||
$join->on('surveillance_cases.lab_code', '=', 'case_lab_results.lab_code')
|
$join->on('surveillance_cases.lab_code', '=', 'case_lab_results.lab_code')
|
||||||
->on('surveillance_cases.surveillance_id', '=', 'case_lab_results.surveillance_id');
|
->on('surveillance_cases.surveillance_id', '=', 'case_lab_results.surveillance_id');
|
||||||
})
|
})
|
||||||
->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) {
|
->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) {
|
||||||
$q->whereRaw(
|
$q->whereRaw(
|
||||||
"(surveillance_cases.year_data * 100 + surveillance_cases.week_data) BETWEEN ? AND ?",
|
"(surveillance_cases.year_data * 100 + surveillance_cases.week_data) BETWEEN ? AND ?",
|
||||||
@@ -670,17 +701,17 @@ class DashboardService
|
|||||||
$join->on('surveillance_cases.lab_code', '=', 'case_lab_results.lab_code')
|
$join->on('surveillance_cases.lab_code', '=', 'case_lab_results.lab_code')
|
||||||
->on('surveillance_cases.surveillance_id', '=', 'case_lab_results.surveillance_id');
|
->on('surveillance_cases.surveillance_id', '=', 'case_lab_results.surveillance_id');
|
||||||
})
|
})
|
||||||
->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) {
|
->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) {
|
||||||
$q->whereRaw(
|
$q->whereRaw(
|
||||||
"(surveillance_cases.year_data * 100 + surveillance_cases.week_data) BETWEEN ? AND ?",
|
"(surveillance_cases.year_data * 100 + surveillance_cases.week_data) BETWEEN ? AND ?",
|
||||||
[
|
[
|
||||||
$startYear * 100 + $startWeek,
|
$startYear * 100 + $startWeek,
|
||||||
$endYear * 100 + $endWeek
|
$endYear * 100 + $endWeek
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
->whereRaw('case_lab_results.is_positive = 1 and surveillance_cases.surveillance_id not in(6) and case_lab_results.indicator="Covid-19"')
|
->whereRaw('case_lab_results.is_positive = 1 and surveillance_cases.surveillance_id not in(6) and case_lab_results.indicator="Covid-19"')
|
||||||
->selectRaw("
|
->selectRaw("
|
||||||
CASE
|
CASE
|
||||||
WHEN patient_age_inday <= 28 THEN '0–28 days'
|
WHEN patient_age_inday <= 28 THEN '0–28 days'
|
||||||
WHEN patient_age_inday <= 364 THEN '29 days–11 months'
|
WHEN patient_age_inday <= 364 THEN '29 days–11 months'
|
||||||
@@ -703,9 +734,9 @@ class DashboardService
|
|||||||
END as age_order,
|
END as age_order,
|
||||||
COUNT(*) as total
|
COUNT(*) as total
|
||||||
")
|
")
|
||||||
->groupBy('age_group', 'age_order')
|
->groupBy('age_group', 'age_order')
|
||||||
->orderBy('age_order')
|
->orderBy('age_order')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,9 +78,7 @@ function buildStackedChart(canvasId, labels, datasets) {
|
|||||||
data: {
|
data: {
|
||||||
labels: labels,
|
labels: labels,
|
||||||
datasets: datasets,
|
datasets: datasets,
|
||||||
datalabels: {
|
|
||||||
display: true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
plugins: [ChartDataLabels],
|
plugins: [ChartDataLabels],
|
||||||
@@ -109,19 +107,22 @@ function buildStackedChart(canvasId, labels, datasets) {
|
|||||||
pointStyle: 'circle'
|
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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
datalabels: {
|
datalabels: {
|
||||||
color: "#000",
|
display: false
|
||||||
anchor: "end",
|
|
||||||
align: "top",
|
|
||||||
clamp: true,
|
|
||||||
clip: false,
|
|
||||||
font: {
|
|
||||||
weight: "bold",
|
|
||||||
size: 10
|
|
||||||
},
|
|
||||||
formatter: function (value) {
|
|
||||||
return value > 0 ? value : null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -152,6 +153,7 @@ function buildChart(id, type, labels, data) {
|
|||||||
}
|
}
|
||||||
const isHorizontal = id === 'sexChart';
|
const isHorizontal = id === 'sexChart';
|
||||||
const isAgeChart = id === 'ageChart';
|
const isAgeChart = id === 'ageChart';
|
||||||
|
const isSentinelChart = id === 'sentinelChart';
|
||||||
const options = {
|
const options = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@@ -165,7 +167,7 @@ function buildChart(id, type, labels, data) {
|
|||||||
plugins: {
|
plugins: {
|
||||||
|
|
||||||
legend: {
|
legend: {
|
||||||
position: isAgeChart ? 'left' : 'bottom',
|
position: isAgeChart || isSentinelChart ? 'left' : 'bottom',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
display: (ctx) => {
|
display: (ctx) => {
|
||||||
const chart = ctx.chart;
|
const chart = ctx.chart;
|
||||||
@@ -277,117 +279,53 @@ function buildChart(id, type, labels, data) {
|
|||||||
options: options
|
options: options
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) {
|
function buildMixedTrendChart(canvasId, labels, samples, lines) {
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
|
const datasets = [];
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
datasets.push({
|
||||||
|
type: 'line',
|
||||||
|
label: line.label,
|
||||||
|
data: line.data,
|
||||||
|
borderColor: line.color,
|
||||||
|
backgroundColor: line.color,
|
||||||
|
tension: 0.4,
|
||||||
|
yAxisID: 'y1',
|
||||||
|
pointStyle: 'line'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
datasets.push({
|
||||||
|
type: 'bar',
|
||||||
|
label: 'Total Cases',
|
||||||
|
data: samples,
|
||||||
|
backgroundColor: '#0B8F3C',
|
||||||
|
yAxisID: 'y'
|
||||||
|
});
|
||||||
|
|
||||||
charts[canvasId] = new Chart(ctx, {
|
charts[canvasId] = new Chart(ctx, {
|
||||||
|
data: { labels, datasets },
|
||||||
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: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: { position: 'bottom' },
|
||||||
position: 'bottom',
|
|
||||||
labels: {
|
|
||||||
usePointStyle: true,
|
|
||||||
padding: 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
datalabels: {
|
datalabels: {
|
||||||
align: "top",
|
display: false
|
||||||
anchor: "end",
|
|
||||||
color: "#555",
|
|
||||||
display: false,
|
|
||||||
font: {
|
|
||||||
size: 10
|
|
||||||
},
|
|
||||||
formatter: function (value, context) {
|
|
||||||
if (Number(value) === 0) return null;
|
|
||||||
|
|
||||||
if (context.dataset.type === 'line') {
|
|
||||||
return value + '%';
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
layout: {
|
|
||||||
padding: {
|
|
||||||
top: 30,
|
|
||||||
bottom: 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: { title: { display: true, text: 'Cases' } },
|
||||||
position: 'left',
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Cases'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
y1: {
|
y1: {
|
||||||
position: 'right',
|
position: 'right',
|
||||||
grid: { drawOnChartArea: false },
|
grid: { drawOnChartArea: false },
|
||||||
title: {
|
title: { display: true, text: '% Positivity' }
|
||||||
display: true,
|
|
||||||
text: '% Positivity'
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,214 +72,372 @@ function formatChartName(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function exportSelectedCharts() {
|
async function exportSelectedCharts() {
|
||||||
|
console.log("Exporting selected charts...");
|
||||||
|
|
||||||
const { jsPDF } = window.jspdf;
|
const { jsPDF } = window.jspdf;
|
||||||
const pdf = new jsPDF("p", "mm", "a4");
|
const pdf = new jsPDF("l", "mm", "a4");
|
||||||
|
|
||||||
|
const margin = 15;
|
||||||
|
const pageWidth = 297;
|
||||||
|
const pageHeight = 210;
|
||||||
|
|
||||||
|
const contentTop = 35;
|
||||||
|
const contentHeight = pageHeight - contentTop - 10;
|
||||||
|
const contentWidth = pageWidth - margin * 2;
|
||||||
|
|
||||||
const selected = document.querySelectorAll("#chartList input:checked");
|
const selected = document.querySelectorAll("#chartList input:checked");
|
||||||
|
|
||||||
const margin = 10;
|
const loadingDiv = document.createElement("div");
|
||||||
const pageWidth = 210;
|
Object.assign(loadingDiv.style, {
|
||||||
const usableWidth = pageWidth - margin * 2;
|
position: "fixed",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
background: "rgb(255, 255, 255)",
|
||||||
|
color: "#fff",
|
||||||
|
padding: "20px",
|
||||||
|
borderRadius: "10px",
|
||||||
|
zIndex: "10000"
|
||||||
|
});
|
||||||
|
loadingDiv.innerHTML = "Generating PDF...";
|
||||||
|
document.body.appendChild(loadingDiv);
|
||||||
|
|
||||||
const gap = 5;
|
function drawHeader() {
|
||||||
const chartWidth = (usableWidth - gap) / 2;
|
pdf.setFillColor(255, 255, 255);
|
||||||
|
pdf.rect(0, 0, pageWidth, 30, "F");
|
||||||
|
|
||||||
let y = 35;
|
pdf.setFontSize(18);
|
||||||
|
pdf.setFont(undefined, "bold");
|
||||||
|
pdf.setTextColor(0);
|
||||||
|
pdf.text(getReportTitle(), margin, 14);
|
||||||
|
|
||||||
// ===== HEADER =====
|
pdf.setFontSize(9);
|
||||||
pdf.setFontSize(18);
|
pdf.setTextColor(100);
|
||||||
pdf.setFont(undefined, "bold");
|
pdf.text(`Generated: ${new Date().toLocaleString()}`, margin, 22);
|
||||||
pdf.text(getReportTitle(), margin, 15);
|
|
||||||
|
|
||||||
pdf.setFontSize(10);
|
pdf.setDrawColor(220);
|
||||||
pdf.setFont(undefined, "normal");
|
pdf.line(margin, 28, pageWidth - margin, 28);
|
||||||
pdf.text(`Generated: ${new Date().toLocaleString()}`, margin, 22);
|
}
|
||||||
|
|
||||||
pdf.setDrawColor(200);
|
drawHeader();
|
||||||
pdf.line(margin, 25, pageWidth - margin, 25);
|
|
||||||
|
|
||||||
// ===== PREPARE ITEMS =====
|
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
for (const cb of selected) {
|
for (const cb of selected) {
|
||||||
|
|
||||||
if (cb.value === "provinceMap") {
|
if (cb.value === "provinceMap") {
|
||||||
const mapImg = await getMapImage();
|
loadingDiv.innerHTML = "Processing map...";
|
||||||
|
const img = await getMapImage();
|
||||||
|
|
||||||
if (mapImg) {
|
if (img) {
|
||||||
items.push({
|
items.push({
|
||||||
id: "provinceMap",
|
id: "provinceMap",
|
||||||
type: "map",
|
type: "map",
|
||||||
img: mapImg
|
img
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const chart = charts[cb.value];
|
||||||
|
if (!chart) continue;
|
||||||
|
|
||||||
continue;
|
items.push({
|
||||||
|
id: cb.value,
|
||||||
|
type: "chart",
|
||||||
|
chart
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const chart = charts[cb.value];
|
|
||||||
if (!chart) continue;
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
id: cb.value,
|
|
||||||
type: "chart",
|
|
||||||
chart
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let i = 0;
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i];
|
||||||
|
|
||||||
const FIXED_HEIGHT = 70;
|
loadingDiv.innerHTML = `Rendering ${formatChartName(item.id)} (${i + 1}/${items.length})`;
|
||||||
const FIXED_WIDTH = chartWidth;
|
|
||||||
|
|
||||||
while (i < items.length) {
|
if (i !== 0) {
|
||||||
|
|
||||||
const left = items[i];
|
|
||||||
const right = items[i + 1] || null;
|
|
||||||
|
|
||||||
let leftHeight = FIXED_HEIGHT;
|
|
||||||
let rightHeight = 0;
|
|
||||||
|
|
||||||
const xLeft = margin;
|
|
||||||
|
|
||||||
pdf.setFontSize(11);
|
|
||||||
pdf.setFont(undefined, "bold");
|
|
||||||
pdf.text(formatChartName(left.id), xLeft, y - 3);
|
|
||||||
|
|
||||||
let leftWidth = FIXED_WIDTH;
|
|
||||||
let leftImg;
|
|
||||||
|
|
||||||
if (left.type === "map") {
|
|
||||||
leftWidth = FIXED_WIDTH;
|
|
||||||
leftHeight = 60;
|
|
||||||
leftImg = left.img;
|
|
||||||
} else {
|
|
||||||
leftImg = left.chart.toBase64Image();
|
|
||||||
|
|
||||||
if (["doughnut", "pie"].includes(left.chart.config.type)) {
|
|
||||||
const size = Math.min(FIXED_WIDTH, FIXED_HEIGHT);
|
|
||||||
leftWidth = size;
|
|
||||||
leftHeight = size;
|
|
||||||
} else {
|
|
||||||
leftHeight = FIXED_HEIGHT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const xLeftAdjusted = xLeft + (FIXED_WIDTH - leftWidth) / 2;
|
|
||||||
|
|
||||||
pdf.addImage(
|
|
||||||
leftImg,
|
|
||||||
"PNG",
|
|
||||||
xLeftAdjusted,
|
|
||||||
y,
|
|
||||||
leftWidth,
|
|
||||||
leftHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
if (right) {
|
|
||||||
|
|
||||||
const xRight = margin + chartWidth + gap;
|
|
||||||
|
|
||||||
pdf.setFontSize(11);
|
|
||||||
pdf.setFont(undefined, "bold");
|
|
||||||
pdf.text(formatChartName(right.id), xRight, y - 3);
|
|
||||||
|
|
||||||
let rightWidth = FIXED_WIDTH;
|
|
||||||
let rightImg;
|
|
||||||
|
|
||||||
if (right.type === "map") {
|
|
||||||
rightWidth = FIXED_WIDTH;
|
|
||||||
rightHeight = 60;
|
|
||||||
rightImg = right.img;
|
|
||||||
} else {
|
|
||||||
rightImg = right.chart.toBase64Image();
|
|
||||||
|
|
||||||
if (["doughnut", "pie"].includes(right.chart.config.type)) {
|
|
||||||
const size = Math.min(FIXED_WIDTH, FIXED_HEIGHT);
|
|
||||||
rightWidth = size;
|
|
||||||
rightHeight = size;
|
|
||||||
} else {
|
|
||||||
rightHeight = FIXED_HEIGHT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const xRightAdjusted = xRight + (FIXED_WIDTH - rightWidth) / 2;
|
|
||||||
|
|
||||||
pdf.addImage(
|
|
||||||
rightImg,
|
|
||||||
"PNG",
|
|
||||||
xRightAdjusted,
|
|
||||||
y,
|
|
||||||
rightWidth,
|
|
||||||
rightHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
i += 2;
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== ROW HEIGHT =====
|
|
||||||
const rowHeight = Math.max(leftHeight, rightHeight);
|
|
||||||
y += rowHeight + 12;
|
|
||||||
|
|
||||||
// ===== PAGE BREAK =====
|
|
||||||
if (y > 260) {
|
|
||||||
pdf.addPage();
|
pdf.addPage();
|
||||||
y = 30;
|
drawHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cardX = margin;
|
||||||
|
const cardY = contentTop - 5;
|
||||||
|
const cardWidth = contentWidth;
|
||||||
|
const cardHeight = contentHeight;
|
||||||
|
|
||||||
|
pdf.setFillColor(255, 255, 255);
|
||||||
|
pdf.roundedRect(cardX, cardY, cardWidth, cardHeight, 3, 3, "F");
|
||||||
|
|
||||||
|
pdf.setDrawColor(230);
|
||||||
|
pdf.roundedRect(cardX, cardY, cardWidth, cardHeight, 3, 3);
|
||||||
|
|
||||||
|
pdf.setFontSize(13);
|
||||||
|
pdf.setFont(undefined, "bold");
|
||||||
|
pdf.setTextColor(0);
|
||||||
|
pdf.text(formatChartName(item.id), cardX + 5, cardY + 8);
|
||||||
|
|
||||||
|
let img;
|
||||||
|
let width;
|
||||||
|
let height;
|
||||||
|
|
||||||
|
if (item.type === "map") {
|
||||||
|
img = item.img;
|
||||||
|
|
||||||
|
width = cardWidth - 50;
|
||||||
|
height = width * 0.65;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const canvas = item.chart.canvas;
|
||||||
|
|
||||||
|
const tempCanvas = document.createElement("canvas");
|
||||||
|
tempCanvas.width = canvas.width;
|
||||||
|
tempCanvas.height = canvas.height;
|
||||||
|
|
||||||
|
const ctx = tempCanvas.getContext("2d", { willReadFrequently: true });
|
||||||
|
ctx.fillStyle = "#ffffff";
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.drawImage(canvas, 0, 0);
|
||||||
|
|
||||||
|
img = tempCanvas.toDataURL("image/png");
|
||||||
|
|
||||||
|
const ratio = canvas.height / canvas.width;
|
||||||
|
|
||||||
|
width = cardWidth - 10;
|
||||||
|
height = width * ratio;
|
||||||
|
|
||||||
|
if (height > cardHeight - 20) {
|
||||||
|
height = cardHeight - 20;
|
||||||
|
width = height / ratio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = cardX + (cardWidth - width) / 2;
|
||||||
|
const contentTopOffset = 14;
|
||||||
|
const availableHeight = cardHeight - contentTopOffset;
|
||||||
|
|
||||||
|
const y = cardY + contentTopOffset + (availableHeight - height) / 2;
|
||||||
|
|
||||||
|
pdf.addImage(img, "PNG", x, y, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(loadingDiv);
|
||||||
|
|
||||||
pdf.save("dashboard_report.pdf");
|
pdf.save("dashboard_report.pdf");
|
||||||
closeChartSelector();
|
closeChartSelector();
|
||||||
}
|
}
|
||||||
function prepareMapForExport() {
|
function prepareMapForExport() {
|
||||||
if (!window.map) return;
|
if (!window.map) return;
|
||||||
|
|
||||||
const bounds = L.featureGroup(Object.values(map._layers)).getBounds();
|
const bounds = L.latLngBounds([
|
||||||
|
[10.3, 102.3],
|
||||||
|
[14.7, 107.6]
|
||||||
|
]);
|
||||||
|
|
||||||
|
map.fitBounds(bounds);
|
||||||
|
|
||||||
if (bounds.isValid()) {
|
|
||||||
map.setView([12.5, 104.9], 6);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function exportFullDashboard() {
|
async function exportFullDashboard() {
|
||||||
|
console.log("Exporting full charts...");
|
||||||
const el = document.querySelector(".content-area");
|
const el = document.querySelector(".content-area");
|
||||||
|
|
||||||
html2canvas(el, { scale: 2 }).then(canvas => {
|
const mapEl = document.getElementById("provinceMap");
|
||||||
|
let originalMapHTML = null;
|
||||||
|
|
||||||
const img = canvas.toDataURL("image/png");
|
if (mapEl) {
|
||||||
|
originalMapHTML = mapEl.innerHTML;
|
||||||
|
|
||||||
const { jsPDF } = window.jspdf;
|
const mapImg = await getMapImage();
|
||||||
const pdf = new jsPDF("p", "mm", "a4");
|
|
||||||
|
|
||||||
const width = 210;
|
if (mapImg) {
|
||||||
const height = (canvas.height * width) / canvas.width;
|
mapEl.innerHTML = `<img src="${mapImg}" style="width:100%;height:100%;object-fit:cover;" />`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prepareMapForExport();
|
||||||
|
|
||||||
pdf.addImage(img, "PNG", 0, 0, width, height);
|
await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
|
||||||
pdf.save("full_dashboard.pdf");
|
|
||||||
|
|
||||||
|
const canvas = await html2canvas(el, {
|
||||||
|
scale: 3,
|
||||||
|
useCORS: true,
|
||||||
|
scrollY: -window.scrollY
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (mapEl && originalMapHTML !== null) {
|
||||||
|
mapEl.innerHTML = originalMapHTML;
|
||||||
|
map.invalidateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = canvas.toDataURL("image/png");
|
||||||
|
|
||||||
|
const { jsPDF } = window.jspdf;
|
||||||
|
const pdf = new jsPDF("l", "mm", "a4");
|
||||||
|
|
||||||
|
const pageWidth = 297;
|
||||||
|
const pageHeight = 210;
|
||||||
|
|
||||||
|
const ratio = Math.min(
|
||||||
|
pageWidth / canvas.width,
|
||||||
|
pageHeight / canvas.height
|
||||||
|
);
|
||||||
|
|
||||||
|
const imgWidth = canvas.width * ratio;
|
||||||
|
const imgHeight = canvas.height * ratio;
|
||||||
|
|
||||||
|
const x = (pageWidth - imgWidth) / 2;
|
||||||
|
const y = (pageHeight - imgHeight) / 2;
|
||||||
|
|
||||||
|
pdf.addImage(img, "PNG", x, y, imgWidth, imgHeight);
|
||||||
|
pdf.save("full_dashboard.pdf");
|
||||||
|
|
||||||
closeChartSelector();
|
closeChartSelector();
|
||||||
}
|
}
|
||||||
|
function getColor(value) {
|
||||||
|
if (value > 50) return "#b91c1c";
|
||||||
|
if (value >= 10) return "#ef4444";
|
||||||
|
if (value > 0) return "#fecaca";
|
||||||
|
return "#f3f4f600";
|
||||||
|
}
|
||||||
async function getMapImage() {
|
async function getMapImage() {
|
||||||
|
const original = document.getElementById("provinceMap");
|
||||||
|
if (!original) return null;
|
||||||
|
|
||||||
const mapEl = document.getElementById("provinceMap");
|
const width = original.offsetWidth;
|
||||||
if (!mapEl) return null;
|
const height = original.offsetHeight;
|
||||||
|
|
||||||
prepareMapForExport();
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = width * 3;
|
||||||
|
canvas.height = height * 3;
|
||||||
|
|
||||||
// wait for map tiles to re-render
|
const ctx = canvas.getContext("2d");
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
ctx.scale(3, 3);
|
||||||
|
|
||||||
const canvas = await html2canvas(mapEl, {
|
|
||||||
useCORS: true,
|
ctx.fillStyle = "#ffffff";
|
||||||
scale: 2
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
const zoom = map.getZoom();
|
||||||
|
const projection = map.options.crs;
|
||||||
|
|
||||||
|
const projectedRings = [];
|
||||||
|
|
||||||
|
let minX = Infinity, minY = Infinity;
|
||||||
|
let maxX = -Infinity, maxY = -Infinity;
|
||||||
|
|
||||||
|
const totals = {};
|
||||||
|
const rows = window.latestProvinceData || [];
|
||||||
|
|
||||||
|
rows.forEach(r => {
|
||||||
|
const province = normalizeProvince(r.patient_province, window.validProvinces);
|
||||||
|
console.log(province, totals[province]);
|
||||||
|
if (!province) return;
|
||||||
|
|
||||||
|
if (!totals[province]) {
|
||||||
|
totals[province] = { total: 0, positive: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
totals[province].total += Number(r.total);
|
||||||
|
totals[province].positive += Number(r.positive);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getColor(value) {
|
||||||
|
if (value > 50) return "#b91c1c";
|
||||||
|
if (value >= 10) return "#ef4444";
|
||||||
|
if (value > 0) return "#fecaca";
|
||||||
|
return "#f3f4f600";
|
||||||
|
}
|
||||||
|
|
||||||
|
map.eachLayer(layer => {
|
||||||
|
if (!layer.toGeoJSON) return;
|
||||||
|
|
||||||
|
const geo = layer.toGeoJSON();
|
||||||
|
|
||||||
|
const features = geo.type === "FeatureCollection"
|
||||||
|
? geo.features
|
||||||
|
: [geo];
|
||||||
|
|
||||||
|
features.forEach(f => {
|
||||||
|
if (!f.geometry) return;
|
||||||
|
|
||||||
|
const coords = f.geometry.coordinates;
|
||||||
|
|
||||||
|
const polygons = f.geometry.type === "MultiPolygon"
|
||||||
|
? coords
|
||||||
|
: [coords];
|
||||||
|
|
||||||
|
polygons.forEach(poly => {
|
||||||
|
poly.forEach(ring => {
|
||||||
|
|
||||||
|
const projected = ring.map(([lng, lat]) => {
|
||||||
|
const latlng = L.latLng(lat, lng);
|
||||||
|
const point = projection.latLngToPoint(latlng, zoom);
|
||||||
|
|
||||||
|
minX = Math.min(minX, point.x);
|
||||||
|
maxX = Math.max(maxX, point.x);
|
||||||
|
minY = Math.min(minY, point.y);
|
||||||
|
maxY = Math.max(maxY, point.y);
|
||||||
|
|
||||||
|
return [point.x, point.y];
|
||||||
|
});
|
||||||
|
|
||||||
|
projectedRings.push({
|
||||||
|
points: projected,
|
||||||
|
properties: f.properties
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const padding = 10;
|
||||||
|
|
||||||
|
const scaleX = (width - padding * 2) / (maxX - minX);
|
||||||
|
const scaleY = (height - padding * 2) / (maxY - minY);
|
||||||
|
const scale = Math.min(scaleX, scaleY);
|
||||||
|
|
||||||
|
const offsetX = (width - (maxX - minX) * scale) / 2;
|
||||||
|
const offsetY = (height - (maxY - minY) * scale) / 2;
|
||||||
|
|
||||||
|
projectedRings.forEach(({ points, properties }) => {
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
points.forEach(([x, y], i) => {
|
||||||
|
const drawX = (x - minX) * scale + offsetX;
|
||||||
|
const drawY = (y - minY) * scale + offsetY;
|
||||||
|
|
||||||
|
if (i === 0) ctx.moveTo(drawX, drawY);
|
||||||
|
else ctx.lineTo(drawX, drawY);
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
const province = properties.ADM1_EN;
|
||||||
|
const value = totals[province]?.total || 0;
|
||||||
|
|
||||||
|
ctx.fillStyle = getColor(value);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.strokeStyle = "#444";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.stroke();
|
||||||
|
});
|
||||||
|
|
||||||
|
const legend = document.querySelector(".map-legend");
|
||||||
|
|
||||||
|
if (legend) {
|
||||||
|
const legendCanvas = await html2canvas(legend, {
|
||||||
|
scale: 3,
|
||||||
|
backgroundColor: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const lw = legend.offsetWidth;
|
||||||
|
const lh = legend.offsetHeight;
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
legendCanvas,
|
||||||
|
width - lw - 10,
|
||||||
|
height - lh - 10,
|
||||||
|
lw,
|
||||||
|
lh
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return canvas.toDataURL("image/png");
|
return canvas.toDataURL("image/png");
|
||||||
}
|
}
|
||||||
function getReportTitle() {
|
function getReportTitle() {
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
|
|||||||
|
|
||||||
const datasets = [];
|
const datasets = [];
|
||||||
|
|
||||||
const allowedPrograms = ['SARI', 'ILI', 'LBM', 'AFI', 'NDS', 'SEQ'];
|
const allowedPrograms = ['SARI', 'ILI', 'LBM', 'AFI', 'NDS'];
|
||||||
|
|
||||||
Object.keys(data).forEach(code => {
|
Object.keys(data).forEach(code => {
|
||||||
|
|
||||||
@@ -144,6 +144,7 @@ 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,
|
||||||
@@ -184,6 +185,7 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
charts['trendChart'] = trendChart;
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -266,6 +268,7 @@ function loadInfluenzaSubtypeDistribution(periodType, startYear, startWeek, endY
|
|||||||
},
|
},
|
||||||
plugins: [ChartDataLabels]
|
plugins: [ChartDataLabels]
|
||||||
});
|
});
|
||||||
|
charts['influenzaSubtypeDistribution'] = influenzaSubtypeChart;
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -346,6 +349,7 @@ function loadCovidDistributedByAgeGroup(periodType, startYear, startWeek, endYea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
charts['covidDistributedByAgeGroup'] = covidDistributedByAgeChart;
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -443,6 +447,7 @@ function loadCovidLineageFrequency(periodType, startYear, startWeek, endYear, en
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
charts['covidLineageFrequency'] = covidLineageFrequencyChart;
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
// Custom right-side scrollable legend
|
// Custom right-side scrollable legend
|
||||||
@@ -1059,7 +1064,7 @@ function getColorByPathogen(name) {
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
loadSummary();
|
loadSummary();
|
||||||
|
|
||||||
new DashboardFilter((startYear, startWeek, endYear, endWeek) => {
|
new DashboardFilter((startYear, startWeek, endYear, endWeek) => {
|
||||||
|
|
||||||
loadTrend('week', startYear, startWeek, endYear, endWeek);
|
loadTrend('week', startYear, startWeek, endYear, endWeek);
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
const standardPrograms = ['SARI', 'ILI', 'LBM', 'AFI'];
|
const standardPrograms = ['SARI', 'ILI', 'LBM', 'AFI', 'NDS'];
|
||||||
const programCode = (window.PROGRAM_CODE || '').trim().toUpperCase();
|
const programCode = (window.PROGRAM_CODE || '').trim().toUpperCase();
|
||||||
let map;
|
let map;
|
||||||
let provinceLayer;
|
|
||||||
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) => {
|
||||||
|
const elements = document.querySelectorAll(".report-period");
|
||||||
|
elements.forEach(el => {
|
||||||
|
el.textContent = 'Week ' + startWeek + ' of ' + startYear + ' to ' + 'Week ' + endWeek + ' of ' + endYear
|
||||||
|
});
|
||||||
|
|
||||||
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())
|
||||||
@@ -15,6 +19,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
function normalizeProvince(name, validSet) {
|
function normalizeProvince(name, validSet) {
|
||||||
if (!name || !validSet) return null;
|
if (!name || !validSet) return null;
|
||||||
@@ -46,6 +51,7 @@ function normalizeProvince(name, validSet) {
|
|||||||
|
|
||||||
return match || null;
|
return match || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAFIDashboard(data) {
|
function renderAFIDashboard(data) {
|
||||||
const pathogenRows = (data.pathogen_distribution || [])
|
const pathogenRows = (data.pathogen_distribution || [])
|
||||||
.sort((a, b) => b.total - a.total);
|
.sort((a, b) => b.total - a.total);
|
||||||
@@ -54,16 +60,22 @@ function renderAFIDashboard(data) {
|
|||||||
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16', '#e11dba', '#f6f63b'
|
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16', '#e11dba', '#f6f63b'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const rows = data.afi_trend || [];
|
||||||
|
|
||||||
|
const pcr = rows.filter(r => r.test_type === 'PCR');
|
||||||
|
const serum = rows.filter(r => r.test_type === 'Serum');
|
||||||
|
|
||||||
|
renderAFITrend(pcr, 'trendChart', colors);
|
||||||
|
//renderAFITrend(serum, 'pathogenChart', colors);
|
||||||
|
|
||||||
renderSummary(data.summary);
|
renderSummary(data.summary);
|
||||||
renderAFITrend(data.afi_trend);
|
|
||||||
renderProvinceHeatmap(data.province_distribution);
|
renderProvinceHeatmap(data.province_distribution);
|
||||||
renderPathogenChart(data.pathogen_distribution);
|
|
||||||
renderDemographics(data);
|
renderDemographics(data);
|
||||||
|
renderPathogenChart(data.pathogen_distribution || []);
|
||||||
renderSentinel(data.sentinel_sites || []);
|
renderSentinel(data.sentinel_sites || []);
|
||||||
renderSubtypeChart(data.subtype_distribution || []);
|
renderSubtypeChart(data.subtype_distribution || []);
|
||||||
|
|
||||||
charts['pathogenChart'].data.datasets[0].backgroundColor = colors;
|
|
||||||
charts['pathogenChart'].update();
|
|
||||||
charts['ageChart'].data.datasets[0].backgroundColor = colors;
|
charts['ageChart'].data.datasets[0].backgroundColor = colors;
|
||||||
charts['ageChart'].update();
|
charts['ageChart'].update();
|
||||||
charts['sexChart'].data.datasets[0].backgroundColor = colors;
|
charts['sexChart'].data.datasets[0].backgroundColor = colors;
|
||||||
@@ -72,9 +84,12 @@ function renderAFIDashboard(data) {
|
|||||||
charts['sentinelChart'].update();
|
charts['sentinelChart'].update();
|
||||||
charts['subtypeChart'].data.datasets[0].backgroundColor = colors;
|
charts['subtypeChart'].data.datasets[0].backgroundColor = colors;
|
||||||
charts['subtypeChart'].update();
|
charts['subtypeChart'].update();
|
||||||
|
charts['pathogenChart'].data.datasets[0].backgroundColor = colors;
|
||||||
|
charts['pathogenChart'].update();
|
||||||
}
|
}
|
||||||
function renderProvinceHeatmap(rows) {
|
function renderProvinceHeatmap(rows) {
|
||||||
|
window.latestProvinceData = rows;
|
||||||
|
|
||||||
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);
|
||||||
@@ -92,7 +107,7 @@ function renderProvinceHeatmap(rows) {
|
|||||||
const validProvinces = new Set(
|
const validProvinces = new Set(
|
||||||
geo.features.map(f => f.properties.ADM1_EN)
|
geo.features.map(f => f.properties.ADM1_EN)
|
||||||
);
|
);
|
||||||
|
window.validProvinces = validProvinces;
|
||||||
const totals = {};
|
const totals = {};
|
||||||
|
|
||||||
rows.forEach(r => {
|
rows.forEach(r => {
|
||||||
@@ -116,7 +131,7 @@ function renderProvinceHeatmap(rows) {
|
|||||||
return "#f3f4f600";
|
return "#f3f4f600";
|
||||||
}
|
}
|
||||||
|
|
||||||
provinceLayer = L.geoJSON(geo, {
|
window.provinceLayer = L.geoJSON(geo, {
|
||||||
style: feature => {
|
style: feature => {
|
||||||
|
|
||||||
const province = feature.properties.ADM1_EN;
|
const province = feature.properties.ADM1_EN;
|
||||||
@@ -124,9 +139,9 @@ function renderProvinceHeatmap(rows) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
color: "#444",
|
color: "#444",
|
||||||
weight: 1,
|
weight: 1.5,
|
||||||
fillColor: getColor(value),
|
fillColor: getColor(value),
|
||||||
fillOpacity: 0.7
|
fillOpacity: 0.8
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
onEachFeature: (feature, layer) => {
|
onEachFeature: (feature, layer) => {
|
||||||
@@ -233,12 +248,27 @@ function renderProgramTrend(rows) {
|
|||||||
const fluRate = rows.map(r => r.influenza_rate || 0);
|
const fluRate = rows.map(r => r.influenza_rate || 0);
|
||||||
const covidRate = rows.map(r => r.covid_rate || 0);
|
const covidRate = rows.map(r => r.covid_rate || 0);
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
{ label: 'Influenza %', data: fluRate, color: '#fa2929' },
|
||||||
|
{ label: 'COVID-19 %', data: covidRate, color: '#1976D2' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ✅ ONLY NDS gets EV + Mpox
|
||||||
|
if (programCode === 'NDS') {
|
||||||
|
const evRate = rows.map(r => r.ev_rate || 0);
|
||||||
|
const mpoxRate = rows.map(r => r.mpox_rate || 0);
|
||||||
|
|
||||||
|
lines.push(
|
||||||
|
{ label: 'EV %', data: evRate, color: '#f59e0b' },
|
||||||
|
{ label: 'Mpox %', data: mpoxRate, color: '#8b5cf6' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
buildMixedTrendChart(
|
buildMixedTrendChart(
|
||||||
'trendChart',
|
'trendChart',
|
||||||
labels,
|
labels,
|
||||||
samples,
|
samples,
|
||||||
fluRate,
|
lines
|
||||||
covidRate
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function renderSummary(summary) {
|
function renderSummary(summary) {
|
||||||
@@ -365,74 +395,43 @@ function renderDashboard(data) {
|
|||||||
|
|
||||||
|
|
||||||
//AFI
|
//AFI
|
||||||
function renderAFITrend(rows) {
|
function renderAFITrend(rows, canvasId, colors) {
|
||||||
|
|
||||||
if (!rows.length) {
|
if (!rows || !rows.length) {
|
||||||
buildStackedChart('trendChart', [], []);
|
buildStackedChart(canvasId, [], []);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { labels, datasets } = transformAFIData(rows);
|
const cleanRows = rows.filter(r => r.pathogen);
|
||||||
|
|
||||||
buildStackedChart('trendChart', labels, datasets);
|
const keyFn = r => `${r.year}-${r.period}`;
|
||||||
}
|
|
||||||
function transformAFIData(rows) {
|
|
||||||
|
|
||||||
const grouped = {};
|
const map = {};
|
||||||
const pathogensSet = new Set();
|
cleanRows.forEach(r => {
|
||||||
|
const key = keyFn(r);
|
||||||
rows.forEach(r => {
|
if (!map[key]) map[key] = {};
|
||||||
|
map[key][r.pathogen] = Number(r.total_tests || r.total || 0);
|
||||||
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 keys = Object.keys(map).sort((a, b) => {
|
||||||
const [yA, wA] = a.split('-W').map(Number);
|
const [y1, w1] = a.split('-').map(Number);
|
||||||
const [yB, wB] = b.split('-W').map(Number);
|
const [y2, w2] = b.split('-').map(Number);
|
||||||
return yA === yB ? wA - wB : yA - yB;
|
return y1 !== y2 ? y1 - y2 : w1 - w2;
|
||||||
});
|
});
|
||||||
|
|
||||||
const pathogenTotals = {};
|
const labels = keys.map(k => `W${k.split('-')[1]}`);
|
||||||
|
|
||||||
rows.forEach(r => {
|
const pathogens = [...new Set(cleanRows.map(r => r.pathogen))];
|
||||||
pathogenTotals[r.pathogen] = (pathogenTotals[r.pathogen] || 0) + r.total;
|
|
||||||
});
|
|
||||||
|
|
||||||
const pathogens = Object.keys(pathogenTotals)
|
const datasets = pathogens.map((p, i) => ({
|
||||||
.sort((a, b) => pathogenTotals[b] - pathogenTotals[a]);
|
|
||||||
const datasets = pathogens.map(p => ({
|
|
||||||
label: p,
|
label: p,
|
||||||
data: labels.map(l => grouped[l][p] || 0),
|
data: keys.map(k => map[k][p] || 0),
|
||||||
backgroundColor: getColorForPathogen(p)
|
backgroundColor: colors[i % colors.length]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { labels: labels.map(l => l.split('-')[1]), datasets };
|
buildStackedChart(canvasId, labels, 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];
|
|
||||||
|
|
||||||
return `hsl(${Math.floor(Math.random() * 360)}, 70%, 60%)`;
|
|
||||||
}
|
|
||||||
function renderPathogenChart(rows) {
|
function renderPathogenChart(rows) {
|
||||||
buildChart(
|
buildChart(
|
||||||
'pathogenChart',
|
'pathogenChart',
|
||||||
|
|||||||
@@ -77,12 +77,14 @@
|
|||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body" style="height:520px;">
|
<div class="card-body" style="height:560px;">
|
||||||
|
|
||||||
<h6 class="fw-bold mb-3">
|
<h6 class="fw-bold mb-3">
|
||||||
Case Trends & Positivity Rate by Epiweek
|
Case Trends & Positivity Rate by Epiweek
|
||||||
</h6>
|
</h6>
|
||||||
|
<p class="text-muted small report-period">
|
||||||
|
(based on selected epiweek range)
|
||||||
|
</p>
|
||||||
<div style="height:460px; position:relative;">
|
<div style="height:460px; position:relative;">
|
||||||
<canvas id="trendChart"></canvas>
|
<canvas id="trendChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,10 +96,12 @@
|
|||||||
<!-- PATHOGEN DISTRIBUTION -->
|
<!-- PATHOGEN DISTRIBUTION -->
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body" style="height:520px">
|
<div class="card-body" style="height:560px">
|
||||||
|
|
||||||
<h6 class="fw-bold mb-3">Pathogen Distribution</h6>
|
<h6 class="fw-bold mb-3">Pathogen Distribution</h6>
|
||||||
|
<p class="text-muted small report-period">
|
||||||
|
(based on selected epiweek range)
|
||||||
|
</p>
|
||||||
<div style="height:460px; position:relative;">
|
<div style="height:460px; position:relative;">
|
||||||
<canvas id="pathogenChart"></canvas>
|
<canvas id="pathogenChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,10 +116,12 @@
|
|||||||
<!-- MAP -->
|
<!-- MAP -->
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body" style="height:520px">
|
<div class="card-body" style="height:560px">
|
||||||
|
|
||||||
<h6 class="fw-bold mb-3">Cases by Province</h6>
|
<h6 class="fw-bold mb-3">Cases by Province</h6>
|
||||||
|
<p class="text-muted small report-period">
|
||||||
|
(based on selected epiweek range)
|
||||||
|
</p>
|
||||||
<div id="provinceMap" style="height:450px;"></div>
|
<div id="provinceMap" style="height:450px;"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -123,7 +129,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body" style="height:520px">
|
<div class="card-body" style="height:560px">
|
||||||
|
|
||||||
<h6 class="fw-bold mb-3">Sentinel Sites & Influenza Subtypes</h6>
|
<h6 class="fw-bold mb-3">Sentinel Sites & Influenza Subtypes</h6>
|
||||||
|
|
||||||
@@ -132,6 +138,9 @@
|
|||||||
<!-- SENTINEL PIE -->
|
<!-- SENTINEL PIE -->
|
||||||
<div class="col-md-6 d-flex flex-column">
|
<div class="col-md-6 d-flex flex-column">
|
||||||
<small class="text-muted mb-2">Cases by Sentinel Site</small>
|
<small class="text-muted mb-2">Cases by Sentinel Site</small>
|
||||||
|
<p class="text-muted small report-period">
|
||||||
|
(based on selected epiweek range)
|
||||||
|
</p>
|
||||||
<div style="height: 460px; position:relative;">
|
<div style="height: 460px; position:relative;">
|
||||||
<canvas id="sentinelChart"></canvas>
|
<canvas id="sentinelChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,6 +149,9 @@
|
|||||||
<!-- SUBTYPE -->
|
<!-- SUBTYPE -->
|
||||||
<div class="col-md-6 d-flex flex-column">
|
<div class="col-md-6 d-flex flex-column">
|
||||||
<small class="text-muted mb-2">Influenza Subtypes</small>
|
<small class="text-muted mb-2">Influenza Subtypes</small>
|
||||||
|
<p class="text-muted small report-period">
|
||||||
|
(based on selected epiweek range)
|
||||||
|
</p>
|
||||||
<div style="height: 460px; position:relative;">
|
<div style="height: 460px; position:relative;">
|
||||||
<canvas id="subtypeChart"></canvas>
|
<canvas id="subtypeChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,8 +168,11 @@
|
|||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<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:480px">
|
||||||
<h6 class="fw-bold mb-3">Sex Distribution</h6>
|
<h6 class="fw-bold mb-3">Sex Distribution</h6>
|
||||||
|
<p class="text-muted small report-period">
|
||||||
|
(based on selected epiweek range)
|
||||||
|
</p>
|
||||||
<div style="height:360px; position:relative;">
|
<div style="height:360px; position:relative;">
|
||||||
<canvas id="sexChart"></canvas>
|
<canvas id="sexChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,8 +182,11 @@
|
|||||||
|
|
||||||
<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:480px">
|
||||||
<h6 class="fw-bold mb-3">Age Distribution</h6>
|
<h6 class="fw-bold mb-3">Age Distribution</h6>
|
||||||
|
<p class="text-muted small report-period">
|
||||||
|
(based on selected epiweek range)
|
||||||
|
</p>
|
||||||
<div style="height:360px; position:relative;">
|
<div style="height:360px; position:relative;">
|
||||||
<canvas id="ageChart"></canvas>
|
<canvas id="ageChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/html-to-image@1.11.11/dist/html-to-image.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
||||||
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
|
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
|
||||||
|
|
||||||
@@ -20,7 +20,9 @@
|
|||||||
<script src="/js/dashboard/export.js"></script>
|
<script src="/js/dashboard/export.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body { margin: 0; }
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.top-navbar {
|
.top-navbar {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
@@ -69,7 +71,9 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item:hover { background: #cce0d4; }
|
.nav-item:hover {
|
||||||
|
background: #cce0d4;
|
||||||
|
}
|
||||||
|
|
||||||
.active-tab {
|
.active-tab {
|
||||||
color: #0B8F3C;
|
color: #0B8F3C;
|
||||||
@@ -88,13 +92,22 @@
|
|||||||
border: 1px solid #E5E7EB;
|
border: 1px solid #E5E7EB;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-select { border-radius: 0px !important; }
|
.form-select {
|
||||||
.shadow-sm { box-shadow: none !important; }
|
border-radius: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.card h3 { color: #0B8F3C; }
|
.shadow-sm {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* EXPORT */
|
.card h3 {
|
||||||
.export-control { position: relative; }
|
color: #0B8F3C;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.export-control {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
#exportItems {
|
#exportItems {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -118,7 +131,7 @@
|
|||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0,0,0,0.4);
|
background: rgba(0, 0, 0, 0.4);
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,14 +160,22 @@
|
|||||||
transition: all 0.5s ease-in-out;
|
transition: all 0.5s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide.active { left: 0; opacity: 1; z-index: 2; }
|
.slide.active {
|
||||||
.slide.prev { left: -100%; opacity: 0; }
|
left: 0;
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide.prev {
|
||||||
|
left: -100%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.slide-btn {
|
.slide-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10%;
|
top: 10%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
background: rgba(0,128,0,0.43);
|
background: rgba(0, 128, 0, 0.43);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 8px 15px;
|
padding: 8px 15px;
|
||||||
@@ -162,108 +183,123 @@
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prev-btn { right: 75px; }
|
.prev-btn {
|
||||||
.next-btn { right: 25px; }
|
right: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn {
|
||||||
|
right: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
.slide-btn:hover {
|
.slide-btn:hover {
|
||||||
background: rgba(7,120,24,0.8);
|
background: rgba(7, 120, 24, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
#floatingExport { display: none !important; }
|
#floatingExport {
|
||||||
.nav-bar, .top-navbar { display: none !important; }
|
display: none !important;
|
||||||
.card { page-break-inside: avoid; }
|
}
|
||||||
|
|
||||||
|
.nav-bar,
|
||||||
|
.top-navbar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="top-navbar">
|
<div class="top-navbar">
|
||||||
<div class="brand-title">
|
<div class="brand-title">
|
||||||
National Reference Medical Laboratory Surveillance Dashboard
|
National Reference Medical Laboratory Surveillance Dashboard
|
||||||
|
</div>
|
||||||
|
<div class="ms-auto small">
|
||||||
|
Last update: 12:05 | 2026-03-15
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-auto small">
|
|
||||||
Last update: 12:05 | 2026-03-15
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav-bar">
|
<div class="nav-bar">
|
||||||
|
|
||||||
<a href="/dashboard" class="nav-item {{ request()->is('dashboard') ? 'active-tab' : '' }}">
|
<a href="/dashboard" class="nav-item {{ request()->is('dashboard') ? 'active-tab' : '' }}">
|
||||||
Overview
|
Overview
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@foreach($programs as $program)
|
@foreach($programs as $program)
|
||||||
@if($program->code === 'SEQ')
|
@if($program->code === 'SEQ')
|
||||||
<a href="/dashboard/seq" class="nav-item {{ request()->is('dashboard/seq') ? 'active-tab' : '' }}">
|
<a href="/dashboard/seq" class="nav-item {{ request()->is('dashboard/seq') ? 'active-tab' : '' }}">
|
||||||
SEQ
|
SEQ
|
||||||
</a>
|
</a>
|
||||||
@else
|
@else
|
||||||
<a href="/dashboard/{{ strtolower($program->code) }}"
|
<a href="/dashboard/{{ strtolower($program->code) }}"
|
||||||
class="nav-item {{ request()->is('dashboard/' . strtolower($program->code)) ? 'active-tab' : '' }}">
|
class="nav-item {{ request()->is('dashboard/' . strtolower($program->code)) ? 'active-tab' : '' }}">
|
||||||
{{ $program->code }}
|
{{ $program->code }}
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
<div class="ms-auto d-flex align-items-center gap-2 pe-3">
|
<div class="ms-auto d-flex align-items-center gap-2 pe-3">
|
||||||
|
|
||||||
<button onclick="reloadDataSource()" class="btn btn-sm btn-theme-outline">
|
<button onclick="reloadDataSource()" class="btn btn-sm btn-theme-outline">
|
||||||
Refresh Data
|
Refresh Data
|
||||||
</button>
|
|
||||||
|
|
||||||
<div id="exportControl" class="d-flex align-items-center gap-2">
|
|
||||||
|
|
||||||
<button id="exportToggle" class="btn btn-sm btn-theme-outline">
|
|
||||||
Export ▸
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div id="exportItems" class="align-items-center gap-2">
|
<div id="exportControl" class="d-flex align-items-center gap-2">
|
||||||
<button class="btn btn-sm btn-light" onclick="openChartSelector()">Charts</button>
|
|
||||||
<button class="btn btn-sm btn-light" onclick="exportFullDashboard()">Screen</button>
|
|
||||||
<button class="btn btn-sm btn-light" onclick="window.print()">Print</button>
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" id="exportClose">✕</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="chartModal" class="export-modal">
|
<button id="exportToggle" class="btn btn-sm btn-theme-outline">
|
||||||
<div class="export-content">
|
Export ▸
|
||||||
<h5>Select Charts</h5>
|
</button>
|
||||||
<div id="chartList"></div>
|
|
||||||
|
|
||||||
<div class="mt-3 d-flex justify-content-end gap-2">
|
<div id="exportItems" class="align-items-center gap-2">
|
||||||
<button onclick="closeChartSelector()">Cancel</button>
|
<button class="btn btn-sm btn-light" onclick="openChartSelector()">Charts</button>
|
||||||
<button onclick="exportSelectedCharts()">Download PDF</button>
|
<button class="btn btn-sm btn-light" onclick="exportFullDashboard()">Screen</button>
|
||||||
|
<button class="btn btn-sm btn-light" onclick="window.print()">Print</button>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" id="exportClose">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="chartModal" class="export-modal">
|
||||||
|
<div class="export-content">
|
||||||
|
<h5>Select Charts</h5>
|
||||||
|
<div id="chartList"></div>
|
||||||
|
|
||||||
|
<div class="mt-3 d-flex justify-content-end gap-2">
|
||||||
|
<button onclick="closeChartSelector()">Cancel</button>
|
||||||
|
<button onclick="exportSelectedCharts()">Download PDF</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<div class="main-wrapper">
|
||||||
|
<div class="content-area">
|
||||||
<div class="main-wrapper">
|
@yield('content')
|
||||||
<div class="content-area">
|
</div>
|
||||||
@yield('content')
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
@yield('scripts')
|
@yield('scripts')
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener("click", (e) => {
|
window.addEventListener("click", (e) => {
|
||||||
const modal = document.getElementById("chartModal");
|
const modal = document.getElementById("chartModal");
|
||||||
if (e.target === modal) modal.style.display = "none";
|
if (e.target === modal) modal.style.display = "none";
|
||||||
});
|
});
|
||||||
|
|
||||||
function reloadDataSource() {
|
function reloadDataSource() {
|
||||||
fetch(`/api/dashboard/reload`)
|
fetch(`/api/dashboard/reload`)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(() => location.reload());
|
.then(() => location.reload());
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user