feat(report): add IPD surgery export report

Add a permissioned IPD surgery report combining discharge surgery, patient, diagnosis, OT, and matched billing details.
This commit is contained in:
DevSoftTech Server 2026-08-24 00:01:58 +05:30
parent 83fce47ee1
commit 225721ae02
7 changed files with 184 additions and 2 deletions

View File

@ -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.*',

View File

@ -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']);

View File

@ -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')

View File

@ -0,0 +1,47 @@
<?php
namespace App\Database\Migrations;
use App\Libraries\RoleRegistry;
use CodeIgniter\Database\Migration;
class AddIpdSurgeryReportPermission extends Migration
{
private const PERMISSION = 'reports.ipd_surgery.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

@ -46,8 +46,8 @@ $can = static fn (string $panel): bool => (bool) ($panels[$panel] ?? false);
</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 if ($can('ipd') || $can('surgery')) : ?>
<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"><?php if ($can('ipd')) : ?><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><?php endif; ?><?php if ($can('surgery')) : ?><li><a href="javascript:load_form('<?= base_url('Report/ipd_surgery_report') ?>','IPD Surgery Report');"><i class="bi bi-arrow-right-short"></i> IPD Surgery Report</a></li><?php endif; ?></ul></div></div></div>
<?php endif; ?>
<?php if ($can('clinical')) : ?>
<div class="col-xl-3 col-md-6">

View File

@ -0,0 +1,18 @@
<?php
$doctors = $doctors ?? [];
$departments = $departments ?? [];
$today = date('Y-m-d');
?>
<section class="content">
<div class="card"><div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2"><div><h5 class="mb-0"><i class="bi bi-scissors me-2"></i>IPD Surgery Report</h5><small class="text-muted">Surgery, discharge, consultant, OT, and matched billing details.</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-3"><label class="form-label" for="surgery_report_from">Surgery from</label><input type="date" class="form-control" id="surgery_report_from" value="<?= esc(date('Y-m-01')) ?>"></div><div class="col-md-3"><label class="form-label" for="surgery_report_to">Surgery to</label><input type="date" class="form-control" id="surgery_report_to" value="<?= esc($today) ?>"></div><div class="col-md-3"><label class="form-label" for="surgery_report_department">Specialty</label><select class="form-select" id="surgery_report_department"><option value="0">All specialties</option><?php foreach ($departments as $department) : ?><option value="<?= (int) ($department->iId ?? 0) ?>"><?= esc($department->vName ?? '') ?></option><?php endforeach; ?></select></div><div class="col-md-3"><label class="form-label" for="surgery_report_doctor">Surgeon / consultant</label><select class="form-select" id="surgery_report_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 gap-2 mt-3"><button type="button" class="btn btn-primary" id="surgery_report_show"><i class="bi bi-search me-1"></i>Show report</button><button type="button" class="btn btn-outline-success" id="surgery_report_excel"><i class="bi bi-file-earmark-excel me-1"></i>Excel</button></div></div></div>
<div class="card mt-3"><div class="card-body"><div id="surgery_report_result" class="table-responsive"><div class="text-center text-muted py-4"><i class="bi bi-scissors d-block mb-2" style="font-size:2rem"></i>Select the surgery date range and show the report.</div></div></div></div>
</section>
<script>
(function () {
function buildUrl(output) { var from = $('#surgery_report_from').val(); var to = $('#surgery_report_to').val(); if (!from || !to) { alert('Select both surgery dates.'); return ''; } return '<?= base_url('Report/ipd_surgery_data') ?>/' + encodeURIComponent(from + 'S' + to) + '/' + encodeURIComponent($('#surgery_report_doctor').val() || '0') + '/' + encodeURIComponent($('#surgery_report_department').val() || '0') + (output ? '/' + output : ''); }
function show() { var url = buildUrl(0); if (!url) return; $('#surgery_report_result').html('<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) { $('#surgery_report_result').html(html); }).fail(function (xhr) { $('#surgery_report_result').html('<div class="alert alert-danger mb-0">' + (xhr.status === 403 ? 'You do not have permission to view this report.' : 'Unable to load the surgery report.') + '</div>'); }); }
$('#surgery_report_show').off('click.surgeryReport').on('click.surgeryReport', show);
$('#surgery_report_excel').off('click.surgeryReport').on('click.surgeryReport', function () { var url = buildUrl(1); if (url) window.open(url, '_blank'); });
})();
</script>

View File

@ -0,0 +1,3 @@
<?php $rows = $rows ?? []; ?>
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3"><strong>Surgery period: <?= esc($min_range ?? '') ?> to <?= esc($max_range ?? '') ?></strong><span class="text-muted small"><?= count($rows) ?> surgery record<?= count($rows) === 1 ? '' : 's' ?></span></div>
<?php if (empty($rows)) : ?><div class="alert alert-info mb-0">No surgery records were found for the selected criteria.</div><?php else : ?><table class="table table-sm table-bordered table-striped align-middle mb-0" style="font-size:.8rem"><thead class="table-light"><tr><th>Surgery Date</th><th>MR No.</th><th>Camp Code</th><th>Patient Name</th><th>Age</th><th>Sex</th><th>Address</th><th>Doctor Name</th><th>Systemic</th><th>Final Diagnosis</th><th>Eye</th><th>Surgery Code</th><th>Surgery Name</th><th>Specialty</th><th>Admission Date</th><th>Discharge Date</th><th>Scheduled Time</th><th>Remarks</th><th>Anaesthetist</th><th>Surgery Start</th><th>Surgery End</th><th>Actual Duration</th><th>Estimated Surgery Cost</th><th>Actual Surgery Cost</th></tr></thead><tbody><?php foreach ($rows as $row) : ?><tr><td><?= esc($row['surgery_date'] ?? '') ?></td><td><?= esc($row['mr_no'] ?? '') ?></td><td><?= esc($row['camp_code'] ?? '') ?></td><td><?= esc($row['patient_name'] ?? '') ?></td><td><?= esc($row['age'] ?? '') ?></td><td><?= esc($row['gender'] ?? '') ?></td><td><?= esc($row['address'] ?? '') ?></td><td><?= esc($row['doctor_name'] ?? '') ?></td><td><?= esc($row['systemic_history'] ?? '') ?></td><td><?= esc($row['final_diagnosis'] ?? '') ?></td><td><?= esc($row['procedure_side'] ?? '') ?></td><td><?= esc($row['surgery_code'] ?? '') ?></td><td><?= esc($row['surgery_name'] ?? '') ?></td><td><?= esc($row['specialty'] ?? '') ?></td><td><?= esc(substr((string) ($row['register_date'] ?? ''), 0, 10)) ?></td><td><?= esc(substr((string) ($row['discharge_date'] ?? ''), 0, 10)) ?></td><td><?= esc($row['scheduled_start_at'] ?? '') ?></td><td><?= esc($row['surgery_remark'] ?? '') ?></td><td><?= esc($row['anesthetist_name'] ?? '') ?></td><td><?= esc($row['actual_start_at'] ?? '') ?></td><td><?= esc($row['actual_end_at'] ?? '') ?></td><td><?= isset($row['actual_duration_minutes']) ? (int) $row['actual_duration_minutes'] . ' min' : '' ?></td><td class="text-end"><?= ($row['estimated_surgery_cost'] ?? null) !== null ? number_format((float) $row['estimated_surgery_cost'], 2) : '' ?></td><td class="text-end"><?= ($row['actual_surgery_cost'] ?? null) !== null ? number_format((float) $row['actual_surgery_cost'], 2) : '' ?></td></tr><?php endforeach; ?></tbody></table><?php endif; ?>