change layout, update content, and change database

This commit is contained in:
2026-03-05 14:58:36 +07:00
parent 16dfb43530
commit 519d0924c8
9 changed files with 119613 additions and 148 deletions

118964
dashboard.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ use Illuminate\Http\Request;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Services\DashboardService; use App\Services\DashboardService;
use Carbon\Carbon; use Carbon\Carbon;
use App\Models\SurveillanceCase;
class DashboardController extends Controller class DashboardController extends Controller
{ {
@@ -27,8 +28,23 @@ class DashboardController extends Controller
*/ */
public function summary(Request $request) public function summary(Request $request)
{ {
$dateFrom = $request->query('date_from', Carbon::now()->subDays(7)->toDateString()); if ($request->has('start_year')) {
$dateTo = $request->query('date_to', Carbon::now()->toDateString());
$startYear = $request->query('start_year');
$startWeek = $request->query('start_week');
$endYear = $request->query('end_year');
$endWeek = $request->query('end_week');
$dateFrom = Carbon::now()->setISODate($startYear, $startWeek)->startOfWeek()->toDateString();
$dateTo = Carbon::now()->setISODate($endYear, $endWeek)->endOfWeek()->toDateString();
} else {
$dateFrom = $request->query('date_from', Carbon::now()->subDays(7)->toDateString());
$dateTo = $request->query('date_to', Carbon::now()->toDateString());
}
$data = $this->service->summaryCards($dateFrom, $dateTo); $data = $this->service->summaryCards($dateFrom, $dateTo);
@@ -42,8 +58,24 @@ class DashboardController extends Controller
public function trend(Request $request) public function trend(Request $request)
{ {
$periodType = $request->query('period_type', 'week'); $periodType = $request->query('period_type', 'week');
$dateFrom = $request->query('date_from');
$dateTo = $request->query('date_to'); if ($request->has('start_year')) {
$startYear = $request->query('start_year');
$startWeek = $request->query('start_week');
$endYear = $request->query('end_year');
$endWeek = $request->query('end_week');
$dateFrom = Carbon::now()->setISODate($startYear, $startWeek)->startOfWeek()->toDateString();
$dateTo = Carbon::now()->setISODate($endYear, $endWeek)->endOfWeek()->toDateString();
} else {
$dateFrom = $request->query('date_from');
$dateTo = $request->query('date_to');
}
$data = $this->service->aggregateAllPrograms( $data = $this->service->aggregateAllPrograms(
$periodType, $periodType,
@@ -59,19 +91,71 @@ class DashboardController extends Controller
*/ */
public function province(Request $request) public function province(Request $request)
{ {
$surveillanceId = $request->query('surveillance_id'); if ($request->has('start_year')) {
$dateFrom = $request->query('date_from');
$dateTo = $request->query('date_to');
$data = $this->service->provinceDistribution( $startYear = $request->query('start_year');
$surveillanceId, $startWeek = $request->query('start_week');
$dateFrom,
$dateTo $endYear = $request->query('end_year');
); $endWeek = $request->query('end_week');
$dateFrom = Carbon::now()
->setISODate($startYear, $startWeek)
->startOfWeek()
->toDateString();
$dateTo = Carbon::now()
->setISODate($endYear, $endWeek)
->endOfWeek()
->toDateString();
} else {
$dateFrom = $request->query('date_from');
$dateTo = $request->query('date_to');
}
$rows = $this->service->provinceDistribution($dateFrom, $dateTo);
$result = [];
foreach ($rows as $row) {
$result[$row->site_province_name] = $row->total;
}
return response()->json($result);
}
public function sentinelMap(Request $request)
{
$startYear = $request->query('start_year');
$startWeek = $request->query('start_week');
$endYear = $request->query('end_year');
$endWeek = $request->query('end_week');
$dateFrom = Carbon::now()->setISODate($startYear, $startWeek)->startOfWeek();
$dateTo = Carbon::now()->setISODate($endYear, $endWeek)->endOfWeek();
$data = $this->service->sentinelMap($dateFrom, $dateTo);
return response()->json($data); return response()->json($data);
} }
public function provinceCircles(Request $request)
{
$startYear = $request->query('start_year');
$startWeek = $request->query('start_week');
$endYear = $request->query('end_year');
$endWeek = $request->query('end_week');
$dateFrom = Carbon::now()->setISODate($startYear, $startWeek)->startOfWeek();
$dateTo = Carbon::now()->setISODate($endYear, $endWeek)->endOfWeek();
$data = $this->service->provinceCircles($dateFrom, $dateTo);
return response()->json($data);
}
/** /**
* Pathogen distribution * Pathogen distribution
*/ */

View File

@@ -158,18 +158,72 @@ class DashboardService
/** /**
* Province distribution * Province distribution
*/ */
public function provinceDistribution($surveillanceId, $dateFrom, $dateTo) public function provinceDistribution($dateFrom, $dateTo)
{ {
return SurveillanceCase::selectRaw(" return SurveillanceCase::selectRaw("
site_province_name, site_province_name,
COUNT(*) as total COUNT(*) as total
") ")
->where('surveillance_id', $surveillanceId) ->join('case_lab_results', 'surveillance_cases.lab_code', '=', 'case_lab_results.lab_code')
->whereBetween('case_date', [$dateFrom, $dateTo])
->whereIn('surveillance_cases.surveillance_id', [1, 2, 3]) // SARI ILI LBM
->where('case_lab_results.is_positive', 1)
->whereBetween('surveillance_cases.case_date', [$dateFrom, $dateTo])
->groupBy('site_province_name') ->groupBy('site_province_name')
->orderByDesc('total') ->orderByDesc('total')
->get(); ->get();
} }
public function sentinelMap($dateFrom, $dateTo)
{
return SurveillanceCase::selectRaw("
sentinel_site_id,
sentinel_site_name,
site_province_name,
surveillance_id,
COUNT(*) as total
")
->join('case_lab_results', 'surveillance_cases.lab_code', '=', 'case_lab_results.lab_code')
->whereIn('surveillance_cases.surveillance_id', [1, 2, 3])
->where('case_lab_results.is_positive', 1)
->whereBetween('surveillance_cases.case_date', [$dateFrom, $dateTo])
->groupBy(
'sentinel_site_id',
'sentinel_site_name',
'site_province_name',
'surveillance_id'
)
->get();
}
public function provinceCircles($dateFrom, $dateTo)
{
return SurveillanceCase::selectRaw("
surveillance_cases.site_province_name,
surveillance_cases.surveillance_id,
COUNT(*) as total
")
->join(
'case_lab_results',
'surveillance_cases.lab_code',
'=',
'case_lab_results.lab_code'
)
->whereIn('surveillance_cases.surveillance_id', [1, 2, 3])
->where('case_lab_results.is_positive', 1)
->whereBetween('surveillance_cases.case_date', [$dateFrom, $dateTo])
->groupBy(
'surveillance_cases.site_province_name',
'surveillance_cases.surveillance_id'
)
->get();
}
/** /**
* Pathogen distribution (positive only) * Pathogen distribution (positive only)

View File

@@ -3,6 +3,8 @@
namespace Database\Seeders; namespace Database\Seeders;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Database\Seeders\LongitudinalSurveillanceSeeder;
use Database\Seeders\SurveillanceSeeder;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder
{ {

File diff suppressed because one or more lines are too long

View File

@@ -10,6 +10,31 @@
<h3 class="fw-bold mb-1">Dashboard Overview</h3> <h3 class="fw-bold mb-1">Dashboard Overview</h3>
<small class="text-muted">National surveillance summary</small> <small class="text-muted">National surveillance summary</small>
</div> </div>
<div class="d-flex align-items-center gap-2 mb-3">
<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> </div>
<!-- Summary Cards --> <!-- Summary Cards -->
@@ -24,14 +49,11 @@
<div class="card shadow-sm mb-4"> <div class="card shadow-sm mb-4">
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="mb-3">
<h5 class="fw-bold mb-0">Epidemic Trend</h5> <h5 class="fw-bold mb-1">Epidemic Trend</h5>
<p class="text-muted small mb-0">
<select id="period_type" class="form-select w-auto"> Weekly reported cases by surveillance program
<option value="week">Epiweek</option> </p>
<option value="month">Month</option>
<option value="year">Year</option>
</select>
</div> </div>
<canvas id="trendChart" height="110"></canvas> <canvas id="trendChart" height="110"></canvas>
@@ -64,9 +86,12 @@
<div class="card-body"> <div class="card-body">
<h5 class="fw-bold">Cases by Provinces</h5> <h5 class="fw-bold">Cases by Provinces</h5>
<p class="text-muted small">(% change vs last period)</p> <p class="text-muted small">(% change vs last period)</p>
<div class="d-flex justify-content-center align-items-center" style="height: 100%;"> <div id="provinceMap" style="height:420px;"></div>
<span class="text-muted">Province heatmap coming next</span> <div class="d-flex gap-3 mb-2 small">
<span><span style="color:#2563eb; font-size:2rem;"></span> SARI</span>
<span><span style="color:#10b981; font-size:2rem;"></span> ILI</span>
<span><span style="color:#9333ea; font-size:2rem;"></span> LBM</span>
</div> </div>
</div> </div>
</div> </div>
@@ -81,7 +106,15 @@
let trendChart; let trendChart;
function loadSummary(dateFrom, dateTo) { function loadSummary() {
const today = new Date();
const past = new Date();
past.setDate(today.getDate() - 7);
const dateFrom = past.toISOString().split('T')[0];
const dateTo = today.toISOString().split('T')[0];
fetch(`/api/dashboard/summary?date_from=${dateFrom}&date_to=${dateTo}`) fetch(`/api/dashboard/summary?date_from=${dateFrom}&date_to=${dateTo}`)
.then(res => res.json()) .then(res => res.json())
@@ -91,38 +124,88 @@
data.forEach(item => { data.forEach(item => {
const colorClass = item.percent_change >= 0 let trendColor = 'text-secondary';
? 'text-danger'
: 'text-success';
html += ` if (item.percent_change > 0) trendColor = 'text-danger';
<div class="col-md-4 mb-3"> if (item.percent_change < 0) trendColor = 'text-success';
<div class="card shadow-sm h-100">
<div class="card-body"> html += `<div class="col-md-2 mb-3">
<h6 class="fw-bold">${item.code}</h6> <div class="card shadow-sm h-100">
<h3 class="mb-1">${item.current_total}</h3> <div class="card-body">
<small class="${colorClass}">
${item.percent_change}% vs last period <div class="d-flex justify-content-between">
</small>
<div class="small text-muted mt-1"> <div>
+${item.last_24h} in 24h <h6 class="fw-bold">${item.code}</h6>
</div> <h3 class="mb-1">${item.current_total}</h3>
</div> <small class="text-muted">Last 7 days</small>
</div> </div>
</div>
`; <div class="text-end">
<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>
`;
}); });
document.getElementById('summary_cards').innerHTML = html; document.getElementById('summary_cards').innerHTML = html;
}); });
} }
function getCurrentEpiWeek() {
function loadTrend(periodType, dateFrom, dateTo) { const today = new Date();
const firstJan = new Date(today.getFullYear(), 0, 1);
fetch(`/api/dashboard/trend?period_type=${periodType}&date_from=${dateFrom}&date_to=${dateTo}`) const days = Math.floor((today - firstJan) / 86400000);
const week = Math.ceil((days + firstJan.getDay() + 1) / 7);
return {
year: today.getFullYear(),
week: week
};
}
function calculateRange(weeksBack) {
const current = getCurrentEpiWeek();
let endYear = current.year;
let endWeek = current.week;
let startYear = endYear;
let startWeek = endWeek - weeksBack + 1;
while (startWeek <= 0) {
startWeek += 52;
startYear--;
}
return {
startYear,
startWeek,
endYear,
endWeek
};
}
function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
fetch(`/api/dashboard/trend?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 => {
console.log(data);
if (trendChart) trendChart.destroy(); if (trendChart) trendChart.destroy();
@@ -143,18 +226,31 @@
}); });
}); });
const labels = Array.from(labelsSet).sort(); 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;
});
const colors = { const colors = {
SARI: '#2563eb', SARI: '#2563eb',
ILI: '#10b981', ILI: '#10b981',
LBM: '#9333ea' LBM: '#9333ea',
NDS: '#f59e0b',
}; };
const datasets = []; const datasets = [];
const allowedPrograms = ['SARI', 'ILI', 'LBM', 'NDS'];
Object.keys(data).forEach(code => { Object.keys(data).forEach(code => {
if (!allowedPrograms.includes(code)) return;
const values = labels.map(label => { const values = labels.map(label => {
const found = data[code].find(row => { const found = data[code].find(row => {
@@ -169,7 +265,6 @@
return rowLabel === label; return rowLabel === label;
}); });
return found ? found.total : 0; return found ? found.total : 0;
}); });
@@ -177,49 +272,289 @@
label: code, label: code,
data: values, data: values,
borderColor: colors[code] || '#000', borderColor: colors[code] || '#000',
backgroundColor: colors[code],
borderWidth: 3,
pointRadius: 4,
fill: false, fill: false,
tension: 0.3 tension: 0.3
}); });
}); });
const displayLabels = labels.map(l => {
const [year, week] = l.split('-');
return `W${String(week).padStart(2, '0')}`;
});
trendChart = new Chart(document.getElementById('trendChart'), { trendChart = new Chart(document.getElementById('trendChart'), {
type: 'line', type: 'line',
data: { data: {
labels: labels, labels: displayLabels,
datasets: datasets datasets: datasets
}, },
options: { options: {
responsive: true, responsive: true,
plugins: { plugins: {
legend: { legend: {
position: 'bottom' position: 'bottom'
} }
},
interaction: {
mode: 'index',
intersect: false
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
},
x: {
grid: {
display: false
}
}
} }
} }
}); });
document.getElementById('trendChart').style.opacity = 1;
}); });
} }
function toggleCustomInputs(show) {
const container = document.getElementById('custom_range_container');
if (!container) return;
container.style.display = show ? 'flex' : 'none';
}
function refreshTrend() {
const startYear = document.getElementById('start_year').value;
const startWeek = document.getElementById('start_week').value;
const endYear = document.getElementById('end_year').value;
const endWeek = document.getElementById('end_week').value;
loadProvinceMap();
loadTrend('week', startYear, startWeek, endYear, endWeek);
}
function populateFilters() {
const currentYear = new Date().getFullYear();
const startYear = document.getElementById('start_year');
const endYear = document.getElementById('end_year');
for (let y = currentYear - 10 ; y <= currentYear; y++) {
startYear.innerHTML += `<option value="${y}">${y}</option>`;
endYear.innerHTML += `<option value="${y}">${y}</option>`;
}
const startWeek = document.getElementById('start_week');
const endWeek = document.getElementById('end_week');
for (let w = 1; w <= 53; w++) {
startWeek.innerHTML += `<option value="${w}">W${w}</option>`;
endWeek.innerHTML += `<option value="${w}">W${w}</option>`;
}
}
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const today = new Date().toISOString().split('T')[0]; populateFilters();
const past = new Date(); toggleCustomInputs(false);
past.setDate(past.getDate() - 30); document.getElementById('custom_range_container').style.display = 'none';
loadProvinceMap();
const dateFrom = past.toISOString().split('T')[0]; const defaultRange = calculateRange(8);
const dateTo = today;
loadSummary(dateFrom, dateTo); document.getElementById('start_year').value = defaultRange.startYear;
loadTrend('week', dateFrom, dateTo); document.getElementById('start_week').value = defaultRange.startWeek;
document.getElementById('period_type') document.getElementById('end_year').value = defaultRange.endYear;
document.getElementById('end_week').value = defaultRange.endWeek;
loadTrend(
'week',
defaultRange.startYear,
defaultRange.startWeek,
defaultRange.endYear,
defaultRange.endWeek
);
loadSummary();
document.getElementById('trend_range')
.addEventListener('change', function () { .addEventListener('change', function () {
loadTrend(this.value, dateFrom, dateTo);
const value = this.value;
if (value === 'custom') {
toggleCustomInputs(true);
return;
}
toggleCustomInputs(false);
const range = calculateRange(parseInt(value));
document.getElementById('start_year').value = range.startYear;
document.getElementById('start_week').value = range.startWeek;
document.getElementById('end_year').value = range.endYear;
document.getElementById('end_week').value = range.endWeek;
document.getElementById('trendChart').style.opacity = 0.4;
refreshTrend();
});
['start_year', 'start_week', 'end_year', 'end_week']
.forEach(id => {
document.getElementById(id)
.addEventListener('change', refreshTrend);
}); });
}); });
let map;
function getColor(value) {
if (value > 50) return '#b91c1c';
if (value > 25) return '#dc2626';
if (value > 10) return '#f97316';
if (value > 5) return '#facc15';
return '#e5e7eb';
}
const colors = {
1: '#2563eb', // SARI
2: '#10b981', // ILI
3: '#9333ea' // LBM
};
function loadSentinelMap() {
fetch(`/api/dashboard/sentinel-map?...`)
.then(r => r.json())
.then(data => {
data.forEach(site => {
L.circleMarker(
[site.lat, site.lng],
{
radius: Math.sqrt(site.total) * 3,
fillColor: colors[site.surveillance_id],
color: '#fff',
weight: 1,
fillOpacity: 0.9
}
)
.bindTooltip(`
${site.sentinel_site_name}<br>
Cases: ${site.total}
`)
.addTo(map);
});
});
}
function loadProvinceMap() {
if (map) map.remove();
map = L.map('provinceMap').setView([12.7, 104.9], 7);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap'
}).addTo(map);
const startYear = document.getElementById('start_year').value;
const startWeek = document.getElementById('start_week').value;
const endYear = document.getElementById('end_year').value;
const endWeek = document.getElementById('end_week').value;
const colors = {
1: '#2563eb', // SARI
2: '#10b981', // ILI
3: '#9333ea' // LBM
};
Promise.all([
fetch('/geo/cambodia_provinces.geojson').then(r => r.json()),
fetch(`/api/dashboard/province-circles?start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`).then(r => r.json())
])
.then(([geojson, data]) => {
L.geoJSON(geojson, {
style: {
fillOpacity: 0,
color: '#ccc',
weight: 1
},
onEachFeature: function (feature, layer) {
const province = feature.properties.ADM1_EN;
const center = layer.getBounds().getCenter();
const rows = data.filter(d => d.site_province_name === province);
const offsets = {
1: -0.15, // SARI left
2: 0, // ILI center
3: 0.15 // LBM right
};
rows.forEach(row => {
const radius = Math.sqrt(row.total) * 4;
const lat = center.lat;
const lng = center.lng + offsets[row.surveillance_id];
L.circleMarker([lat, lng], {
radius: radius,
fillColor: colors[row.surveillance_id],
color: '#fff',
weight: 1,
fillOpacity: 0.9
})
.bindTooltip(`
<strong>${province}</strong><br>
${row.surveillance_id === 1 ? 'SARI' :
row.surveillance_id === 2 ? 'ILI' : 'LBM'}<br>
Cases: ${row.total}
`)
.addTo(map);
});
}
}).addTo(map);
});
}
</script> </script>
@endsection @endsection

View File

@@ -7,66 +7,67 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<style> <style>
body { body {
margin: 0; margin: 0;
} }
/* SIDEBAR */ /* HEADER */
.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 { .top-navbar {
height: 60px; height: 60px;
border-bottom: 4px solid #0B8F3C; background: #0B8F3C;
background: #FFFFFF; color: white;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 20px; padding: 0 25px;
} }
.brand-title { .brand-title {
font-weight: 600; font-weight: 600;
font-size: 18px; font-size: 18px;
color: #1E63B6; }
/* NAV BAR */
.nav-bar {
display: flex;
background: white;
border-bottom: 1px solid #dcdcdc;
padding-left: 15px;
}
/* NAV ITEMS */
.nav-item {
padding: 12px 18px;
text-decoration: none;
color: #262626;
font-weight: 500;
border-bottom: 3px solid transparent;
font-size:14px;
}
.nav-item:hover {
background: #cce0d4;
}
/* ACTIVE TAB */
.active-tab {
color: #0B8F3C;
border-bottom: 3px solid #0B8F3C;
background: #e5efe8;
}
/* CONTENT */
.content-area {
padding: 25px;
}
.brand-title {
font-weight: 600;
font-size: 18px;
color: #f8f9fa;
} }
.content-area { .content-area {
@@ -95,50 +96,40 @@
<body> <body>
<!-- Sidebar --> <!-- TOP HEADER -->
<div class="sidebar d-flex flex-column justify-content-between"> <div class="top-navbar">
<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 class="brand-title">
National Reference Medical Laboratory Surveillance Dashboard
</div> </div>
<div class="mb-3"> <div class="ms-auto small">
<a href="#" class="nav-link-custom"> Last update: 12:05 |
<span class="nav-icon">⚙️</span> Data latency: 510 min |
<span class="nav-text">Settings</span> User: National - Read Only
</a>
</div> </div>
</div> </div>
<!-- NAVIGATION BAR -->
<div class="nav-bar">
<a href="/dashboard" class="nav-item {{ request()->is('dashboard') ? 'active-tab' : '' }}">
Overview
</a>
@foreach($programs as $program)
<a href="/dashboard/{{ strtolower($program->code) }}"
class="nav-item {{ request()->is('dashboard/' . strtolower($program->code)) ? 'active-tab' : '' }}">
{{ $program->code }}
</a>
@endforeach
</div>
<!-- Main Wrapper --> <!-- Main Wrapper -->
<div class="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 --> <!-- Page Content -->
<div class="content-area"> <div class="content-area">

View File

@@ -6,4 +6,6 @@ use App\Http\Controllers\Api\DashboardController;
Route::get('/dashboard/summary', [DashboardController::class, 'summary']); Route::get('/dashboard/summary', [DashboardController::class, 'summary']);
Route::get('/dashboard/trend', [DashboardController::class, 'trend']); Route::get('/dashboard/trend', [DashboardController::class, 'trend']);
Route::get('/dashboard/province', [DashboardController::class, 'province']); Route::get('/dashboard/province', [DashboardController::class, 'province']);
Route::get('/dashboard/pathogen', [DashboardController::class, 'pathogen']); Route::get('/dashboard/pathogen', [DashboardController::class, 'pathogen']);
Route::get('/dashboard/sentinel-map', [DashboardController::class, 'sentinelMap']);
Route::get('/dashboard/province-circles', [DashboardController::class, 'provinceCircles']);

View File

@@ -39,7 +39,7 @@ services:
ports: ports:
- "3308:3306" - "3308:3306"
volumes: volumes:
- dbdata:/var/lib/mysql - ./docker/mysql/init.sql:/docker-entrypoint-initdb.d/dashboard.sql
networks: networks:
- dashboard - dashboard
@@ -62,5 +62,6 @@ services:
networks: networks:
dashboard: dashboard:
volumes: volumes:
dbdata: dbdata: