feat(report): add searchable permissioned IPD census hub

Organize the report hub into permission-filtered searchable panels and add a protected IPD census and discharge report.
This commit is contained in:
DevSoftTech Server 2026-08-23 20:36:09 +05:30
parent 8e80bc4a7b
commit 83fce47ee1
7 changed files with 417 additions and 41 deletions

View File

@ -149,6 +149,7 @@ class AuthGroups extends ShieldAuthGroups
'reports.nabh_audit.view' => 'Can view NABH audit report',
'reports.billing_operations.view' => 'Can view billing operations reports',
'reports.document_issue.view' => 'Can view document issue reports',
'reports.ipd_census.view' => 'Can view IPD census and discharge reports',
'finance.workflow.view' => 'Can access Accounts and Finance workflow dashboard',
'finance.access' => 'Can access finance procurement and payable workflows',
'finance.cash.billing.submit' => 'Can create billing cash entries and submit cash statements',
@ -253,6 +254,7 @@ class AuthGroups extends ShieldAuthGroups
'reports.nabh_audit.view',
'reports.billing_operations.view',
'reports.document_issue.view',
'reports.ipd_census.view',
'finance.*',
'diagnosis.*',
'doctor_work.*',
@ -282,6 +284,7 @@ class AuthGroups extends ShieldAuthGroups
'reports.nabh_audit.view',
'reports.billing_operations.view',
'reports.document_issue.view',
'reports.ipd_census.view',
'finance.*',
'diagnosis.*',
'doctor_work.*',
@ -310,6 +313,7 @@ class AuthGroups extends ShieldAuthGroups
'reports.nabh_audit.view',
'reports.billing_operations.view',
'reports.document_issue.view',
'reports.ipd_census.view',
'finance.*',
'diagnosis.*',
'doctor_work.*',

View File

@ -1058,6 +1058,11 @@ $routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segme
$routes->get('Report/report_total_payment_total_amount_show/(:segment)/(:segment)/(:segment)', 'Report::report_total_payment_total_amount_show/$1/$2/$3', ['filter' => $reportsCollectionFilter]);
$routes->get('Report/report_total_payment_total_amount_show/(:segment)/(:segment)/(:segment)/(:num)', 'Report::report_total_payment_total_amount_show/$1/$2/$3/$4', ['filter' => $reportsCollectionFilter]);
$reportsIpdCensusFilter = 'permission:reports.ipd_census.view';
$routes->get('Report/ipd_census_report', 'Report::ipd_census_report', ['filter' => $reportsIpdCensusFilter]);
$routes->get('Report/ipd_census_data/(:segment)/(:segment)/(:segment)/(:segment)', 'Report::ipd_census_data/$1/$2/$3/$4', ['filter' => $reportsIpdCensusFilter]);
$routes->get('Report/ipd_census_data/(:segment)/(:segment)/(:segment)/(:segment)/(:num)', 'Report::ipd_census_data/$1/$2/$3/$4/$5', ['filter' => $reportsIpdCensusFilter]);
$routes->get('Report/billing_operations_report', 'Report::billing_operations_report', ['filter' => 'permission:reports.billing_operations.view']);
$routes->get('Report/billing_operations_report_data/(:segment)/(:segment)', 'Report::billing_operations_report_data/$1/$2', ['filter' => 'permission:reports.billing_operations.view']);
$routes->get('Report/billing_operations_report_data/(:segment)/(:segment)/(:num)', 'Report::billing_operations_report_data/$1/$2/$3', ['filter' => 'permission:reports.billing_operations.view']);

View File

