feat(dashboard): add sidebar, navbar, dynamic routes, summary & trend API, NIPH theme

This commit is contained in:
2026-03-03 16:40:56 +07:00
commit eb26018853
74 changed files with 12244 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
@extends('layouts.app')
@section('content')
<h3>{{ $selected->code }} - {{ $selected->name_en }}</h3>
<div class="row mb-4">
<div class="col-md-3">
<div class="card p-3">
<h6>Total Cases</h6>
<h4 id="totalCases">-</h4>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<h5>Epidemic Trend</h5>
<canvas id="trendChart"></canvas>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const surveillanceId = {{ $selected->id }};
const dateFrom = '2026-01-01';
const dateTo = new Date().toISOString().split('T')[0];
fetch(`/api/dashboard/trend?surveillance_id=${surveillanceId}&period_type=week&date_from=${dateFrom}&date_to=${dateTo}`)
.then(res => res.json())
.then(data => {
const labels = data.map(d => `${d.year}-${d.period}`);
const totals = data.map(d => d.total);
new Chart(document.getElementById('trendChart'), {
type: 'line',
data: {
labels: labels,
datasets: [{
label: '{{ $selected->code }}',
data: totals,
borderColor: 'blue',
fill: false
}]
}
});
});
});
</script>
@endsection

View File

@@ -0,0 +1,223 @@
<!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>

View File

