devsofttech_hms_ci4/app/Controllers/Report.php

2042 lines
81 KiB
PHP

<?php
namespace App\Controllers;
use Mpdf\Mpdf;
class Report extends BaseController
{
public function index()
{
return view('report/index');
}
public function collection_report()
{
$employees = $this->db->table('users')
->select('id, username')
->where('active', 1)
->orderBy('username', 'ASC')
->get()
->getResult();
$payModes = [
(object) ['id' => 1, 'mode_desc' => 'Cash'],
(object) ['id' => 2, 'mode_desc' => 'Bank'],
];
return view('report/collection_report', [
'employees' => $employees,
'pay_modes' => $payModes,
]);
}
public function report_total_payment_app_show(
string $dateRange,
string $employeeIds,
string $payModeId,
string $orderFirst = '0',
int $output = 0
) {
[$minRange, $maxRange] = $this->parseDateRange($dateRange);
$builder = $this->db->table('payment_history p');
$builder->select("p.id as pay_id, p.payment_date,")
->select("date_format(p.payment_date,'%d-%m-%Y %H:%i') as str_payment_date", false)
->select("case p.payof_type when 1 then 'OPD' when 2 then 'Charges' when 3 then 'ORG' when 4 then 'IPD' else 'Other' end as pay_type", false)
->select('p.payof_id, p.payof_code')
->select("if(p.credit_debit=0,p.amount,0) as cr_amount", false)
->select("if(p.credit_debit=1,p.amount,0) as dr_amount", false)
->select('p.remark, p.update_by')
->select("case p.payof_type when 1 then o.P_name when 2 then inv.inv_name when 3 then oc.insurance_company_name when 4 then pat.p_fname else '' end as patient_name", false)
->select("case p.payment_mode when 1 then 'Cash' when 2 then 'Bank' else 'Other' end as pay_mode", false)
->select('b.bank_name as bank_name, s.pay_type as bank_type')
->join('opd_master o', 'p.payof_type = 1 and o.opd_id = p.payof_id', 'left')
->join('invoice_master inv', 'p.payof_type = 2 and inv.id = p.payof_id', 'left')
->join('organization_case_master oc', 'p.payof_type = 3 and oc.id = p.payof_id', 'left')
->join('ipd_master im', 'p.payof_type = 4 and im.id = p.payof_id', 'left')
->join('patient_master pat', 'pat.id = im.p_id', 'left')
->join('hospital_bank_payment_source s', 's.id = p.pay_bank_id', 'left')
->join('hospital_bank b', 'b.id = s.bank_id', 'left')
->where('p.payment_date >=', $minRange)
->where('p.payment_date <=', $maxRange);
$empList = $this->normalizeIdList($employeeIds);
if (! empty($empList)) {
$builder->whereIn('p.update_by_id', $empList);
}
$payModeId = (int) $payModeId;
if ($payModeId < 1) {
$builder->whereIn('p.payment_mode', [1, 2]);
} else {
$builder->where('p.payment_mode', $payModeId);
}
$builder->orderBy('p.id', 'ASC');
$orderFirst = (int) $orderFirst;
if ($orderFirst === 1) {
$builder->orderBy('p.update_by', 'ASC');
} elseif ($orderFirst === 2) {
$builder->orderBy('p.payment_mode', 'ASC');
}
$rows = $builder->get()->getResult();
$totalCr = 0.0;
$totalDr = 0.0;
foreach ($rows as $row) {
$totalCr += (float) ($row->cr_amount ?? 0);
$totalDr += (float) ($row->dr_amount ?? 0);
}
$data = [
'rows' => $rows,
'min_range' => $minRange,
'max_range' => $maxRange,
'total_cr' => $totalCr,
'total_dr' => $totalDr,
'net_total' => $totalCr - $totalDr,
];
$content = view('report/collection_report_table', $data);
if ($output === 1) {
ExportExcel($content, 'Payment');
return $this->response->setBody('');
}
if ($output === 2) {
$mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
if (! is_dir($mpdfTempDir)) {
mkdir($mpdfTempDir, 0755, true);
}
$mpdf = new Mpdf([
'tempDir' => $mpdfTempDir,
'format' => 'A4-L',
'margin_top' => 8,
'margin_bottom' => 8,
'margin_left' => 8,
'margin_right' => 8,
]);
$pdfHtml = '<style>'
. 'body{font-family:Arial,sans-serif;color:#111;font-size:12px;}'
. 'table{width:100%;border-collapse:collapse;}'
. 'th,td{border:1px solid #9aa4ad;padding:6px 8px;}'
. 'th{text-align:left;background:#f5f6f7;}'
. '.text-end{text-align:right;}'
. '.table-warning td,.table-warning th{background:#fff6cc !important;}'
. '</style>'
. '<h3 style="margin:0 0 10px 0;">Collection Report</h3>'
. $content;
$mpdf->WriteHTML($pdfHtml);
$fileName = 'Collection_Report_' . date('Ymd_His') . '.pdf';
return $this->response
->setHeader('Content-Type', 'application/pdf')
->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
->setBody($mpdf->Output($fileName, 'S'));
}
return $this->response->setBody($content);
}
public function report_total_payment_total_amount_show(
string $dateRange,
string $employeeIds,
string $payModeId,
int $output = 0
) {
[$minRange, $maxRange] = $this->parseDateRange($dateRange);
$builder = $this->db->table('payment_history p');
$builder->select('p.update_by_id as user_id')
->select('COALESCE(u.username, MAX(p.update_by)) as user_name', false)
->select('SUM(CASE WHEN p.credit_debit = 0 AND p.payof_type = 1 THEN p.amount ELSE 0 END) as opd_amount', false)
->select('SUM(CASE WHEN p.credit_debit = 0 AND p.payof_type = 2 THEN p.amount ELSE 0 END) as charge_amount', false)
->select('SUM(CASE WHEN p.credit_debit = 0 AND p.payof_type = 4 THEN p.amount ELSE 0 END) as ipd_amount', false)
->select('SUM(CASE WHEN p.credit_debit = 0 AND p.payof_type = 3 THEN p.amount ELSE 0 END) as org_amount', false)
->select('SUM(CASE WHEN p.credit_debit = 1 THEN p.amount ELSE 0 END) as return_amount', false)
->select('SUM(CASE WHEN p.credit_debit = 0 THEN p.amount ELSE 0 END) - SUM(CASE WHEN p.credit_debit = 1 THEN p.amount ELSE 0 END) as total_amount', false)
->join('users u', 'u.id = p.update_by_id', 'left')
->where('p.payment_date >=', $minRange)
->where('p.payment_date <=', $maxRange);
$empList = $this->normalizeIdList($employeeIds);
if (! empty($empList)) {
$builder->whereIn('p.update_by_id', $empList);
}
$payMode = (int) $payModeId;
if ($payMode < 1) {
$builder->whereIn('p.payment_mode', [1, 2]);
} else {
$builder->where('p.payment_mode', $payMode);
}
$rows = $builder
->groupBy(['p.update_by_id', 'u.username'])
->orderBy('u.username', 'ASC')
->orderBy('p.update_by_id', 'ASC')
->get()
->getResult();
$totals = [
'opd_amount' => 0.0,
'charge_amount' => 0.0,
'ipd_amount' => 0.0,
'org_amount' => 0.0,
'return_amount' => 0.0,
'total_amount' => 0.0,
];
foreach ($rows as $row) {
$totals['opd_amount'] += (float) ($row->opd_amount ?? 0);
$totals['charge_amount'] += (float) ($row->charge_amount ?? 0);
$totals['ipd_amount'] += (float) ($row->ipd_amount ?? 0);
$totals['org_amount'] += (float) ($row->org_amount ?? 0);
$totals['return_amount'] += (float) ($row->return_amount ?? 0);
$totals['total_amount'] += (float) ($row->total_amount ?? 0);
}
$content = view('report/collection_report_total_table', [
'rows' => $rows,
'totals' => $totals,
'min_range' => $minRange,
'max_range' => $maxRange,
]);
if ($output === 1) {
ExportExcel($content, 'Collection_Report_Total_' . date('YmdHis'));
return $this->response->setBody('');
}
if ($output === 2) {
$mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
if (! is_dir($mpdfTempDir)) {
mkdir($mpdfTempDir, 0755, true);
}
$mpdf = new Mpdf([
'tempDir' => $mpdfTempDir,
'format' => 'A4-L',
'margin_top' => 8,
'margin_bottom' => 8,
'margin_left' => 8,
'margin_right' => 8,
]);
$pdfHtml = '<style>'
. 'body{font-family:Arial,sans-serif;color:#111;font-size:12px;}'
. 'table{width:100%;border-collapse:collapse;}'
. 'th,td{border:1px solid #9aa4ad;padding:6px 8px;}'
. 'th{text-align:left;background:#f5f6f7;}'
. '.text-end{text-align:right;}'
. '.table-warning td,.table-warning th{background:#fff6cc !important;}'
. '</style>'
. '<h3 style="margin:0 0 10px 0;">Collection Report - Total Amount</h3>'
. $content;
$mpdf->WriteHTML($pdfHtml);
$fileName = 'Collection_Report_Total_' . date('Ymd_His') . '.pdf';
return $this->response
->setHeader('Content-Type', 'application/pdf')
->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
->setBody($mpdf->Output($fileName, 'S'));
}
return $this->response->setBody($content);
}
private function parseDateRange(string $dateRange): array
{
$rangeParts = explode('S', $dateRange);
$startRaw = $rangeParts[0] ?? '';
$endRaw = $rangeParts[1] ?? '';
$startRaw = str_replace('T', ' ', $startRaw);
$endRaw = str_replace('T', ' ', $endRaw);
$start = trim($startRaw);
$end = trim($endRaw);
if ($start === '' || $end === '') {
$today = date('Y-m-d');
$start = $today . ' 00:00:00';
$end = $today . ' 23:59:59';
}
return [$start, $end];
}
private function normalizeIdList(string $idList): array
{
$idList = trim($idList);
if ($idList === '' || $idList === '0') {
return [];
}
$idList = str_replace('S', ',', $idList);
$parts = array_filter(array_map('trim', explode(',', $idList)));
$ids = [];
foreach ($parts as $part) {
$value = (int) $part;
if ($value > 0) {
$ids[] = $value;
}
}
return array_values(array_unique($ids));
}
public function billing_operations_report()
{
$doctors = $this->db->table('opd_master')
->select('doc_name')
->where('doc_name IS NOT NULL', null, false)
->where("doc_name != ''", null, false)
->groupBy('doc_name')
->orderBy('doc_name', 'ASC')
->get()
->getResult();
return view('report/billing_operations_report', [
'doctors' => $doctors,
]);
}
public function billing_operations_report_data(string $dateRange, string $doctor = '0', int $output = 0)
{
[$minRange, $maxRange] = $this->parseDateRange($dateRange);
$doctorFilter = trim(urldecode($doctor));
$opdBuilder = $this->db->table('opd_master o')
->select('o.doc_name')
->select('COUNT(o.opd_id) as total_opd', false)
->where('o.apointment_date >=', $minRange)
->where('o.apointment_date <=', $maxRange)
->where('o.doc_name IS NOT NULL', null, false)
->where("o.doc_name != ''", null, false);
if ($doctorFilter !== '' && $doctorFilter !== '0') {
$opdBuilder->where('o.doc_name', $doctorFilter);
}
$opdRows = $opdBuilder
->groupBy('o.doc_name')
->orderBy('total_opd', 'DESC')
->get()
->getResult();
$invoiceRows = $this->db->table('invoice_master m')
->select('m.id, m.inv_name, m.inv_date, m.total_amount, m.discount_amount')
->where('m.inv_date >=', $minRange)
->where('m.inv_date <=', $maxRange)
->where('m.invoice_status', 1)
->orderBy('m.id', 'DESC')
->get()
->getResultArray();
$invoiceIds = array_values(array_filter(array_map(static function (array $row): int {
return (int) ($row['id'] ?? 0);
}, $invoiceRows)));
$testAgg = [];
$paymentAgg = [];
if (! empty($invoiceIds)) {
$testRows = $this->db->table('invoice_item')
->select('inv_master_id')
->select('COUNT(id) as test_count', false)
->select('SUM(item_qty) as test_qty', false)
->select('SUM(item_amount) as test_amount', false)
->whereIn('inv_master_id', $invoiceIds)
->groupBy('inv_master_id')
->get()
->getResultArray();
foreach ($testRows as $row) {
$testAgg[(int) ($row['inv_master_id'] ?? 0)] = $row;
}
$payRows = $this->db->table('payment_history')
->select('payof_id')
->select("SUM(IF(credit_debit=0, amount, 0)) as total_received", false)
->select("SUM(IF(credit_debit=1, amount, 0)) as total_refund", false)
->where('payof_type', 2)
->whereIn('payof_id', $invoiceIds)
->groupBy('payof_id')
->get()
->getResultArray();
foreach ($payRows as $row) {
$paymentAgg[(int) ($row['payof_id'] ?? 0)] = $row;
}
}
$invoiceDetailRows = [];
$invoiceSummary = [
'invoice_count' => 0,
'total_invoice_amount' => 0.0,
'total_discount' => 0.0,
'total_test_count' => 0,
'total_test_qty' => 0.0,
'total_test_amount' => 0.0,
'total_received' => 0.0,
'total_refund' => 0.0,
];
foreach ($invoiceRows as $row) {
$invId = (int) ($row['id'] ?? 0);
$test = $testAgg[$invId] ?? ['test_count' => 0, 'test_qty' => 0, 'test_amount' => 0];
$pay = $paymentAgg[$invId] ?? ['total_received' => 0, 'total_refund' => 0];
$invoiceAmount = (float) ($row['total_amount'] ?? 0);
$discountAmount = (float) ($row['discount_amount'] ?? 0);
$received = (float) ($pay['total_received'] ?? 0);
$refund = (float) ($pay['total_refund'] ?? 0);
$netReceived = $received - $refund;
$invoiceSummary['invoice_count']++;
$invoiceSummary['total_invoice_amount'] += $invoiceAmount;
$invoiceSummary['total_discount'] += $discountAmount;
$invoiceSummary['total_test_count'] += (int) ($test['test_count'] ?? 0);
$invoiceSummary['total_test_qty'] += (float) ($test['test_qty'] ?? 0);
$invoiceSummary['total_test_amount'] += (float) ($test['test_amount'] ?? 0);
$invoiceSummary['total_received'] += $received;
$invoiceSummary['total_refund'] += $refund;
$invoiceDetailRows[] = (object) [
'id' => $invId,
'inv_name' => (string) ($row['inv_name'] ?? ''),
'inv_date' => (string) ($row['inv_date'] ?? ''),
'invoice_amount' => $invoiceAmount,
'discount_amount' => $discountAmount,
'test_count' => (int) ($test['test_count'] ?? 0),
'test_qty' => (float) ($test['test_qty'] ?? 0),
'test_amount' => (float) ($test['test_amount'] ?? 0),
'received_amount' => $received,
'refund_amount' => $refund,
'net_received' => $netReceived,
'pending_amount' => $invoiceAmount - $netReceived,
];
}
$ipdPaymentRows = $this->db->table('payment_history p')
->select('p.payof_id as ipd_id, i.ipd_code, pat.p_fname as patient_name')
->select("SUM(IF(p.credit_debit=0, p.amount, 0)) as total_received", false)
->select("SUM(IF(p.credit_debit=1, p.amount, 0)) as total_refund", false)
->join('ipd_master i', 'i.id = p.payof_id', 'left')
->join('patient_master pat', 'pat.id = i.p_id', 'left')
->where('p.payof_type', 4)
->where('p.payment_date >=', $minRange)
->where('p.payment_date <=', $maxRange)
->groupBy('p.payof_id')
->orderBy('total_received', 'DESC')
->get()
->getResult();
$ipdSummary = [
'total_received' => 0.0,
'total_refund' => 0.0,
'net_received' => 0.0,
'ipd_count' => 0,
];
foreach ($ipdPaymentRows as $row) {
$received = (float) ($row->total_received ?? 0);
$refund = (float) ($row->total_refund ?? 0);
$ipdSummary['total_received'] += $received;
$ipdSummary['total_refund'] += $refund;
$ipdSummary['ipd_count']++;
}
$ipdSummary['net_received'] = $ipdSummary['total_received'] - $ipdSummary['total_refund'];
$data = [
'min_range' => $minRange,
'max_range' => $maxRange,
'doctor_filter' => $doctorFilter,
'opd_rows' => $opdRows,
'invoice_summary' => $invoiceSummary,
'invoice_rows' => $invoiceDetailRows,
'ipd_summary' => $ipdSummary,
'ipd_payment_rows' => $ipdPaymentRows,
];
$content = view('report/billing_operations_report_table', $data);
if ($output === 1) {
ExportExcel($content, 'Billing_Operations_Report');
return $this->response->setBody('');
}
if ($output === 2) {
$mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
if (! is_dir($mpdfTempDir)) {
mkdir($mpdfTempDir, 0755, true);
}
$mpdf = new Mpdf([
'tempDir' => $mpdfTempDir,
'format' => 'A4-L',
'margin_top' => 8,
'margin_bottom' => 8,
'margin_left' => 8,
'margin_right' => 8,
]);
$pdfHtml = '<style>'
. 'body{font-family:Arial,sans-serif;color:#111;font-size:12px;}'
. 'table{width:100%;border-collapse:collapse;}'
. 'th,td{border:1px solid #9aa4ad;padding:6px 8px;}'
. 'th{text-align:left;background:#f5f6f7;}'
. '.text-end{text-align:right;}'
. '.table-light td,.table-light th{background:#f5f6f7 !important;}'
. '</style>'
. '<h3 style="margin:0 0 10px 0;">Billing Operations Report</h3>'
. $content;
$mpdf->WriteHTML($pdfHtml);
$fileName = 'Billing_Operations_Report_' . date('Ymd_His') . '.pdf';
return $this->response
->setHeader('Content-Type', 'application/pdf')
->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
->setBody($mpdf->Output($fileName, 'S'));
}
return $this->response->setBody($content);
}
public function diagnosis_report()
{
$itemTypes = $this->db->table('hc_item_type')
->select('itype_id, group_desc')
->whereIn('is_ipd_opd', [0, 1])
->orderBy('group_desc', 'ASC')
->get()
->getResult();
return view('report/diagnosis_report', [
'item_types' => $itemTypes,
]);
}
public function document_list()
{
$doctors = $this->db->table('doctor_master')
->select('id, p_fname')
->where('active', 1)
->orderBy('p_fname', 'ASC')
->get()
->getResult();
return view('report/document_list', [
'doclist' => $doctors,
]);
}
public function document_list_data(string $dateRange, int $drId, int $output = 0)
{
[$minRange, $maxRange] = $this->parseDateRange($dateRange);
$uhidFilter = trim((string) $this->request->getGet('uhid'));
if (! $this->db->tableExists('patient_doc') || ! $this->db->tableExists('doctor_master') || ! $this->db->tableExists('doc_format_master')) {
$content = '<div class="alert alert-danger mb-0">Document report tables are not available in this database.</div>';
return $this->response->setBody($content);
}
$patientTable = null;
if ($this->db->tableExists('patient_master_exten')) {
$patientTable = 'patient_master_exten';
} elseif ($this->db->tableExists('patient_master')) {
$patientTable = 'patient_master';
}
if ($patientTable === null) {
$content = '<div class="alert alert-danger mb-0">Patient master table not found for Document Issue Report.</div>';
return $this->response->setBody($content);
}
$docFields = $this->db->getFieldNames('patient_doc') ?? [];
$patientFields = $this->db->getFieldNames($patientTable) ?? [];
$doctorFields = $this->db->getFieldNames('doctor_master') ?? [];
$formatFields = $this->db->getFieldNames('doc_format_master') ?? [];
$dateIssueCol = $this->resolveExistingColumn($docFields, ['date_issue', 'issue_date', 'created_at']);
if ($dateIssueCol === null) {
$content = '<div class="alert alert-danger mb-0">Issue date column was not found in patient_doc table.</div>';
return $this->response->setBody($content);
}
$docPatientFkCol = $this->resolveExistingColumn($docFields, ['p_id', 'patient_id']);
$docDoctorFkCol = $this->resolveExistingColumn($docFields, ['dr_id', 'doc_id', 'doctor_id']);
$docFormatFkCol = $this->resolveExistingColumn($docFields, ['doc_format_id', 'format_id', 'df_id']);
if ($docPatientFkCol === null || $docDoctorFkCol === null || $docFormatFkCol === null) {
$content = '<div class="alert alert-danger mb-0">Document link columns are missing in patient_doc table.</div>';
return $this->response->setBody($content);
}
$patientPkCol = $this->resolveExistingColumn($patientFields, ['id']);
$patientNameCol = $this->resolveExistingColumn($patientFields, ['p_fname', 'patient_name', 'name']);
$patientRelativeCol = $this->resolveExistingColumn($patientFields, ['p_relative']);
$patientRelativeNameCol = $this->resolveExistingColumn($patientFields, ['p_rname', 'relative_name']);
$patientCodeCol = $this->resolveExistingColumn($patientFields, ['p_code', 'patient_code', 'uhid_no']);
$doctorPkCol = $this->resolveExistingColumn($doctorFields, ['id']);
$doctorNameCol = $this->resolveExistingColumn($doctorFields, ['p_fname', 'name', 'doc_name']);
$formatPkCol = $this->resolveExistingColumn($formatFields, ['df_id', 'id']);
$formatNameCol = $this->resolveExistingColumn($formatFields, ['doc_name', 'name', 'format_name']);
if ($patientPkCol === null || $patientNameCol === null || $doctorPkCol === null || $doctorNameCol === null || $formatPkCol === null || $formatNameCol === null) {
$content = '<div class="alert alert-danger mb-0">Required patient columns are missing for Document Issue Report.</div>';
return $this->response->setBody($content);
}
try {
$builder = $this->db->table('patient_doc d');
$builder->select('d.id AS doc_id')
->select('f.' . $formatNameCol . ' AS doc_name', false)
->select('doc.' . $doctorNameCol . ' AS dr_name', false)
->select('doc.' . $doctorPkCol . ' AS dr_id', false)
->select('p.' . $patientNameCol . ' AS p_fname', false)
->select('p.' . $patientPkCol . ' AS id', false)
->select($patientRelativeNameCol !== null ? ('p.' . $patientRelativeNameCol . ' AS p_rname') : "'' AS p_rname", false)
->select($patientRelativeCol !== null ? ('p.' . $patientRelativeCol . ' AS p_relative') : "'' AS p_relative", false)
->select($patientCodeCol !== null ? ('p.' . $patientCodeCol . ' AS p_code') : "'' AS p_code", false)
->select('d.' . $dateIssueCol . ' AS date_issue', false)
->select("DATE_FORMAT(d." . $dateIssueCol . ", '%d-%m-%Y') AS str_date_issue", false)
->join($patientTable . ' p', 'p.' . $patientPkCol . '=d.' . $docPatientFkCol, 'inner')
->join('doctor_master doc', 'd.' . $docDoctorFkCol . '=doc.' . $doctorPkCol, 'inner')
->join('doc_format_master f', 'd.' . $docFormatFkCol . '=f.' . $formatPkCol, 'inner')
->where('d.' . $dateIssueCol . ' >=', $minRange)
->where('d.' . $dateIssueCol . ' <=', $maxRange)
->orderBy('d.' . $dateIssueCol, 'DESC')
->orderBy('d.id', 'DESC');
if ($drId > 0) {
$builder->where('doc.id', $drId);
}
if ($uhidFilter !== '' && $patientCodeCol !== null) {
$builder->like('p.' . $patientCodeCol, $uhidFilter);
}
$rows = $builder->get()->getResult();
} catch (\Throwable $e) {
log_message('error', 'Document Issue Report query failed: {message}', ['message' => $e->getMessage()]);
$content = '<div class="alert alert-danger mb-0">Unable to load Document Issue Report data. Please check database schema.</div>';
return $this->response->setBody($content);
}
$data = [
'rows' => $rows,
'min_range' => $minRange,
'max_range' => $maxRange,
'doctor_id' => $drId,
'uhid_filter' => $uhidFilter,
];
$content = view('report/document_list_table', $data);
if ($output === 1) {
ExportExcel($content, 'document_List_' . date('YmdHis'));
return $this->response->setBody('');
}
if ($output === 2) {
$mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
if (! is_dir($mpdfTempDir)) {
mkdir($mpdfTempDir, 0755, true);
}
$mpdf = new Mpdf([
'tempDir' => $mpdfTempDir,
'format' => 'A4',
'margin_top' => 8,
'margin_bottom' => 8,
'margin_left' => 8,
'margin_right' => 8,
]);
$pdfHtml = '<style>'
. 'body{font-family:Arial,sans-serif;color:#111;font-size:12px;}'
. 'table{width:100%;border-collapse:collapse;}'
. 'th,td{border:1px solid #9aa4ad;padding:6px 8px;}'
. 'th{text-align:left;background:#f5f6f7;}'
. '.text-end{text-align:right;}'
. '.table-light td,.table-light th{background:#f5f6f7 !important;}'
. '</style>'
. '<h3 style="margin:0 0 10px 0;">Document Issue Report</h3>'
. $content;
$mpdf->WriteHTML($pdfHtml);
$fileName = 'Document_Issue_Report_' . date('Ymd_His') . '.pdf';
return $this->response
->setHeader('Content-Type', 'application/pdf')
->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
->setBody($mpdf->Output($fileName, 'S'));
}
return $this->response->setBody($content);
}
public function diagnosis_report_data(
string $dateRange,
string $invoiceType = '0',
string $diagnosisIds = '0',
int $output = 0
) {
[$minRange, $maxRange] = $this->parseDateRange($dateRange);
$netAmountExpr = "ROUND(IF(m.invoice_status=1, (t.item_amount - (t.item_amount * m.discount_amount / NULLIF(m.total_amount, 0))), 0), 2)";
$builder = $this->db->table('invoice_master m');
$builder->select('y.group_desc, t.item_name')
->select('COUNT(t.id) as no_test', false)
->select('SUM(t.item_qty) as no_qty', false)
->select('SUM(' . $netAmountExpr . ') as total_amount', false)
->select('SUM(IF(m.invoice_status=1, t.item_amount, 0)) as total_act_amount', false)
->select('SUM(CASE WHEN m.ipd_id = 0 AND IFNULL(m.insurance_case_id, 0) = 0 THEN ' . $netAmountExpr . ' ELSE 0 END) as direct_patient_amount', false)
->select('SUM(CASE WHEN m.ipd_id > 0 AND IFNULL(m.insurance_case_id, 0) = 0 THEN ' . $netAmountExpr . ' ELSE 0 END) as ipd_direct_amount', false)
->select('SUM(CASE WHEN m.ipd_id > 0 AND IFNULL(m.insurance_case_id, 0) > 0 THEN ' . $netAmountExpr . ' ELSE 0 END) as ipd_tpa_amount', false)
->select('SUM(CASE WHEN m.ipd_id = 0 AND IFNULL(m.insurance_case_id, 0) > 0 THEN ' . $netAmountExpr . ' ELSE 0 END) as org_credit_amount', false)
->join('invoice_item t', 'm.id = t.inv_master_id', 'inner')
->join('hc_item_type y', 't.item_type = y.itype_id', 'inner')
->where('y.is_ipd_opd IN (0, 1)', null, false)
->where('m.invoice_status', 1)
->where('m.inv_date >=', $minRange)
->where('m.inv_date <=', $maxRange);
// Invoice type filter
$invoiceTypeInt = (int) $invoiceType;
if ($invoiceTypeInt === 1) {
// IPD
$builder->where('m.ipd_id >', 0);
} elseif ($invoiceTypeInt === 2) {
// OPD
$builder->where('m.ipd_id', 0)
->where('m.insurance_case_id', 0);
} elseif ($invoiceTypeInt === 3) {
// Organization
$builder->where('m.insurance_case_id >', 0);
}
$diagList = $this->normalizeIdList($diagnosisIds);
if (!empty($diagList)) {
$builder->whereIn('t.item_type', $diagList);
}
$builder->groupBy(['y.group_desc', 't.item_id', 't.item_name'])
->orderBy('y.group_desc', 'ASC')
->orderBy('t.item_name', 'ASC');
$rows = $builder->get()->getResult();
$invoiceTypeLabel = ['All Types', 'IPD', 'OPD', 'Organization'][$invoiceTypeInt] ?? 'All Types';
$data = [
'rows' => $rows,
'min_range' => $minRange,
'max_range' => $maxRange,
'invoice_type_label' => $invoiceTypeLabel,
];
$content = view('report/diagnosis_report_table', $data);
if ($output === 1) {
ExportExcel($content, 'Diagnosis_Report');
return $this->response->setBody('');
}
if ($output === 2) {
$mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
if (! is_dir($mpdfTempDir)) {
mkdir($mpdfTempDir, 0755, true);
}
$mpdf = new Mpdf([
'tempDir' => $mpdfTempDir,
'format' => 'A4-L',
'margin_top' => 8,
'margin_bottom' => 8,
'margin_left' => 8,
'margin_right' => 8,
]);
$pdfHtml = '<style>'
. 'body{font-family:Arial,sans-serif;color:#111;font-size:12px;}'
. 'table{width:100%;border-collapse:collapse;}'
. 'th,td{border:1px solid #9aa4ad;padding:6px 8px;}'
. 'th{background:#f5f6f7;text-align:left;}'
. '.text-end{text-align:right;}'
. '.table-info td,.table-info th{background:#d9edf7 !important;}'
. '.table-secondary td,.table-secondary th{background:#eceff1 !important;}'
. '.table-primary td,.table-primary th{background:#dbe9ff !important;}'
. '.mb-3{margin-bottom:12px;}'
. '</style>'
. '<h3 style="margin:0 0 10px 0;">Diagnosis Report</h3>'
. $content;
$mpdf->WriteHTML($pdfHtml);
$fileName = 'Diagnosis_Report_' . date('Ymd_His') . '.pdf';
return $this->response
->setHeader('Content-Type', 'application/pdf')
->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
->setBody($mpdf->Output($fileName, 'S'));
}
return $this->response->setBody($content);
}
// ==================== Insurance Credit Reports ====================
public function insurance_credit_main()
{
return view('report/insurance_credit_main');
}
public function echs_ipd_list_main()
{
return $this->insurance_ipd_report();
}
public function echs_opd_list_main()
{
return $this->insurance_opd_report();
}
public function echs_ipd_list_data(
string $dateRange,
string $orgStatus = '-1',
string $ipdType = '0',
int $output = 0
) {
$insuranceId = $this->mapLegacyInsuranceFilter($ipdType);
return $this->insurance_ipd_report_data($dateRange, $insuranceId, $orgStatus, $output);
}
public function echs_opd_list_data(
string $dateRange,
string $orgStatus = '-1',
string $ipdType = '0',
int $output = 0
) {
$insuranceId = $this->mapLegacyInsuranceFilter($ipdType);
return $this->insurance_opd_report_data($dateRange, $insuranceId, $orgStatus, $output);
}
private function mapLegacyInsuranceFilter(string $legacyType): string
{
$legacyValue = (int) trim($legacyType);
if ($legacyValue > 0) {
return 'G' . $legacyValue;
}
if ($legacyValue < 0) {
return (string) abs($legacyValue);
}
return '0';
}
public function insurance_opd_report()
{
$insuranceGroups = $this->db->table('h_insurance_group')
->orderBy('tpa_group', 'ASC')
->get()
->getResult();
$insuranceCompanies = $this->db->table('hc_insurance')
->orderBy('short_name', 'ASC')
->get()
->getResult();
return view('report/insurance_opd_report', [
'insurance_groups' => $insuranceGroups,
'insurance_companies' => $insuranceCompanies,
]);
}
public function insurance_ipd_report()
{
$insuranceGroups = $this->db->table('h_insurance_group')
->orderBy('tpa_group', 'ASC')
->get()
->getResult();
$insuranceCompanies = $this->db->table('hc_insurance')
->orderBy('short_name', 'ASC')
->get()
->getResult();
return view('report/insurance_ipd_report', [
'insurance_groups' => $insuranceGroups,
'insurance_companies' => $insuranceCompanies,
]);
}
public function insurance_combined_report()
{
$insuranceGroups = $this->db->table('h_insurance_group')
->orderBy('tpa_group', 'ASC')
->get()
->getResult();
$insuranceCompanies = $this->db->table('hc_insurance')
->orderBy('short_name', 'ASC')
->get()
->getResult();
return view('report/insurance_combined_report', [
'insurance_groups' => $insuranceGroups,
'insurance_companies' => $insuranceCompanies,
]);
}
public function ayushman_unmapped_report()
{
$specialities = [];
if ($this->db->tableExists('ayushman_package_master')) {
$specialities = $this->db->table('ayushman_package_master')
->select('speciality_code, speciality_name')
->groupBy('speciality_code, speciality_name')
->orderBy('speciality_name', 'ASC')
->get()
->getResult();
}
return view('report/ayushman_unmapped_report', [
'specialities' => $specialities,
]);
}
public function ayushman_unmapped_report_data(string $specialityCode = '0', int $output = 0)
{
if (! $this->db->tableExists('ayushman_package_master')) {
return $this->response->setBody('<div class="alert alert-warning">Ayushman package master table is not available.</div>');
}
$builder = $this->db->table('ayushman_package_master a')
->select('a.speciality_code, a.speciality_name, a.procedure_code, a.procedure_name, a.package_amount, a.preauth_required, a.pre_investigations, a.post_investigations')
->where('(a.linked_package_id IS NULL OR a.linked_package_id = 0)', null, false)
->orderBy('a.speciality_name', 'ASC')
->orderBy('a.procedure_name', 'ASC');
if ($specialityCode !== '0') {
$builder->where('a.speciality_code', $specialityCode);
}
$rows = $builder->get()->getResult();
$data = [
'rows' => $rows,
'speciality_code' => $specialityCode,
];
$content = view('report/ayushman_unmapped_report_table', $data);
if ($output === 1) {
ExportExcel($content, 'Ayushman_Unmapped_Procedures_' . date('YmdHis'));
return $this->response->setBody('');
}
return $this->response->setBody($content);
}
public function ayushman_case_dashboard()
{
return view('report/ayushman_case_dashboard');
}
public function ayushman_case_dashboard_data(string $dateRange, string $alertType = 'all', int $output = 0)
{
[$minRange, $maxRange] = $this->parseDateRange($dateRange);
$builder = $this->db->table('organization_case_master o')
->select('o.id as case_id, o.case_id_code, o.preauth_send, o.doc_recd, o.final_bill_send, o.org_approved_status, o.Org_insurance_comp')
->select('i.id as ipd_id, i.ipd_code')
->select("DATE_FORMAT(i.register_date, '%d-%m-%Y') as str_register_date", false)
->select('p.p_code, p.p_fname')
->select('ins.short_name as insurance_company')
->select('(SELECT COUNT(*) FROM ipd_package ip JOIN ayushman_package_master a ON a.procedure_code = ip.org_code WHERE ip.ipd_id = i.id) as ayushman_proc_count', false)
->select('(SELECT COUNT(*) FROM ipd_package ip JOIN ayushman_package_master a ON a.procedure_code = ip.org_code WHERE ip.ipd_id = i.id AND (a.linked_package_id IS NULL OR a.linked_package_id = 0)) as unmapped_count', false)
->select('(SELECT COUNT(*) FROM ipd_package ip JOIN ayushman_package_master a ON a.procedure_code = ip.org_code WHERE ip.ipd_id = i.id AND a.preauth_required = 1) as preauth_required_count', false)
->join('ipd_master i', 'o.id = i.case_id', 'inner')
->join('patient_master p', 'i.p_id = p.id', 'inner')
->join('hc_insurance ins', 'ins.id = o.insurance_id', 'left')
->where('o.case_type', 1)
->where('i.register_date >=', $minRange)
->where('i.register_date <=', $maxRange)
->groupStart()
->like('o.Org_insurance_comp', 'ayushman')
->orLike('o.Org_insurance_comp', 'pmjay')
->orLike('ins.short_name', 'ayushman')
->orLike('ins.ins_company_name', 'ayushman')
->groupEnd()
->orderBy('i.id', 'DESC');
$rows = $builder->get()->getResultArray();
$alertType = strtolower(trim($alertType));
if (! in_array($alertType, ['all', 'preauth-pending', 'docs-pending', 'mapping-gap'], true)) {
$alertType = 'all';
}
$rows = array_values(array_filter($rows, static function (array $row) use ($alertType): bool {
$preauthPending = (int) ($row['preauth_required_count'] ?? 0) > 0 && (int) ($row['preauth_send'] ?? 0) !== 1;
$docsPending = (int) ($row['doc_recd'] ?? 0) !== 1;
$mappingGap = (int) ($row['unmapped_count'] ?? 0) > 0;
if ($alertType === 'preauth-pending') {
return $preauthPending;
}
if ($alertType === 'docs-pending') {
return $docsPending;
}
if ($alertType === 'mapping-gap') {
return $mappingGap;
}
return true;
}));
$summary = [
'total_cases' => count($rows),
'preauth_pending' => 0,
'docs_pending' => 0,
'mapping_gaps' => 0,
];
foreach ($rows as $row) {
if ((int) ($row['preauth_required_count'] ?? 0) > 0 && (int) ($row['preauth_send'] ?? 0) !== 1) {
$summary['preauth_pending']++;
}
if ((int) ($row['doc_recd'] ?? 0) !== 1) {
$summary['docs_pending']++;
}
if ((int) ($row['unmapped_count'] ?? 0) > 0) {
$summary['mapping_gaps']++;
}
}
$data = [
'rows' => $rows,
'summary' => $summary,
'min_range' => $minRange,
'max_range' => $maxRange,
'alert_type' => $alertType,
];
$content = view('report/ayushman_case_dashboard_table', $data);
if ($output === 1) {
ExportExcel($content, 'Ayushman_Case_Dashboard_' . date('YmdHis'));
return $this->response->setBody('');
}
return $this->response->setBody($content);
}
public function insurance_opd_report_data(
string $dateRange,
string $insuranceId = '0',
string $caseStatus = '-1',
int $output = 0
) {
[$minRange, $maxRange] = $this->parseDateRange($dateRange);
$builder = $this->db->table('organization_case_master o');
$builder->select('o.id, o.case_id_code')
->select("CONCAT(o.insurance_no, '/', o.insurance_no_1) as case_info", false)
->select('p.p_code, p.p_fname')
->select('ins.short_name as insurance_company')
->select("DATE_FORMAT(o.date_registration, '%d-%m-%Y') as str_date_registration", false)
->select("(CASE o.status WHEN 0 THEN 'Pending' WHEN 1 THEN 'Bill Complete' WHEN 2 THEN 'Submitted' ELSE 'Unknown' END) as org_submit_status", false)
->select('(o.inv_opd_amt + o.inv_opd_charge_amt) as charge_amount', false)
->select('o.inv_opd_med_amt as med_amount')
->select('((o.inv_opd_amt + o.inv_opd_charge_amt) + o.inv_opd_med_amt) as net_amount', false)
->join('patient_master p', 'o.p_id = p.id', 'inner')
->join('hc_insurance ins', 'ins.id = o.insurance_id', 'inner')
->where('o.case_type', 0)
->where('o.date_registration >=', $minRange)
->where('o.date_registration <=', $maxRange)
->where('((o.inv_opd_amt + o.inv_opd_charge_amt) + o.inv_opd_med_amt) >', 0);
// Insurance filter
if ($insuranceId !== '0') {
if (strpos($insuranceId, 'G') === 0) {
// Insurance Group
$groupId = (int) substr($insuranceId, 1);
$builder->where('ins.group_ins', $groupId);
} else {
// Individual Insurance
$builder->where('o.insurance_id', (int) $insuranceId);
}
}
// Status filter
$statusInt = (int) $caseStatus;
if ($statusInt >= 0) {
$builder->where('o.status', $statusInt);
}
$builder->orderBy('o.id', 'ASC');
$rows = $builder->get()->getResult();
$data = [
'rows' => $rows,
'min_range' => $minRange,
'max_range' => $maxRange,
'case_type' => 'OPD',
];
$content = view('report/insurance_opd_report_table', $data);
if ($output === 1) {
ExportExcel($content, 'Insurance_OPD_Report');
return $this->response->setBody('');
}
return $this->response->setBody($content);
}
public function insurance_ipd_report_data(
string $dateRange,
string $insuranceId = '0',
string $caseStatus = '-1',
int $output = 0
) {
[$minRange, $maxRange] = $this->parseDateRange($dateRange);
$builder = $this->db->table('organization_case_master o');
$builder->select('o.id, o.case_id_code')
->select("CONCAT(o.insurance_no, '/', o.insurance_no_1) as case_info", false)
->select('i.ipd_code, p.p_code, p.p_fname')
->select('ins.short_name as insurance_company')
->select("DATE_FORMAT(i.register_date, '%d-%m-%Y') as str_register_date", false)
->select("DATE_FORMAT(i.discharge_date, '%d-%m-%Y') as str_discharge_date", false)
->select("(CASE o.status WHEN 0 THEN 'Pending' WHEN 1 THEN 'Bill Complete' WHEN 2 THEN 'Submitted' ELSE 'Unknown' END) as org_submit_status", false)
->select('(i.charge_amount + i.chargeamount1 + i.chargeamount2) as charge_amount', false)
->select('i.med_amount')
->select('(i.Discount + i.Discount2 + i.Discount3) as discount', false)
->select('((i.charge_amount + i.chargeamount1 + i.chargeamount2 + i.med_amount) - (i.Discount + i.Discount2 + i.Discount3)) as net_amount', false)
->select('o.amount_recived, o.amount_deduction')
->join('ipd_master i', 'o.id = i.case_id', 'inner')
->join('patient_master p', 'i.p_id = p.id', 'inner')
->join('hc_insurance ins', 'ins.id = o.insurance_id', 'inner')
->where('o.case_type', 1)
->where('i.ipd_status', 1)
->where('i.register_date >=', $minRange)
->where('i.register_date <=', $maxRange);
// Insurance filter
if ($insuranceId !== '0') {
if (strpos($insuranceId, 'G') === 0) {
// Insurance Group
$groupId = (int) substr($insuranceId, 1);
$builder->where('ins.group_ins', $groupId);
} else {
// Individual Insurance
$builder->where('o.insurance_id', (int) $insuranceId);
}
}
// Status filter
$statusInt = (int) $caseStatus;
if ($statusInt >= 0) {
$builder->where('o.status', $statusInt);
}
$builder->orderBy('i.id', 'ASC');
$rows = $builder->get()->getResult();
$data = [
'rows' => $rows,
'min_range' => $minRange,
'max_range' => $maxRange,
'case_type' => 'IPD',
];
$content = view('report/insurance_ipd_report_table', $data);
if ($output === 1) {
ExportExcel($content, 'Insurance_IPD_Report');
return $this->response->setBody('');
}
return $this->response->setBody($content);
}
public function insurance_combined_report_data(
string $dateRange,
string $caseType = '-1',
string $insuranceId = '0',
int $output = 0
) {
[$minRange, $maxRange] = $this->parseDateRange($dateRange);
$caseTypeInt = (int) $caseType;
$data = [
'min_range' => $minRange,
'max_range' => $maxRange,
'opd_rows' => [],
'ipd_rows' => [],
];
// Get OPD data if needed
if ($caseTypeInt === -1 || $caseTypeInt === 0) {
$builder = $this->db->table('organization_case_master o');
$builder->select('ins.short_name as insurance_company')
->select('COUNT(o.id) as total_cases', false)
->select('SUM(o.inv_opd_amt + o.inv_opd_charge_amt) as total_charge', false)
->select('SUM(o.inv_opd_med_amt) as total_med', false)
->select('SUM((o.inv_opd_amt + o.inv_opd_charge_amt) + o.inv_opd_med_amt) as total_net', false)
->join('hc_insurance ins', 'ins.id = o.insurance_id', 'inner')
->where('o.case_type', 0)
->where('o.date_registration >=', $minRange)
->where('o.date_registration <=', $maxRange);
if ($insuranceId !== '0') {
if (strpos($insuranceId, 'G') === 0) {
$groupId = (int) substr($insuranceId, 1);
$builder->where('ins.group_ins', $groupId);
} else {
$builder->where('o.insurance_id', (int) $insuranceId);
}
}
$builder->groupBy('ins.short_name')->orderBy('ins.short_name', 'ASC');
$data['opd_rows'] = $builder->get()->getResult();
}
// Get IPD data if needed
if ($caseTypeInt === -1 || $caseTypeInt === 1) {
$builder = $this->db->table('organization_case_master o');
$builder->select('ins.short_name as insurance_company')
->select('COUNT(i.id) as total_cases', false)
->select('SUM(i.charge_amount + i.chargeamount1 + i.chargeamount2) as total_charge', false)
->select('SUM(i.med_amount) as total_med', false)
->select('SUM(i.Discount + i.Discount2 + i.Discount3) as total_discount', false)
->select('SUM((i.charge_amount + i.chargeamount1 + i.chargeamount2 + i.med_amount) - (i.Discount + i.Discount2 + i.Discount3)) as total_net', false)
->select('SUM(o.amount_recived) as total_received', false)
->select('SUM(o.amount_deduction) as total_deducted', false)
->join('ipd_master i', 'o.id = i.case_id', 'inner')
->join('hc_insurance ins', 'ins.id = o.insurance_id', 'inner')
->where('o.case_type', 1)
->where('i.ipd_status', 1)
->where('i.register_date >=', $minRange)
->where('i.register_date <=', $maxRange);
if ($insuranceId !== '0') {
if (strpos($insuranceId, 'G') === 0) {
$groupId = (int) substr($insuranceId, 1);
$builder->where('ins.group_ins', $groupId);
} else {
$builder->where('o.insurance_id', (int) $insuranceId);
}
}
$builder->groupBy('ins.short_name')->orderBy('ins.short_name', 'ASC');
$data['ipd_rows'] = $builder->get()->getResult();
}
$content = view('report/insurance_combined_report_table', $data);
if ($output === 1) {
ExportExcel($content, 'Insurance_Combined_Report');
return $this->response->setBody('');
}
return $this->response->setBody($content);
}
public function nabh_audit_report()
{
return view('report/nabh_audit_report');
}
public function nabh_audit_report_data(
string $dateRange,
string $module = 'all',
string $status = 'all',
int $output = 0
) {
[$minRange, $maxRange] = $this->parseDateRange($dateRange);
$module = strtolower(trim($module));
if (! in_array($module, ['all', 'ipd', 'opd', 'radiology'], true)) {
$module = 'all';
}
$status = strtolower(trim($status));
if (! in_array($status, ['all', 'critical-missing', 'compliant'], true)) {
$status = 'all';
}
$ipdRows = [];
$opdRows = [];
if ($module === 'all' || $module === 'ipd') {
$ipdRows = $this->getIpdNabhAuditRows($minRange, $maxRange);
}
if ($module === 'all' || $module === 'opd') {
$opdRows = $this->getOpdNabhAuditRows($minRange, $maxRange);
}
$radiologyRows = [];
if ($module === 'all' || $module === 'radiology') {
$radiologyRows = $this->getRadiologyNabhAuditRows($minRange, $maxRange);
}
$rows = array_merge($ipdRows, $opdRows, $radiologyRows);
if ($status === 'critical-missing') {
$rows = array_values(array_filter($rows, static function (array $row): bool {
return (int) ($row['critical_missing_count'] ?? 0) > 0;
}));
} elseif ($status === 'compliant') {
$rows = array_values(array_filter($rows, static function (array $row): bool {
return (int) ($row['critical_missing_count'] ?? 0) === 0;
}));
}
usort($rows, static function (array $a, array $b): int {
return strcmp((string) ($b['encounter_datetime'] ?? ''), (string) ($a['encounter_datetime'] ?? ''));
});
$summary = $this->buildNabhSummary($rows);
$data = [
'rows' => $rows,
'summary' => $summary,
'min_range' => $minRange,
'max_range' => $maxRange,
'module_filter' => $module,
'status_filter' => $status,
];
$content = view('report/nabh_audit_report_table', $data);
if ($output === 1) {
ExportExcel($content, 'NABH_Audit_Report');
return $this->response->setBody('');
}
return $this->response->setBody($content);
}
private function buildNabhSummary(array $rows): array
{
$summary = [
'all' => ['label' => 'Overall', 'total' => 0, 'compliant' => 0, 'critical_missing' => 0, 'avg_completion' => 0.0],
'ipd' => ['label' => 'IPD Discharge', 'total' => 0, 'compliant' => 0, 'critical_missing' => 0, 'avg_completion' => 0.0],
'opd' => ['label' => 'OPD Consult', 'total' => 0, 'compliant' => 0, 'critical_missing' => 0, 'avg_completion' => 0.0],
'radiology' => ['label' => 'Radiology Edit', 'total' => 0, 'compliant' => 0, 'critical_missing' => 0, 'avg_completion' => 0.0],
];
$completionAccumulator = [
'all' => 0.0,
'ipd' => 0.0,
'opd' => 0.0,
'radiology' => 0.0,
];
foreach ($rows as $row) {
$module = strtolower((string) ($row['module_key'] ?? 'all'));
if (! isset($summary[$module])) {
$module = 'all';
}
$isCompliant = (int) ($row['critical_missing_count'] ?? 0) === 0;
$completion = (float) ($row['completion_percent'] ?? 0);
$summary['all']['total']++;
$completionAccumulator['all'] += $completion;
if ($isCompliant) {
$summary['all']['compliant']++;
} else {
$summary['all']['critical_missing']++;
}
if ($module !== 'all') {
$summary[$module]['total']++;
$completionAccumulator[$module] += $completion;
if ($isCompliant) {
$summary[$module]['compliant']++;
} else {
$summary[$module]['critical_missing']++;
}
}
}
foreach ($summary as $key => $item) {
$total = (int) ($item['total'] ?? 0);
$summary[$key]['avg_completion'] = $total > 0
? round($completionAccumulator[$key] / $total, 1)
: 0.0;
}
return $summary;
}
private function getIpdNabhAuditRows(string $minRange, string $maxRange): array
{
if (! $this->db->tableExists('ipd_master')) {
return [];
}
$ipdFields = $this->db->getFieldNames('ipd_master') ?? [];
if (! in_array('id', $ipdFields, true)) {
return [];
}
$dateCol = $this->resolveExistingColumn($ipdFields, [
'discharge_date',
'discharged_at',
'discharge_datetime',
'register_date',
'admit_date',
'created_at',
]);
if ($dateCol === null) {
return [];
}
$ipdCodeCol = $this->resolveExistingColumn($ipdFields, ['ipd_code', 'ipd_no', 'ipd_number']);
$patientFkCol = $this->resolveExistingColumn($ipdFields, ['p_id', 'patient_id']);
$statusCol = $this->resolveExistingColumn($ipdFields, ['ipd_status', 'discharge_status', 'discharg_status', 'is_discharged']);
$builder = $this->db->table('ipd_master i')
->select('i.id as encounter_id')
->select('i.' . $dateCol . ' as encounter_datetime', false);
if ($ipdCodeCol !== null) {
$builder->select('i.' . $ipdCodeCol . ' as encounter_code', false);
} else {
$builder->select("'' as encounter_code", false);
}
if ($patientFkCol !== null) {
$builder->select('i.' . $patientFkCol . ' as patient_id', false);
} else {
$builder->select('0 as patient_id', false);
}
$patientNameExpr = "''";
$patientCodeExpr = "''";
if ($patientFkCol !== null && $this->db->tableExists('patient_master')) {
$patientFields = $this->db->getFieldNames('patient_master') ?? [];
$nameCol = $this->resolveExistingColumn($patientFields, ['p_fname', 'patient_name', 'name']);
$codeCol = $this->resolveExistingColumn($patientFields, ['p_code', 'patient_code', 'uhid_no']);
$pkCol = $this->resolveExistingColumn($patientFields, ['id']);
if ($pkCol !== null) {
$builder->join('patient_master p', 'p.' . $pkCol . ' = i.' . $patientFkCol, 'left');
if ($nameCol !== null) {
$patientNameExpr = 'p.' . $nameCol;
}
if ($codeCol !== null) {
$patientCodeExpr = 'p.' . $codeCol;
}
}
}
$builder->select($patientNameExpr . ' as patient_name', false)
->select($patientCodeExpr . ' as patient_code', false)
->where('i.' . $dateCol . ' >=', $minRange)
->where('i.' . $dateCol . ' <=', $maxRange)
->orderBy('i.' . $dateCol, 'DESC');
if ($statusCol !== null) {
$builder->where('i.' . $statusCol, 1);
}
$resultRows = $builder->get()->getResultArray();
$finalRows = [];
foreach ($resultRows as $row) {
$ipdId = (int) ($row['encounter_id'] ?? 0);
if ($ipdId <= 0) {
continue;
}
$checklist = $this->buildIpdChecklistForAudit($ipdId, (int) ($row['patient_id'] ?? 0));
$total = (int) ($checklist['total_count'] ?? 0);
$okCount = (int) ($checklist['ok_count'] ?? 0);
$finalRows[] = [
'module_key' => 'ipd',
'module_label' => 'IPD Discharge',
'encounter_id' => $ipdId,
'encounter_code' => (string) ($row['encounter_code'] ?? ''),
'patient_code' => (string) ($row['patient_code'] ?? ''),
'patient_name' => (string) ($row['patient_name'] ?? ''),
'encounter_datetime' => (string) ($row['encounter_datetime'] ?? ''),
'ok_count' => $okCount,
'total_count' => $total,
'completion_percent' => $total > 0 ? round(($okCount * 100) / $total, 1) : 0.0,
'critical_missing_count' => (int) ($checklist['critical_missing_count'] ?? 0),
'critical_missing_labels' => implode(', ', $checklist['critical_missing'] ?? []),
];
}
return $finalRows;
}
private function buildIpdChecklistForAudit(int $ipdId, int $patientId): array
{
$hasAdmissionReason = $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_complaint',
$ipdId,
['comp_report', 'comp_remark']
) || $this->tableHasAnyNonEmptyValueByIpd('ipd_discharge_complaint_remark', $ipdId, ['comp_remark']);
$hasDiagnosis = $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_diagnosis',
$ipdId,
['comp_report', 'comp_remark']
) || $this->tableHasAnyNonEmptyValueByIpd('ipd_discharge_diagnosis_remark', $ipdId, ['comp_remark']);
$hasCourse = $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_course',
$ipdId,
['comp_report', 'comp_remark']
) || $this->tableHasAnyNonEmptyValueByIpd('ipd_discharge_course_remark', $ipdId, ['comp_remark']);
$hasProcedure = $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_surgery',
$ipdId,
['surgery_name', 'surgery_remark']
) || $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_procedure',
$ipdId,
['procedure_name', 'procedure_remark']
);
$hasCondition = $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_1_b_final',
$ipdId,
['discharge_condition', 'condition_at_discharge', 'condition_remark', 'remark']
) || $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_general_exam_col',
$ipdId,
['value']
);
$hasMedication = $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_drug',
$ipdId,
['drug_name', 'drug_dose', 'drug_day']
) || $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_prescrption_prescribed',
$ipdId,
['med_name', 'med_type', 'qty', 'no_of_days', 'remark']
);
$instructionText = $this->firstNonEmptyValueByIpd(
'ipd_discharge_instructions',
$ipdId,
['comp_remark', 'footer_text']
);
$reviewAfter = $this->firstNonEmptyValueByIpd('ipd_discharge_instructions', $ipdId, ['review_after']);
$hasFollowUp = trim($instructionText) !== '' || trim($reviewAfter) !== '';
$allergySnapshot = $this->getLatestOpdAllergySnapshotByPatient($patientId);
$hasAllergyAdr = trim((string) ($allergySnapshot['drug_allergy_status'] ?? '')) !== ''
|| trim((string) ($allergySnapshot['drug_allergy_details'] ?? '')) !== ''
|| trim((string) ($allergySnapshot['adr_history'] ?? '')) !== '';
$instructionLower = strtolower((string) $instructionText);
$hasRedFlags = strpos($instructionLower, 'emergency') !== false
|| strpos($instructionLower, 'warning') !== false
|| strpos($instructionLower, 'red flag') !== false
|| strpos($instructionLower, 'immediately') !== false
|| strpos($instructionLower, 'return if') !== false;
$items = [
['label' => 'Reason for admission / presenting complaints', 'ok' => $hasAdmissionReason, 'critical' => true],
['label' => 'Final diagnosis documented', 'ok' => $hasDiagnosis, 'critical' => true],
['label' => 'Course / treatment in hospital documented', 'ok' => $hasCourse, 'critical' => true],
['label' => 'Surgery/procedure documented (if applicable)', 'ok' => $hasProcedure, 'critical' => false],
['label' => 'Condition at discharge documented', 'ok' => $hasCondition, 'critical' => true],
['label' => 'Discharge medication documented', 'ok' => $hasMedication, 'critical' => true],
['label' => 'Follow-up instructions / review plan documented', 'ok' => $hasFollowUp, 'critical' => true],
['label' => 'Drug allergy / ADR history documented', 'ok' => $hasAllergyAdr, 'critical' => false],
['label' => 'Red-flag / emergency return advice documented', 'ok' => $hasRedFlags, 'critical' => false],
];
$criticalMissing = [];
$okCount = 0;
foreach ($items as $item) {
if (! empty($item['ok'])) {
$okCount++;
continue;
}
if (! empty($item['critical'])) {
$criticalMissing[] = (string) ($item['label'] ?? '');
}
}
return [
'ok_count' => $okCount,
'total_count' => count($items),
'critical_missing' => $criticalMissing,
'critical_missing_count' => count($criticalMissing),
];
}
private function getOpdNabhAuditRows(string $minRange, string $maxRange): array
{
if (! $this->db->tableExists('opd_master')) {
return [];
}
$opdFields = $this->db->getFieldNames('opd_master') ?? [];
if (! in_array('opd_id', $opdFields, true)) {
return [];
}
$dateCol = $this->resolveExistingColumn($opdFields, [
'apointment_date',
'appointment_date',
'created_at',
'entry_date',
]);
if ($dateCol === null) {
return [];
}
$opdCodeCol = $this->resolveExistingColumn($opdFields, ['opd_code', 'opd_no']);
$patientFkCol = $this->resolveExistingColumn($opdFields, ['p_id', 'patient_id']);
$builder = $this->db->table('opd_master o')
->select('o.opd_id as encounter_id')
->select('o.' . $dateCol . ' as encounter_datetime', false);
if ($opdCodeCol !== null) {
$builder->select('o.' . $opdCodeCol . ' as encounter_code', false);
} else {
$builder->select("'' as encounter_code", false);
}
if ($patientFkCol !== null) {
$builder->select('o.' . $patientFkCol . ' as patient_id', false);
} else {
$builder->select('0 as patient_id', false);
}
$patientNameExpr = "''";
$patientCodeExpr = "''";
if ($patientFkCol !== null && $this->db->tableExists('patient_master')) {
$patientFields = $this->db->getFieldNames('patient_master') ?? [];
$nameCol = $this->resolveExistingColumn($patientFields, ['p_fname', 'patient_name', 'name']);
$codeCol = $this->resolveExistingColumn($patientFields, ['p_code', 'patient_code', 'uhid_no']);
$pkCol = $this->resolveExistingColumn($patientFields, ['id']);
if ($pkCol !== null) {
$builder->join('patient_master p', 'p.' . $pkCol . ' = o.' . $patientFkCol, 'left');
if ($nameCol !== null) {
$patientNameExpr = 'p.' . $nameCol;
}
if ($codeCol !== null) {
$patientCodeExpr = 'p.' . $codeCol;
}
}
}
$prescriptionCols = [];
if ($this->db->tableExists('opd_prescription')) {
$prescriptionFields = $this->db->getFieldNames('opd_prescription') ?? [];
if (in_array('id', $prescriptionFields, true) && in_array('opd_id', $prescriptionFields, true)) {
$builder->join(
'opd_prescription pr',
'pr.id = (SELECT MAX(px.id) FROM opd_prescription px WHERE px.opd_id = o.opd_id)',
'left',
false
);
foreach ([
'complaints',
'diagnosis',
'Provisional_diagnosis',
'Finding_Examinations',
'Prescriber_Remarks',
'investigation',
'advice',
'next_visit',
'refer_to',
'bp',
'diastolic',
'pulse',
'temp',
'spo2',
'rr_min',
'height',
'weight',
'drug_allergy_status',
'drug_allergy_details',
'adr_history',
'current_medications',
] as $column) {
if (in_array($column, $prescriptionFields, true)) {
$builder->select('pr.' . $column, false);
$prescriptionCols[] = $column;
}
}
}
}
$builder->select($patientNameExpr . ' as patient_name', false)
->select($patientCodeExpr . ' as patient_code', false)
->where('o.' . $dateCol . ' >=', $minRange)
->where('o.' . $dateCol . ' <=', $maxRange)
->orderBy('o.' . $dateCol, 'DESC');
$resultRows = $builder->get()->getResultArray();
$finalRows = [];
foreach ($resultRows as $row) {
$checklist = $this->buildOpdChecklistForAudit($row, $prescriptionCols);
$total = (int) ($checklist['total_count'] ?? 0);
$okCount = (int) ($checklist['ok_count'] ?? 0);
$finalRows[] = [
'module_key' => 'opd',
'module_label' => 'OPD Consult',
'encounter_id' => (int) ($row['encounter_id'] ?? 0),
'encounter_code' => (string) ($row['encounter_code'] ?? ''),
'patient_code' => (string) ($row['patient_code'] ?? ''),
'patient_name' => (string) ($row['patient_name'] ?? ''),
'encounter_datetime' => (string) ($row['encounter_datetime'] ?? ''),
'ok_count' => $okCount,
'total_count' => $total,
'completion_percent' => $total > 0 ? round(($okCount * 100) / $total, 1) : 0.0,
'critical_missing_count' => (int) ($checklist['critical_missing_count'] ?? 0),
'critical_missing_labels' => implode(', ', $checklist['critical_missing'] ?? []),
];
}
return $finalRows;
}
private function getRadiologyNabhAuditRows(string $minRange, string $maxRange): array
{
if (! $this->db->tableExists('lab_log')) {
return [];
}
$logFields = $this->db->getFieldNames('lab_log') ?? [];
foreach (['lab_repo_id', 'log_insert_time', 'log_type', 'log_Faults', 'comments'] as $requiredField) {
if (! in_array($requiredField, $logFields, true)) {
return [];
}
}
$builder = $this->db->table('lab_log lg')
->select('lg.lab_repo_id as encounter_id')
->select('lg.log_insert_time as encounter_datetime')
->select('lg.log_by as edited_by')
->select('lg.comments as comments')
->where('lg.log_type', 'Report Edit')
->where('lg.log_Faults', 'Radiology')
->where('lg.log_insert_time >=', $minRange)
->where('lg.log_insert_time <=', $maxRange)
->orderBy('lg.id', 'DESC');
if ($this->db->tableExists('lab_request')) {
$labReqFields = $this->db->getFieldNames('lab_request') ?? [];
if (in_array('id', $labReqFields, true)) {
$builder->join('lab_request lr', 'lr.id = lg.lab_repo_id', 'left');
if (in_array('report_name', $labReqFields, true)) {
$builder->select('lr.report_name as report_name');
} else {
$builder->select("'' as report_name", false);
}
if (in_array('patient_id', $labReqFields, true) && $this->db->tableExists('patient_master')) {
$patientFields = $this->db->getFieldNames('patient_master') ?? [];
if (in_array('id', $patientFields, true)) {
$builder->join('patient_master p', 'p.id = lr.patient_id', 'left');
if (in_array('p_code', $patientFields, true)) {
$builder->select('p.p_code as patient_code');
} else {
$builder->select("'' as patient_code", false);
}
if (in_array('p_fname', $patientFields, true)) {
$builder->select('p.p_fname as patient_name');
} else {
$builder->select("'' as patient_name", false);
}
}
} else {
$builder->select("'' as patient_code", false);
$builder->select("'' as patient_name", false);
}
}
}
$resultRows = $builder->get()->getResultArray();
$finalRows = [];
foreach ($resultRows as $row) {
$reason = $this->extractEditReasonFromLabLogComment((string) ($row['comments'] ?? ''));
$hasReason = $reason !== '' && strtolower($reason) !== 'na';
$encounterCode = trim((string) ($row['report_name'] ?? ''));
if ($encounterCode === '') {
$encounterCode = 'REQ#' . (int) ($row['encounter_id'] ?? 0);
}
$finalRows[] = [
'module_key' => 'radiology',
'module_label' => 'Radiology Edit',
'encounter_id' => (int) ($row['encounter_id'] ?? 0),
'encounter_code' => $encounterCode,
'patient_code' => (string) ($row['patient_code'] ?? ''),
'patient_name' => (string) ($row['patient_name'] ?? ''),
'encounter_datetime' => (string) ($row['encounter_datetime'] ?? ''),
'ok_count' => $hasReason ? 1 : 0,
'total_count' => 1,
'completion_percent' => $hasReason ? 100.0 : 0.0,
'critical_missing_count' => $hasReason ? 0 : 1,
'critical_missing_labels' => $hasReason ? '-' : 'Edit reason missing',
];
}
return $finalRows;
}
private function extractEditReasonFromLabLogComment(string $comment): string
{
$comment = trim($comment);
if ($comment === '') {
return '';
}
if (preg_match('/\[reason:(.*?)(?:\]\s*\[|\]$)/i', $comment, $matches) === 1) {
return trim((string) ($matches[1] ?? ''));
}
$startPos = stripos($comment, '[reason:');
if ($startPos === false) {
return '';
}
$raw = substr($comment, $startPos + 8);
$beforePos = stripos($raw, '[before:');
if ($beforePos !== false) {
$raw = substr($raw, 0, $beforePos);
}
return trim(str_replace(']', '', $raw));
}
private function buildOpdChecklistForAudit(array $row, array $availableCols): array
{
$hasComplaints = $this->rowHasAnyNonEmptyValue($row, ['complaints']);
$hasDiagnosis = $this->rowHasAnyNonEmptyValue($row, ['diagnosis', 'Provisional_diagnosis']);
$hasClinicalCourse = $this->rowHasAnyNonEmptyValue($row, ['Finding_Examinations', 'Prescriber_Remarks']);
$hasProcedure = $this->rowHasAnyNonEmptyValue($row, ['investigation']);
$hasCondition = $this->rowHasAnyNonEmptyValue($row, ['bp', 'diastolic', 'pulse', 'temp', 'spo2', 'rr_min', 'height', 'weight']);
$hasMedicationPlan = $this->rowHasAnyNonEmptyValue($row, ['advice', 'current_medications']);
$hasFollowUp = $this->rowHasAnyNonEmptyValue($row, ['next_visit', 'refer_to']);
$hasAllergyAdr = $this->rowHasAnyNonEmptyValue($row, ['drug_allergy_status', 'drug_allergy_details', 'adr_history']);
$adviceText = strtolower(trim((string) ($row['advice'] ?? '')) . ' ' . trim((string) ($row['Prescriber_Remarks'] ?? '')));
$hasRedFlags = strpos($adviceText, 'emergency') !== false
|| strpos($adviceText, 'warning') !== false
|| strpos($adviceText, 'red flag') !== false
|| strpos($adviceText, 'immediately') !== false
|| strpos($adviceText, 'return if') !== false;
// If OPD prescription table is unavailable for the row, mark item as not applicable by treating as empty.
if ($availableCols === []) {
$hasComplaints = false;
$hasDiagnosis = false;
$hasClinicalCourse = false;
$hasProcedure = false;
$hasCondition = false;
$hasMedicationPlan = false;
$hasFollowUp = false;
$hasAllergyAdr = false;
$hasRedFlags = false;
}
$items = [
['label' => 'Reason for consultation / presenting complaints', 'ok' => $hasComplaints, 'critical' => true],
['label' => 'Diagnosis documented', 'ok' => $hasDiagnosis, 'critical' => true],
['label' => 'Clinical findings / assessment documented', 'ok' => $hasClinicalCourse, 'critical' => true],
['label' => 'Investigations/procedures documented (if applicable)', 'ok' => $hasProcedure, 'critical' => false],
['label' => 'Clinical condition / vitals documented', 'ok' => $hasCondition, 'critical' => true],
['label' => 'Treatment/medication plan documented', 'ok' => $hasMedicationPlan, 'critical' => true],
['label' => 'Follow-up or referral plan documented', 'ok' => $hasFollowUp, 'critical' => true],
['label' => 'Drug allergy / ADR history documented', 'ok' => $hasAllergyAdr, 'critical' => false],
['label' => 'Red-flag / emergency return advice documented', 'ok' => $hasRedFlags, 'critical' => false],
];
$criticalMissing = [];
$okCount = 0;
foreach ($items as $item) {
if (! empty($item['ok'])) {
$okCount++;
continue;
}
if (! empty($item['critical'])) {
$criticalMissing[] = (string) ($item['label'] ?? '');
}
}
return [
'ok_count' => $okCount,
'total_count' => count($items),
'critical_missing' => $criticalMissing,
'critical_missing_count' => count($criticalMissing),
];
}
private function resolveExistingColumn(array $fields, array $candidates): ?string
{
foreach ($candidates as $candidate) {
if (in_array($candidate, $fields, true)) {
return $candidate;
}
}
return null;
}
private function rowHasAnyNonEmptyValue(array $row, array $fields): bool
{
foreach ($fields as $field) {
$value = trim((string) ($row[$field] ?? ''));
if ($value !== '') {
return true;
}
}
return false;
}
private function tableHasAnyNonEmptyValueByIpd(string $table, int $ipdId, array $preferredFields): bool
{
if ($ipdId <= 0 || ! $this->db->tableExists($table)) {
return false;
}
$fields = $this->db->getFieldNames($table) ?? [];
if (! in_array('ipd_id', $fields, true)) {
return false;
}
$selected = [];
foreach ($preferredFields as $field) {
if (in_array($field, $fields, true)) {
$selected[] = $field;
}
}
if ($selected === []) {
return false;
}
$rows = $this->db->table($table)
->select(implode(',', $selected))
->where('ipd_id', $ipdId)
->get()
->getResultArray();
foreach ($rows as $row) {
if ($this->rowHasAnyNonEmptyValue($row, $selected)) {
return true;
}
}
return false;
}
private function firstNonEmptyValueByIpd(string $table, int $ipdId, array $preferredFields): string
{
if ($ipdId <= 0 || ! $this->db->tableExists($table)) {
return '';
}
$fields = $this->db->getFieldNames($table) ?? [];
if (! in_array('ipd_id', $fields, true)) {
return '';
}
$selected = [];
foreach ($preferredFields as $field) {
if (in_array($field, $fields, true)) {
$selected[] = $field;
}
}
if ($selected === []) {
return '';
}
$builder = $this->db->table($table)->where('ipd_id', $ipdId);
if (in_array('id', $fields, true)) {
$builder->orderBy('id', 'DESC');
}
$row = $builder->get(1)->getRowArray() ?? [];
foreach ($selected as $field) {
$value = trim((string) ($row[$field] ?? ''));
if ($value !== '') {
return $value;
}
}
return '';
}
private function getLatestOpdAllergySnapshotByPatient(int $patientId): array
{
if ($patientId <= 0 || ! $this->db->tableExists('opd_master') || ! $this->db->tableExists('opd_prescription')) {
return [];
}
$opdFields = $this->db->getFieldNames('opd_master') ?? [];
$rxFields = $this->db->getFieldNames('opd_prescription') ?? [];
if (! in_array('opd_id', $opdFields, true) || ! in_array('p_id', $opdFields, true) || ! in_array('opd_id', $rxFields, true)) {
return [];
}
$selects = [];
foreach (['drug_allergy_status', 'drug_allergy_details', 'adr_history'] as $field) {
if (in_array($field, $rxFields, true)) {
$selects[] = 'r.' . $field;
}
}
if ($selects === []) {
return [];
}
$builder = $this->db->table('opd_master o')
->select(implode(',', $selects), false)
->join('opd_prescription r', 'r.opd_id = o.opd_id', 'inner')
->where('o.p_id', $patientId);
if (in_array('apointment_date', $opdFields, true)) {
$builder->orderBy('o.apointment_date', 'DESC');
}
if (in_array('id', $rxFields, true)) {
$builder->orderBy('r.id', 'DESC');
}
$row = $builder->get(1)->getRowArray();
return is_array($row) ? $row : [];
}
}