feat: add clinical drill-down dashboards
Add interactive OPD, IPD, and diagnosis dashboards with historical date ranges, KPI summaries, doctor and department workload, referral breakdowns, and diagnostic workflow statuses.
This commit is contained in:
parent
9872e1c726
commit
bd9b1771a9
|
|
@ -7,6 +7,9 @@ use CodeIgniter\Router\RouteCollection;
|
|||
*/
|
||||
$routes->get('/', 'Home::index');
|
||||
$routes->get('dashboard', 'Home::dashboard');
|
||||
$routes->get('dashboard/opd', 'Home::opdDashboard');
|
||||
$routes->get('dashboard/ipd', 'Home::ipdDashboard');
|
||||
$routes->get('dashboard/diagnosis', 'Home::diagnosisDashboard');
|
||||
$routes->get('my-profile', 'Home::myProfile');
|
||||
$routes->post('my-profile/save', 'Home::myProfileSave');
|
||||
$routes->post('my-profile/save-settings', 'Home::myProfileSaveSettings');
|
||||
|
|
|
|||
|
|
@ -96,6 +96,24 @@ class Home extends BaseController
|
|||
->where('i.ipd_status', 0)
|
||||
->countAllResults();
|
||||
|
||||
$diagnosisToday = $this->db->table('invoice_item ii')
|
||||
->join('invoice_master im', 'im.id = ii.inv_master_id', 'inner')
|
||||
->whereIn('ii.item_type', [1, 2, 3, 4, 5, 6, 30])
|
||||
->where('im.invoice_status', 1)
|
||||
->where('im.inv_date >=', $today)
|
||||
->where('im.inv_date <', date('Y-m-d', strtotime('+1 day')))
|
||||
->countAllResults();
|
||||
|
||||
$diagnosisCompletedToday = $this->db->table('invoice_item ii')
|
||||
->join('invoice_master im', 'im.id = ii.inv_master_id', 'inner')
|
||||
->join('lab_request lr', 'lr.charge_item_id = ii.id AND lr.charge_id = im.id', 'left')
|
||||
->whereIn('ii.item_type', [1, 2, 3, 4, 5, 6, 30])
|
||||
->where('im.invoice_status', 1)
|
||||
->where('im.inv_date >=', $today)
|
||||
->where('im.inv_date <', date('Y-m-d', strtotime('+1 day')))
|
||||
->where('lr.status', 2)
|
||||
->countAllResults();
|
||||
|
||||
$opdOrgList = $this->db->table('opd_master o')
|
||||
->select('ins.short_name as org_name')
|
||||
->select('COUNT(o.opd_id) as total_cases', false)
|
||||
|
|
@ -206,6 +224,8 @@ class Home extends BaseController
|
|||
'discharge_today' => $dischargeToday,
|
||||
'current_ipd' => $currentIpd,
|
||||
'current_org_ipd' => $currentOrgIpd,
|
||||
'diagnosis_today' => $diagnosisToday,
|
||||
'diagnosis_completed_today' => $diagnosisCompletedToday,
|
||||
'opd_org_list' => $opdOrgList,
|
||||
'opd_doctor_list' => $opdDoctorList,
|
||||
'ipd_doctor_list' => $ipdDoctorList,
|
||||
|
|
@ -219,6 +239,168 @@ class Home extends BaseController
|
|||
return view('dashboard/index', $data);
|
||||
}
|
||||
|
||||
public function opdDashboard()
|
||||
{
|
||||
if (($response = $this->dashboardAccessResponse('dashboard/opd', 'OPD Dashboard')) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
[$fromDate, $toDate, $fromDateTime, $toExclusive] = $this->dashboardDateRange();
|
||||
$base = $this->db->table('opd_master')
|
||||
->where('apointment_date >=', $fromDateTime)
|
||||
->where('apointment_date <', $toExclusive);
|
||||
|
||||
$totals = $this->db->table('opd_master')
|
||||
->select('COUNT(opd_id) total_visits, COUNT(DISTINCT p_id) unique_patients', false)
|
||||
->select('SUM(CASE WHEN opd_status = 2 THEN 1 ELSE 0 END) completed_visits', false)
|
||||
->select('SUM(CASE WHEN opd_status = 3 THEN 1 ELSE 0 END) cancelled_visits', false)
|
||||
->where('apointment_date >=', $fromDateTime)
|
||||
->where('apointment_date <', $toExclusive)
|
||||
->get()->getRowArray() ?? [];
|
||||
|
||||
$doctorRows = (clone $base)
|
||||
->select("COALESCE(NULLIF(doc_name, ''), 'Unassigned') label", false)
|
||||
->select('COUNT(opd_id) total, COUNT(DISTINCT p_id) patients', false)
|
||||
->groupBy('label')->orderBy('total', 'DESC')->get()->getResultArray();
|
||||
$departmentRows = (clone $base)
|
||||
->select("COALESCE(NULLIF(doc_spec, ''), 'Unassigned') label", false)
|
||||
->select('COUNT(opd_id) total, COUNT(DISTINCT p_id) patients', false)
|
||||
->groupBy('label')->orderBy('total', 'DESC')->get()->getResultArray();
|
||||
$organizationRows = (clone $base)
|
||||
->select("CASE WHEN o.insurance_id IS NULL OR o.insurance_id <= 1 THEN 'Direct' ELSE COALESCE(NULLIF(ins.short_name, ''), 'Organization') END label", false)
|
||||
->select('COUNT(o.opd_id) total', false)
|
||||
->join('hc_insurance ins', 'ins.id = o.insurance_id', 'left')
|
||||
->from('opd_master o', true)
|
||||
->groupBy('label')->orderBy('total', 'DESC')->get()->getResultArray();
|
||||
|
||||
$referralRows = $this->db->query(
|
||||
"SELECT COALESCE(NULLIF(CONCAT_WS(' ', NULLIF(r.title, ''), NULLIF(r.f_name, '')), ''), NULLIF(x.refer_name, ''), 'Not recorded') label,
|
||||
COUNT(o.opd_id) total, COUNT(DISTINCT o.p_id) patients
|
||||
FROM opd_master o
|
||||
LEFT JOIN (
|
||||
SELECT opd_code, MAX(NULLIF(refer_by_id, 0)) refer_id,
|
||||
MAX(NULLIF(NULLIF(refer_by_other, ''), '0')) refer_name
|
||||
FROM invoice_master GROUP BY opd_code
|
||||
) x ON x.opd_code = o.opd_code
|
||||
LEFT JOIN refer_master r ON r.id = x.refer_id
|
||||
WHERE o.apointment_date >= ? AND o.apointment_date < ?
|
||||
GROUP BY label ORDER BY total DESC",
|
||||
[$fromDateTime, $toExclusive]
|
||||
)->getResultArray();
|
||||
|
||||
return view('dashboard/opd', compact('fromDate', 'toDate', 'totals', 'doctorRows', 'departmentRows', 'organizationRows', 'referralRows'));
|
||||
}
|
||||
|
||||
public function ipdDashboard()
|
||||
{
|
||||
if (($response = $this->dashboardAccessResponse('dashboard/ipd', 'IPD Dashboard')) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
[$fromDate, $toDate] = $this->dashboardDateRange();
|
||||
$totals = $this->db->table('ipd_master')
|
||||
->select('COUNT(id) admissions', false)
|
||||
->select('SUM(CASE WHEN ipd_status = 0 THEN 1 ELSE 0 END) active_cases', false)
|
||||
->where('register_date >=', $fromDate)->where('register_date <=', $toDate)
|
||||
->get()->getRowArray() ?? [];
|
||||
$totals['discharges'] = $this->db->table('ipd_master')
|
||||
->where('ipd_status', 1)->where('discharge_date >=', $fromDate)->where('discharge_date <=', $toDate)
|
||||
->countAllResults();
|
||||
$totals['current_in_house'] = $this->db->table('ipd_master')->where('ipd_status', 0)->countAllResults();
|
||||
|
||||
$departmentRows = $this->db->table('ipd_master i')
|
||||
->select("COALESCE(NULLIF(d.vName, ''), 'Unassigned') label", false)->select('COUNT(i.id) total', false)
|
||||
->join('hc_department d', 'd.iId = i.dept_id', 'left')
|
||||
->where('i.register_date >=', $fromDate)->where('i.register_date <=', $toDate)
|
||||
->groupBy('label')->orderBy('total', 'DESC')->get()->getResultArray();
|
||||
$doctorRows = $this->db->table('ipd_master i')
|
||||
->select("COALESCE(NULLIF(d.p_fname, ''), NULLIF(i.r_doc_name, ''), 'Unassigned') label", false)
|
||||
->select('COUNT(DISTINCT i.id) total', false)
|
||||
->join('ipd_master_doc_list l', 'l.ipd_id = i.id', 'left')
|
||||
->join('doctor_master d', 'd.id = l.doc_id', 'left')
|
||||
->where('i.register_date >=', $fromDate)->where('i.register_date <=', $toDate)
|
||||
->groupBy('label')->orderBy('total', 'DESC')->get()->getResultArray();
|
||||
$referralRows = $this->db->table('ipd_master i')
|
||||
->select("COALESCE(NULLIF(CONCAT_WS(' ', NULLIF(r.title, ''), NULLIF(r.f_name, '')), ''), 'Not recorded') label", false)
|
||||
->select('COUNT(i.id) total', false)
|
||||
->join('refer_master r', 'r.id = i.refer_by', 'left')
|
||||
->where('i.register_date >=', $fromDate)->where('i.register_date <=', $toDate)
|
||||
->groupBy('label')->orderBy('total', 'DESC')->get()->getResultArray();
|
||||
$organizationRows = $this->db->table('ipd_master i')
|
||||
->select("CASE WHEN i.case_id IS NULL OR i.case_id = 0 THEN 'Direct' ELSE COALESCE(NULLIF(ins.short_name, ''), 'Organization') END label", false)
|
||||
->select('COUNT(i.id) total', false)
|
||||
->join('organization_case_master oc', 'oc.id = i.case_id', 'left')
|
||||
->join('hc_insurance ins', 'ins.id = oc.insurance_id', 'left')
|
||||
->where('i.register_date >=', $fromDate)->where('i.register_date <=', $toDate)
|
||||
->groupBy('label')->orderBy('total', 'DESC')->get()->getResultArray();
|
||||
|
||||
return view('dashboard/ipd', compact('fromDate', 'toDate', 'totals', 'doctorRows', 'departmentRows', 'organizationRows', 'referralRows'));
|
||||
}
|
||||
|
||||
public function diagnosisDashboard()
|
||||
{
|
||||
if (($response = $this->dashboardAccessResponse('dashboard/diagnosis', 'Diagnosis Dashboard')) !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
[$fromDate, $toDate] = $this->dashboardDateRange();
|
||||
$modalityNames = [1 => 'Ultrasound', 2 => 'MRI', 3 => 'X-Ray', 4 => 'CT Scan', 5 => 'Pathology', 6 => 'Echo', 30 => 'Biopsy'];
|
||||
$rows = $this->db->table('invoice_item ii')
|
||||
->select('ii.item_type modality_id')->select('COUNT(DISTINCT ii.id) total', false)
|
||||
->select('SUM(CASE WHEN lr.id IS NOT NULL THEN 1 ELSE 0 END) samples_received', false)
|
||||
->select('SUM(CASE WHEN lr.id IS NULL THEN 1 ELSE 0 END) pending_collection', false)
|
||||
->select('SUM(CASE WHEN lr.id IS NOT NULL AND IFNULL(lr.status, 0) < 2 THEN 1 ELSE 0 END) pending_reports', false)
|
||||
->select('SUM(CASE WHEN lr.status = 2 THEN 1 ELSE 0 END) completed_reports', false)
|
||||
->join('invoice_master im', 'im.id = ii.inv_master_id', 'inner')
|
||||
->join('lab_request lr', 'lr.charge_item_id = ii.id AND lr.charge_id = im.id', 'left')
|
||||
->whereIn('ii.item_type', array_keys($modalityNames))->where('im.invoice_status', 1)
|
||||
->where('im.inv_date >=', $fromDate)->where('im.inv_date <=', $toDate)
|
||||
->groupBy('ii.item_type')->get()->getResultArray();
|
||||
$modalityRows = [];
|
||||
foreach ($modalityNames as $id => $name) {
|
||||
$match = array_values(array_filter($rows, static fn (array $row): bool => (int) $row['modality_id'] === $id))[0] ?? [];
|
||||
$modalityRows[] = array_merge(['modality_id' => $id, 'label' => $name, 'total' => 0, 'samples_received' => 0, 'pending_collection' => 0, 'pending_reports' => 0, 'completed_reports' => 0], $match);
|
||||
}
|
||||
$totals = ['total' => 0, 'samples_received' => 0, 'pending_collection' => 0, 'pending_reports' => 0, 'completed_reports' => 0];
|
||||
foreach ($modalityRows as $row) {
|
||||
foreach (array_keys($totals) as $key) {
|
||||
$totals[$key] += (int) $row[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return view('dashboard/diagnosis', compact('fromDate', 'toDate', 'totals', 'modalityRows'));
|
||||
}
|
||||
|
||||
private function dashboardAccessResponse(string $route, string $title)
|
||||
{
|
||||
if (($authRedirect = $this->ensureAuthenticated()) !== null) {
|
||||
return $authRedirect;
|
||||
}
|
||||
if (! $this->request->isAJAX()) {
|
||||
return view('welcome_message', ['initial_route' => base_url($route), 'initial_title' => $title]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return array{0:string,1:string,2:string,3:string} */
|
||||
private function dashboardDateRange(): array
|
||||
{
|
||||
$today = date('Y-m-d');
|
||||
$fromDate = (string) ($this->request->getGet('from_date') ?: $today);
|
||||
$toDate = (string) ($this->request->getGet('to_date') ?: $fromDate);
|
||||
if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $fromDate) || strtotime($fromDate) === false) {
|
||||
$fromDate = $today;
|
||||
}
|
||||
if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $toDate) || strtotime($toDate) === false) {
|
||||
$toDate = $fromDate;
|
||||
}
|
||||
if ($fromDate > $toDate) {
|
||||
[$fromDate, $toDate] = [$toDate, $fromDate];
|
||||
}
|
||||
return [$fromDate, $toDate, $fromDate . ' 00:00:00', date('Y-m-d', strtotime($toDate . ' +1 day')) . ' 00:00:00'];
|
||||
}
|
||||
|
||||
public function myProfile()
|
||||
{
|
||||
$authRedirect = $this->ensureAuthenticated();
|
||||
|
|
|
|||
15
app/Views/dashboard/diagnosis.php
Normal file
15
app/Views/dashboard/diagnosis.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
$totalTests = (int) ($totals['total'] ?? 0);
|
||||
$samplesReceived = (int) ($totals['samples_received'] ?? 0);
|
||||
$pendingCollection = (int) ($totals['pending_collection'] ?? 0);
|
||||
$pendingReports = (int) ($totals['pending_reports'] ?? 0);
|
||||
$completedReports = (int) ($totals['completed_reports'] ?? 0);
|
||||
?>
|
||||
<div class="pagetitle d-flex align-items-center justify-content-between flex-wrap gap-2"><div><h1>Diagnosis Dashboard</h1><div class="text-muted small">Pathology and imaging workload from order to verified report</div></div><button class="btn btn-light" type="button" onclick="load_form('<?= base_url('dashboard') ?>','Dashboard');"><i class="bi bi-arrow-left"></i> Main Dashboard</button></div>
|
||||
<section class="section dashboard dashboard-detail">
|
||||
<div class="card dashboard-filter-card"><div class="card-body py-3"><div class="row g-2 align-items-end"><div class="col-sm-4 col-lg-3"><label class="form-label" for="diagnosis_from_date">From date</label><input class="form-control" id="diagnosis_from_date" type="date" value="<?= esc($fromDate) ?>"></div><div class="col-sm-4 col-lg-3"><label class="form-label" for="diagnosis_to_date">To date</label><input class="form-control" id="diagnosis_to_date" type="date" value="<?= esc($toDate) ?>"></div><div class="col-sm-4 col-lg-2"><button class="btn btn-primary w-100" id="diagnosis_apply_range" type="button"><i class="bi bi-funnel"></i> Apply</button></div><div class="col-lg-4"><div class="btn-group w-100"><button class="btn btn-outline-secondary diagnosis-range" data-days="0" type="button">Today</button><button class="btn btn-outline-secondary diagnosis-range" data-days="6" type="button">7 days</button><button class="btn btn-outline-secondary diagnosis-range" data-days="29" type="button">30 days</button></div></div></div></div></div>
|
||||
<div class="row g-3 mb-3"><?php foreach ([['Tests ordered', $totalTests, 'bi-clipboard2-pulse', 'primary'], ['Samples / scans received', $samplesReceived, 'bi-inbox', 'info'], ['Pending collection', $pendingCollection, 'bi-hourglass', 'warning'], ['Pending reports', $pendingReports, 'bi-file-earmark-medical', 'danger'], ['Completed reports', $completedReports, 'bi-patch-check', 'success']] as [$label, $value, $icon, $tone]) : ?><div class="col-6 col-xl"><div class="card h-100 dashboard-kpi"><div class="card-body"><div class="dashboard-kpi-icon text-bg-<?= $tone ?>"><i class="bi <?= $icon ?>"></i></div><div><div class="dashboard-kpi-value"><?= $value ?></div><div class="text-muted small"><?= esc($label) ?></div></div></div></div></div><?php endforeach ?></div>
|
||||
<div class="card"><div class="card-body"><h5 class="card-title">Department status</h5><div class="table-responsive"><table class="table align-middle dashboard-status-table"><thead><tr><th>Department</th><th class="text-end">Ordered</th><th class="text-end">Received</th><th class="text-end">Pending collection</th><th class="text-end">Pending report</th><th class="text-end">Completed</th><th>Completion</th></tr></thead><tbody><?php foreach ($modalityRows as $row) : ?><?php $completion = (int) $row['total'] > 0 ? round(((int) $row['completed_reports'] / (int) $row['total']) * 100) : 0; ?><tr><td><strong><?= esc($row['label']) ?></strong></td><td class="text-end"><?= (int) $row['total'] ?></td><td class="text-end"><?= (int) $row['samples_received'] ?></td><td class="text-end"><?= (int) $row['pending_collection'] ?></td><td class="text-end"><?= (int) $row['pending_reports'] ?></td><td class="text-end text-success fw-semibold"><?= (int) $row['completed_reports'] ?></td><td style="min-width:140px"><div class="progress"><div class="progress-bar bg-success" style="width:<?= $completion ?>%"></div></div><span class="small text-muted"><?= $completion ?>%</span></td></tr><?php endforeach ?></tbody></table></div><div class="text-muted small mt-2">Completed means the report is verified (status 2). Draft or unverified reports remain pending.</div></div></div>
|
||||
</section>
|
||||
<?= view('dashboard/partials/detail_assets') ?>
|
||||
<script>(function(){function loadRange(from,to){load_form('<?= base_url('dashboard/diagnosis') ?>?from_date='+encodeURIComponent(from)+'&to_date='+encodeURIComponent(to),'Diagnosis Dashboard');}$('#diagnosis_apply_range').off('click.diagnosisDash').on('click.diagnosisDash',function(){loadRange($('#diagnosis_from_date').val(),$('#diagnosis_to_date').val());});$('.diagnosis-range').off('click.diagnosisDash').on('click.diagnosisDash',function(){var end=new Date(),start=new Date();start.setDate(end.getDate()-Number($(this).data('days')||0));function ymd(d){return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');}loadRange(ymd(start),ymd(end));});})();</script>
|
||||
|
|
@ -5,6 +5,8 @@ $admitToday = $admit_today ?? 0;
|
|||
$dischargeToday = $discharge_today ?? 0;
|
||||
$currentIpd = $current_ipd ?? 0;
|
||||
$currentOrgIpd = $current_org_ipd ?? 0;
|
||||
$diagnosisToday = $diagnosis_today ?? 0;
|
||||
$diagnosisCompletedToday = $diagnosis_completed_today ?? 0;
|
||||
$opdOrgList = $opd_org_list ?? [];
|
||||
$opdDoctorList = $opd_doctor_list ?? [];
|
||||
$ipdDoctorList = $ipd_doctor_list ?? [];
|
||||
|
|
@ -26,8 +28,8 @@ $trendIpd = $trend_ipd ?? [];
|
|||
|
||||
<section class="section dashboard">
|
||||
<div class="row">
|
||||
<div class="col-xxl-3 col-md-6">
|
||||
<div class="card info-card sales-card">
|
||||
<div class="col-xxl col-md-6">
|
||||
<div class="card info-card sales-card dashboard-drill-card" role="button" tabindex="0" data-dashboard-url="<?= base_url('dashboard/opd') ?>" data-dashboard-title="OPD Dashboard">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">OPD <span>| Today</span></h5>
|
||||
<div class="d-flex align-items-center">
|
||||
|
|
@ -43,8 +45,8 @@ $trendIpd = $trend_ipd ?? [];
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xxl-3 col-md-6">
|
||||
<div class="card info-card revenue-card">
|
||||
<div class="col-xxl col-md-6">
|
||||
<div class="card info-card revenue-card dashboard-drill-card" role="button" tabindex="0" data-dashboard-url="<?= base_url('dashboard/ipd') ?>" data-dashboard-title="IPD Dashboard">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Admit & Discharge <span>| Today</span></h5>
|
||||
<div class="d-flex align-items-center">
|
||||
|
|
@ -60,8 +62,8 @@ $trendIpd = $trend_ipd ?? [];
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xxl-3 col-md-6">
|
||||
<div class="card info-card customers-card">
|
||||
<div class="col-xxl col-md-6">
|
||||
<div class="card info-card customers-card dashboard-drill-card" role="button" tabindex="0" data-dashboard-url="<?= base_url('dashboard/ipd') ?>" data-dashboard-title="IPD Dashboard">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Current IPD <span>| In-house</span></h5>
|
||||
<div class="d-flex align-items-center">
|
||||
|
|
@ -77,7 +79,16 @@ $trendIpd = $trend_ipd ?? [];
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xxl-3 col-md-6">
|
||||
<div class="col-xxl col-md-6">
|
||||
<div class="card info-card dashboard-drill-card" role="button" tabindex="0" data-dashboard-url="<?= base_url('dashboard/diagnosis') ?>" data-dashboard-title="Diagnosis Dashboard">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Diagnosis <span>| Today</span></h5>
|
||||
<div class="d-flex align-items-center"><div class="card-icon rounded-circle d-flex align-items-center justify-content-center"><i class="bi bi-clipboard2-pulse"></i></div><div class="ps-3"><h6><?= esc((string) $diagnosisToday) ?></h6><span class="text-muted small pt-1">Completed: <?= esc((string) $diagnosisCompletedToday) ?></span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xxl col-md-6">
|
||||
<div class="card info-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Daily Trend <span>| Current Month</span></h5>
|
||||
|
|
@ -241,9 +252,17 @@ $trendIpd = $trend_ipd ?? [];
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<style>.dashboard-drill-card{cursor:pointer;transition:transform .16s ease,box-shadow .16s ease}.dashboard-drill-card:hover,.dashboard-drill-card:focus{transform:translateY(-2px);box-shadow:0 8px 20px rgba(25,55,95,.13);outline:2px solid rgba(36,107,253,.25);outline-offset:2px}</style>
|
||||
|
||||
<script src="<?= base_url('assets/vendor/chart.js/chart.umd.js') ?>"></script>
|
||||
<script>
|
||||
(function () {
|
||||
$('.dashboard-drill-card').off('click.dashboardDrill keydown.dashboardDrill').on('click.dashboardDrill keydown.dashboardDrill', function(event) {
|
||||
if (event.type === 'keydown' && event.key !== 'Enter' && event.key !== ' ') return;
|
||||
event.preventDefault();
|
||||
load_form($(this).data('dashboard-url'), $(this).data('dashboard-title'));
|
||||
});
|
||||
|
||||
const labels = <?= json_encode($trendLabels) ?>;
|
||||
const opdData = <?= json_encode($trendOpd) ?>;
|
||||
const ipdData = <?= json_encode($trendIpd) ?>;
|
||||
|
|
|
|||
22
app/Views/dashboard/ipd.php
Normal file
22
app/Views/dashboard/ipd.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
$admissions = (int) ($totals['admissions'] ?? 0);
|
||||
$discharges = (int) ($totals['discharges'] ?? 0);
|
||||
$activeCases = (int) ($totals['active_cases'] ?? 0);
|
||||
$currentInHouse = (int) ($totals['current_in_house'] ?? 0);
|
||||
$maxDepartment = max(array_merge([1], array_map(static fn (array $row): int => (int) $row['total'], $departmentRows ?? [])));
|
||||
?>
|
||||
<div class="pagetitle d-flex align-items-center justify-content-between flex-wrap gap-2"><div><h1>IPD Dashboard</h1><div class="text-muted small">Admissions, discharges, current census, and care distribution</div></div><button class="btn btn-light" type="button" onclick="load_form('<?= base_url('dashboard') ?>','Dashboard');"><i class="bi bi-arrow-left"></i> Main Dashboard</button></div>
|
||||
<section class="section dashboard dashboard-detail">
|
||||
<div class="card dashboard-filter-card"><div class="card-body py-3"><div class="row g-2 align-items-end">
|
||||
<div class="col-sm-4 col-lg-3"><label class="form-label" for="ipd_from_date">From date</label><input class="form-control" id="ipd_from_date" type="date" value="<?= esc($fromDate) ?>"></div><div class="col-sm-4 col-lg-3"><label class="form-label" for="ipd_to_date">To date</label><input class="form-control" id="ipd_to_date" type="date" value="<?= esc($toDate) ?>"></div><div class="col-sm-4 col-lg-2"><button class="btn btn-primary w-100" id="ipd_apply_range" type="button"><i class="bi bi-funnel"></i> Apply</button></div><div class="col-lg-4"><div class="btn-group w-100"><button class="btn btn-outline-secondary ipd-range" data-days="0" type="button">Today</button><button class="btn btn-outline-secondary ipd-range" data-days="6" type="button">7 days</button><button class="btn btn-outline-secondary ipd-range" data-days="29" type="button">30 days</button></div></div>
|
||||
</div></div></div>
|
||||
<div class="row g-3 mb-3"><?php foreach ([['Admissions', $admissions, 'bi-box-arrow-in-right', 'primary'], ['Discharges', $discharges, 'bi-box-arrow-right', 'success'], ['Still active from range', $activeCases, 'bi-activity', 'warning'], ['Current in-house', $currentInHouse, 'bi-hospital', 'info']] as [$label, $value, $icon, $tone]) : ?><div class="col-6 col-xl-3"><div class="card h-100 dashboard-kpi"><div class="card-body"><div class="dashboard-kpi-icon text-bg-<?= $tone ?>"><i class="bi <?= $icon ?>"></i></div><div><div class="dashboard-kpi-value"><?= $value ?></div><div class="text-muted small"><?= esc($label) ?></div></div></div></div></div><?php endforeach ?></div>
|
||||
<div class="row g-3">
|
||||
<div class="col-xl-7"><div class="card h-100"><div class="card-body"><h5 class="card-title">Admissions by department</h5><?php if (empty($departmentRows)) : ?><div class="dashboard-empty">No admissions in this period.</div><?php else : ?><div class="dashboard-bars"><?php foreach ($departmentRows as $row) : ?><div class="dashboard-bar-row"><div class="d-flex justify-content-between"><span><?= esc($row['label']) ?></span><strong><?= (int) $row['total'] ?></strong></div><div class="progress"><div class="progress-bar bg-success" style="width:<?= round(((int) $row['total'] / $maxDepartment) * 100, 1) ?>%"></div></div></div><?php endforeach ?></div><?php endif ?></div></div></div>
|
||||
<div class="col-xl-5"><div class="card h-100"><div class="card-body"><h5 class="card-title">Doctor workload</h5><?= view('dashboard/partials/ranked_table', ['rows' => $doctorRows, 'labelHeading' => 'Doctor', 'showPatients' => false]) ?></div></div></div>
|
||||
<div class="col-xl-6"><div class="card h-100"><div class="card-body"><h5 class="card-title">Referral source</h5><?= view('dashboard/partials/ranked_table', ['rows' => $referralRows, 'labelHeading' => 'Referral', 'showPatients' => false]) ?></div></div></div>
|
||||
<div class="col-xl-6"><div class="card h-100"><div class="card-body"><h5 class="card-title">Patient category</h5><?= view('dashboard/partials/ranked_table', ['rows' => $organizationRows, 'labelHeading' => 'Category', 'showPatients' => false]) ?></div></div></div>
|
||||
</div>
|
||||
</section>
|
||||
<?= view('dashboard/partials/detail_assets') ?>
|
||||
<script>(function(){function loadRange(from,to){load_form('<?= base_url('dashboard/ipd') ?>?from_date='+encodeURIComponent(from)+'&to_date='+encodeURIComponent(to),'IPD Dashboard');}$('#ipd_apply_range').off('click.ipdDash').on('click.ipdDash',function(){loadRange($('#ipd_from_date').val(),$('#ipd_to_date').val());});$('.ipd-range').off('click.ipdDash').on('click.ipdDash',function(){var end=new Date(),start=new Date();start.setDate(end.getDate()-Number($(this).data('days')||0));function ymd(d){return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');}loadRange(ymd(start),ymd(end));});})();</script>
|
||||
42
app/Views/dashboard/opd.php
Normal file
42
app/Views/dashboard/opd.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
$totalVisits = (int) ($totals['total_visits'] ?? 0);
|
||||
$uniquePatients = (int) ($totals['unique_patients'] ?? 0);
|
||||
$completedVisits = (int) ($totals['completed_visits'] ?? 0);
|
||||
$cancelledVisits = (int) ($totals['cancelled_visits'] ?? 0);
|
||||
$maxDoctor = max(array_merge([1], array_map(static fn (array $row): int => (int) $row['total'], $doctorRows ?? [])));
|
||||
?>
|
||||
<div class="pagetitle d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<div><h1>OPD Dashboard</h1><div class="text-muted small">Visit activity, clinical workload, and referral sources</div></div>
|
||||
<button class="btn btn-light" type="button" onclick="load_form('<?= base_url('dashboard') ?>','Dashboard');"><i class="bi bi-arrow-left"></i> Main Dashboard</button>
|
||||
</div>
|
||||
|
||||
<section class="section dashboard dashboard-detail">
|
||||
<div class="card dashboard-filter-card">
|
||||
<div class="card-body py-3">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-sm-4 col-lg-3"><label class="form-label" for="opd_from_date">From date</label><input class="form-control" id="opd_from_date" type="date" value="<?= esc($fromDate) ?>"></div>
|
||||
<div class="col-sm-4 col-lg-3"><label class="form-label" for="opd_to_date">To date</label><input class="form-control" id="opd_to_date" type="date" value="<?= esc($toDate) ?>"></div>
|
||||
<div class="col-sm-4 col-lg-2"><button class="btn btn-primary w-100" id="opd_apply_range" type="button"><i class="bi bi-funnel"></i> Apply</button></div>
|
||||
<div class="col-lg-4"><div class="btn-group w-100" role="group" aria-label="OPD quick date range"><button class="btn btn-outline-secondary opd-range" data-days="0" type="button">Today</button><button class="btn btn-outline-secondary opd-range" data-days="6" type="button">7 days</button><button class="btn btn-outline-secondary opd-range" data-days="29" type="button">30 days</button></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<?php foreach ([['Visits', $totalVisits, 'bi-people', 'primary'], ['Unique patients', $uniquePatients, 'bi-person-vcard', 'info'], ['Completed visits', $completedVisits, 'bi-check2-circle', 'success'], ['Cancelled', $cancelledVisits, 'bi-x-circle', 'danger']] as [$label, $value, $icon, $tone]) : ?>
|
||||
<div class="col-6 col-xl-3"><div class="card h-100 dashboard-kpi"><div class="card-body"><div class="dashboard-kpi-icon text-bg-<?= $tone ?>"><i class="bi <?= $icon ?>"></i></div><div><div class="dashboard-kpi-value"><?= $value ?></div><div class="text-muted small"><?= esc($label) ?></div></div></div></div></div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-xl-7"><div class="card h-100"><div class="card-body"><h5 class="card-title">Doctor workload <span>| <?= esc(date('d M', strtotime($fromDate))) ?> - <?= esc(date('d M Y', strtotime($toDate))) ?></span></h5>
|
||||
<?php if (empty($doctorRows)) : ?><div class="dashboard-empty">No OPD visits in this period.</div><?php else : ?>
|
||||
<div class="dashboard-bars"><?php foreach ($doctorRows as $row) : ?><div class="dashboard-bar-row"><div class="d-flex justify-content-between gap-3"><span><?= esc($row['label']) ?></span><strong><?= (int) $row['total'] ?></strong></div><div class="progress"><div class="progress-bar" style="width:<?= round(((int) $row['total'] / $maxDoctor) * 100, 1) ?>%"></div></div><div class="small text-muted"><?= (int) $row['patients'] ?> unique patient<?= (int) $row['patients'] === 1 ? '' : 's' ?></div></div><?php endforeach ?></div>
|
||||
<?php endif ?></div></div></div>
|
||||
<div class="col-xl-5"><div class="card h-100"><div class="card-body"><h5 class="card-title">Department distribution</h5><?= view('dashboard/partials/ranked_table', ['rows' => $departmentRows, 'labelHeading' => 'Department', 'showPatients' => true]) ?></div></div></div>
|
||||
<div class="col-xl-6"><div class="card h-100"><div class="card-body"><h5 class="card-title">Referral source</h5><div class="text-muted small mb-2">Unique patients and visits, derived from the billing referral recorded against each OPD code.</div><?= view('dashboard/partials/ranked_table', ['rows' => $referralRows, 'labelHeading' => 'Referral', 'showPatients' => true]) ?></div></div></div>
|
||||
<div class="col-xl-6"><div class="card h-100"><div class="card-body"><h5 class="card-title">Patient category</h5><?= view('dashboard/partials/ranked_table', ['rows' => $organizationRows, 'labelHeading' => 'Category', 'showPatients' => false]) ?></div></div></div>
|
||||
</div>
|
||||
</section>
|
||||
<?= view('dashboard/partials/detail_assets') ?>
|
||||
<script>(function(){function loadRange(from,to){load_form('<?= base_url('dashboard/opd') ?>?from_date='+encodeURIComponent(from)+'&to_date='+encodeURIComponent(to),'OPD Dashboard');}$('#opd_apply_range').off('click.opdDash').on('click.opdDash',function(){loadRange($('#opd_from_date').val(),$('#opd_to_date').val());});$('.opd-range').off('click.opdDash').on('click.opdDash',function(){var end=new Date(),start=new Date();start.setDate(end.getDate()-Number($(this).data('days')||0));function ymd(d){return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');}loadRange(ymd(start),ymd(end));});})();</script>
|
||||
10
app/Views/dashboard/partials/detail_assets.php
Normal file
10
app/Views/dashboard/partials/detail_assets.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<style>
|
||||
.dashboard-detail .card{border:1px solid #dce4ee;box-shadow:0 4px 14px rgba(35,55,80,.06)}
|
||||
.dashboard-filter-card{border-left:4px solid #246bfd!important}
|
||||
.dashboard-kpi .card-body{display:flex;align-items:center;gap:14px;padding:18px}
|
||||
.dashboard-kpi-icon{width:44px;height:44px;display:grid;place-items:center;border-radius:8px;font-size:20px;flex:0 0 44px}
|
||||
.dashboard-kpi-value{font-size:24px;line-height:1.1;font-weight:700;color:#183153}
|
||||
.dashboard-bars{display:grid;gap:15px}.dashboard-bar-row .progress,.dashboard-status-table .progress{height:7px;background:#e9eef5;margin:5px 0}.dashboard-bar-row .progress-bar{background:#246bfd}
|
||||
.dashboard-empty{text-align:center;color:#7b8794;padding:28px 12px!important}.dashboard-status-table th{white-space:nowrap;font-size:12px;text-transform:uppercase;color:#627084}.dashboard-status-table td{padding-top:12px;padding-bottom:12px}
|
||||
@media(max-width:575.98px){.dashboard-kpi .card-body{padding:14px;align-items:flex-start}.dashboard-kpi-value{font-size:21px}.dashboard-kpi-icon{width:38px;height:38px;flex-basis:38px}.pagetitle .btn{width:100%}}
|
||||
</style>
|
||||
2
app/Views/dashboard/partials/ranked_table.php
Normal file
2
app/Views/dashboard/partials/ranked_table.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php $rows = $rows ?? []; $labelHeading = $labelHeading ?? 'Name'; $showPatients = $showPatients ?? false; ?>
|
||||
<div class="table-responsive"><table class="table table-sm table-hover align-middle mb-0"><thead><tr><th><?= esc($labelHeading) ?></th><?php if ($showPatients) : ?><th class="text-end">Patients</th><?php endif ?><th class="text-end">Cases</th></tr></thead><tbody><?php if (empty($rows)) : ?><tr><td colspan="<?= $showPatients ? 3 : 2 ?>" class="dashboard-empty">No data in this period.</td></tr><?php else : ?><?php foreach ($rows as $row) : ?><tr><td><?= esc($row['label'] ?? 'Unassigned') ?></td><?php if ($showPatients) : ?><td class="text-end"><?= (int) ($row['patients'] ?? 0) ?></td><?php endif ?><td class="text-end fw-semibold"><?= (int) ($row['total'] ?? 0) ?></td></tr><?php endforeach ?><?php endif ?></tbody></table></div>
|
||||
Loading…
Reference in New Issue
Block a user