@@ -0,0 +1,225 @@
@extends('layouts.app')
@section('content')
<div class="container-fluid">
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h3 class="fw-bold mb-1">Dashboard Overview</h3>
<small class="text-muted">National surveillance summary</small>
</div>
</div>
<!-- Summary Cards -->
<div class="row mb-4" id="summary_cards"></div>
<div class="row">
<!-- LEFT COLUMN -->
<div class="col-lg-8 d-flex flex-column">
<!-- Trend Chart -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="fw-bold mb-0">Epidemic Trend</h5>
<select id="period_type" class="form-select w-auto">
<option value="week">Epiweek</option>
<option value="month">Month</option>
<option value="year">Year</option>
</select>
</div>
<canvas id="trendChart" height="110"></canvas>
</div>
</div>
<!-- Alerts -->
<div class="card shadow-sm flex-grow-1">
<div class="card-body">
<h5 class="fw-bold">Recent Alerts & Notifications</h5>
<ul class="list-group list-group-flush mt-3">
<li class="list-group-item">
Monitoring influenza increase in selected provinces.
</li>
<li class="list-group-item">
🔔 SARS-CoV-2 positivity rate under review.
</li>
</ul>
</div>
</div>
</div>
<!-- RIGHT COLUMN -->
<div class="col-lg-4">
<div class="card shadow-sm h-100">
<div class="card-body">
<h5 class="fw-bold">Cases by Provinces</h5>
<p class="text-muted small">(% change vs last period)</p>
<div class="d-flex justify-content-center align-items-center" style="height: 100%;">
<span class="text-muted">Province heatmap coming next</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let trendChart;
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 => {
const colorClass = item.percent_change >= 0
? 'text-danger'
: 'text-success';
html += `
<div class="col-md-4 mb-3">
<div class="card shadow-sm h-100">
<div class="card-body">
<h6 class="fw-bold">${item.code}</h6>
<h3 class="mb-1">${item.current_total}</h3>
<small class="${colorClass}">
${item.percent_change}% vs last period
</small>
<div class="small text-muted mt-1">
+${item.last_24h} in 24h
</div>
</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 => {
console.log(data);
if (trendChart) trendChart.destroy();
const labelsSet = new Set();
Object.values(data).forEach(program => {
program.forEach(row => {
let label;
if (periodType === 'year') {
label = row.period.toString();
} else {
label = `${row.year}-${row.period}`;
}
labelsSet.add(label);
});
});
const labels = Array.from(labelsSet).sort();
const colors = {
SARI: '#2563eb',
ILI: '#10b981',
LBM: '#9333ea'
};
const datasets = [];
Object.keys(data).forEach(code => {
const values = labels.map(label => {
const found = data[code].find(row => {
let rowLabel;
if (periodType === 'year') {
rowLabel = row.period.toString();
} else {
rowLabel = `${row.year}-${row.period}`;
}
return rowLabel === label;
});
return found ? found.total : 0;
});
datasets.push({
label: code,
data: values,
borderColor: colors[code] || '#000',
fill: false,
tension: 0.3
});
});
trendChart = new Chart(document.getElementById('trendChart'), {
type: 'line',
data: {
labels: labels,
datasets: datasets
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
});
}
document.addEventListener('DOMContentLoaded', function () {
const today = new Date().toISOString().split('T')[0];
const past = new Date();
past.setDate(past.getDate() - 30);
const dateFrom = past.toISOString().split('T')[0];
const dateTo = today;
loadSummary(dateFrom, dateTo);
loadTrend('week', dateFrom, dateTo);
document.getElementById('period_type')
.addEventListener('change', function () {
loadTrend(this.value, dateFrom, dateTo);
});
});
</script>
@endsection

View File

@@ -0,0 +1,152 @@
<!DOCTYPE html>
<html>
<head>
<title>NRML Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<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"></script>
<style>
body {
margin: 0;
}
/* SIDEBAR */
.sidebar {
width: 220px;
height: 100vh;
position: fixed;
left: 0;
top: 0;
background-color: #0B8F3C;
padding-top: 20px;
}
.nav-link-custom {
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
display: block;
padding: 14px 20px;
font-weight: 500;
transition: 0.2s;
}
.nav-link-custom:hover {
background-color: #06632A;
color: white;
}
.active-link {
border-left: 5px solid #F4C430;
background-color: rgba(255, 255, 255, 0.1);
}
.main-wrapper {
margin-left: 220px;
}
.content-area {
padding: 30px;
background: #f8f9fa;
min-height: calc(100vh - 60px);
}
/* TOP NAVBAR */
.top-navbar {
height: 60px;
border-bottom: 4px solid #0B8F3C;
background: #FFFFFF;
display: flex;
align-items: center;
padding: 0 20px;
}
.brand-title {
font-weight: 600;
font-size: 18px;
color: #1E63B6;
}
.content-area {
padding: 30px;
background: #f8f9fa;
min-height: calc(100vh - 60px);
}
.brand-logo {
width: 32px;
height: 32px;
object-fit: contain;
margin-right: 10px;
}
.card {
border-radius: 10px;
border: 1px solid #E5E7EB;
}
.card h3 {
color: #0B8F3C;
}
</style>
</head>
<body>
<!-- Sidebar -->
<div class="sidebar d-flex flex-column justify-content-between">
<div>
<a href="/dashboard" class="nav-link-custom {{ request()->is('dashboard') ? 'active-link' : '' }}">
<span class="nav-text">Overview</span>
</a>
@foreach($programs as $program)
<a href="/dashboard/{{ strtolower($program->code) }}"
class="nav-link-custom {{ request()->is('dashboard/' . strtolower($program->code)) ? 'active-link' : '' }}">
<span class="nav-text">{{ $program->code }}</span>
</a>
@endforeach
</div>
<div class="mb-3">
<a href="#" class="nav-link-custom">
<span class="nav-icon">⚙️</span>
<span class="nav-text">Settings</span>
</a>
</div>
</div>
<!-- Main Wrapper -->
<div class="main-wrapper">
<!-- Top Navbar -->
<div class="top-navbar">
<img src="{{ asset('images/nrml-logo.png') }}" class="brand-logo" alt="NRML Logo">
<div class="brand-title">
National Reference Medical Laboratory Surveillance Dashboard
</div>
<div class="ms-auto text-muted small">
Status: Active Surveillance
</div>
</div>
<!-- Page Content -->
<div class="content-area">
@yield('content')
</div>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long