@ -6,9 +6,44 @@ use Mpdf\Mpdf;
class Report extends BaseController
{
private const PANEL_PERMISSIONS = [
'billing' => 'reports.collection.view',
'operations' => 'reports.billing_operations.view',
'clinical' => 'diagnosis.report.view',
'ipd' => 'reports.ipd_census.view',
'insurance' => 'reports.insurance_credit.view',
'quality' => 'reports.nabh_audit.view',
'documents' => 'reports.document_issue.view',
];
public function index()
{
return view('report/index');
$panels = [];
foreach (self::PANEL_PERMISSIONS as $panel => $permission) {
$panels[$panel] = $this->canViewReportPanel($permission);
}
return view('report/index', ['panels' => $panels]);
}
private function canViewReportPanel(string $permission): bool
{
if (! function_exists('auth')) {
return false;
}
$user = auth()->user();
return $user !== null && method_exists($user, 'can') && $user->can($permission);
}
private function requireReportPermission(string $permission)
{
if ($this->canViewReportPanel($permission)) {
return null;
}
return $this->response->setStatusCode(403)->setBody('Access denied');
}
public function collection_report()
@ -31,6 +66,114 @@ class Report extends BaseController
]);
}
public function ipd_census_report()
{
if ($response = $this->requireReportPermission(self::PANEL_PERMISSIONS['ipd'])) {
return $response;
}
$doctors = $this->db->table('doctor_master')
->select('id, p_fname')
->where('active', 1)
->orderBy('p_fname', 'ASC')
->get()
->getResult();
$departments = $this->db->table('hc_department')
->select('iId, vName')
->orderBy('vName', 'ASC')
->get()
->getResult();
return view('report/ipd_census_report', [
'doctors' => $doctors,
'departments' => $departments,
]);
}
public function ipd_census_data(
string $dateRange,
string $doctorId = '0',
string $departmentId = '0',
string $status = 'all',
int $output = 0
) {
if ($response = $this->requireReportPermission(self::PANEL_PERMISSIONS['ipd'])) {
return $response;
}
[$minRange, $maxRange] = $this->parseDateRangeDateOnly($dateRange);
$doctorList = $this->db->table('ipd_master_doc_list')
->select('ipd_id, MIN(doc_id) AS doc_id', false)
->groupBy('ipd_id')
->getCompiledSelect();
$builder = $this->db->table('ipd_master i')
->select('i.id, i.ipd_code, i.register_date, i.discharge_date, i.ipd_status')
->select('p.p_code, p.p_fname')
->select("COALESCE(NULLIF(d.vName, ''), 'Unassigned') AS department_name", false)
->select("COALESCE(NULLIF(dm.p_fname, ''), NULLIF(i.r_doc_name, ''), 'Unassigned') AS doctor_name", false)
->select("CASE WHEN i.ipd_status = 1 THEN DATEDIFF(i.discharge_date, i.register_date) + 1 ELSE DATEDIFF('{$maxRange}', i.register_date) + 1 END AS stay_days", false)
->join('patient_master p', 'p.id = i.p_id', 'left')
->join('hc_department d', 'd.iId = i.dept_id', 'left')
->join("({$doctorList}) dl", 'dl.ipd_id = i.id', 'left', false)
->join('doctor_master dm', 'dm.id = dl.doc_id', 'left')
->where("(DATE(i.register_date) BETWEEN '{$minRange}' AND '{$maxRange}' OR DATE(i.discharge_date) BETWEEN '{$minRange}' AND '{$maxRange}')", null, false);
if ((int) $doctorId > 0) {
$builder->where('dl.doc_id', (int) $doctorId);
}
if ((int) $departmentId > 0) {
$builder->where('i.dept_id', (int) $departmentId);
}
if ($status === 'active') {
$builder->where('i.ipd_status', 0);
} elseif ($status === 'discharged') {
$builder->where('i.ipd_status', 1);
}
$rows = $builder->orderBy('i.register_date', 'DESC')->get()->getResult();
$summary = [
'admissions' => 0,
'discharges' => 0,
'active_cases' => 0,
'average_stay' => 0.0,
];
$dischargedStayDays = 0;
foreach ($rows as $row) {
$registered = substr((string) ($row->register_date ?? ''), 0, 10);
$discharged = substr((string) ($row->discharge_date ?? ''), 0, 10);
if ($registered >= $minRange && $registered <= $maxRange) {
$summary['admissions']++;
}
if ((int) ($row->ipd_status ?? 0) === 1 && $discharged >= $minRange && $discharged <= $maxRange) {
$summary['discharges']++;
$dischargedStayDays += max(0, (int) ($row->stay_days ?? 0));
}
if ((int) ($row->ipd_status ?? 0) === 0) {
$summary['active_cases']++;
}
}
$summary['average_stay'] = $summary['discharges'] > 0
? round($dischargedStayDays / $summary['discharges'], 1)
: 0.0;
$content = view('report/ipd_census_report_table', [
'rows' => $rows,
'summary' => $summary,
'min_range' => $minRange,
'max_range' => $maxRange,
]);
if ($output === 1) {
ExportExcel($content, 'IPD_Census_' . date('YmdHis'));
return $this->response->setBody('');
}
return $this->response->setBody($content);
}
public function report_opd_total()
{
$doctors = $this->db->table('doctor_master')

View File

@ -0,0 +1,56 @@
<?php
namespace App\Database\Migrations;
use App\Libraries\RoleRegistry;
use CodeIgniter\Database\Migration;
class AddIpdCensusReportPermission extends Migration
{
private const PERMISSION = 'reports.ipd_census.view';
public function up()
{
if (! $this->tableExists('auth_roles') || ! $this->tableExists('auth_role_permissions')) {
return;
}
$now = date('Y-m-d H:i:s');
foreach (['superadmin', 'admin', 'developer'] as $alias) {
$role = $this->db->table('auth_roles')->select('id')->where('alias', $alias)->get(1)->getRowArray();
if ($role === null) {
continue;
}
$exists = $this->db->table('auth_role_permissions')
->where('role_id', (int) $role['id'])
->where('permission', self::PERMISSION)
->countAllResults();
if ($exists === 0) {
$this->db->table('auth_role_permissions')->insert([
'role_id' => (int) $role['id'],
'permission' => self::PERMISSION,
'created_at' => $now,
]);
}
}
(new RoleRegistry($this->db))->publish();
}
public function down()
{
if ($this->tableExists('auth_role_permissions')) {
$this->db->table('auth_role_permissions')->where('permission', self::PERMISSION)->delete();
}
if ($this->tableExists('auth_roles')) {
(new RoleRegistry($this->db))->publish();
}
}
private function tableExists(string $table): bool
{
return $this->db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getRowArray() !== null;
}
}

View File

@ -1,57 +1,61 @@
<?php
$panels = $panels ?? [];
$can = static fn (string $panel): bool => (bool) ($panels[$panel] ?? false);
?>
<section class="content report-hub">
<div class="report-hub-head mb-3">
<h2 class="report-hub-title mb-1">Report Panel</h2>
<p class="report-hub-sub mb-0">Quick access to billing, diagnosis, insurance, and operational report sections.</p>
<div>
<h2 class="report-hub-title mb-1">Report Panel</h2>
<p class="report-hub-sub mb-0">Find the operational report you need. Only reports assigned to your user are shown.</p>
</div>
<div class="report-search-wrap">
<i class="bi bi-search"></i><input type="search" id="report-panel-search" aria-label="Find a report" placeholder="Find a report, e.g. IPD, payment, audit">
</div>
</div>
<div class="row g-3">
<div class="row g-3" id="report-panel-grid">
<?php if ($can('billing')) : ?>
<div class="col-xl-3 col-md-6">
<div class="card report-card h-100">
<div class="card-body">
<div class="report-card-head">
<span class="report-icon report-icon-blue"><i class="bi bi-bar-chart-line"></i></span>
<h5 class="report-card-title mb-0">Reports</h5>
<span class="report-icon report-icon-blue"><i class="bi bi-cash-stack"></i></span>
<h5 class="report-card-title mb-0">Billing &amp; Collection</h5>
</div>
<ul class="report-links">
<li><a href="javascript:load_form('<?= base_url('Report/collection_report') ?>','Collection Report');"><i class="bi bi-arrow-right-short"></i> Total Payment (Cash and Bank)</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/report_opd_total') ?>','OPD Total Report');"><i class="bi bi-arrow-right-short"></i> OPD Total Report</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/report_opd_patient_list') ?>','OPD Patient List');"><i class="bi bi-arrow-right-short"></i> OPD Patient List</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/billing_operations_report') ?>','Billing Operations Report');"><i class="bi bi-arrow-right-short"></i> Billing Operations Report</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/bank_deposits') ?>','Bank Transaction Audit');"><i class="bi bi-arrow-right-short"></i> Bank Transaction Audit</a></li>
</ul>
</div>
</div>
</div>
<?php endif; ?>
<?php if ($can('operations')) : ?>
<div class="col-xl-3 col-md-6">
<div class="card report-card h-100">
<div class="card-body">
<div class="report-card-head">
<span class="report-icon report-icon-blue"><i class="bi bi-bar-chart-line"></i></span>
<h5 class="report-card-title mb-0">Reports</h5>
<span class="report-icon report-icon-amber"><i class="bi bi-receipt-cutoff"></i></span>
<h5 class="report-card-title mb-0">Billing Operations</h5>
</div>
<ul class="report-links">
<li><a href="javascript:load_form('<?= base_url('Report/collection_report') ?>','Collection Report');"><i class="bi bi-arrow-right-short"></i> Total Payment (Cash and Bank)</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/report_opd_total') ?>','OPD Total Report');"><i class="bi bi-arrow-right-short"></i> OPD Total Report</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/report_opd_patient_list') ?>','OPD Patient List');"><i class="bi bi-arrow-right-short"></i> OPD Patient List</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/billing_operations_report') ?>','Billing Operations Report');"><i class="bi bi-arrow-right-short"></i> Billing Operations Report</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/index') ?>','Accounts And Finance');"><i class="bi bi-arrow-right-short"></i> Accounts And Finance Dashboard</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/medical_store_credit_account') ?>','Medical Store Credit Account');"><i class="bi bi-arrow-right-short"></i> Medical Store Credit Account</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/cashbook') ?>','Billing Cash Statement Submission');"><i class="bi bi-arrow-right-short"></i> Billing Cash Statement Submission</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/cashbook') ?>','Accounts Accept and Verify');"><i class="bi bi-arrow-right-short"></i> Accounts Accept and Verify</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/bank_deposits') ?>','Bank Transaction Audit');"><i class="bi bi-arrow-right-short"></i> Bank Transaction Audit</a></li>
<li><a href="javascript:load_form('<?= base_url('Medical/credit_payout_request') ?>','Credit Payout Request :Pharmacy');"><i class="bi bi-arrow-right-short"></i> Medical Credit Payout Request</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/nabh_audit_report') ?>','NABH Audit Report');"><i class="bi bi-arrow-right-short"></i> NABH Audit Report</a></li>
</ul>
</div>
</div>
</div>
<?php endif; ?>
<?php if ($can('ipd')) : ?>
<div class="col-xl-3 col-md-6"><div class="card report-card h-100"><div class="card-body"><div class="report-card-head"><span class="report-icon report-icon-green"><i class="bi bi-hospital"></i></span><h5 class="report-card-title mb-0">IPD Operations</h5></div><ul class="report-links"><li><a href="javascript:load_form('<?= base_url('Report/ipd_census_report') ?>','IPD Census &amp; Discharge Report');"><i class="bi bi-arrow-right-short"></i> IPD Census &amp; Discharge</a></li></ul></div></div></div>
<?php endif; ?>
<?php if ($can('clinical')) : ?>
<div class="col-xl-3 col-md-6">
<div class="card report-card h-100">
<div class="card-body">
<div class="report-card-head">
<span class="report-icon report-icon-cyan"><i class="bi bi-clipboard2-pulse"></i></span>
<h5 class="report-card-title mb-0">Reports Diagnosis</h5>
<h5 class="report-card-title mb-0">Clinical &amp; Diagnosis</h5>
</div>
<ul class="report-links">
<li><a href="javascript:load_form('<?= base_url('Report/diagnosis_report') ?>','Diagnosis Report');"><i class="bi bi-arrow-right-short"></i> Diagnosis Report</a></li>
@ -59,27 +63,14 @@
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card report-card h-100">
<div class="card-body">
<div class="report-card-head">
<span class="report-icon report-icon-green"><i class="bi bi-hospital"></i></span>
<h5 class="report-card-title mb-0">Reports IPD</h5>
</div>
<ul class="report-links">
<li><a href="javascript:load_form('<?= base_url('Report/insurance_ipd_report') ?>','Insurance Case IPD');"><i class="bi bi-arrow-right-short"></i> Insurance Case IPD</a></li>
</ul>
</div>
</div>
</div>
<?php endif; ?>
<?php if ($can('insurance')) : ?>
<div class="col-xl-3 col-md-6">
<div class="card report-card h-100">
<div class="card-body">
<div class="report-card-head">
<span class="report-icon report-icon-indigo"><i class="bi bi-shield-check"></i></span>
<h5 class="report-card-title mb-0">Reports Insurance Credit</h5>
<h5 class="report-card-title mb-0">Insurance &amp; Credit</h5>
</div>
<ul class="report-links">
<li><a href="javascript:load_form('<?= base_url('Report/insurance_ipd_report') ?>','Insurance Case IPD');"><i class="bi bi-arrow-right-short"></i> Insurance Case IPD</a></li>
@ -91,13 +82,18 @@
</div>
</div>
</div>
<?php endif; ?>
<?php if ($can('quality')) : ?>
<div class="col-xl-3 col-md-6"><div class="card report-card h-100"><div class="card-body"><div class="report-card-head"><span class="report-icon report-icon-rose"><i class="bi bi-clipboard2-check"></i></span><h5 class="report-card-title mb-0">Quality &amp; Audit</h5></div><ul class="report-links"><li><a href="javascript:load_form('<?= base_url('Report/nabh_audit_report') ?>','NABH Audit Report');"><i class="bi bi-arrow-right-short"></i> NABH Audit Report</a></li></ul></div></div></div>
<?php endif; ?>
<?php if ($can('documents')) : ?>
<div class="col-xl-3 col-md-6">
<div class="card report-card h-100 report-card-muted">
<div class="card-body">
<div class="report-card-head">
<span class="report-icon report-icon-slate"><i class="bi bi-file-earmark-text"></i></span>
<h5 class="report-card-title mb-0">Reports Document</h5>
<h5 class="report-card-title mb-0">Documents</h5>
</div>
<ul class="report-links">
<li><a href="javascript:load_form('<?= base_url('Report/document_list') ?>','Document Issue Report');"><i class="bi bi-arrow-right-short"></i> Document Issue Report</a></li>
@ -105,7 +101,9 @@
</div>
</div>
</div>
<?php endif; ?>
</div>
<div id="report-panel-empty" class="report-empty text-center py-4" style="display:none;">No report matches this search.</div>
</section>
<style>
@ -119,10 +117,16 @@
--hub-shadow: 0 10px 22px rgba(30, 48, 84, 0.08);
background: linear-gradient(145deg, #f7f9fd 0%, #edf2f9 100%);
border: 1px solid var(--hub-border);
border-radius: 14px;
border-radius: 8px;
padding: 18px;
}
.report-hub-head { display: flex; align-items: end; justify-content: space-between; gap: 16px; }
.report-search-wrap { position: relative; min-width: min(100%, 320px); }
.report-search-wrap i { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: var(--hub-sub); }
.report-search-wrap input { width: 100%; border: 1px solid var(--hub-border); border-radius: 6px; padding: 9px 11px 9px 33px; outline: none; color: var(--hub-text); }
.report-search-wrap input:focus { border-color: #7798ca; box-shadow: 0 0 0 3px rgba(31, 77, 144, .12); }
.report-hub-title {
color: #24334f;
font-weight: 700;
@ -136,7 +140,7 @@
.report-card {
border: 1px solid var(--hub-border);
border-radius: 12px;
border-radius: 8px;
box-shadow: 0 2px 0 rgba(255, 255, 255, 0.9) inset;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
background: #fff;
@ -176,6 +180,8 @@
.report-icon-green { background: #e7f9ef; color: #1d9960; }
.report-icon-indigo { background: #ece9ff; color: #4b49b6; }
.report-icon-slate { background: #eef1f6; color: #6b7280; }
.report-icon-amber { background: #fff3d8; color: #a86400; }
.report-icon-rose { background: #ffeaed; color: #bc3c55; }
.report-links {
list-style: none;
@ -219,5 +225,26 @@
.report-hub {
padding: 14px;
}
.report-hub-head { align-items: stretch; flex-direction: column; }
.report-search-wrap { min-width: 100%; }
}
</style>
<script>
(function () {
var input = document.getElementById('report-panel-search');
var grid = document.getElementById('report-panel-grid');
var empty = document.getElementById('report-panel-empty');
if (!input || !grid || !empty) return;
input.addEventListener('input', function () {
var query = input.value.toLowerCase().trim();
var visible = 0;
Array.prototype.forEach.call(grid.children, function (column) {
var matches = !query || column.textContent.toLowerCase().indexOf(query) !== -1;
column.style.display = matches ? '' : 'none';
if (matches) visible++;
});
empty.style.display = visible ? 'none' : '';
});
})();
</script>

View File

@ -0,0 +1,105 @@
<?php
$doctors = $doctors ?? [];
$departments = $departments ?? [];
?>
<section class="content">
<div class="card">
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<h5 class="mb-0"><i class="bi bi-hospital me-2"></i>IPD Census &amp; Discharge Report</h5>
<small class="text-muted">Admissions, discharges, current cases, and length of stay.</small>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="load_form('<?= base_url('Report/index') ?>', 'Report Panel')">
<i class="bi bi-arrow-left me-1"></i>Report Panel
</button>
</div>
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-4 col-xl-3">
<label class="form-label" for="ipd_census_from">From date</label>
<input type="date" class="form-control" id="ipd_census_from">
</div>
<div class="col-md-4 col-xl-3">
<label class="form-label" for="ipd_census_to">To date</label>
<input type="date" class="form-control" id="ipd_census_to">
</div>
<div class="col-md-4 col-xl-2">
<label class="form-label" for="ipd_census_status">Case status</label>
<select class="form-select" id="ipd_census_status">
<option value="all">All relevant cases</option>
<option value="active">Active only</option>
<option value="discharged">Discharged only</option>
</select>
</div>
<div class="col-md-6 col-xl-2">
<label class="form-label" for="ipd_census_department">Department</label>
<select class="form-select" id="ipd_census_department">
<option value="0">All departments</option>
<?php foreach ($departments as $department) : ?>
<option value="<?= (int) ($department->iId ?? 0) ?>"><?= esc($department->vName ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6 col-xl-2">
<label class="form-label" for="ipd_census_doctor">Doctor</label>
<select class="form-select" id="ipd_census_doctor">
<option value="0">All doctors</option>
<?php foreach ($doctors as $doctor) : ?>
<option value="<?= (int) ($doctor->id ?? 0) ?>"><?= esc($doctor->p_fname ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<button type="button" class="btn btn-primary" id="ipd_census_show"><i class="bi bi-search me-1"></i>Show report</button>
<button type="button" class="btn btn-outline-success" id="ipd_census_excel"><i class="bi bi-file-earmark-excel me-1"></i>Excel</button>
<button type="button" class="btn btn-outline-secondary ipd-census-range" data-days="0">Today</button>
<button type="button" class="btn btn-outline-secondary ipd-census-range" data-days="6">7 days</button>
<button type="button" class="btn btn-outline-secondary ipd-census-range" data-days="29">30 days</button>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-body">
<div id="ipd_census_result" class="table-responsive">
<div class="text-center text-muted py-4"><i class="bi bi-bar-chart-steps d-block mb-2" style="font-size:2rem"></i>Select the date range and show the report.</div>
</div>
</div>
</div>
</section>
<script>
(function () {
var from = document.getElementById('ipd_census_from');
var to = document.getElementById('ipd_census_to');
var result = document.getElementById('ipd_census_result');
if (!from || !to || !result) return;
function ymd(date) { return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0'); }
var today = new Date();
from.value = ymd(new Date(today.getFullYear(), today.getMonth(), 1));
to.value = ymd(today);
function reportUrl(output) {
if (!from.value || !to.value) { alert('Select both dates.'); return ''; }
return '<?= base_url('Report/ipd_census_data') ?>/'
+ encodeURIComponent(from.value + 'S' + to.value)
+ '/' + encodeURIComponent(document.getElementById('ipd_census_doctor').value || '0')
+ '/' + encodeURIComponent(document.getElementById('ipd_census_department').value || '0')
+ '/' + encodeURIComponent(document.getElementById('ipd_census_status').value || 'all')
+ (output ? '/' + output : '');
}
function showReport() {
var url = reportUrl(0);
if (!url) return;
result.innerHTML = '<div class="text-center text-muted py-4"><span class="spinner-border spinner-border-sm me-2"></span>Loading report...</div>';
$.get(url).done(function (html) { result.innerHTML = html; }).fail(function (xhr) { result.innerHTML = '<div class="alert alert-danger mb-0">' + (xhr.status === 403 ? 'You do not have permission to view this report.' : 'Unable to load the report.') + '</div>'; });
}
$('#ipd_census_show').off('click.ipdCensus').on('click.ipdCensus', showReport);
$('#ipd_census_excel').off('click.ipdCensus').on('click.ipdCensus', function () { var url = reportUrl(1); if (url) window.open(url, '_blank'); });
$('.ipd-census-range').off('click.ipdCensus').on('click.ipdCensus', function () { var end = new Date(); var start = new Date(); start.setDate(start.getDate() - Number($(this).data('days') || 0)); from.value = ymd(start); to.value = ymd(end); showReport(); });
})();
</script>

View File

@ -0,0 +1,36 @@
<?php
$rows = $rows ?? [];
$summary = $summary ?? [];
?>
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<div><strong>Period:</strong> <?= esc($min_range ?? '') ?> to <?= esc($max_range ?? '') ?></div>
<span class="text-muted small"><?= count($rows) ?> relevant case<?= count($rows) === 1 ? '' : 's' ?></span>
</div>
<div class="row g-2 mb-3">
<?php foreach ([
['Admissions', (int) ($summary['admissions'] ?? 0), 'primary'],
['Discharges', (int) ($summary['discharges'] ?? 0), 'success'],
['Active cases', (int) ($summary['active_cases'] ?? 0), 'warning'],
['Avg. stay', number_format((float) ($summary['average_stay'] ?? 0), 1) . ' days', 'info'],
] as [$label, $value, $tone]) : ?>
<div class="col-6 col-lg-3"><div class="border rounded p-2 bg-light"><div class="text-muted small"><?= esc($label) ?></div><div class="fs-5 fw-semibold text-<?= esc($tone) ?>"><?= esc((string) $value) ?></div></div></div>
<?php endforeach; ?>
</div>
<?php if (empty($rows)) : ?>
<div class="alert alert-info mb-0">No IPD cases match the selected filters.</div>
<?php else : ?>
<table class="table table-sm table-striped table-bordered align-middle mb-0">
<thead class="table-light"><tr><th>#</th><th>IPD code</th><th>UHID</th><th>Patient</th><th>Department</th><th>Doctor</th><th>Admission</th><th>Discharge</th><th>Status</th><th class="text-end">Stay</th></tr></thead>
<tbody>
<?php foreach ($rows as $index => $row) : ?>
<tr>
<td><?= $index + 1 ?></td><td><?= esc($row->ipd_code ?? '') ?></td><td><?= esc($row->p_code ?? '') ?></td><td><?= esc($row->p_fname ?? '') ?></td>
<td><?= esc($row->department_name ?? '') ?></td><td><?= esc($row->doctor_name ?? '') ?></td>
<td><?= esc(substr((string) ($row->register_date ?? ''), 0, 10)) ?></td><td><?= esc(substr((string) ($row->discharge_date ?? ''), 0, 10) ?: '-') ?></td>
<td><span class="badge text-bg-<?= (int) ($row->ipd_status ?? 0) === 1 ? 'success' : 'warning' ?>"><?= (int) ($row->ipd_status ?? 0) === 1 ? 'Discharged' : 'Active' ?></span></td><td class="text-end"><?= max(0, (int) ($row->stay_days ?? 0)) ?> days</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>