From 83fce47ee1c16839581ddf161d75d345726eb372 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Sun, 23 Aug 2026 20:36:09 +0530 Subject: [PATCH] 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. --- app/Config/AuthGroups.php | 4 + app/Config/Routes.php | 5 + app/Controllers/Report.php | 145 +++++++++++++++++- ...23-000001_AddIpdCensusReportPermission.php | 56 +++++++ app/Views/report/index.php | 107 ++++++++----- app/Views/report/ipd_census_report.php | 105 +++++++++++++ app/Views/report/ipd_census_report_table.php | 36 +++++ 7 files changed, 417 insertions(+), 41 deletions(-) create mode 100644 app/Database/Migrations/2026-08-23-000001_AddIpdCensusReportPermission.php create mode 100644 app/Views/report/ipd_census_report.php create mode 100644 app/Views/report/ipd_census_report_table.php diff --git a/app/Config/AuthGroups.php b/app/Config/AuthGroups.php index 56b6e97..dcd48a4 100644 --- a/app/Config/AuthGroups.php +++ b/app/Config/AuthGroups.php @@ -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.*', diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 96947e7..17d7cd4 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -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']); diff --git a/app/Controllers/Report.php b/app/Controllers/Report.php index 3abbbee..6d13096 100644 --- a/app/Controllers/Report.php +++ b/app/Controllers/Report.php @@ -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') diff --git a/app/Database/Migrations/2026-08-23-000001_AddIpdCensusReportPermission.php b/app/Database/Migrations/2026-08-23-000001_AddIpdCensusReportPermission.php new file mode 100644 index 0000000..8c0d700 --- /dev/null +++ b/app/Database/Migrations/2026-08-23-000001_AddIpdCensusReportPermission.php @@ -0,0 +1,56 @@ +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; + } +} diff --git a/app/Views/report/index.php b/app/Views/report/index.php index e89b94b..2f2540b 100644 --- a/app/Views/report/index.php +++ b/app/Views/report/index.php @@ -1,57 +1,61 @@ + (bool) ($panels[$panel] ?? false); +?>
-

Report Panel

-

Quick access to billing, diagnosis, insurance, and operational report sections.

+
+

Report Panel

+

Find the operational report you need. Only reports assigned to your user are shown.

+
+
+ +
-
+
+ + + - + + + + +
-
Reports Diagnosis
+
Clinical & Diagnosis
- -
-
-
-
- -
Reports IPD
-
- -
-
-
- + +
-
Reports Insurance Credit
+
Insurance & Credit
+ + +
Quality & Audit
+ +
-
Reports Document
+
Documents
+
+
+ + diff --git a/app/Views/report/ipd_census_report.php b/app/Views/report/ipd_census_report.php new file mode 100644 index 0000000..ec1bdff --- /dev/null +++ b/app/Views/report/ipd_census_report.php @@ -0,0 +1,105 @@ + + +
+
+
+
+
IPD Census & Discharge Report
+ Admissions, discharges, current cases, and length of stay. +
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + + +
+
+
+
+
+
+
Select the date range and show the report.
+
+
+
+
+ + \ No newline at end of file diff --git a/app/Views/report/ipd_census_report_table.php b/app/Views/report/ipd_census_report_table.php new file mode 100644 index 0000000..1e2a7a6 --- /dev/null +++ b/app/Views/report/ipd_census_report_table.php @@ -0,0 +1,36 @@ + + +
+
Period: to
+ relevant case +
+
+ +
+ +
+ +
No IPD cases match the selected filters.
+ + + + + $row) : ?> + + + + + + + + +
#IPD codeUHIDPatientDepartmentDoctorAdmissionDischargeStatusStay
ipd_code ?? '') ?>p_code ?? '') ?>p_fname ?? '') ?>department_name ?? '') ?>doctor_name ?? '') ?>register_date ?? ''), 0, 10)) ?>discharge_date ?? ''), 0, 10) ?: '-') ?>ipd_status ?? 0) === 1 ? 'Discharged' : 'Active' ?>stay_days ?? 0)) ?> days
+ \ No newline at end of file