diff --git a/app/Config/AuthGroups.php b/app/Config/AuthGroups.php index dcd48a4..623e99a 100644 --- a/app/Config/AuthGroups.php +++ b/app/Config/AuthGroups.php @@ -150,6 +150,7 @@ class AuthGroups extends ShieldAuthGroups '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', + 'reports.ipd_surgery.view' => 'Can view IPD surgery 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', @@ -255,6 +256,7 @@ class AuthGroups extends ShieldAuthGroups 'reports.billing_operations.view', 'reports.document_issue.view', 'reports.ipd_census.view', + 'reports.ipd_surgery.view', 'finance.*', 'diagnosis.*', 'doctor_work.*', @@ -285,6 +287,7 @@ class AuthGroups extends ShieldAuthGroups 'reports.billing_operations.view', 'reports.document_issue.view', 'reports.ipd_census.view', + 'reports.ipd_surgery.view', 'finance.*', 'diagnosis.*', 'doctor_work.*', @@ -314,6 +317,7 @@ class AuthGroups extends ShieldAuthGroups 'reports.billing_operations.view', 'reports.document_issue.view', 'reports.ipd_census.view', + 'reports.ipd_surgery.view', 'finance.*', 'diagnosis.*', 'doctor_work.*', diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 17d7cd4..54f3e96 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1063,6 +1063,11 @@ $routes->get('Report/ipd_census_report', 'Report::ipd_census_report', ['filter' $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]); +$reportsIpdSurgeryFilter = 'permission:reports.ipd_surgery.view'; +$routes->get('Report/ipd_surgery_report', 'Report::ipd_surgery_report', ['filter' => $reportsIpdSurgeryFilter]); +$routes->get('Report/ipd_surgery_data/(:segment)/(:segment)/(:segment)', 'Report::ipd_surgery_data/$1/$2/$3', ['filter' => $reportsIpdSurgeryFilter]); +$routes->get('Report/ipd_surgery_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::ipd_surgery_data/$1/$2/$3/$4', ['filter' => $reportsIpdSurgeryFilter]); + $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 6d13096..098228e 100644 --- a/app/Controllers/Report.php +++ b/app/Controllers/Report.php @@ -11,6 +11,7 @@ class Report extends BaseController 'operations' => 'reports.billing_operations.view', 'clinical' => 'diagnosis.report.view', 'ipd' => 'reports.ipd_census.view', + 'surgery' => 'reports.ipd_surgery.view', 'insurance' => 'reports.insurance_credit.view', 'quality' => 'reports.nabh_audit.view', 'documents' => 'reports.document_issue.view', @@ -174,6 +175,110 @@ class Report extends BaseController return $this->response->setBody($content); } + public function ipd_surgery_report() + { + if ($response = $this->requireReportPermission(self::PANEL_PERMISSIONS['surgery'])) { + return $response; + } + + return view('report/ipd_surgery_report', [ + '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(), + ]); + } + + public function ipd_surgery_data( + string $dateRange, + string $doctorId = '0', + string $departmentId = '0', + int $output = 0 + ) { + if ($response = $this->requireReportPermission(self::PANEL_PERMISSIONS['surgery'])) { + return $response; + } + + [$minRange, $maxRange] = $this->parseDateRangeDateOnly($dateRange); + $diagnoses = $this->db->table('ipd_discharge_diagnosis') + ->select("ipd_id, GROUP_CONCAT(DISTINCT NULLIF(TRIM(COALESCE(snomed_term, comp_report, comp_remark)), '') ORDER BY id SEPARATOR '; ') AS final_diagnosis", false) + ->groupBy('ipd_id') + ->getCompiledSelect(); + $billedProcedures = $this->db->table('invoice_master im') + ->select('CONVERT(LOWER(TRIM(ii.item_name)) USING utf8mb4) COLLATE utf8mb4_general_ci AS billed_name', false) + ->select('SUM(COALESCE(ii.item_amount, 0)) AS actual_surgery_cost', false) + ->join('invoice_item ii', 'ii.inv_master_id = im.id', 'inner') + ->where('im.ipd_id >', 0) + ->where('im.invoice_status', 1) + ->groupBy('im.ipd_id, CONVERT(LOWER(TRIM(ii.item_name)) USING utf8mb4) COLLATE utf8mb4_general_ci') + ->getCompiledSelect(); + + $builder = $this->db->table('ipd_master i') + ->select('i.id AS ipd_id, i.ipd_code, i.register_date, i.reg_time, i.discharge_date, i.discharge_time') + ->select('p.p_code AS mr_no, p.referby AS camp_code, p.p_fname AS patient_name, p.age, p.gender') + ->select("CONCAT_WS(', ', NULLIF(TRIM(COALESCE(p.add1, i.add1)), ''), NULLIF(TRIM(COALESCE(p.add2, i.add2)), ''), NULLIF(TRIM(COALESCE(p.city, i.city)), ''), NULLIF(TRIM(COALESCE(p.district, '')), ''), NULLIF(TRIM(COALESCE(p.state, i.state)), ''), NULLIF(TRIM(p.zip), '')) AS address", false) + ->select("CONCAT_WS(', ', NULLIF(IF(COALESCE(p.is_hypertesion, 0) = 1, 'Hypertension', ''), ''), NULLIF(IF(COALESCE(p.is_niddm, 0) = 1, 'Diabetes', ''), ''), NULLIF(IF(COALESCE(p.is_smoking, 0) = 1, 'Smoking', ''), ''), NULLIF(IF(COALESCE(p.is_alcohol, 0) = 1, 'Alcohol use', ''), ''), NULLIF(TRIM(p.Others), '')) AS systemic_history", false) + ->select("COALESCE(NULLIF(sds.surgery_date, '0000-00-00'), NULLIF(i.surgery_date, '0000-00-00')) AS surgery_date", false) + ->select("COALESCE(NULLIF(sds.surgery_name, ''), NULLIF(i.surgery_name, ''), NULLIF(smaster.term_name, '')) AS surgery_name", false) + ->select('smaster.term_code AS surgery_code') + ->select('sds.surgery_remark') + ->select("COALESCE(NULLIF(dept.vName, ''), 'Unassigned') AS specialty", false) + ->select("COALESCE(NULLIF(surgeon.p_fname, ''), NULLIF(i.r_doc_name, ''), 'Unassigned') AS doctor_name", false) + ->select('diagnosis.final_diagnosis') + ->select('sm.surg_amount AS estimated_surgery_cost, billed.actual_surgery_cost') + ->join('patient_master p', 'p.id = i.p_id', 'left') + ->join('ipd_discharge_surgery sds', 'sds.ipd_id = i.id', 'left') + ->join('ipd_discharge_surgery_master smaster', 'smaster.id = sds.surgery_id', 'left') + ->join('hc_surgery sm', 'sm.id = sds.surgery_id', 'left') + ->join('hc_department dept', 'dept.iId = i.dept_id', 'left') + ->join('doctor_master surgeon', 'surgeon.id = sds.surgery_by_doc_id', 'left') + ->join("({$diagnoses}) diagnosis", 'diagnosis.ipd_id = i.id', 'left', false) + ->join("({$billedProcedures}) billed", "billed.ipd_id = i.id AND billed.billed_name = CONVERT(LOWER(TRIM(COALESCE(sds.surgery_name, i.surgery_name, ''))) USING utf8mb4) COLLATE utf8mb4_general_ci", 'left', false) + ->where("COALESCE(NULLIF(sds.surgery_date, '0000-00-00'), NULLIF(i.surgery_date, '0000-00-00')) BETWEEN '{$minRange}' AND '{$maxRange}'", null, false) + ->where("(sds.id IS NOT NULL OR COALESCE(i.issurgery, 0) = 1)", null, false); + + if ((int) $doctorId > 0) { + $builder->groupStart()->where('sds.surgery_by_doc_id', (int) $doctorId)->orWhere('i.r_doc_id', (int) $doctorId)->groupEnd(); + } + if ((int) $departmentId > 0) { + $builder->where('i.dept_id', (int) $departmentId); + } + + if ($this->db->tableExists('ipd_ot_cases')) { + $otCases = $this->db->table('ipd_ot_cases') + ->select('ipd_id') + ->select('MAX(scheduled_start_at) AS scheduled_start_at, MAX(actual_start_at) AS actual_start_at, MAX(actual_end_at) AS actual_end_at', false) + ->select("GROUP_CONCAT(DISTINCT NULLIF(anesthetist_name_snapshot, '') SEPARATOR '; ') AS anesthetist_name", false) + ->select("GROUP_CONCAT(DISTINCT NULLIF(procedure_side, '') SEPARATOR '; ') AS procedure_side", false) + ->groupBy('ipd_id') + ->getCompiledSelect(); + $builder->select('ot.scheduled_start_at, ot.actual_start_at, ot.actual_end_at, ot.anesthetist_name, ot.procedure_side') + ->join("({$otCases}) ot", 'ot.ipd_id = i.id', 'left', false); + } else { + $builder->select("NULL AS scheduled_start_at, NULL AS actual_start_at, NULL AS actual_end_at, '' AS anesthetist_name, '' AS procedure_side", false); + } + + $rows = $builder->orderBy('surgery_date', 'ASC')->orderBy('i.id', 'ASC')->get()->getResultArray(); + foreach ($rows as &$row) { + $start = ! empty($row['actual_start_at']) ? strtotime((string) $row['actual_start_at']) : false; + $end = ! empty($row['actual_end_at']) ? strtotime((string) $row['actual_end_at']) : false; + $row['actual_duration_minutes'] = $start !== false && $end !== false && $end >= $start + ? (int) round(($end - $start) / 60) + : null; + } + unset($row); + + $content = view('report/ipd_surgery_report_table', [ + 'rows' => $rows, + 'min_range' => $minRange, + 'max_range' => $maxRange, + ]); + if ($output === 1) { + ExportExcel($content, 'IPD_Surgery_Report_' . 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-000002_AddIpdSurgeryReportPermission.php b/app/Database/Migrations/2026-08-23-000002_AddIpdSurgeryReportPermission.php new file mode 100644 index 0000000..256a5ea --- /dev/null +++ b/app/Database/Migrations/2026-08-23-000002_AddIpdSurgeryReportPermission.php @@ -0,0 +1,47 @@ +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; + } +} \ No newline at end of file diff --git a/app/Views/report/index.php b/app/Views/report/index.php index 2f2540b..bc13a62 100644 --- a/app/Views/report/index.php +++ b/app/Views/report/index.php @@ -46,8 +46,8 @@ $can = static fn (string $panel): bool => (bool) ($panels[$panel] ?? false); - -
| Surgery Date | MR No. | Camp Code | Patient Name | Age | Sex | Address | Doctor Name | Systemic | Final Diagnosis | Eye | Surgery Code | Surgery Name | Specialty | Admission Date | Discharge Date | Scheduled Time | Remarks | Anaesthetist | Surgery Start | Surgery End | Actual Duration | Estimated Surgery Cost | Actual Surgery Cost |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| = esc($row['surgery_date'] ?? '') ?> | = esc($row['mr_no'] ?? '') ?> | = esc($row['camp_code'] ?? '') ?> | = esc($row['patient_name'] ?? '') ?> | = esc($row['age'] ?? '') ?> | = esc($row['gender'] ?? '') ?> | = esc($row['address'] ?? '') ?> | = esc($row['doctor_name'] ?? '') ?> | = esc($row['systemic_history'] ?? '') ?> | = esc($row['final_diagnosis'] ?? '') ?> | = esc($row['procedure_side'] ?? '') ?> | = esc($row['surgery_code'] ?? '') ?> | = esc($row['surgery_name'] ?? '') ?> | = esc($row['specialty'] ?? '') ?> | = esc(substr((string) ($row['register_date'] ?? ''), 0, 10)) ?> | = esc(substr((string) ($row['discharge_date'] ?? ''), 0, 10)) ?> | = esc($row['scheduled_start_at'] ?? '') ?> | = esc($row['surgery_remark'] ?? '') ?> | = esc($row['anesthetist_name'] ?? '') ?> | = esc($row['actual_start_at'] ?? '') ?> | = esc($row['actual_end_at'] ?? '') ?> | = isset($row['actual_duration_minutes']) ? (int) $row['actual_duration_minutes'] . ' min' : '' ?> | = ($row['estimated_surgery_cost'] ?? null) !== null ? number_format((float) $row['estimated_surgery_cost'], 2) : '' ?> | = ($row['actual_surgery_cost'] ?? null) !== null ? number_format((float) $row['actual_surgery_cost'], 2) : '' ?> |