fix bugs
This commit is contained in:
@@ -234,27 +234,6 @@ class DashboardController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Sentinel Map
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
// public function sentinelMap(Request $request)
|
|
||||||
|
|
||||||
// {
|
|
||||||
// $range = $this->getEpiRange($request);
|
|
||||||
|
|
||||||
// if (!$range) {
|
|
||||||
// return response()->json(['error' => 'Missing epiweek range'], 400);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return response()->json($data);
|
|
||||||
// }
|
|
||||||
|
|
||||||
public function fetchSourceData()
|
public function fetchSourceData()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ class DashboardService
|
|||||||
|
|
||||||
public function summaryCards()
|
public function summaryCards()
|
||||||
{
|
{
|
||||||
$programs = Surveillance::orderBy('id')->get();
|
|
||||||
$results = [];
|
|
||||||
|
|
||||||
$today = date('Y-m-d');
|
$today = date('Y-m-d');
|
||||||
|
|
||||||
$currentFrom = date('Y-m-d', strtotime('-6 days'));
|
$currentFrom = date('Y-m-d', strtotime('-6 days'));
|
||||||
@@ -39,15 +36,23 @@ class DashboardService
|
|||||||
$prevFrom = date('Y-m-d', strtotime('-13 days'));
|
$prevFrom = date('Y-m-d', strtotime('-13 days'));
|
||||||
$prevTo = date('Y-m-d', strtotime('-7 days'));
|
$prevTo = date('Y-m-d', strtotime('-7 days'));
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
$programs = Surveillance::where('id', '!=', 6)
|
||||||
|
->orderBy('id')
|
||||||
|
->get();
|
||||||
|
|
||||||
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', [$currentFrom, $currentTo])
|
->whereBetween('case_date', [$currentFrom, $currentTo])
|
||||||
->count();
|
->distinct('lab_code')
|
||||||
|
->count('lab_code');
|
||||||
|
|
||||||
$previous = SurveillanceCase::where('surveillance_id', $program->id)
|
$previous = SurveillanceCase::where('surveillance_id', $program->id)
|
||||||
->whereBetween('case_date', [$prevFrom, $prevTo])
|
->whereBetween('case_date', [$prevFrom, $prevTo])
|
||||||
->count();
|
->distinct('lab_code')
|
||||||
|
->count('lab_code');
|
||||||
|
|
||||||
$percentChange = $previous > 0
|
$percentChange = $previous > 0
|
||||||
? round((($current - $previous) / $previous) * 100, 1)
|
? round((($current - $previous) / $previous) * 100, 1)
|
||||||
@@ -62,6 +67,44 @@ class DashboardService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| A/H5N1 Summary Card
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
$h5n1Current = SurveillanceCase::join('case_lab_results', function ($join) {
|
||||||
|
$join->on('surveillance_cases.lab_code', '=', 'case_lab_results.lab_code')
|
||||||
|
->on('surveillance_cases.surveillance_id', '=', 'case_lab_results.surveillance_id');
|
||||||
|
})
|
||||||
|
->whereBetween('surveillance_cases.case_date', [$currentFrom, $currentTo])
|
||||||
|
->where('case_lab_results.subtype', 'A/H5N1')
|
||||||
|
->where('surveillance_cases.surveillance_id', '!=', 6)
|
||||||
|
->distinct('surveillance_cases.lab_code')
|
||||||
|
->count('surveillance_cases.lab_code');
|
||||||
|
|
||||||
|
$h5n1Previous = SurveillanceCase::join('case_lab_results', function ($join) {
|
||||||
|
$join->on('surveillance_cases.lab_code', '=', 'case_lab_results.lab_code')
|
||||||
|
->on('surveillance_cases.surveillance_id', '=', 'case_lab_results.surveillance_id');
|
||||||
|
})
|
||||||
|
->whereBetween('surveillance_cases.case_date', [$prevFrom, $prevTo])
|
||||||
|
->where('case_lab_results.subtype', 'A/H5N1')
|
||||||
|
->where('surveillance_cases.surveillance_id', '!=', 6)
|
||||||
|
->distinct('surveillance_cases.lab_code')
|
||||||
|
->count('surveillance_cases.lab_code');
|
||||||
|
|
||||||
|
$h5n1Percent = $h5n1Previous > 0
|
||||||
|
? round((($h5n1Current - $h5n1Previous) / $h5n1Previous) * 100, 1)
|
||||||
|
: ($h5n1Current > 0 ? 100 : 0);
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'surveillance_id' => null,
|
||||||
|
'code' => 'H5N1',
|
||||||
|
'current_total' => $h5n1Current,
|
||||||
|
'previous_total' => $h5n1Previous,
|
||||||
|
'percent_change' => $h5n1Percent
|
||||||
|
];
|
||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,8 +144,8 @@ class DashboardService
|
|||||||
AND week_data >= ?
|
AND week_data >= ?
|
||||||
)
|
)
|
||||||
)",
|
)",
|
||||||
[$startYear, $startYear, $startWeek]
|
[$startYear, $startYear, $startWeek]
|
||||||
)
|
)
|
||||||
|
|
||||||
->whereRaw(
|
->whereRaw(
|
||||||
"(year_data < ?
|
"(year_data < ?
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'name' => env('APP_NAME', 'Laravel'),
|
'name' => env('APP_NAME', 'NRML Dashboard'),
|
||||||
|
|
||||||
'lookback_days' => [
|
'lookback_days' => [
|
||||||
'SARI' => 30,
|
'SARI' => 30,
|
||||||
@@ -61,7 +61,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'url' => env('APP_URL', 'http://localhost'),
|
'url' => env('APP_URL', 'http://localhost:8000'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
|
import * as Charts from "./globals.js";
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById('chartSelectorBtn')
|
||||||
|
?.addEventListener('click', openChartSelector);
|
||||||
const toggle = document.getElementById("exportToggle");
|
const toggle = document.getElementById("exportToggle");
|
||||||
const items = document.getElementById("exportItems");
|
const items = document.getElementById("exportItems");
|
||||||
const close = document.getElementById("exportClose");
|
const close = document.getElementById("exportClose");
|
||||||
|
|
||||||
|
|
||||||
if (!toggle || !items) return;
|
if (!toggle || !items) return;
|
||||||
|
|
||||||
toggle.addEventListener("click", () => {
|
toggle.addEventListener("click", () => {
|
||||||
@@ -20,12 +27,31 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
toggle.innerText = "Export ▸";
|
toggle.innerText = "Export ▸";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
document
|
||||||
|
.getElementById('btnCharts')
|
||||||
|
?.addEventListener('click', openChartSelector);
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById('btnExport')
|
||||||
|
?.addEventListener('click', exportFullDashboard);
|
||||||
|
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelector('#chartCancel')
|
||||||
|
?.addEventListener('click', closeChartSelector);
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelector('#chartDownload')
|
||||||
|
?.addEventListener('click', exportSelectedCharts);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function openChartSelector() {
|
function openChartSelector() {
|
||||||
|
|
||||||
if (typeof charts === "undefined" || Object.keys(charts).length === 0) {
|
if (typeof Charts.charts === "undefined" || Object.keys(Charts.charts).length === 0) {
|
||||||
alert("Charts are still loading. Please try again.");
|
alert("Charts are still loading. Please try again.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -35,7 +61,7 @@ function openChartSelector() {
|
|||||||
|
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
|
|
||||||
Object.keys(charts).forEach(id => {
|
Object.keys(Charts.charts).forEach(id => {
|
||||||
list.innerHTML += `
|
list.innerHTML += `
|
||||||
<label style="display:block;margin-bottom:6px;">
|
<label style="display:block;margin-bottom:6px;">
|
||||||
<input type="checkbox" value="${id}" checked>
|
<input type="checkbox" value="${id}" checked>
|
||||||
@@ -99,7 +125,7 @@ async function exportSelectedCharts() {
|
|||||||
left: "50%",
|
left: "50%",
|
||||||
transform: "translate(-50%, -50%)",
|
transform: "translate(-50%, -50%)",
|
||||||
background: "rgb(255, 255, 255)",
|
background: "rgb(255, 255, 255)",
|
||||||
color: "#fff",
|
color: "#000000",
|
||||||
padding: "20px",
|
padding: "20px",
|
||||||
borderRadius: "10px",
|
borderRadius: "10px",
|
||||||
zIndex: "10000"
|
zIndex: "10000"
|
||||||
@@ -141,7 +167,7 @@ async function exportSelectedCharts() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const chart = charts[cb.value];
|
const chart = Charts.charts[cb.value];
|
||||||
if (!chart) continue;
|
if (!chart) continue;
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
@@ -185,7 +211,7 @@ async function exportSelectedCharts() {
|
|||||||
if (item.type === "map") {
|
if (item.type === "map") {
|
||||||
img = item.img;
|
img = item.img;
|
||||||
|
|
||||||
width = cardWidth - 50;
|
width = cardWidth - 60;
|
||||||
height = width * 0.65;
|
height = width * 0.65;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -352,12 +378,7 @@ async function getMapImage() {
|
|||||||
totals[province].positive += Number(r.positive);
|
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
window.map.eachLayer(layer => {
|
window.map.eachLayer(layer => {
|
||||||
if (!layer.toGeoJSON) return;
|
if (!layer.toGeoJSON) return;
|
||||||
@@ -478,9 +499,7 @@ async function getMapImage() {
|
|||||||
ctx.fillStyle = item.fillColor;
|
ctx.fillStyle = item.fillColor;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
ctx.strokeStyle = item.strokeColor;
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Chart.register({
|
|||||||
});
|
});
|
||||||
Chart.register(ChartDataLabels);
|
Chart.register(ChartDataLabels);
|
||||||
Chart.defaults.devicePixelRatio = 2;
|
Chart.defaults.devicePixelRatio = 2;
|
||||||
const charts = {};
|
export const charts = {};
|
||||||
|
|
||||||
|
|
||||||
function buildStackedChart(canvasId, labels, data) {
|
function buildStackedChart(canvasId, labels, data) {
|
||||||
@@ -147,7 +147,7 @@ function buildStackedChart(canvasId, labels, data) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function buildChart(id, type, labels, data) {
|
export function buildChart(id, type, labels, data) {
|
||||||
|
|
||||||
const ctx = document.getElementById(id);
|
const ctx = document.getElementById(id);
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
@@ -160,7 +160,7 @@ function buildChart(id, type, labels, data) {
|
|||||||
labels = [];
|
labels = [];
|
||||||
data = [];
|
data = [];
|
||||||
}
|
}
|
||||||
const isHorizontal = id === 'sexChart';
|
const isHorizontal = id === 'sexChart' || id === 'influenzaSubtypeDistribution';
|
||||||
const isAgeChart = id === 'ageChart';
|
const isAgeChart = id === 'ageChart';
|
||||||
const isSentinelChart = id === 'sentinelChart';
|
const isSentinelChart = id === 'sentinelChart';
|
||||||
const options = {
|
const options = {
|
||||||
@@ -292,7 +292,7 @@ function buildChart(id, type, labels, data) {
|
|||||||
});
|
});
|
||||||
charts[id].$totalTested = 0;
|
charts[id].$totalTested = 0;
|
||||||
}
|
}
|
||||||
function buildMixedTrendChart(canvasId, labels, samples, lines) {
|
export function buildMixedTrendChart(canvasId, labels, samples, lines) {
|
||||||
|
|
||||||
const ctx = document.getElementById(canvasId);
|
const ctx = document.getElementById(canvasId);
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
@@ -346,3 +346,160 @@ function buildMixedTrendChart(canvasId, labels, samples, lines) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildDistributionChart(
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
rows,
|
||||||
|
labelKey,
|
||||||
|
valueKey = 'total',
|
||||||
|
colorResolver = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
const labels = rows.map(r => r[labelKey]);
|
||||||
|
buildChart(
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
labels,
|
||||||
|
rows.map(r => r[valueKey])
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!charts[id]) return;
|
||||||
|
|
||||||
|
charts[id].data.datasets[0].backgroundColor = labels.map(
|
||||||
|
(label, index) =>
|
||||||
|
colorResolver
|
||||||
|
? colorResolver(label, index)
|
||||||
|
: COLORS[index % COLORS.length]
|
||||||
|
);
|
||||||
|
|
||||||
|
charts[id].update();
|
||||||
|
}
|
||||||
|
export function getSubtypeColor(label, index = 0) {
|
||||||
|
|
||||||
|
const specialColors = {
|
||||||
|
'A/H5N1': '#dc2626',
|
||||||
|
'Influenza': '#b90c00'
|
||||||
|
};
|
||||||
|
|
||||||
|
return specialColors[label]
|
||||||
|
|| COLORS[index % COLORS.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const COLORS = [
|
||||||
|
|
||||||
|
'#ef4444', // blue
|
||||||
|
'#10b981', // emerald
|
||||||
|
'#f59e0b', // amber
|
||||||
|
'#ef4444', // red
|
||||||
|
'#8b5cf6', // violet
|
||||||
|
'#14b8a6', // teal
|
||||||
|
'#f97316', // orange
|
||||||
|
'#84cc16', // lime
|
||||||
|
'#e11dba', // fuchsia
|
||||||
|
'#f6f63b', // yellow
|
||||||
|
|
||||||
|
'#0ea5e9', // sky
|
||||||
|
'#22c55e', // green
|
||||||
|
'#a855f7', // purple
|
||||||
|
'#ec4899', // pink
|
||||||
|
'#06b6d4', // cyan
|
||||||
|
'#65a30d', // olive
|
||||||
|
'#dc2626', // dark red
|
||||||
|
'#1d4ed8', // strong blue
|
||||||
|
'#7c3aed', // deep violet
|
||||||
|
'#059669', // dark emerald
|
||||||
|
'#c2410c', // burnt orange
|
||||||
|
'#be123c', // rose
|
||||||
|
'#4338ca', // indigo
|
||||||
|
'#0f766e', // dark teal
|
||||||
|
'#9333ea', // bright purple
|
||||||
|
'#15803d', // forest green
|
||||||
|
'#ea580c', // deep orange
|
||||||
|
'#0284c7', // ocean blue
|
||||||
|
'#ca8a04', // mustard
|
||||||
|
'#db2777' // magenta
|
||||||
|
];
|
||||||
|
export const SUBTYPE_COLORS = {
|
||||||
|
'A/H1N1pdm': '#f0d401',
|
||||||
|
'A/H3N2': '#00ffff',
|
||||||
|
'A/H9N2': '#2563eb',
|
||||||
|
'A/H5N1': '#dc2626',
|
||||||
|
'A/Unsubtypable': '#f455d7',
|
||||||
|
'B/Yam': '#9333ea',
|
||||||
|
'B/Vic': '#086037',
|
||||||
|
'B/Unsubtypable': '#66ff00',
|
||||||
|
'B/Victoria': '#9333ea',
|
||||||
|
'H1N1pdm': '#f0d401',
|
||||||
|
'H3N2': '#00ffff',
|
||||||
|
'H9N2': '#2563eb',
|
||||||
|
'J.2.4': '#8c6060',
|
||||||
|
'K': '#55f49a',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SURVEILLANCE_COLORS = {
|
||||||
|
'LBM': '#f0d401',
|
||||||
|
'ILI': '#2563eb',
|
||||||
|
'SARI': '#dc2626',
|
||||||
|
'NDS': '#9333ea',
|
||||||
|
'AFI': '#086037',
|
||||||
|
'SEQ': '#66ff00'
|
||||||
|
};
|
||||||
|
export function buildPeriodLabels(data, startYear, endYear) {
|
||||||
|
|
||||||
|
const yearlyView = (endYear - startYear) >= 5;
|
||||||
|
|
||||||
|
if (yearlyView) {
|
||||||
|
|
||||||
|
const labels = [...new Set(
|
||||||
|
data.flatMap(program =>
|
||||||
|
program.map(row => row.year)
|
||||||
|
)
|
||||||
|
)].sort((a, b) => a - b);
|
||||||
|
|
||||||
|
return {
|
||||||
|
yearlyView,
|
||||||
|
labels
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelsSet = new Set();
|
||||||
|
|
||||||
|
data.forEach(program => {
|
||||||
|
program.forEach(row => {
|
||||||
|
labelsSet.add(`${row.year}-${row.period}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
yearlyView,
|
||||||
|
labels: [...labelsSet].sort((a, b) => {
|
||||||
|
|
||||||
|
const [yearA, weekA] = a.split('-').map(Number);
|
||||||
|
const [yearB, weekB] = b.split('-').map(Number);
|
||||||
|
|
||||||
|
if (yearA !== yearB) {
|
||||||
|
return yearA - yearB;
|
||||||
|
}
|
||||||
|
|
||||||
|
return weekA - weekB;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function buildPeriods(data, field, startYear, endYear) {
|
||||||
|
|
||||||
|
const yearlyView = (endYear - startYear) >= 5;
|
||||||
|
|
||||||
|
const periods = yearlyView
|
||||||
|
? [...new Set(
|
||||||
|
data.map(row => row[field].split('-')[0])
|
||||||
|
)].sort((a, b) => a - b)
|
||||||
|
: [...new Set(
|
||||||
|
data.map(row => row[field])
|
||||||
|
)].sort();
|
||||||
|
|
||||||
|
return {
|
||||||
|
yearlyView,
|
||||||
|
periods
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
export const COLORS = [
|
|
||||||
|
|
||||||
'#ef4444', // blue
|
|
||||||
'#10b981', // emerald
|
|
||||||
'#f59e0b', // amber
|
|
||||||
'#ef4444', // red
|
|
||||||
'#8b5cf6', // violet
|
|
||||||
'#14b8a6', // teal
|
|
||||||
'#f97316', // orange
|
|
||||||
'#84cc16', // lime
|
|
||||||
'#e11dba', // fuchsia
|
|
||||||
'#f6f63b', // yellow
|
|
||||||
|
|
||||||
'#0ea5e9', // sky
|
|
||||||
'#22c55e', // green
|
|
||||||
'#a855f7', // purple
|
|
||||||
'#ec4899', // pink
|
|
||||||
'#06b6d4', // cyan
|
|
||||||
'#65a30d', // olive
|
|
||||||
'#dc2626', // dark red
|
|
||||||
'#1d4ed8', // strong blue
|
|
||||||
'#7c3aed', // deep violet
|
|
||||||
'#059669', // dark emerald
|
|
||||||
'#c2410c', // burnt orange
|
|
||||||
'#be123c', // rose
|
|
||||||
'#4338ca', // indigo
|
|
||||||
'#0f766e', // dark teal
|
|
||||||
'#9333ea', // bright purple
|
|
||||||
'#15803d', // forest green
|
|
||||||
'#ea580c', // deep orange
|
|
||||||
'#0284c7', // ocean blue
|
|
||||||
'#ca8a04', // mustard
|
|
||||||
'#db2777' // magenta
|
|
||||||
];
|
|
||||||
export const SUBTYPE_COLORS = {
|
|
||||||
'A/H1N1pdm': '#f0d401',
|
|
||||||
'A/H3N2': '#00ffff',
|
|
||||||
'A/H9N2': '#2563eb',
|
|
||||||
'A/H5N1': '#dc2626',
|
|
||||||
'A/Unsubtypable': '#f455d7',
|
|
||||||
'B/Yam': '#9333ea',
|
|
||||||
'B/Vic': '#086037',
|
|
||||||
'B/Unsubtypable': '#66ff00',
|
|
||||||
'B/Victoria': '#9333ea',
|
|
||||||
'H1N1pdm': '#f0d401',
|
|
||||||
'H3N2': '#00ffff',
|
|
||||||
'H9N2': '#2563eb',
|
|
||||||
'J.2.4': '#8c6060',
|
|
||||||
'K': '#55f49a',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SURVEILLANCE_COLORS = {
|
|
||||||
'LBM': '#f0d401',
|
|
||||||
'ILI': '#2563eb',
|
|
||||||
'SARI': '#dc2626',
|
|
||||||
'NDS': '#9333ea',
|
|
||||||
'AFI': '#086037',
|
|
||||||
'SEQ': '#66ff00'
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { COLORS, SUBTYPE_COLORS, SURVEILLANCE_COLORS } from "./globals.js";
|
import * as Charts from "./dashboard/globals.js";
|
||||||
|
|
||||||
let trendChart;
|
let trendChart;
|
||||||
let influenzaSubtypeChart;
|
|
||||||
let covidDistributedByAgeChart;
|
let covidDistributedByAgeChart;
|
||||||
let covidLineageFrequencyChart;
|
let covidLineageFrequencyChart;
|
||||||
let influenzaSubtypeFrequencyChart;
|
let influenzaSubtypeFrequencyChart;
|
||||||
@@ -31,38 +29,46 @@ function loadSummary() {
|
|||||||
if (item.percent_change > 0) trendColor = 'text-danger';
|
if (item.percent_change > 0) trendColor = 'text-danger';
|
||||||
if (item.percent_change < 0) trendColor = 'text-success';
|
if (item.percent_change < 0) trendColor = 'text-success';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="col-md-2 mb-3">
|
<div class="col-md-2 mb-3">
|
||||||
<div class="card shadow-sm h-100">
|
<div class="card shadow-sm h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h6 class="fw-bold">${item.code} Cases</h6>
|
<h6 class="fw-bold">
|
||||||
<h3 class="mb-1">${item.current_total}</h3>
|
${item.code === 'H5N1'
|
||||||
<small class="text-muted">Last 7 days</small>
|
? 'A/H5N1 Cases'
|
||||||
</div>
|
: `${item.code} Cases`}
|
||||||
|
</h6>
|
||||||
<div class="text-end">
|
<h3 class="mb-1">${item.current_total}</h3>
|
||||||
|
<small class="text-muted">Last 7 days</small>
|
||||||
<div class="${trendColor} fw-bold">
|
|
||||||
${item.percent_change > 0 ? '▲' : item.percent_change < 0 ? '▼' : '–'}
|
|
||||||
${Math.abs(item.percent_change)}%
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small class="text-muted">
|
<div class="text-end">
|
||||||
${item.previous_total ?? 0} last week
|
|
||||||
</small>
|
<div class="${trendColor} fw-bold">
|
||||||
|
${item.percent_change > 0 ? '▲' : item.percent_change < 0 ? '▼' : '–'}
|
||||||
|
${Math.abs(item.percent_change)}%
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<small class="text-muted">
|
||||||
|
${item.previous_total ?? 0} last week
|
||||||
|
</small>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
`;
|
||||||
`;
|
|
||||||
|
|
||||||
window._summaryData = data;
|
window._summaryData = data;
|
||||||
updateAlerts();
|
updateAlerts();
|
||||||
@@ -89,58 +95,15 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
|
|
||||||
if (trendChart) trendChart.destroy();
|
if (trendChart) trendChart.destroy();
|
||||||
const totalYears = endYear - startYear;
|
|
||||||
const useYearlyView = totalYears >= 5;
|
|
||||||
|
|
||||||
// const labelsSet = new Set();
|
const {
|
||||||
|
yearlyView: useYearlyView,
|
||||||
// Object.values(data).forEach(program => {
|
labels
|
||||||
// program.forEach(row => {
|
} = Charts.buildPeriodLabels(
|
||||||
// labelsSet.add(`${row.year}-${row.period}`);
|
Object.values(data),
|
||||||
// });
|
startYear,
|
||||||
// });
|
endYear
|
||||||
|
);
|
||||||
// const labels = Array.from(labelsSet).sort((a, b) => {
|
|
||||||
|
|
||||||
// const [yearA, weekA] = a.split('-').map(Number);
|
|
||||||
// const [yearB, weekB] = b.split('-').map(Number);
|
|
||||||
|
|
||||||
// if (yearA !== yearB) return yearA - yearB;
|
|
||||||
// return weekA - weekB;
|
|
||||||
|
|
||||||
// });
|
|
||||||
let labels = [];
|
|
||||||
|
|
||||||
if (useYearlyView) {
|
|
||||||
|
|
||||||
labels = [...new Set(
|
|
||||||
Object.values(data)
|
|
||||||
.flat()
|
|
||||||
.map(row => row.year)
|
|
||||||
)].sort((a, b) => a - b);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
const labelsSet = new Set();
|
|
||||||
|
|
||||||
Object.values(data).forEach(program => {
|
|
||||||
program.forEach(row => {
|
|
||||||
labelsSet.add(`${row.year}-${row.period}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
labels = Array.from(labelsSet).sort((a, b) => {
|
|
||||||
|
|
||||||
const [yearA, weekA] = a.split('-').map(Number);
|
|
||||||
const [yearB, weekB] = b.split('-').map(Number);
|
|
||||||
|
|
||||||
if (yearA !== yearB) return yearA - yearB;
|
|
||||||
|
|
||||||
return weekA - weekB;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const datasets = [];
|
const datasets = [];
|
||||||
@@ -151,17 +114,14 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
|
|||||||
|
|
||||||
if (!allowedPrograms.includes(code)) return;
|
if (!allowedPrograms.includes(code)) return;
|
||||||
|
|
||||||
// const values = labels.map(label => {
|
|
||||||
// const found = data[code].find(row => `${row.year}-${row.period}` === label);
|
|
||||||
// return found ? found.total : 0;
|
|
||||||
// });
|
|
||||||
const values = labels.map(label => {
|
const values = labels.map(label => {
|
||||||
|
|
||||||
if (useYearlyView) {
|
if (useYearlyView) {
|
||||||
|
|
||||||
return data[code]
|
return data[code]
|
||||||
.filter(row => row.year == label)
|
.filter(row => String(row.year) === String(label))
|
||||||
.reduce((sum, row) => sum + row.total, 0);
|
.reduce((sum, row) => sum + Number(row.total || 0), 0);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,8 +136,8 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
|
|||||||
datasets.push({
|
datasets.push({
|
||||||
label: code,
|
label: code,
|
||||||
data: values,
|
data: values,
|
||||||
borderColor: SURVEILLANCE_COLORS[code],
|
borderColor: Charts.SURVEILLANCE_COLORS[code],
|
||||||
backgroundColor: SURVEILLANCE_COLORS[code],
|
backgroundColor: Charts.SURVEILLANCE_COLORS[code],
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
pointRadius: 4,
|
pointRadius: 4,
|
||||||
maxBarThickness: 50,
|
maxBarThickness: 50,
|
||||||
@@ -201,6 +161,7 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
|
|||||||
return `${year}-W${String(week).padStart(2, '0')}`;
|
return `${year}-W${String(week).padStart(2, '0')}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
trendChart = new Chart(document.getElementById('trendChart'), {
|
trendChart = new Chart(document.getElementById('trendChart'), {
|
||||||
|
|
||||||
type: 'line',
|
type: 'line',
|
||||||
@@ -244,7 +205,7 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
charts['trendChart'] = trendChart;
|
Charts.charts['trendChart'] = trendChart;
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -254,88 +215,19 @@ function loadInfluenzaSubtypeDistribution(periodType, startYear, startWeek, endY
|
|||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
|
||||||
let displayLabels = data.map(item => item.subtype);
|
const displayData = data.map(item => ({
|
||||||
let dataset = data.map(item => item.total);
|
...item,
|
||||||
|
subtype: item.subtype === "B/VIC" ? "B/Vic" : item.subtype
|
||||||
|
}));
|
||||||
|
|
||||||
// const colors = displayLabels.map(
|
Charts.buildDistributionChart(
|
||||||
// label => SUBTYPE_COLORS[label] || '#9ca3af'
|
'influenzaSubtypeDistribution',
|
||||||
// );
|
'bar',
|
||||||
|
displayData,
|
||||||
if (influenzaSubtypeChart) influenzaSubtypeChart.destroy();
|
'subtype',
|
||||||
influenzaSubtypeChart = new Chart(document.getElementById('influenzaSubtypeDistribution'), {
|
'total',
|
||||||
type: 'bar',
|
label => Charts.SUBTYPE_COLORS[label] || '#9ca3af'
|
||||||
data: {
|
);
|
||||||
labels: displayLabels,
|
|
||||||
datasets: [{
|
|
||||||
data: dataset,
|
|
||||||
backgroundColor: displayLabels.map(
|
|
||||||
label => SUBTYPE_COLORS[label] || '#9ca3af'
|
|
||||||
),
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
layout: {
|
|
||||||
padding: {
|
|
||||||
top: 20,
|
|
||||||
right: 30,
|
|
||||||
bottom: 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
indexAxis: 'y',
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: false,
|
|
||||||
position: 'right'
|
|
||||||
},
|
|
||||||
datalabels: {
|
|
||||||
color: '#000000',
|
|
||||||
borderRadius: 6,
|
|
||||||
z: 1000,
|
|
||||||
padding: {
|
|
||||||
top: 6,
|
|
||||||
bottom: 6,
|
|
||||||
},
|
|
||||||
|
|
||||||
font: {
|
|
||||||
weight: 'bold',
|
|
||||||
size: 12
|
|
||||||
},
|
|
||||||
formatter: (value) => value,
|
|
||||||
anchor: 'end',
|
|
||||||
align: 'end',
|
|
||||||
offset: 4,
|
|
||||||
clamp: true,
|
|
||||||
clip: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
stacked: true,
|
|
||||||
beginAtZero: true,
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Number of Positive Influenza Subtypes'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
stacked: true,
|
|
||||||
beginAtZero: true,
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Influenza Subtypes'
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
display: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [ChartDataLabels]
|
|
||||||
});
|
|
||||||
charts['influenzaSubtypeDistribution'] = influenzaSubtypeChart;
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -344,83 +236,7 @@ function loadCovidDistributedByAgeGroup(periodType, startYear, startWeek, endYea
|
|||||||
fetch(`/api/dashboard/covid-distributed-by-age-group?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
|
fetch(`/api/dashboard/covid-distributed-by-age-group?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
Charts.buildDistributionChart('covidDistributedByAgeGroup', 'bar', data, 'age_group');
|
||||||
let displayLabels = data.map(item => item.age_group);
|
|
||||||
let dataset = data.map(item => item.total);
|
|
||||||
|
|
||||||
|
|
||||||
if (covidDistributedByAgeChart) covidDistributedByAgeChart.destroy();
|
|
||||||
covidDistributedByAgeChart = new Chart(document.getElementById('covidDistributedByAgeGroup'), {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
labels: displayLabels,
|
|
||||||
datasets: [{
|
|
||||||
label: 'Total Covid-19 Detected',
|
|
||||||
data: dataset,
|
|
||||||
backgroundColor: COLORS,
|
|
||||||
maxBarThickness: 50
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
layout: {
|
|
||||||
padding: {
|
|
||||||
top: 50,
|
|
||||||
bottom: 10,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: false,
|
|
||||||
position: 'bottom'
|
|
||||||
},
|
|
||||||
datalabels: {
|
|
||||||
color: '#000',
|
|
||||||
borderRadius: 6,
|
|
||||||
z: 1000,
|
|
||||||
padding: {
|
|
||||||
top: 6,
|
|
||||||
bottom: 6,
|
|
||||||
left: 10,
|
|
||||||
right: 10
|
|
||||||
},
|
|
||||||
font: {
|
|
||||||
weight: 'bold',
|
|
||||||
size: 12
|
|
||||||
},
|
|
||||||
formatter: (value) => value,
|
|
||||||
anchor: 'end',
|
|
||||||
align: 'end',
|
|
||||||
offset: 4,
|
|
||||||
clamp: true,
|
|
||||||
clip: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
stacked: true,
|
|
||||||
beginAtZero: true,
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Patient Age Group'
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
display: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
stacked: true,
|
|
||||||
beginAtZero: true,
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Number of Positive SARS-CoV-2'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
charts['covidDistributedByAgeGroup'] = covidDistributedByAgeChart;
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -430,36 +246,23 @@ function loadCovidLineageFrequency(periodType, startYear, startWeek, endYear, en
|
|||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
|
||||||
// const weeks = [...new Set(data.map(item => item.week))].sort();
|
const {
|
||||||
const totalYears = endYear - startYear;
|
yearlyView: useYearlyView,
|
||||||
const useYearlyView = totalYears >= 5;
|
periods
|
||||||
|
} = Charts.buildPeriods(
|
||||||
|
data,
|
||||||
|
'week',
|
||||||
|
startYear,
|
||||||
|
endYear
|
||||||
|
);
|
||||||
|
|
||||||
let periods;
|
|
||||||
|
|
||||||
if (useYearlyView) {
|
|
||||||
|
|
||||||
periods = [...new Set(
|
|
||||||
data.map(item => item.week.split('-')[0])
|
|
||||||
)].sort((a, b) => a - b);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
periods = [...new Set(
|
|
||||||
data.map(item => item.week)
|
|
||||||
)].sort();
|
|
||||||
|
|
||||||
}
|
|
||||||
const lineages = [...new Set(data.map(item => item.lineage))];
|
const lineages = [...new Set(data.map(item => item.lineage))];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const datasets = lineages.map((lineage, index) => {
|
const datasets = lineages.map((lineage, index) => {
|
||||||
// const lineageData = weeks.map(week => {
|
|
||||||
// const found = data.find(
|
|
||||||
// item => item.week === week && item.lineage === lineage
|
|
||||||
// );
|
|
||||||
// return found ? found.total : 0;
|
|
||||||
// });
|
|
||||||
|
|
||||||
const lineageData = periods.map(period => {
|
const lineageData = periods.map(period => {
|
||||||
|
|
||||||
@@ -492,7 +295,7 @@ function loadCovidLineageFrequency(periodType, startYear, startWeek, endYear, en
|
|||||||
// borderColor: 'transparent',
|
// borderColor: 'transparent',
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
backgroundColor: hexToRGBA(colors[index % colors.length], 0.3),
|
backgroundColor: hexToRGBA(Charts.COLORS[index % Charts.COLORS.length], 0.3),
|
||||||
stack: 'total'
|
stack: 'total'
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -545,11 +348,8 @@ function loadCovidLineageFrequency(periodType, startYear, startWeek, endYear, en
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
charts['covidLineageFrequency'] = covidLineageFrequencyChart;
|
Charts.charts['covidLineageFrequency'] = covidLineageFrequencyChart;
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// Custom right-side scrollable legend
|
|
||||||
// -------------------------
|
|
||||||
const legendContainer = document.getElementById('legendContainer');
|
const legendContainer = document.getElementById('legendContainer');
|
||||||
legendContainer.innerHTML = '';
|
legendContainer.innerHTML = '';
|
||||||
datasets.forEach((dataset, index) => {
|
datasets.forEach((dataset, index) => {
|
||||||
@@ -602,31 +402,22 @@ function loadInfluenzaSubtypeFrequency(periodType, startYear, startWeek, endYear
|
|||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
|
||||||
const totalYears = endYear - startYear;
|
const {
|
||||||
const useYearlyView = totalYears >= 5;
|
yearlyView: useYearlyView,
|
||||||
|
periods
|
||||||
let periods;
|
} = Charts.buildPeriods(
|
||||||
|
data,
|
||||||
if (useYearlyView) {
|
'week',
|
||||||
|
startYear,
|
||||||
periods = [...new Set(
|
endYear
|
||||||
data.map(item => item.week.split('-')[0])
|
);
|
||||||
)].sort((a, b) => a - b);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
periods = [...new Set(
|
|
||||||
data.map(item => item.week)
|
|
||||||
)].sort();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const lineages = [...new Set(
|
const lineages = [...new Set(
|
||||||
data.map(item => item.lineage)
|
data.map(item => item.lineage === 'B/VIC' ? 'B/Vic' : item.lineage)
|
||||||
)];
|
)];
|
||||||
|
|
||||||
const lineageColors = lineages.map(
|
const lineageColors = lineages.map(
|
||||||
label => SUBTYPE_COLORS[label] || '#9ca3af'
|
label => Charts.SUBTYPE_COLORS[label] || '#9ca3af'
|
||||||
);
|
);
|
||||||
|
|
||||||
const datasets = lineages.map((lineage, index) => {
|
const datasets = lineages.map((lineage, index) => {
|
||||||
@@ -747,7 +538,7 @@ function loadInfluenzaSubtypeFrequency(periodType, startYear, startWeek, endYear
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
charts['influenzaSubtypeFrequency'] =
|
Charts.charts['influenzaSubtypeFrequency'] =
|
||||||
influenzaSubtypeFrequencyChart;
|
influenzaSubtypeFrequencyChart;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -857,9 +648,9 @@ function hexToRGBA(hex, alpha) {
|
|||||||
}
|
}
|
||||||
function updateAlerts() {
|
function updateAlerts() {
|
||||||
|
|
||||||
if (!window._summaryData || !window._provinceData) return;
|
if (!window._summaryData || !window.latestProvinceData) return;
|
||||||
|
|
||||||
const raw = buildAlerts(window._summaryData, window._provinceData);
|
const raw = buildAlerts(window._summaryData, window.latestProvinceData);
|
||||||
const finalAlerts = processAlerts(raw);
|
const finalAlerts = processAlerts(raw);
|
||||||
|
|
||||||
renderAlerts(finalAlerts);
|
renderAlerts(finalAlerts);
|
||||||
@@ -1088,6 +879,7 @@ function normalizeProvince(name, validSet) {
|
|||||||
|
|
||||||
return match || null;
|
return match || null;
|
||||||
}
|
}
|
||||||
|
window.normalizeProvince = normalizeProvince;
|
||||||
function getRadius(total) {
|
function getRadius(total) {
|
||||||
if (!total) return 0;
|
if (!total) return 0;
|
||||||
const r = Math.sqrt(total);
|
const r = Math.sqrt(total);
|
||||||
@@ -1095,10 +887,10 @@ function getRadius(total) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPositivityColor(subtype) {
|
function getPositivityColor(subtype) {
|
||||||
return SUBTYPE_COLORS[subtype] || "#9ca3af";
|
return Charts.SUBTYPE_COLORS[subtype] || "#9ca3af";
|
||||||
}
|
}
|
||||||
const getColorByPathogen = name =>
|
const getColorByPathogen = name =>
|
||||||
SUBTYPE_COLORS[name] || '#9ca3af';
|
Charts.SUBTYPE_COLORS[name] || '#9ca3af';
|
||||||
|
|
||||||
function addPositivityLegend() {
|
function addPositivityLegend() {
|
||||||
|
|
||||||
@@ -1157,7 +949,7 @@ function addPositivityLegend() {
|
|||||||
>
|
>
|
||||||
|
|
||||||
<span style="
|
<span style="
|
||||||
background:${SUBTYPE_COLORS[item]};
|
background:${Charts.SUBTYPE_COLORS[item]};
|
||||||
width:10px;
|
width:10px;
|
||||||
height:10px;
|
height:10px;
|
||||||
display:inline-block;
|
display:inline-block;
|
||||||
@@ -1237,7 +1029,7 @@ function loadProvinceMap(
|
|||||||
map = L.map('provinceMap')
|
map = L.map('provinceMap')
|
||||||
.setView([12.7, 104.9], 7);
|
.setView([12.7, 104.9], 7);
|
||||||
|
|
||||||
|
window.map = map;
|
||||||
|
|
||||||
L.tileLayer(
|
L.tileLayer(
|
||||||
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
@@ -1258,7 +1050,7 @@ function loadProvinceMap(
|
|||||||
])
|
])
|
||||||
.then(([geojson, data]) => {
|
.then(([geojson, data]) => {
|
||||||
|
|
||||||
window._provinceData = data;
|
window.latestProvinceData = data;
|
||||||
|
|
||||||
updateAlerts();
|
updateAlerts();
|
||||||
|
|
||||||
@@ -1268,7 +1060,7 @@ function loadProvinceMap(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
Object.keys(SUBTYPE_COLORS)
|
Object.keys(Charts.SUBTYPE_COLORS)
|
||||||
.forEach(subtype => {
|
.forEach(subtype => {
|
||||||
|
|
||||||
subtypeLayers[subtype] =
|
subtypeLayers[subtype] =
|
||||||
@@ -1331,7 +1123,7 @@ function loadProvinceMap(
|
|||||||
">
|
">
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
${r.pathogen_name}
|
${r.pathogen_name === 'B/VIC' ? 'B/Vic' : r.pathogen_name}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span style="font-weight:600">
|
<span style="font-weight:600">
|
||||||
@@ -1411,17 +1203,17 @@ function loadProvinceMap(
|
|||||||
{
|
{
|
||||||
radius: getRadius(row.total),
|
radius: getRadius(row.total),
|
||||||
fillColor: getColorByPathogen(
|
fillColor: getColorByPathogen(
|
||||||
row.pathogen_name
|
row.pathogen_name === 'B/VIC' ? 'B/Vic' : row.pathogen_name
|
||||||
),
|
),
|
||||||
fillOpacity: 0.85,
|
fillOpacity: 0.85,
|
||||||
stroke: false
|
stroke: false
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (subtypeLayers[row.pathogen_name]) {
|
if (subtypeLayers[row.pathogen_name === 'B/VIC' ? 'B/Vic' : row.pathogen_name]) {
|
||||||
marker.addTo(
|
marker.addTo(
|
||||||
subtypeLayers[
|
subtypeLayers[
|
||||||
row.pathogen_name
|
row.pathogen_name === 'B/VIC' ? 'B/Vic' : row.pathogen_name
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { COLORS, SUBTYPE_COLORS } from "./globals.js";
|
import * as Charts from "./dashboard/globals.js";
|
||||||
const standardPrograms = ['SARI', 'ILI', 'LBM', 'AFI', 'NDS'];
|
const standardPrograms = ['SARI', 'ILI', 'LBM', 'AFI', 'NDS'];
|
||||||
const programCode = (window.PROGRAM_CODE || '').trim().toUpperCase();
|
const programCode = (window.PROGRAM_CODE || '').trim().toUpperCase();
|
||||||
|
|
||||||
@@ -31,34 +31,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
function buildDistributionChart(
|
|
||||||
id,
|
|
||||||
type,
|
|
||||||
rows,
|
|
||||||
labelKey,
|
|
||||||
valueKey = 'total',
|
|
||||||
colorResolver = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
const labels = rows.map(r => r[labelKey]);
|
|
||||||
buildChart(
|
|
||||||
id,
|
|
||||||
type,
|
|
||||||
labels,
|
|
||||||
rows.map(r => r[valueKey])
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!charts[id]) return;
|
|
||||||
|
|
||||||
charts[id].data.datasets[0].backgroundColor = labels.map(
|
|
||||||
(label, index) =>
|
|
||||||
colorResolver
|
|
||||||
? colorResolver(label, index)
|
|
||||||
: COLORS[index % COLORS.length]
|
|
||||||
);
|
|
||||||
|
|
||||||
charts[id].update();
|
|
||||||
}
|
|
||||||
function renderTrend(valueId, changeId, current, previous, suffix = '') {
|
function renderTrend(valueId, changeId, current, previous, suffix = '') {
|
||||||
|
|
||||||
const valueEl = document.getElementById(valueId);
|
const valueEl = document.getElementById(valueId);
|
||||||
@@ -90,16 +63,7 @@ function renderTrend(valueId, changeId, current, previous, suffix = '') {
|
|||||||
changeEl.className = "text-muted";
|
changeEl.className = "text-muted";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getSubtypeColor(label, index = 0) {
|
|
||||||
|
|
||||||
const specialColors = {
|
|
||||||
'A/H5N1': '#dc2626',
|
|
||||||
'Influenza': '#b90c00'
|
|
||||||
};
|
|
||||||
|
|
||||||
return specialColors[label]
|
|
||||||
|| COLORS[index % COLORS.length];
|
|
||||||
}
|
|
||||||
function renderSummary(summary = {}) {
|
function renderSummary(summary = {}) {
|
||||||
|
|
||||||
const mappings = [
|
const mappings = [
|
||||||
@@ -127,41 +91,41 @@ function renderDashboard(data = {}) {
|
|||||||
renderSummary(data.summary);
|
renderSummary(data.summary);
|
||||||
renderProvinceHeatmap(data.province_distribution || []);
|
renderProvinceHeatmap(data.province_distribution || []);
|
||||||
|
|
||||||
buildDistributionChart(
|
Charts.buildDistributionChart(
|
||||||
'pathogenChart',
|
'pathogenChart',
|
||||||
'doughnut',
|
'doughnut',
|
||||||
(data.pathogen_distribution || [])
|
(data.pathogen_distribution || [])
|
||||||
.sort((a, b) => b.total - a.total),
|
.sort((a, b) => b.total - a.total),
|
||||||
'pathogen',
|
'pathogen',
|
||||||
'total',
|
'total',
|
||||||
getSubtypeColor
|
Charts.getSubtypeColor
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
buildDistributionChart(
|
Charts.buildDistributionChart(
|
||||||
'ageChart',
|
'ageChart',
|
||||||
'doughnut',
|
'doughnut',
|
||||||
data.age_distribution || [],
|
data.age_distribution || [],
|
||||||
'age_group'
|
'age_group'
|
||||||
);
|
);
|
||||||
|
|
||||||
buildDistributionChart(
|
Charts.buildDistributionChart(
|
||||||
'sexChart',
|
'sexChart',
|
||||||
'bar',
|
'bar',
|
||||||
data.sex_distribution || [],
|
data.sex_distribution || [],
|
||||||
'patient_sex'
|
'patient_sex'
|
||||||
);
|
);
|
||||||
|
|
||||||
buildDistributionChart(
|
Charts.buildDistributionChart(
|
||||||
'subtypeChart',
|
'subtypeChart',
|
||||||
'bar',
|
'bar',
|
||||||
data.subtype_distribution || [],
|
data.subtype_distribution || [],
|
||||||
'subtype',
|
'subtype',
|
||||||
'total',
|
'total',
|
||||||
getSubtypeColor
|
Charts.getSubtypeColor
|
||||||
);
|
);
|
||||||
|
|
||||||
buildDistributionChart(
|
Charts.buildDistributionChart(
|
||||||
'sentinelChart',
|
'sentinelChart',
|
||||||
'pie',
|
'pie',
|
||||||
data.sentinel_sites || [],
|
data.sentinel_sites || [],
|
||||||
@@ -178,21 +142,21 @@ function renderAFIDashboard(data = {}) {
|
|||||||
renderAFITrend(
|
renderAFITrend(
|
||||||
data.afi_case_trend.section_1,
|
data.afi_case_trend.section_1,
|
||||||
'afiSection1Trend',
|
'afiSection1Trend',
|
||||||
COLORS,
|
Chart.COLORS,
|
||||||
'trend'
|
'trend'
|
||||||
);
|
);
|
||||||
|
|
||||||
renderAFITrend(
|
renderAFITrend(
|
||||||
data.afi_case_trend.section_2,
|
data.afi_case_trend.section_2,
|
||||||
'afiSection2Trend',
|
'afiSection2Trend',
|
||||||
COLORS,
|
Chart.COLORS,
|
||||||
'trend'
|
'trend'
|
||||||
);
|
);
|
||||||
|
|
||||||
renderAFITrend(
|
renderAFITrend(
|
||||||
data.afi_case_trend.section_3,
|
data.afi_case_trend.section_3,
|
||||||
'afiSection3Trend',
|
'afiSection3Trend',
|
||||||
COLORS,
|
Chart.COLORS,
|
||||||
'trend'
|
'trend'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -200,21 +164,21 @@ function renderAFIDashboard(data = {}) {
|
|||||||
renderAFITrend(
|
renderAFITrend(
|
||||||
data.afi_case_trend.section_1,
|
data.afi_case_trend.section_1,
|
||||||
'afiPcrChart',
|
'afiPcrChart',
|
||||||
COLORS,
|
Chart.COLORS,
|
||||||
'donut'
|
'donut'
|
||||||
);
|
);
|
||||||
|
|
||||||
renderAFITrend(
|
renderAFITrend(
|
||||||
data.afi_case_trend.section_2,
|
data.afi_case_trend.section_2,
|
||||||
'afiMultiplexChart',
|
'afiMultiplexChart',
|
||||||
COLORS,
|
Chart.COLORS,
|
||||||
'donut'
|
'donut'
|
||||||
);
|
);
|
||||||
|
|
||||||
renderAFITrend(
|
renderAFITrend(
|
||||||
data.afi_case_trend.section_3,
|
data.afi_case_trend.section_3,
|
||||||
'afiElisaChart',
|
'afiElisaChart',
|
||||||
COLORS,
|
Chart.COLORS,
|
||||||
'donut'
|
'donut'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -466,7 +430,7 @@ function renderProgramTrend(rows = []) {
|
|||||||
|
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
|
|
||||||
buildMixedTrendChart(
|
Charts.buildMixedTrendChart(
|
||||||
'trendChart',
|
'trendChart',
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
@@ -720,7 +684,7 @@ function renderProgramTrend(rows = []) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildMixedTrendChart(
|
Charts.buildMixedTrendChart(
|
||||||
'trendChart',
|
'trendChart',
|
||||||
labels,
|
labels,
|
||||||
samples,
|
samples,
|
||||||
@@ -758,7 +722,7 @@ function renderAFITrend(
|
|||||||
|
|
||||||
if (type === 'donut') {
|
if (type === 'donut') {
|
||||||
|
|
||||||
buildDistributionChart(
|
Charts.buildDistributionChart(
|
||||||
canvasId,
|
canvasId,
|
||||||
'doughnut',
|
'doughnut',
|
||||||
[],
|
[],
|
||||||
@@ -768,7 +732,7 @@ function renderAFITrend(
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
buildMixedTrendChart(
|
Charts.buildMixedTrendChart(
|
||||||
canvasId,
|
canvasId,
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
@@ -862,7 +826,7 @@ function renderAFITrend(
|
|||||||
total
|
total
|
||||||
}));
|
}));
|
||||||
|
|
||||||
buildDistributionChart(
|
Charts.buildDistributionChart(
|
||||||
canvasId,
|
canvasId,
|
||||||
'doughnut',
|
'doughnut',
|
||||||
donutRows,
|
donutRows,
|
||||||
@@ -886,12 +850,12 @@ function renderAFITrend(
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (charts[canvasId]) {
|
if (Charts.charts[canvasId]) {
|
||||||
|
|
||||||
charts[canvasId].$afiTotalCases =
|
Charts.charts[canvasId].$afiTotalCases =
|
||||||
donutTotal;
|
donutTotal;
|
||||||
|
|
||||||
charts[canvasId].update();
|
Charts.charts[canvasId].update();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -941,8 +905,8 @@ function renderAFITrend(
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
color:
|
color:
|
||||||
COLORS[
|
Charts.COLORS[
|
||||||
i % COLORS.length
|
i % Charts.COLORS.length
|
||||||
]
|
]
|
||||||
|
|
||||||
})
|
})
|
||||||
@@ -954,7 +918,7 @@ function renderAFITrend(
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
buildMixedTrendChart(
|
Charts.buildMixedTrendChart(
|
||||||
canvasId,
|
canvasId,
|
||||||
labels,
|
labels,
|
||||||
totalCases,
|
totalCases,
|
||||||
@@ -965,7 +929,7 @@ function renderAFITrend(
|
|||||||
|
|
||||||
|
|
||||||
function renderPathogenChart(rows = []) {
|
function renderPathogenChart(rows = []) {
|
||||||
buildDistributionChart(
|
Charts.buildDistributionChart(
|
||||||
'pathogenChart',
|
'pathogenChart',
|
||||||
'doughnut',
|
'doughnut',
|
||||||
rows,
|
rows,
|
||||||
@@ -973,7 +937,7 @@ function renderPathogenChart(rows = []) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
function renderSentinel(rows = []) {
|
function renderSentinel(rows = []) {
|
||||||
buildDistributionChart(
|
Charts.buildDistributionChart(
|
||||||
'sentinelChart',
|
'sentinelChart',
|
||||||
'pie',
|
'pie',
|
||||||
rows,
|
rows,
|
||||||
@@ -982,27 +946,26 @@ function renderSentinel(rows = []) {
|
|||||||
}
|
}
|
||||||
function renderSubtypeChart(rows = []) {
|
function renderSubtypeChart(rows = []) {
|
||||||
|
|
||||||
console.log('renderSubtypeChart');
|
|
||||||
|
|
||||||
buildDistributionChart(
|
Charts.buildDistributionChart(
|
||||||
'subtypeChart',
|
'subtypeChart',
|
||||||
'bar',
|
'bar',
|
||||||
rows,
|
rows,
|
||||||
'subtype',
|
'subtype',
|
||||||
'total',
|
'total',
|
||||||
getSubtypeColor
|
Charts.getSubtypeColor
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function renderDemographics(data = {}) {
|
function renderDemographics(data = {}) {
|
||||||
|
|
||||||
buildDistributionChart(
|
Charts.buildDistributionChart(
|
||||||
'ageChart',
|
'ageChart',
|
||||||
'doughnut',
|
'doughnut',
|
||||||
data.age_distribution || [],
|
data.age_distribution || [],
|
||||||
'age_group'
|
'age_group'
|
||||||
);
|
);
|
||||||
|
|
||||||
buildDistributionChart(
|
Charts.buildDistributionChart(
|
||||||
'sexChart',
|
'sexChart',
|
||||||
'bar',
|
'bar',
|
||||||
data.sex_distribution || [],
|
data.sex_distribution || [],
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { COLORS, SUBTYPE_COLORS } from "./globals.js";
|
import * as Charts from "./dashboard/globals.js";
|
||||||
|
|
||||||
let sequencingTotalChart;
|
let sequencingTotalChart;
|
||||||
let covidLineageFrequencyChart;
|
let covidLineageFrequencyChart;
|
||||||
@@ -70,7 +70,7 @@ function renderSequencingTotalChart(rows) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
charts['sequencingTotalChart'] = sequencingTotalChart;
|
Charts.charts['sequencingTotalChart'] = sequencingTotalChart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ function loadCovidLineageFrequency(periodType, startYear, startWeek, endYear, en
|
|||||||
// borderColor: 'transparent',
|
// borderColor: 'transparent',
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
backgroundColor: hexToRGBA(COLORS[index % COLORS.length], 0.3),
|
backgroundColor: hexToRGBA(Charts.COLORS[index % Charts.COLORS.length], 0.3),
|
||||||
stack: 'total'
|
stack: 'total'
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -203,7 +203,7 @@ function loadCovidLineageFrequency(periodType, startYear, startWeek, endYear, en
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
charts['covidLineageFrequency'] = covidLineageFrequencyChart;
|
Charts.charts['covidLineageFrequency'] = covidLineageFrequencyChart;
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
// Custom right-side scrollable legend
|
// Custom right-side scrollable legend
|
||||||
@@ -284,7 +284,7 @@ function loadInfluenzaSubtypeFrequency(periodType, startYear, startWeek, endYear
|
|||||||
)];
|
)];
|
||||||
|
|
||||||
const lineageColors = lineages.map(
|
const lineageColors = lineages.map(
|
||||||
label => SUBTYPE_COLORS[label] || '#9ca3af'
|
label => Charts.SUBTYPE_COLORS[label] || '#9ca3af'
|
||||||
);
|
);
|
||||||
|
|
||||||
const datasets = lineages.map((lineage, index) => {
|
const datasets = lineages.map((lineage, index) => {
|
||||||
@@ -405,7 +405,7 @@ function loadInfluenzaSubtypeFrequency(periodType, startYear, startWeek, endYear
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
charts['influenzaSubtypeFrequency'] =
|
Charts.charts['influenzaSubtypeFrequency'] =
|
||||||
influenzaSubtypeFrequencyChart;
|
influenzaSubtypeFrequencyChart;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<x-guest-layout>
|
<x-guest-layout>
|
||||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400 p-4">
|
||||||
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
|
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -60,14 +60,14 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@if (Route::has('password.request'))
|
<!-- @if (Route::has('password.request'))
|
||||||
<a
|
<a
|
||||||
href="{{ route('password.request') }}"
|
href="{{ route('password.request') }}"
|
||||||
class="text-sm text-green-600 hover:text-green-700"
|
class="text-sm text-green-600 hover:text-green-700"
|
||||||
>
|
>
|
||||||
Forgot password?
|
Forgot password?
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||||
|
|
||||||
<script src="/js/dashboard/filter.js"></script>
|
<script src="/js/dashboard/filter.js"></script>
|
||||||
<script src="/js/dashboard/charts.js"></script>
|
<script src="/js/dashboard/globals.js" type="module"></script>
|
||||||
<script src="/js/dashboard/export.js"></script>
|
<script src="/js/dashboard/export.js" type="module"></script>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -278,8 +278,8 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div id="exportItems" class="align-items-center gap-2">
|
<div id="exportItems" class="align-items-center gap-2">
|
||||||
<button class="btn btn-sm btn-light" onclick="openChartSelector()">Charts</button>
|
<button class="btn btn-sm btn-light" id="btnCharts">Charts</button>
|
||||||
<button class="btn btn-sm btn-light" onclick="exportFullDashboard()">Screen</button>
|
<button class="btn btn-sm btn-light" id="btnExport">Screen</button>
|
||||||
<button class="btn btn-sm btn-light" onclick="window.print()">Print</button>
|
<button class="btn btn-sm btn-light" onclick="window.print()">Print</button>
|
||||||
<button class="btn btn-sm btn-outline-secondary" id="exportClose">✕</button>
|
<button class="btn btn-sm btn-outline-secondary" id="exportClose">✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -290,8 +290,8 @@
|
|||||||
<div id="chartList"></div>
|
<div id="chartList"></div>
|
||||||
|
|
||||||
<div class="mt-3 d-flex justify-content-end gap-2">
|
<div class="mt-3 d-flex justify-content-end gap-2">
|
||||||
<button onclick="closeChartSelector()">Cancel</button>
|
<button id="chartCancel" class="btn btn-sm btn-secondary">Cancel</button>
|
||||||
<button onclick="exportSelectedCharts()">Download PDF</button>
|
<button id="chartDownload" class="btn btn-sm btn-primary">Download PDF</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -328,7 +328,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
updateLastUpdated();
|
|
||||||
|
const lastUpdated = localStorage.getItem('dashboardLastUpdated');
|
||||||
|
|
||||||
|
if (lastUpdated) {
|
||||||
|
document.getElementById('lastUpdated').innerHTML =
|
||||||
|
`Last update: ${lastUpdated}`;
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
window.addEventListener("click", (e) => {
|
window.addEventListener("click", (e) => {
|
||||||
const modal = document.getElementById("chartModal");
|
const modal = document.getElementById("chartModal");
|
||||||
@@ -360,7 +367,21 @@
|
|||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
||||||
updateLastUpdated();
|
const now = new Date();
|
||||||
|
|
||||||
|
const time = now.toLocaleTimeString([], {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
const date = now.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
const timestamp = `${time} | ${date}`;
|
||||||
|
|
||||||
|
localStorage.setItem(
|
||||||
|
'dashboardLastUpdated',
|
||||||
|
timestamp
|
||||||
|
);
|
||||||
|
|
||||||
location.reload();
|
location.reload();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user