add sequencing tab
This commit is contained in:
77579
case_lab_results.sql
Normal file
77579
case_lab_results.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -155,19 +155,49 @@ class DashboardController extends Controller
|
|||||||
// return response()->json(['error' => 'Missing epiweek range'], 400);
|
// return response()->json(['error' => 'Missing epiweek range'], 400);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return response()->json($data);
|
// return response()->json($data);
|
||||||
}
|
// }
|
||||||
|
|
||||||
public function fetchSourceData(){
|
public function fetchSourceData()
|
||||||
try{
|
{
|
||||||
|
try {
|
||||||
$this->dataRetrievalService->getSurveillanceData();
|
$this->dataRetrievalService->getSurveillanceData();
|
||||||
return response()->json(['message' => 'Data loaded successfully!'], 200);
|
return response()->json(['message' => 'Data loaded successfully!'], 200);
|
||||||
}
|
} catch (\Exception $e) {
|
||||||
catch (\Exception $e)
|
|
||||||
{
|
|
||||||
return response()->json(['error' => 'Data loaded unsuccessfully!'], 400);
|
return response()->json(['error' => 'Data loaded unsuccessfully!'], 400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sequencing Dashboard
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function sequencing(Request $request)
|
||||||
|
{
|
||||||
|
$surveillanceId = (int) $request->query('surveillance_id');
|
||||||
|
$range = $this->getEpiRange($request);
|
||||||
|
|
||||||
|
if (!$surveillanceId || !$range) {
|
||||||
|
return response()->json(['error' => 'Missing parameters'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'trend' => $this->service->sequencingTrend(
|
||||||
|
$surveillanceId,
|
||||||
|
$range['startYear'],
|
||||||
|
$range['startWeek'],
|
||||||
|
$range['endYear'],
|
||||||
|
$range['endWeek']
|
||||||
|
),'distribution' => $this->service->subtypeDistribution(
|
||||||
|
$surveillanceId,
|
||||||
|
$range['startYear'],
|
||||||
|
$range['startWeek'],
|
||||||
|
$range['endYear'],
|
||||||
|
$range['endWeek']
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -727,28 +727,20 @@ class DashboardService
|
|||||||
$q->select('lab_code')
|
$q->select('lab_code')
|
||||||
->from('surveillance_cases')
|
->from('surveillance_cases')
|
||||||
->where('surveillance_id', $surveillanceId)
|
->where('surveillance_id', $surveillanceId)
|
||||||
->whereRaw(
|
->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek])
|
||||||
"(year_data > ? OR (year_data = ? AND week_data >= ?))",
|
->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]);
|
||||||
[$startYear, $startYear, $startWeek]
|
|
||||||
)
|
|
||||||
->whereRaw(
|
|
||||||
"(year_data < ? OR (year_data = ? AND week_data <= ?))",
|
|
||||||
[$endYear, $endYear, $endWeek]
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
|
|
||||||
->where('case_lab_results.is_positive', 1)
|
->where('case_lab_results.is_positive', 1)
|
||||||
|
|
||||||
->selectRaw("
|
->selectRaw("
|
||||||
case_lab_results.subtype,
|
COALESCE(NULLIF(case_lab_results.subtype, ''), 'Unsubtyped') as subtype,
|
||||||
COUNT(DISTINCT case_lab_results.lab_code) as total
|
COUNT(DISTINCT case_lab_results.lab_code) as total
|
||||||
")
|
")
|
||||||
|
|
||||||
->whereNotNull('case_lab_results.subtype')
|
|
||||||
->where('case_lab_results.subtype', '!=', '')
|
|
||||||
->where('case_lab_results.subtype', '!=', 'Positive')
|
->where('case_lab_results.subtype', '!=', 'Positive')
|
||||||
|
|
||||||
->groupBy('case_lab_results.subtype')
|
->groupBy('subtype')
|
||||||
->orderByDesc('total')
|
->orderByDesc('total')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
@@ -846,5 +838,40 @@ class DashboardService
|
|||||||
->get();
|
->get();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| sequencing trend
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
public function sequencingTrend($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
|
||||||
|
{
|
||||||
|
return CaseLabResult::join(
|
||||||
|
'surveillance_cases',
|
||||||
|
'case_lab_results.lab_code',
|
||||||
|
'=',
|
||||||
|
'surveillance_cases.lab_code'
|
||||||
|
)
|
||||||
|
->where('surveillance_cases.surveillance_id', $surveillanceId)
|
||||||
|
|
||||||
|
->where('case_lab_results.is_positive', 1)
|
||||||
|
|
||||||
|
->whereNotNull('subtype')
|
||||||
|
|
||||||
|
->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) {
|
||||||
|
$q->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek])
|
||||||
|
->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]);
|
||||||
|
})
|
||||||
|
|
||||||
|
->selectRaw("
|
||||||
|
surveillance_cases.week_data as period,
|
||||||
|
subtype,
|
||||||
|
COUNT(DISTINCT surveillance_cases.lab_code) as total
|
||||||
|
")
|
||||||
|
|
||||||
|
->groupBy('period', 'subtype')
|
||||||
|
->orderBy('period')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -481,3 +481,5 @@ function renderSubtypeChart(rows) {
|
|||||||
rows.map(r => r.total)
|
rows.map(r => r.total)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Seq
|
||||||
|
|||||||
238
dashboard/public/js/sequencing.js
Normal file
238
dashboard/public/js/sequencing.js
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
let sequencingChart;
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const canvas = document.getElementById('sequencingChart');
|
||||||
|
if (!canvas) return;
|
||||||
|
new DashboardFilter((startYear, startWeek, endYear, endWeek) => {
|
||||||
|
|
||||||
|
fetch(`/api/dashboard/sequencing?surveillance_id=${window.SURVEILLANCE_ID}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
renderSequencingChart(data.trend || []);
|
||||||
|
renderSequencingCountChart(data.trend || []);
|
||||||
|
renderSequencingPieChart(data.distribution || []);
|
||||||
|
renderSequencingTotalChart(data.trend || []);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderSequencingCountChart(rows) {
|
||||||
|
|
||||||
|
const ctx = document.getElementById('sequencingCountChart');
|
||||||
|
|
||||||
|
Chart.getChart('sequencingCountChart')?.destroy();
|
||||||
|
|
||||||
|
rows = processTopSubtypes(rows);
|
||||||
|
|
||||||
|
const weeks = [...new Set(rows.map(r => r.period))];
|
||||||
|
const subtypes = [...new Set(rows.map(r => r.subtype))];
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
'#2563eb', '#10b981', '#f59e0b', '#ef4444',
|
||||||
|
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16'
|
||||||
|
];
|
||||||
|
|
||||||
|
const datasets = subtypes.map((sub, i) => ({
|
||||||
|
label: sub,
|
||||||
|
data: weeks.map(w => {
|
||||||
|
const found = rows.find(r => r.period === w && r.subtype === sub);
|
||||||
|
return found ? found.total : 0;
|
||||||
|
}),
|
||||||
|
backgroundColor: colors[i % colors.length]
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: weeks.map(w => `W${w}`),
|
||||||
|
datasets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
x: { stacked: true },
|
||||||
|
y: { stacked: true }
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'bottom',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function renderSequencingPieChart(rows) {
|
||||||
|
|
||||||
|
const ctx = document.getElementById('sequencingPieChart');
|
||||||
|
|
||||||
|
Chart.getChart('sequencingPieChart')?.destroy();
|
||||||
|
|
||||||
|
const top = rows.slice(0, 8);
|
||||||
|
|
||||||
|
const labels = top.map(r => r.subtype);
|
||||||
|
const values = top.map(r => r.total);
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
datasets: [{
|
||||||
|
data: values
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function renderSequencingTotalChart(rows) {
|
||||||
|
|
||||||
|
const ctx = document.getElementById('sequencingTotalChart');
|
||||||
|
|
||||||
|
Chart.getChart('sequencingTotalChart')?.destroy();
|
||||||
|
|
||||||
|
const totals = {};
|
||||||
|
|
||||||
|
rows.forEach(r => {
|
||||||
|
totals[r.period] = (totals[r.period] || 0) + Number(r.total);
|
||||||
|
});
|
||||||
|
|
||||||
|
const weeks = Object.keys(totals);
|
||||||
|
const values = Object.values(totals);
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: weeks.map(w => `W${w}`),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Total Samples',
|
||||||
|
data: values,
|
||||||
|
backgroundColor: '#0B8F3C'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false }
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function processTopSubtypes(rows) {
|
||||||
|
|
||||||
|
const totals = {};
|
||||||
|
|
||||||
|
rows.forEach(r => {
|
||||||
|
totals[r.subtype] = (totals[r.subtype] || 0) + Number(r.total);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sorted = Object.entries(totals)
|
||||||
|
.sort((a, b) => b[1] - a[1]);
|
||||||
|
|
||||||
|
const top = sorted.slice(0, 8).map(([k]) => k);
|
||||||
|
|
||||||
|
return rows.map(r => {
|
||||||
|
if (!top.includes(r.subtype)) {
|
||||||
|
return { ...r, subtype: 'Others' };
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function renderSequencingChart(rows) {
|
||||||
|
|
||||||
|
rows = processTopSubtypes(rows);
|
||||||
|
|
||||||
|
const ctx = document.getElementById('sequencingChart');
|
||||||
|
|
||||||
|
if (sequencingChart) {
|
||||||
|
sequencingChart.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
'#2563eb', '#10b981', '#f59e0b', '#ef4444',
|
||||||
|
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16',
|
||||||
|
'#6b7280'
|
||||||
|
];
|
||||||
|
|
||||||
|
const aggregated = {};
|
||||||
|
|
||||||
|
rows.forEach(r => {
|
||||||
|
const key = `${r.period}_${r.subtype}`;
|
||||||
|
if (!aggregated[key]) {
|
||||||
|
aggregated[key] = { ...r };
|
||||||
|
} else {
|
||||||
|
aggregated[key].total += Number(r.total);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const cleanRows = Object.values(aggregated);
|
||||||
|
|
||||||
|
const weeks = [...new Set(cleanRows.map(r => r.period))];
|
||||||
|
const subtypes = [...new Set(cleanRows.map(r => r.subtype))]
|
||||||
|
.sort((a, b) => {
|
||||||
|
const sum = s => cleanRows
|
||||||
|
.filter(r => r.subtype === s)
|
||||||
|
.reduce((t, r) => t + r.total, 0);
|
||||||
|
return sum(b) - sum(a);
|
||||||
|
});
|
||||||
|
|
||||||
|
const datasets = subtypes.map((sub, i) => {
|
||||||
|
return {
|
||||||
|
label: sub,
|
||||||
|
data: weeks.map(w => {
|
||||||
|
const weekRows = cleanRows.filter(r => r.period === w);
|
||||||
|
const total = weekRows.reduce((s, r) => s + Number(r.total), 0);
|
||||||
|
|
||||||
|
const found = weekRows.find(r => r.subtype === sub);
|
||||||
|
return total ? ((found?.total || 0) / total) * 100 : 0;
|
||||||
|
}),
|
||||||
|
fill: true,
|
||||||
|
tension: 0.3,
|
||||||
|
backgroundColor: colors[i % colors.length],
|
||||||
|
borderColor: colors[i % colors.length]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
sequencingChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: weeks.map(w => `W${w}`),
|
||||||
|
datasets: datasets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'bottom',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
datalabels: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { stacked: true },
|
||||||
|
y: {
|
||||||
|
stacked: true,
|
||||||
|
max: 100,
|
||||||
|
ticks: {
|
||||||
|
callback: v => v + '%'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -21,29 +21,14 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div id="custom_range_container" style="display:none;" class="align-items-center gap-1">
|
<div id="custom_range_container" style="display:none;" class="align-items-center gap-1">
|
||||||
|
|
||||||
<select id="start_year" class="form-select"></select>
|
<select id="start_year" class="form-select"></select>
|
||||||
<select id="start_week" class="form-select"></select>
|
<select id="start_week" class="form-select"></select>
|
||||||
|
|
||||||
<span class="mx-1">to</span>
|
<span class="mx-1">to</span>
|
||||||
|
|
||||||
<select id="end_year" class="form-select"></select>
|
<select id="end_year" class="form-select"></select>
|
||||||
<select id="end_week" class="form-select"></select>
|
<select id="end_week" class="form-select"></select>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- STATUS -->
|
|
||||||
<div class="alert alert-info mb-4">
|
|
||||||
<b>Current {{ $selected->code }} Status:</b>
|
|
||||||
<span id="activityStatus">Loading...</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- SUMMARY CARDS -->
|
<!-- SUMMARY CARDS -->
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
|
|
||||||
|
|||||||
@@ -1,223 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>NRML Surveillance Dashboard</title>
|
|
||||||
|
|
||||||
<!-- Bootstrap 5 -->
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Chart.js -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
||||||
</head>
|
|
||||||
<body class="bg-light">
|
|
||||||
|
|
||||||
<div class="container-fluid py-4">
|
|
||||||
|
|
||||||
<h3 class="mb-4">NRML Surveillance Dashboard</h3>
|
|
||||||
|
|
||||||
<!-- Filters -->
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-body row g-3 align-items-end">
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Surveillance Program</label>
|
|
||||||
<select id="surveillance_id" class="form-select">
|
|
||||||
@foreach($programs as $program)
|
|
||||||
<option value="{{ $program->id }}">
|
|
||||||
{{ $program->code }} - {{ $program->name_en }}
|
|
||||||
</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Period</label>
|
|
||||||
<select id="period_type" class="form-select">
|
|
||||||
<option value="week">Epiweek</option>
|
|
||||||
<option value="month">Month</option>
|
|
||||||
<option value="year">Year</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Date From</label>
|
|
||||||
<input type="date" id="date_from" class="form-control">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Date To</label>
|
|
||||||
<input type="date" id="date_to" class="form-control">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-2">
|
|
||||||
<button onclick="loadDashboard()" class="btn btn-primary w-100">
|
|
||||||
Apply
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Summary Cards -->
|
|
||||||
<div class="row" id="summary_cards"></div>
|
|
||||||
|
|
||||||
<!-- Trend Chart -->
|
|
||||||
<div class="card mt-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5>Epidemic Trend</h5>
|
|
||||||
<canvas id="trendChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Province Table -->
|
|
||||||
<div class="card mt-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5>Cases by Province</h5>
|
|
||||||
<table class="table table-bordered" id="provinceTable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Province</th>
|
|
||||||
<th>Total Cases</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
let trendChart;
|
|
||||||
|
|
||||||
function loadDashboard() {
|
|
||||||
|
|
||||||
const surveillanceId = document.getElementById('surveillance_id').value;
|
|
||||||
const periodType = document.getElementById('period_type').value;
|
|
||||||
const dateFrom = document.getElementById('date_from').value;
|
|
||||||
const dateTo = document.getElementById('date_to').value;
|
|
||||||
|
|
||||||
loadSummary(dateFrom, dateTo);
|
|
||||||
loadTrend(periodType, dateFrom, dateTo);
|
|
||||||
loadProvince(surveillanceId, dateFrom, dateTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSummary(dateFrom, dateTo) {
|
|
||||||
|
|
||||||
fetch(`/api/dashboard/summary?date_from=${dateFrom}&date_to=${dateTo}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
|
|
||||||
data.forEach(item => {
|
|
||||||
html += `
|
|
||||||
<div class="col-md-3 mb-3">
|
|
||||||
<div class="card shadow-sm">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6>${item.code}</h6>
|
|
||||||
<h4>${item.current_total}</h4>
|
|
||||||
<small class="${item.percent_change >= 0 ? 'text-success' : 'text-danger'}">
|
|
||||||
${item.percent_change}%
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('summary_cards').innerHTML = html;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTrend(periodType, dateFrom, dateTo) {
|
|
||||||
|
|
||||||
fetch(`/api/dashboard/trend?period_type=${periodType}&date_from=${dateFrom}&date_to=${dateTo}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
|
|
||||||
if (trendChart) trendChart.destroy();
|
|
||||||
|
|
||||||
const labelsSet = new Set();
|
|
||||||
|
|
||||||
Object.values(data).forEach(program => {
|
|
||||||
program.forEach(row => {
|
|
||||||
labelsSet.add(`${row.year ?? ''}-${row.period}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const labels = Array.from(labelsSet).sort();
|
|
||||||
|
|
||||||
const datasets = [];
|
|
||||||
|
|
||||||
const colors = {
|
|
||||||
SARI: 'red',
|
|
||||||
ILI: 'blue',
|
|
||||||
LBM: 'green'
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(data).forEach(code => {
|
|
||||||
|
|
||||||
const values = labels.map(label => {
|
|
||||||
const found = data[code].find(row =>
|
|
||||||
`${row.year ?? ''}-${row.period}` === label
|
|
||||||
);
|
|
||||||
return found ? found.total : 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
datasets.push({
|
|
||||||
label: code,
|
|
||||||
data: values,
|
|
||||||
borderColor: colors[code] || 'black',
|
|
||||||
fill: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
trendChart = new Chart(document.getElementById('trendChart'), {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: labels,
|
|
||||||
datasets: datasets
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadProvince(surveillanceId, dateFrom, dateTo) {
|
|
||||||
|
|
||||||
fetch(`/api/dashboard/province?surveillance_id=${surveillanceId}&date_from=${dateFrom}&date_to=${dateTo}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
|
|
||||||
data.forEach(item => {
|
|
||||||
html += `
|
|
||||||
<tr>
|
|
||||||
<td>${item.site_province_name}</td>
|
|
||||||
<td>${item.total}</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector('#provinceTable tbody').innerHTML = html;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
|
|
||||||
const today = new Date().toISOString().split('T')[0];
|
|
||||||
const past = new Date();
|
|
||||||
past.setDate(past.getDate() - 30);
|
|
||||||
|
|
||||||
document.getElementById('date_from').value = past.toISOString().split('T')[0];
|
|
||||||
document.getElementById('date_to').value = today;
|
|
||||||
|
|
||||||
loadDashboard();
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
69
dashboard/resources/views/dashboard/sequencing.blade.php
Normal file
69
dashboard/resources/views/dashboard/sequencing.blade.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mb-3">
|
||||||
|
|
||||||
|
<h4 class="fw-bold">Sequencing Analysis</h4>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
|
||||||
|
<select id="trend_range" class="form-select w-auto">
|
||||||
|
<option value="8" selected>Last 8 weeks</option>
|
||||||
|
<option value="12">Last 12 weeks</option>
|
||||||
|
<option value="26">Last 26 weeks</option>
|
||||||
|
<option value="custom">Custom range</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div id="custom_range_container" style="display:none;" class="align-items-center gap-1">
|
||||||
|
<select id="start_year" class="form-select"></select>
|
||||||
|
<select id="start_week" class="form-select"></select>
|
||||||
|
<span class="mx-1">to</span>
|
||||||
|
<select id="end_year" class="form-select"></select>
|
||||||
|
<select id="end_week" class="form-select"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body" style="height:600px;">
|
||||||
|
<canvas id="sequencingChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
|
||||||
|
<!-- Counts -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body" style="height:600px;">
|
||||||
|
<h6 class="fw-bold">Lineage Counts Over Time</h6>
|
||||||
|
<canvas id="sequencingCountChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Distribution -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body" style="height:600px;">
|
||||||
|
<h6 class="fw-bold">Total Sequenced Samples Over Time</h6>
|
||||||
|
<canvas id="sequencingTotalChart" style="height: 550px;"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script>
|
||||||
|
window.SURVEILLANCE_ID = 6;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script src="/js/sequencing.js"></script>
|
||||||
|
@endsection
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
background: white;
|
background: white;
|
||||||
border-bottom: 1px solid #dcdcdc;
|
border-bottom: 1px solid #dcdcdc;
|
||||||
padding-left: 15px;
|
padding: 0 20px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
@@ -49,6 +49,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* NAV ITEMS */
|
/* NAV ITEMS */
|
||||||
|
.btn-theme-outline {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #0B8F3C;
|
||||||
|
border: 1px solid #0B8F3C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-theme-outline:hover {
|
||||||
|
background-color: #cce0d4;
|
||||||
|
color: #0B8F3C;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
padding: 12px 18px;
|
padding: 12px 18px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -92,7 +103,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border-radius: 10px;
|
border-radius: 0px !important;
|
||||||
border: 1px solid #E5E7EB;
|
border: 1px solid #E5E7EB;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,9 +123,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ms-auto small">
|
<div class="ms-auto small">
|
||||||
Last update: 12:05 |
|
Last update: 12:05 | 2026-03-15
|
||||||
Data latency: 5–10 min |
|
|
||||||
User: National - Read Only
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -126,16 +135,34 @@
|
|||||||
Overview
|
Overview
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@foreach($programs as $program)
|
<!-- @foreach($programs as $program)
|
||||||
<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>
|
||||||
|
@endforeach -->
|
||||||
|
@foreach($programs->where('code', '!=', 'NDS') as $program)
|
||||||
|
|
||||||
|
@if($program->code === 'SEQ')
|
||||||
|
|
||||||
|
<a href="/dashboard/seq"
|
||||||
|
class="nav-item {{ request()->is('dashboard/seq') ? 'active-tab' : '' }}">
|
||||||
|
SEQ
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<a href="/dashboard/{{ strtolower($program->code) }}"
|
||||||
|
class="nav-item {{ request()->is('dashboard/' . strtolower($program->code)) ? 'active-tab' : '' }}">
|
||||||
|
{{ $program->code }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
<button type="button" onclick="reloadDataSource()" class="btn btn-sm btn-warning" style="height: 27px; position: absolute; right: 40px; top: 70px;">
|
<div class="ms-auto d-flex align-items-center gap-4 pe-3">
|
||||||
Refresh Data
|
<button type="button" onclick="reloadDataSource()" class="btn btn-sm btn-theme-outline">
|
||||||
</button>
|
Refresh Data
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ Route::get('/dashboard/program', [DashboardController::class, 'program']);
|
|||||||
Route::get('/dashboard/province-circles', [DashboardController::class, 'provinceCircles']);
|
Route::get('/dashboard/province-circles', [DashboardController::class, 'provinceCircles']);
|
||||||
Route::get('/dashboard/sentinel-map', [DashboardController::class, 'sentinelMap']);
|
Route::get('/dashboard/sentinel-map', [DashboardController::class, 'sentinelMap']);
|
||||||
Route::get('/dashboard/reload', [DashboardController::class, 'fetchSourceData']);
|
Route::get('/dashboard/reload', [DashboardController::class, 'fetchSourceData']);
|
||||||
|
Route::get('/dashboard/sequencing', [DashboardController::class, 'sequencing']);
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ Route::get('/', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Route::get('/dashboard/seq', function () {
|
||||||
|
return view('dashboard.sequencing');
|
||||||
|
});
|
||||||
Route::get('/dashboard', [DashboardController::class, 'overview']);
|
Route::get('/dashboard', [DashboardController::class, 'overview']);
|
||||||
Route::get('/dashboard/{code}', [DashboardController::class, 'detail']);
|
Route::get('/dashboard/{code}', [DashboardController::class, 'detail']);
|
||||||
Route::get('/test-change', function () {
|
|
||||||
return "TEST_CHANGE_WORKING";
|
|
||||||
});
|
|
||||||
|
|||||||
47973
surveillance_cases.sql
Normal file
47973
surveillance_cases.sql
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user