feat(dashboard): add sidebar, navbar, dynamic routes, summary & trend API, NIPH theme
This commit is contained in:
92
dashboard/app/Http/Controllers/Api/DashboardController.php
Normal file
92
dashboard/app/Http/Controllers/Api/DashboardController.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\DashboardService;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
protected $service;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$programs = \App\Models\Surveillance::all();
|
||||
return view('dashboard.index', compact('programs'));
|
||||
}
|
||||
public function __construct(DashboardService $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary cards
|
||||
* GET /api/dashboard/summary?date_from=2026-01-01&date_to=2026-03-01
|
||||
*/
|
||||
public function summary(Request $request)
|
||||
{
|
||||
$dateFrom = $request->query('date_from', Carbon::now()->subDays(7)->toDateString());
|
||||
$dateTo = $request->query('date_to', Carbon::now()->toDateString());
|
||||
|
||||
$data = $this->service->summaryCards($dateFrom, $dateTo);
|
||||
|
||||
return response()->json($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trend chart
|
||||
* GET /api/dashboard/trend?surveillance_id=1&period_type=week&date_from=...&date_to=...
|
||||
*/
|
||||
public function trend(Request $request)
|
||||
{
|
||||
$periodType = $request->query('period_type', 'week');
|
||||
$dateFrom = $request->query('date_from');
|
||||
$dateTo = $request->query('date_to');
|
||||
|
||||
$data = $this->service->aggregateAllPrograms(
|
||||
$periodType,
|
||||
$dateFrom,
|
||||
$dateTo
|
||||
);
|
||||
|
||||
return response()->json($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Province distribution
|
||||
*/
|
||||
public function province(Request $request)
|
||||
{
|
||||
$surveillanceId = $request->query('surveillance_id');
|
||||
$dateFrom = $request->query('date_from');
|
||||
$dateTo = $request->query('date_to');
|
||||
|
||||
$data = $this->service->provinceDistribution(
|
||||
$surveillanceId,
|
||||
$dateFrom,
|
||||
$dateTo
|
||||
);
|
||||
|
||||
return response()->json($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pathogen distribution
|
||||
*/
|
||||
public function pathogen(Request $request)
|
||||
{
|
||||
$surveillanceId = $request->query('surveillance_id');
|
||||
$dateFrom = $request->query('date_from');
|
||||
$dateTo = $request->query('date_to');
|
||||
|
||||
$data = $this->service->pathogenDistribution(
|
||||
$surveillanceId,
|
||||
$dateFrom,
|
||||
$dateTo
|
||||
);
|
||||
|
||||
return response()->json($data);
|
||||
}
|
||||
}
|
||||
8
dashboard/app/Http/Controllers/Controller.php
Normal file
8
dashboard/app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
20
dashboard/app/Http/Controllers/DashboardController.php
Normal file
20
dashboard/app/Http/Controllers/DashboardController.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Surveillance;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function overview()
|
||||
{
|
||||
$programs = Surveillance::all();
|
||||
return view('dashboard.overview', compact('programs'));
|
||||
}
|
||||
|
||||
public function detail($code)
|
||||
{
|
||||
$selected = Surveillance::where('code', strtoupper($code))->firstOrFail();
|
||||
return view('dashboard.detail', compact('selected'));
|
||||
}
|
||||
}
|
||||
19
dashboard/app/Models/CaseLabResult.php
Normal file
19
dashboard/app/Models/CaseLabResult.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CaseLabResult extends Model
|
||||
{
|
||||
protected $table = 'case_lab_results';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'lab_code',
|
||||
'is_positive',
|
||||
'pathogen_name',
|
||||
'subtype',
|
||||
'indicator'
|
||||
];
|
||||
}
|
||||
16
dashboard/app/Models/LastSyncedLog.php
Normal file
16
dashboard/app/Models/LastSyncedLog.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class LastSyncedLog extends Model
|
||||
{
|
||||
protected $table = 'last_synced_logs';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'surveillance_id',
|
||||
'last_synced_date'
|
||||
];
|
||||
}
|
||||
24
dashboard/app/Models/Surveillance.php
Normal file
24
dashboard/app/Models/Surveillance.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Surveillance extends Model
|
||||
{
|
||||
protected $table = 'surveillances';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'name_en',
|
||||
'name_kh',
|
||||
'start_date',
|
||||
'end_date'
|
||||
];
|
||||
|
||||
public function cases()
|
||||
{
|
||||
return $this->hasMany(SurveillanceCase::class, 'surveillance_id');
|
||||
}
|
||||
}
|
||||
31
dashboard/app/Models/SurveillanceCase.php
Normal file
31
dashboard/app/Models/SurveillanceCase.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SurveillanceCase extends Model
|
||||
{
|
||||
protected $table = 'surveillance_cases';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'lab_code',
|
||||
'case_date',
|
||||
'is_newcase',
|
||||
'sentinel_site_name',
|
||||
'site_province_name',
|
||||
'surveillance_id',
|
||||
'year_data',
|
||||
'week_data',
|
||||
'patient_age_inday',
|
||||
'patient_sex',
|
||||
'is_alive',
|
||||
'patient_privince'
|
||||
];
|
||||
|
||||
public function surveillance()
|
||||
{
|
||||
return $this->belongsTo(Surveillance::class, 'surveillance_id');
|
||||
}
|
||||
}
|
||||
48
dashboard/app/Models/User.php
Normal file
48
dashboard/app/Models/User.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
28
dashboard/app/Providers/AppServiceProvider.php
Normal file
28
dashboard/app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use App\Models\Surveillance;
|
||||
use Illuminate\Support\Facades\View;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
View::composer('layouts.app', function ($view) {
|
||||
$view->with('programs', Surveillance::orderBy('id')->get());
|
||||
});
|
||||
}
|
||||
}
|
||||
248
dashboard/app/Services/DashboardService.php
Normal file
248
dashboard/app/Services/DashboardService.php
Normal file
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\Surveillance;
|
||||
use App\Models\SurveillanceCase;
|
||||
use App\Models\CaseLabResult;
|
||||
|
||||
class DashboardService
|
||||
{
|
||||
/**
|
||||
* Get all surveillance programs
|
||||
*/
|
||||
public function getPrograms()
|
||||
{
|
||||
return Surveillance::orderBy('id')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary cards (dynamic)
|
||||
*/
|
||||
public function summaryCards($dateFrom, $dateTo)
|
||||
{
|
||||
$programs = $this->getPrograms();
|
||||
$results = [];
|
||||
|
||||
$days = Carbon::parse($dateFrom)->diffInDays($dateTo);
|
||||
|
||||
foreach ($programs as $program) {
|
||||
|
||||
$current = SurveillanceCase::where('surveillance_id', $program->id)
|
||||
->whereBetween('case_date', [$dateFrom, $dateTo])
|
||||
->count();
|
||||
|
||||
$previousFrom = Carbon::parse($dateFrom)->subDays($days + 1);
|
||||
$previousTo = Carbon::parse($dateFrom)->subDay();
|
||||
|
||||
$previous = SurveillanceCase::where('surveillance_id', $program->id)
|
||||
->whereBetween('case_date', [$previousFrom, $previousTo])
|
||||
->count();
|
||||
|
||||
$percentChange = $previous > 0
|
||||
? round((($current - $previous) / $previous) * 100, 1)
|
||||
: 0;
|
||||
|
||||
$last24h = SurveillanceCase::where('surveillance_id', $program->id)
|
||||
->where('case_date', '>=', Carbon::now()->subDay())
|
||||
->count();
|
||||
|
||||
$results[] = [
|
||||
'surveillance_id' => $program->id,
|
||||
'code' => $program->code,
|
||||
'name_en' => $program->name_en,
|
||||
'name_kh' => $program->name_kh,
|
||||
'current_total' => $current,
|
||||
'percent_change' => $percentChange,
|
||||
'last_24h' => $last24h,
|
||||
];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate cases by period
|
||||
* periodType: week | month | year
|
||||
*/
|
||||
|
||||
public function aggregateAllPrograms($periodType, $dateFrom, $dateTo)
|
||||
{
|
||||
$programs = Surveillance::all();
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($programs as $program) {
|
||||
|
||||
$query = SurveillanceCase::where('surveillance_id', $program->id)
|
||||
->whereBetween('case_date', [$dateFrom, $dateTo]);
|
||||
|
||||
switch ($periodType) {
|
||||
|
||||
case 'week':
|
||||
$query->selectRaw("
|
||||
YEAR(case_date) as year,
|
||||
WEEK(case_date, 3) as period,
|
||||
COUNT(*) as total
|
||||
")
|
||||
->groupByRaw("YEAR(case_date), WEEK(case_date, 3)")
|
||||
->orderByRaw("YEAR(case_date), WEEK(case_date, 3)");
|
||||
break;
|
||||
|
||||
case 'month':
|
||||
$query->selectRaw("
|
||||
YEAR(case_date) as year,
|
||||
MONTH(case_date) as period,
|
||||
COUNT(*) as total
|
||||
")
|
||||
->groupByRaw("YEAR(case_date), MONTH(case_date)")
|
||||
->orderByRaw("YEAR(case_date), MONTH(case_date)");
|
||||
break;
|
||||
|
||||
case 'year':
|
||||
$query->selectRaw("
|
||||
YEAR(case_date) as period,
|
||||
COUNT(*) as total
|
||||
")
|
||||
->groupByRaw("YEAR(case_date)")
|
||||
->orderByRaw("YEAR(case_date)");
|
||||
break;
|
||||
}
|
||||
|
||||
$results[$program->code] = $query->get();
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
public function aggregateCases($surveillanceId, $periodType, $dateFrom, $dateTo)
|
||||
{
|
||||
$query = SurveillanceCase::where('surveillance_id', $surveillanceId)
|
||||
->whereBetween('case_date', [$dateFrom, $dateTo]);
|
||||
|
||||
switch ($periodType) {
|
||||
|
||||
case 'week':
|
||||
$query->selectRaw("
|
||||
YEAR(case_date) as year,
|
||||
WEEK(case_date, 3) as period,
|
||||
COUNT(*) as total
|
||||
")
|
||||
->groupByRaw("YEAR(case_date), WEEK(case_date, 3)")
|
||||
->orderByRaw("YEAR(case_date), WEEK(case_date, 3)");
|
||||
break;
|
||||
|
||||
case 'month':
|
||||
$query->selectRaw("
|
||||
YEAR(case_date) as year,
|
||||
MONTH(case_date) as period,
|
||||
COUNT(*) as total
|
||||
")
|
||||
->groupByRaw("YEAR(case_date), MONTH(case_date)")
|
||||
->orderByRaw("YEAR(case_date), MONTH(case_date)");
|
||||
break;
|
||||
|
||||
case 'year':
|
||||
$query->selectRaw("
|
||||
YEAR(case_date) as period,
|
||||
COUNT(*) as total
|
||||
")
|
||||
->groupByRaw("YEAR(case_date)")
|
||||
->orderByRaw("YEAR(case_date)");
|
||||
break;
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Province distribution
|
||||
*/
|
||||
public function provinceDistribution($surveillanceId, $dateFrom, $dateTo)
|
||||
{
|
||||
return SurveillanceCase::selectRaw("
|
||||
site_province_name,
|
||||
COUNT(*) as total
|
||||
")
|
||||
->where('surveillance_id', $surveillanceId)
|
||||
->whereBetween('case_date', [$dateFrom, $dateTo])
|
||||
->groupBy('site_province_name')
|
||||
->orderByDesc('total')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pathogen distribution (positive only)
|
||||
*/
|
||||
public function pathogenDistribution($surveillanceId, $dateFrom, $dateTo)
|
||||
{
|
||||
return CaseLabResult::selectRaw("
|
||||
case_lab_results.pathogen_name,
|
||||
COUNT(*) as total
|
||||
")
|
||||
->join('surveillance_cases', 'case_lab_results.lab_code', '=', 'surveillance_cases.lab_code')
|
||||
->where('surveillance_cases.surveillance_id', $surveillanceId)
|
||||
->whereBetween('surveillance_cases.case_date', [$dateFrom, $dateTo])
|
||||
->where('case_lab_results.is_positive', 1)
|
||||
->groupBy('case_lab_results.pathogen_name')
|
||||
->orderByDesc('total')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Positivity rate
|
||||
*/
|
||||
public function positivityRate($surveillanceId, $dateFrom, $dateTo)
|
||||
{
|
||||
$total = CaseLabResult::join('surveillance_cases', 'case_lab_results.lab_code', '=', 'surveillance_cases.lab_code')
|
||||
->where('surveillance_cases.surveillance_id', $surveillanceId)
|
||||
->whereBetween('surveillance_cases.case_date', [$dateFrom, $dateTo])
|
||||
->count();
|
||||
|
||||
$positive = CaseLabResult::join('surveillance_cases', 'case_lab_results.lab_code', '=', 'surveillance_cases.lab_code')
|
||||
->where('surveillance_cases.surveillance_id', $surveillanceId)
|
||||
->whereBetween('surveillance_cases.case_date', [$dateFrom, $dateTo])
|
||||
->where('case_lab_results.is_positive', 1)
|
||||
->count();
|
||||
|
||||
return $total > 0
|
||||
? round(($positive / $total) * 100, 1)
|
||||
: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Age distribution (grouped)
|
||||
*/
|
||||
public function ageDistribution($surveillanceId, $dateFrom, $dateTo)
|
||||
{
|
||||
return SurveillanceCase::selectRaw("
|
||||
CASE
|
||||
WHEN patient_age_inday < 365 THEN '0-1y'
|
||||
WHEN patient_age_inday < 1825 THEN '1-5y'
|
||||
WHEN patient_age_inday < 6570 THEN '5-18y'
|
||||
WHEN patient_age_inday < 21900 THEN '18-60y'
|
||||
ELSE '60+y'
|
||||
END as age_group,
|
||||
COUNT(*) as total
|
||||
")
|
||||
->where('surveillance_id', $surveillanceId)
|
||||
->whereBetween('case_date', [$dateFrom, $dateTo])
|
||||
->groupBy('age_group')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sex distribution
|
||||
*/
|
||||
public function sexDistribution($surveillanceId, $dateFrom, $dateTo)
|
||||
{
|
||||
return SurveillanceCase::selectRaw("
|
||||
patient_sex,
|
||||
COUNT(*) as total
|
||||
")
|
||||
->where('surveillance_id', $surveillanceId)
|
||||
->whereBetween('case_date', [$dateFrom, $dateTo])
|
||||
->groupBy('patient_sex')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user