feat(dashboard): add sidebar, navbar, dynamic routes, summary & trend API, NIPH theme
This commit is contained in:
11
dashboard/resources/css/app.css
Normal file
11
dashboard/resources/css/app.css
Normal file
@@ -0,0 +1,11 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
1
dashboard/resources/js/app.js
Normal file
1
dashboard/resources/js/app.js
Normal file
@@ -0,0 +1 @@
|
||||
import './bootstrap';
|
||||
4
dashboard/resources/js/bootstrap.js
vendored
Normal file
4
dashboard/resources/js/bootstrap.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
54
dashboard/resources/views/dashboard/detail.blade.php
Normal file
54
dashboard/resources/views/dashboard/detail.blade.php
Normal 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
|
||||
223
dashboard/resources/views/dashboard/index.blade.php
Normal file
223
dashboard/resources/views/dashboard/index.blade.php
Normal 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>
|
||||
225
dashboard/resources/views/dashboard/overview.blade.php
Normal file
225
dashboard/resources/views/dashboard/overview.blade.php
Normal 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
|
||||
152
dashboard/resources/views/layouts/app.blade.php
Normal file
152
dashboard/resources/views/layouts/app.blade.php
Normal 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>
|
||||
277
dashboard/resources/views/welcome.blade.php
Normal file
277
dashboard/resources/views/welcome.blade.php
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user