diff --git a/app/Config/AuthGroups.php b/app/Config/AuthGroups.php
index 5161cb9..3f9fb04 100644
--- a/app/Config/AuthGroups.php
+++ b/app/Config/AuthGroups.php
@@ -102,6 +102,7 @@ class AuthGroups extends ShieldAuthGroups
'template.ct' => 'Can access CT templates',
'template.mri' => 'Can access MRI templates',
'template.echo' => 'Can access ECHO templates',
+ 'template.discharge' => 'Can access IPD discharge templates',
];
/**
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 4275dd2..c12805d 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -233,6 +233,15 @@ $routes->match(['get', 'post'], 'Ipd_discharge/ipd_select/(:num)/(:num)', 'Ipd_d
$routes->get('Ipd_discharge/section_past_data', 'Ipd_discharge::section_past_data');
$routes->get('Ipd_discharge/section_template_list', 'Ipd_discharge::section_template_list');
$routes->post('Ipd_discharge/section_template_save', 'Ipd_discharge::section_template_save');
+$routes->get('Ipd_discharge/surgery_master_lookup', 'Ipd_discharge::surgery_master_lookup');
+$routes->get('Ipd_discharge/surgery_master_list', 'Ipd_discharge::surgery_master_list');
+$routes->post('Ipd_discharge/surgery_master_save', 'Ipd_discharge::surgery_master_save');
+$routes->post('Ipd_discharge/surgery_master_delete', 'Ipd_discharge::surgery_master_delete');
+$routes->get('Ipd_discharge/diagnosis_icd_lookup', 'Ipd_discharge::diagnosis_icd_lookup');
+$routes->post('Ipd_discharge/diagnosis_icd_seed_starter', 'Ipd_discharge::diagnosis_icd_seed_starter');
+$routes->get('Ipd_discharge/dietary_master_list', 'Ipd_discharge::dietary_master_list');
+$routes->post('Ipd_discharge/dietary_master_save', 'Ipd_discharge::dietary_master_save');
+$routes->post('Ipd_discharge/dietary_master_delete', 'Ipd_discharge::dietary_master_delete');
$routes->match(['get', 'post'], 'Ipd_discharge/preview_discharge_report/(:num)', 'Ipd_discharge::preview_discharge_report/$1');
$routes->get('Ipd_discharge/show_discharge/(:num)', 'Ipd_discharge::show_discharge/$1');
$routes->get('Ipd_discharge/show_discharge/(:num)/(:num)', 'Ipd_discharge::show_discharge/$1/$2');
@@ -521,12 +530,21 @@ $routes->post('Opdcase/showfee', 'Opdcase::showfee');
$routes->post('Opdcase/confirm_opd', 'Opdcase::confirm_opd');
$routes->get('Opdcase/invoice/(:num)', 'Opd::invoice/$1');
+$routes->get('Report', 'Report::index');
+$routes->get('Report/index', 'Report::index');
+$routes->get('report', 'Report::index');
+$routes->get('report/index', 'Report::index');
+
$routes->get('Report/collection_report', 'Report::collection_report');
$routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)', 'Report::report_total_payment_app_show/$1/$2/$3');
$routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)/(:num)', 'Report::report_total_payment_app_show/$1/$2/$3/$4');
$routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)/(:segment)', 'Report::report_total_payment_app_show/$1/$2/$3/$4');
$routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)/(:segment)/(:num)', 'Report::report_total_payment_app_show/$1/$2/$3/$4/$5');
+$routes->get('Report/billing_operations_report', 'Report::billing_operations_report');
+$routes->get('Report/billing_operations_report_data/(:segment)/(:segment)', 'Report::billing_operations_report_data/$1/$2');
+$routes->get('Report/billing_operations_report_data/(:segment)/(:segment)/(:num)', 'Report::billing_operations_report_data/$1/$2/$3');
+
$routes->get('Report/diagnosis_report', 'Report::diagnosis_report');
$routes->get('Report/diagnosis_report_data/(:segment)', 'Report::diagnosis_report_data/$1');
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)', 'Report::diagnosis_report_data/$1/$2');
@@ -549,6 +567,14 @@ $routes->get('Report/insurance_combined_report', 'Report::insurance_combined_rep
$routes->get('Report/insurance_combined_report_data/(:segment)/(:segment)/(:segment)', 'Report::insurance_combined_report_data/$1/$2/$3');
$routes->get('Report/insurance_combined_report_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::insurance_combined_report_data/$1/$2/$3/$4');
+// Legacy Report4 compatibility for Insurance Case reports
+$routes->get('Report4/echs_ipd_list_main', 'Report::echs_ipd_list_main');
+$routes->get('Report4/echs_opd_list_main', 'Report::echs_opd_list_main');
+$routes->get('Report4/echs_ipd_list_data/(:segment)/(:segment)/(:segment)', 'Report::echs_ipd_list_data/$1/$2/$3');
+$routes->get('Report4/echs_ipd_list_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::echs_ipd_list_data/$1/$2/$3/$4');
+$routes->get('Report4/echs_opd_list_data/(:segment)/(:segment)/(:segment)', 'Report::echs_opd_list_data/$1/$2/$3');
+$routes->get('Report4/echs_opd_list_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::echs_opd_list_data/$1/$2/$3/$4');
+
// Organization Packing Routes
$routes->get('org-packing', 'OrgPacking::index');
$routes->post('org-packing/search', 'OrgPacking::search');
diff --git a/app/Controllers/Billing/CaseMaster.php b/app/Controllers/Billing/CaseMaster.php
index 14237ca..272b5ef 100644
--- a/app/Controllers/Billing/CaseMaster.php
+++ b/app/Controllers/Billing/CaseMaster.php
@@ -4,8 +4,9 @@ namespace App\Controllers\Billing;
use App\Controllers\BaseController;
use App\Models\InvoiceModel;
-use App\Models\ItemModel;
+use App\Models\OrganizationCaseBillDetailModel;
use App\Models\OrganizationCaseModel;
+use Mpdf\Mpdf;
class CaseMaster extends BaseController
{
@@ -118,8 +119,6 @@ class CaseMaster extends BaseController
'input_insurance_id' => 'required|min_length[2]|max_length[30]',
'input_card_holder_name' => 'required|min_length[2]|max_length[50]',
];
-
- $validation = service('validation');
$validation->setRules($rules);
if (! $validation->withRequest($this->request)->run()) {
$errorText = implode("\n", $validation->getErrors());
@@ -157,7 +156,6 @@ class CaseMaster extends BaseController
];
$existing = $this->db->table('organization_case_master')
- ->where('status', 0)
->where('case_type', $caseType)
->where('p_id', $pId)
->get()
@@ -484,7 +482,11 @@ class CaseMaster extends BaseController
$data['payment_history'] = $this->db->query($sql)->getResult();
if ($print > 0) {
- return view('Invoice/caseinvoice_print_V', $data);
+ $html = view('Invoice/caseinvoice_print_V', $data);
+ $caseCode = (string) ($data['orgcase'][0]->case_id_code ?? $casecode);
+ $fileName = 'Org_Invoice_' . preg_replace('/[^A-Za-z0-9\-_]/', '_', $caseCode) . '_' . date('YmdHis') . '.pdf';
+
+ return $this->renderInlinePdf($html, $fileName, 'Organization Invoice ' . $caseCode);
}
return view('Invoice/caseinvoice_V', $data);
@@ -794,8 +796,10 @@ class CaseMaster extends BaseController
->get()
->getResult();
- $data['showinvoice'] = $this->db->query('select * from v_case_invoice v where v.id=' . $caseid . ' order by s_Date')->getResult();
- $data['invoiceGtotal'] = $this->db->query('select sum(amount) as GTotal from v_case_invoice v where v.id=' . $caseid)->getResult();
+ $organizationCaseModel = new OrganizationCaseModel();
+ $invoiceData = $organizationCaseModel->getInvoiceRowsAndTotal($caseid);
+ $data['showinvoice'] = $invoiceData['rows'];
+ $data['invoiceGtotal'] = [(object) ['GTotal' => $invoiceData['total']]];
return view('Invoice/caseinvoice_V', $data);
}
@@ -922,7 +926,13 @@ class CaseMaster extends BaseController
->getResult();
if ($print === 1) {
- return $this->response->setBody((string) ($data['orgcase'][0]->contingent_bill ?? ''));
+ $content = (string) ($data['orgcase'][0]->contingent_bill ?? '');
+ $pageHead = '';
+ $html = $pageHead . $content;
+ $caseCode = (string) ($data['orgcase'][0]->case_id_code ?? $caseid);
+ $fileName = 'Contingent_Bill_' . preg_replace('/[^A-Za-z0-9\-_]/', '_', $caseCode) . '_' . date('YmdHis') . '.pdf';
+
+ return $this->renderInlinePdf($html, $fileName, 'Contingent Bill ' . $caseCode);
}
return view('Invoice/Org_Contingent_bill', $data);
@@ -958,10 +968,10 @@ class CaseMaster extends BaseController
->get()
->getResult();
- $sql = 'select * from v_case_invoice v where v.id=' . $caseid . ' order by Charge_type,Adate';
- $data['showinvoice'] = $this->db->query($sql)->getResult();
-
- $data['invoiceGtotal'] = $this->db->query('select sum(amount) as GTotal from v_case_invoice v where v.id=' . $caseid)->getResult();
+ $organizationCaseModel = new OrganizationCaseModel();
+ $invoiceData = $organizationCaseModel->getInvoiceRowsAndTotal($caseid);
+ $data['showinvoice'] = $invoiceData['rows'];
+ $data['invoiceGtotal'] = [(object) ['GTotal' => $invoiceData['total']]];
$claimId = (string) ($data['orgcase'][0]->insurance_no_1 ?? '');
@@ -1360,4 +1370,28 @@ class CaseMaster extends BaseController
);
}
}
-}
+
+ private function renderInlinePdf(string $html, string $fileName, string $title)
+ {
+ $mpdf = new Mpdf([
+ 'mode' => 'utf-8',
+ 'format' => 'A4',
+ 'orientation' => 'P',
+ 'margin_left' => 8,
+ 'margin_right' => 8,
+ 'margin_top' => 8,
+ 'margin_bottom' => 8,
+ 'default_font' => 'dejavusans',
+ 'tempDir' => WRITEPATH . 'cache',
+ ]);
+
+ $mpdf->SetTitle($title);
+ $mpdf->WriteHTML($html);
+
+ return $this->response
+ ->setHeader('Content-Type', 'application/pdf')
+ ->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
+ ->setBody($mpdf->Output($fileName, 'S'));
+ }
+
+ }
diff --git a/app/Controllers/Billing/Ipd.php b/app/Controllers/Billing/Ipd.php
index 654b9a7..79d5267 100644
--- a/app/Controllers/Billing/Ipd.php
+++ b/app/Controllers/Billing/Ipd.php
@@ -7,6 +7,7 @@ use App\Models\BedAssignmentHistoryModel;
use App\Models\ItemIpdModel;
use App\Models\IpdBillingModel;
use App\Models\IpdModel;
+use App\Models\NursingBedsideItemModel;
use App\Models\PaymentModel;
class Ipd extends BaseController
@@ -15,6 +16,7 @@ class Ipd extends BaseController
protected BedAssignmentHistoryModel $bedAssignmentModel;
protected ItemIpdModel $itemIpdModel;
protected IpdModel $ipdEditModel;
+ protected NursingBedsideItemModel $nursingBedsideItemModel;
public function __construct()
{
@@ -22,6 +24,7 @@ class Ipd extends BaseController
$this->bedAssignmentModel = new BedAssignmentHistoryModel();
$this->itemIpdModel = new ItemIpdModel();
$this->ipdEditModel = new IpdModel();
+ $this->nursingBedsideItemModel = new NursingBedsideItemModel();
helper(['common', 'form', 'age']);
}
@@ -193,6 +196,7 @@ class Ipd extends BaseController
}
if ($tab === 'ipd-charges') {
+ $this->syncNursingChargesToInvoice($ipdId);
$panelData['ipd_charges_grouped'] = $this->ipdModel->getIpdChargesGrouped($ipdId);
$panelData['ipd_charges_total'] = $this->ipdModel->getIpdChargesTotal($ipdId);
$panelData['ipd_packages'] = $this->ipdModel->getIpdPackages($ipdId);
@@ -203,8 +207,14 @@ class Ipd extends BaseController
$insCompId = 1;
}
+ $panelData['bedside_items_by_category'] = $this->nursingBedsideItemModel->getBillableGroupedByCategory($insCompId);
+
$panelData['ipd_insurance_id'] = $insCompId;
$panelData['doc_list'] = $this->ipdModel->getIpdDoctorList();
+ $doctorIds = array_map(static fn ($doc) => (int) ($doc->id ?? 0), $panelData['doc_list']);
+ $doctorIds = array_values(array_filter($doctorIds, static fn ($id) => $id > 0));
+ $panelData['doctor_visit_fee_types'] = $this->ipdModel->getDoctorVisitFeeTypes();
+ $panelData['doctor_visit_fee_map'] = $this->ipdModel->getDoctorVisitFeeMap($doctorIds);
$panelData['item_types'] = $this->itemIpdModel->getItemTypes();
$itemLists = [];
@@ -419,6 +429,238 @@ class Ipd extends BaseController
return $this->response->setJSON(['ok' => true]);
}
+ public function addBedsideCharge(int $ipdId)
+ {
+ $permission = $this->requireAnyPermission([
+ 'billing.access',
+ 'billing.ipd.invoice',
+ 'billing.ipd.current-admission',
+ ]);
+ if ($permission) {
+ return $permission;
+ }
+
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['error' => 'Invalid request']);
+ }
+
+ $bedsideItemId = (int) ($this->request->getPost('bedside_item_id') ?? 0);
+ $qty = (float) ($this->request->getPost('item_qty') ?? 1);
+ $itemDate = (string) ($this->request->getPost('item_date') ?? date('Y-m-d'));
+ $visitTime = (string) ($this->request->getPost('visit_time') ?? '');
+ $comment = (string) ($this->request->getPost('comment') ?? '');
+ $docId = (int) ($this->request->getPost('doc_id') ?? 0);
+ $referName = (string) ($this->request->getPost('refer_name') ?? '');
+
+ if ($bedsideItemId <= 0) {
+ return $this->response->setStatusCode(400)->setJSON(['error' => 'Select a bedside item']);
+ }
+
+ $caseMeta = $this->ipdModel->getIpdCaseMeta($ipdId);
+ $insId = (int) ($caseMeta['insurance_id'] ?? 0);
+
+ $item = $this->nursingBedsideItemModel->getBillableItemForInsurance($bedsideItemId, $insId);
+ if (! is_array($item) || empty($item)) {
+ return $this->response->setStatusCode(404)->setJSON(['error' => 'Bedside item not found or not billable']);
+ }
+
+ if ($qty <= 0) {
+ $qty = 1;
+ }
+
+ $rateInput = $this->request->getPost('item_rate');
+ $insuranceRate = $item['insurance_rate'] ?? null;
+ $rate = ($rateInput !== null && $rateInput !== '')
+ ? (float) $rateInput
+ : ($insuranceRate !== null ? (float) $insuranceRate : (float) ($item['default_rate'] ?? 0));
+ if ($rate < 0) {
+ $rate = 0;
+ }
+
+ $orgCode = trim((string) ($item['insurance_code'] ?? ''));
+
+ $docName = '';
+ if ($docId > 0) {
+ $docName = $this->ipdModel->getDoctorNameById($docId);
+ } else {
+ $docName = $referName;
+ }
+
+ $typeId = $this->resolveBedsideItemTypeId((string) ($item['category'] ?? 'Bedside'));
+ $amount = $rate * $qty;
+
+ $nursingChargeId = $this->insertNursingIpdChargeRow([
+ 'ipd_id' => $ipdId,
+ 'item_id' => $bedsideItemId,
+ 'item_name' => (string) ($item['item_name'] ?? ''),
+ 'item_type' => (string) ($item['item_type'] ?? 'Bedside'),
+ 'doctor_id' => $docId > 0 ? $docId : null,
+ 'doctor_name' => $docName !== '' ? $docName : null,
+ 'visit_date' => $itemDate,
+ 'visit_time' => $visitTime !== '' ? $visitTime : null,
+ 'consultation_type' => null,
+ 'duration' => null,
+ 'specialization' => null,
+ 'hospital_clinic' => null,
+ 'is_outside_doctor' => 0,
+ 'rate' => $rate,
+ 'qty' => (int) $qty,
+ 'amount' => $amount,
+ 'performed_by' => null,
+ 'performed_date' => $itemDate,
+ 'performed_time' => $visitTime !== '' ? $visitTime : null,
+ 'remarks' => $comment,
+ 'include_in_bill' => 1,
+ 'updated_by' => null,
+ ]);
+
+ $invoiceComment = $comment;
+ if ($nursingChargeId > 0) {
+ $marker = '[NURSING_CHARGE_ID:' . $nursingChargeId . ']';
+ $invoiceComment = trim($invoiceComment . ' ' . $marker);
+ }
+
+ $insertData = [
+ 'ipd_id' => $ipdId,
+ 'item_type' => $typeId,
+ 'item_id' => $bedsideItemId,
+ 'item_name' => (string) ($item['item_name'] ?? ''),
+ 'item_rate' => $rate,
+ 'item_added_date' => date('Y-m-d H:i:s'),
+ 'item_qty' => $qty,
+ 'item_amount' => $amount,
+ 'doc_id' => $docId,
+ 'doc_name' => $docName,
+ 'date_item' => $itemDate,
+ 'comment' => $invoiceComment,
+ 'ins_id' => $insId,
+ 'org_code' => $orgCode,
+ ];
+
+ $insertId = $this->ipdEditModel->insertIpdItem($insertData);
+
+ return $this->response->setJSON([
+ 'ok' => $insertId > 0,
+ 'id' => $insertId,
+ ]);
+ }
+
+ public function addDoctorVisitCharge(int $ipdId)
+ {
+ $permission = $this->requireAnyPermission([
+ 'billing.access',
+ 'billing.ipd.invoice',
+ 'billing.ipd.current-admission',
+ ]);
+ if ($permission) {
+ return $permission;
+ }
+
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['error' => 'Invalid request']);
+ }
+
+ $docId = (int) ($this->request->getPost('doc_id') ?? 0);
+ $feeTypeId = (int) ($this->request->getPost('fee_type_id') ?? 0);
+ $qty = (float) ($this->request->getPost('item_qty') ?? 1);
+ $itemDate = (string) ($this->request->getPost('item_date') ?? date('Y-m-d'));
+ $visitTime = (string) ($this->request->getPost('visit_time') ?? '');
+ $duration = (int) ($this->request->getPost('duration') ?? 0);
+ $hospitalClinic = trim((string) ($this->request->getPost('hospital_clinic') ?? ''));
+ $isOutsideDoctor = (int) ($this->request->getPost('is_outside_doctor') ?? 0) === 1 ? 1 : 0;
+ $comment = trim((string) ($this->request->getPost('comment') ?? ''));
+
+ if ($docId <= 0) {
+ return $this->response->setStatusCode(400)->setJSON(['error' => 'Select doctor']);
+ }
+
+ if ($qty <= 0) {
+ $qty = 1;
+ }
+
+ $feeDetail = $this->ipdModel->getDoctorVisitFeeDetail($docId, $feeTypeId);
+
+ $rateInput = $this->request->getPost('item_rate');
+ $rate = ($rateInput !== null && $rateInput !== '')
+ ? (float) $rateInput
+ : (float) ($feeDetail['amount'] ?? 0);
+ if ($rate < 0) {
+ $rate = 0;
+ }
+
+ $docName = $this->ipdModel->getDoctorNameById($docId);
+ if ($docName === '') {
+ return $this->response->setStatusCode(404)->setJSON(['error' => 'Doctor not found']);
+ }
+
+ $visitLabel = trim((string) ($feeDetail['fee_desc'] ?? 'Doctor Visit'));
+ if ($visitLabel === '') {
+ $visitLabel = 'Doctor Visit';
+ }
+
+ $itemName = 'Doctor Visit - ' . $visitLabel;
+ $typeId = $this->resolveDoctorVisitItemTypeId();
+ $amount = $rate * $qty;
+
+ $caseMeta = $this->ipdModel->getIpdCaseMeta($ipdId);
+ $insId = (int) ($caseMeta['insurance_id'] ?? 0);
+
+ $nursingChargeId = $this->insertNursingIpdChargeRow([
+ 'ipd_id' => $ipdId,
+ 'item_id' => (int) ($feeDetail['id'] ?? $docId),
+ 'item_name' => $itemName,
+ 'item_type' => 'Doctor Visit',
+ 'doctor_id' => $docId,
+ 'doctor_name' => $docName,
+ 'visit_date' => $itemDate,
+ 'visit_time' => $visitTime !== '' ? $visitTime : null,
+ 'consultation_type' => (string) ($feeDetail['fee_type'] ?? ''),
+ 'duration' => $duration > 0 ? $duration : null,
+ 'specialization' => null,
+ 'hospital_clinic' => $hospitalClinic !== '' ? $hospitalClinic : null,
+ 'is_outside_doctor' => $isOutsideDoctor,
+ 'rate' => $rate,
+ 'qty' => (int) $qty,
+ 'amount' => $amount,
+ 'performed_by' => null,
+ 'performed_date' => $itemDate,
+ 'performed_time' => $visitTime !== '' ? $visitTime : null,
+ 'remarks' => $comment,
+ 'include_in_bill' => 1,
+ 'updated_by' => null,
+ ]);
+
+ $invoiceComment = $comment;
+ if ($nursingChargeId > 0) {
+ $marker = '[NURSING_CHARGE_ID:' . $nursingChargeId . ']';
+ $invoiceComment = trim($invoiceComment . ' ' . $marker);
+ }
+
+ $insertData = [
+ 'ipd_id' => $ipdId,
+ 'item_type' => $typeId,
+ 'item_id' => (int) ($feeDetail['id'] ?? $docId),
+ 'item_name' => $itemName,
+ 'item_rate' => $rate,
+ 'item_added_date' => date('Y-m-d H:i:s'),
+ 'item_qty' => $qty,
+ 'item_amount' => $amount,
+ 'doc_id' => $docId,
+ 'doc_name' => $docName,
+ 'date_item' => $itemDate,
+ 'comment' => $invoiceComment,
+ 'ins_id' => $insId,
+ 'org_code' => '',
+ ];
+
+ $insertId = $this->ipdEditModel->insertIpdItem($insertData);
+
+ return $this->response->setJSON([
+ 'ok' => $insertId > 0,
+ 'id' => $insertId,
+ ]);
+ }
+
public function deleteIpdCharge(int $itemId)
{
$permission = $this->requireAnyPermission([
@@ -434,8 +676,165 @@ class Ipd extends BaseController
}
$result = $this->ipdEditModel->deleteIpdInvoiceItem($itemId);
+ if ($result !== 1) {
+ return $this->response->setJSON([
+ 'ok' => false,
+ 'error' => 'Unable to remove charge item. Please refresh and try again.',
+ ]);
+ }
- return $this->response->setJSON(['ok' => $result === 1]);
+ return $this->response->setJSON([
+ 'ok' => true,
+ 'deleted_id' => $itemId,
+ ]);
+ }
+
+ public function updateNursingCharge(int $ipdId, int $chargeId)
+ {
+ $permission = $this->requireAnyPermission([
+ 'billing.access',
+ 'billing.ipd.invoice',
+ 'billing.ipd.current-admission',
+ ]);
+ if ($permission) {
+ return $permission;
+ }
+
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['error' => 'Invalid request']);
+ }
+
+ if ($chargeId <= 0 || $ipdId <= 0 || ! $this->db->tableExists('nursing_ipd_charges')) {
+ return $this->response->setStatusCode(400)->setJSON(['error' => 'Invalid charge reference']);
+ }
+
+ $row = $this->db->table('nursing_ipd_charges')
+ ->where('charge_id', $chargeId)
+ ->where('ipd_id', $ipdId)
+ ->get()
+ ->getRowArray();
+
+ if (! is_array($row) || empty($row)) {
+ return $this->response->setStatusCode(404)->setJSON(['error' => 'Charge history row not found']);
+ }
+
+ $rate = (float) ($this->request->getPost('rate') ?? ($row['rate'] ?? 0));
+ $qty = (float) ($this->request->getPost('qty') ?? ($row['qty'] ?? 1));
+ $comment = trim((string) ($this->request->getPost('comment') ?? ($row['remarks'] ?? '')));
+ $visitDate = (string) ($this->request->getPost('visit_date') ?? ($row['visit_date'] ?? date('Y-m-d')));
+ $visitTime = (string) ($this->request->getPost('visit_time') ?? ($row['visit_time'] ?? ''));
+
+ if ($qty <= 0) {
+ $qty = 1;
+ }
+ if ($rate < 0) {
+ $rate = 0;
+ }
+
+ $amount = $rate * $qty;
+ $user = auth()->user();
+ $updatedBy = ($user && isset($user->username))
+ ? (string) $user->username
+ : (($user && isset($user->email)) ? (string) $user->email : 'system');
+
+ $this->db->table('nursing_ipd_charges')
+ ->where('charge_id', $chargeId)
+ ->where('ipd_id', $ipdId)
+ ->update([
+ 'rate' => $rate,
+ 'qty' => $qty,
+ 'amount' => $amount,
+ 'remarks' => $comment,
+ 'visit_date' => $visitDate,
+ 'visit_time' => $visitTime,
+ 'updated_by' => $updatedBy . ' [' . date('Y-m-d H:i:s') . ']',
+ ]);
+
+ $marker = '[NURSING_CHARGE_ID:' . $chargeId . ']';
+ $invoiceRows = $this->db->table('ipd_invoice_item')
+ ->select('id')
+ ->where('ipd_id', $ipdId)
+ ->like('comment', $marker)
+ ->get()
+ ->getResultArray();
+
+ $invoiceComment = trim($comment . ' ' . $marker);
+ foreach ($invoiceRows as $invoiceRow) {
+ $invoiceId = (int) ($invoiceRow['id'] ?? 0);
+ if ($invoiceId <= 0) {
+ continue;
+ }
+ $this->ipdEditModel->updateIpdItem([
+ 'item_rate' => $rate,
+ 'item_qty' => $qty,
+ 'item_amount' => $amount,
+ 'date_item' => $visitDate,
+ 'comment' => $invoiceComment,
+ ], $invoiceId);
+ }
+
+ return $this->response->setJSON(['ok' => true]);
+ }
+
+ public function deleteNursingCharge(int $ipdId, int $chargeId)
+ {
+ $permission = $this->requireAnyPermission([
+ 'billing.access',
+ 'billing.ipd.invoice',
+ 'billing.ipd.current-admission',
+ ]);
+ if ($permission) {
+ return $permission;
+ }
+
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['error' => 'Invalid request']);
+ }
+
+ if ($chargeId <= 0 || $ipdId <= 0 || ! $this->db->tableExists('nursing_ipd_charges')) {
+ return $this->response->setStatusCode(400)->setJSON(['error' => 'Invalid charge reference']);
+ }
+
+ $row = $this->db->table('nursing_ipd_charges')
+ ->where('charge_id', $chargeId)
+ ->where('ipd_id', $ipdId)
+ ->get()
+ ->getRowArray();
+
+ if (! is_array($row) || empty($row)) {
+ return $this->response->setStatusCode(404)->setJSON(['error' => 'Charge history row not found']);
+ }
+
+ $user = auth()->user();
+ $updatedBy = ($user && isset($user->username))
+ ? (string) $user->username
+ : (($user && isset($user->email)) ? (string) $user->email : 'system');
+
+ $this->db->table('nursing_ipd_charges')
+ ->where('charge_id', $chargeId)
+ ->where('ipd_id', $ipdId)
+ ->update([
+ 'include_in_bill' => 0,
+ 'updated_by' => $updatedBy . ' [' . date('Y-m-d H:i:s') . ']',
+ ]);
+
+ $marker = '[NURSING_CHARGE_ID:' . $chargeId . ']';
+ $invoiceRows = $this->db->table('ipd_invoice_item')
+ ->select('id')
+ ->where('ipd_id', $ipdId)
+ ->like('comment', $marker)
+ ->get()
+ ->getResultArray();
+
+ foreach ($invoiceRows as $invoiceRow) {
+ $invoiceId = (int) ($invoiceRow['id'] ?? 0);
+ if ($invoiceId <= 0) {
+ continue;
+ }
+ $this->ipdEditModel->deleteIpdInvoiceItem($invoiceId);
+ }
+
+ return $this->response->setJSON(['ok' => true]);
}
public function updateIpdChargePackage(int $itemId)
@@ -939,4 +1338,141 @@ class Ipd extends BaseController
return $this->response->setStatusCode(403)->setBody('Access denied');
}
+
+ private function resolveBedsideItemTypeId(string $category): int
+ {
+ $groupDesc = 'Bedside - ' . trim($category);
+ if ($groupDesc === 'Bedside -') {
+ $groupDesc = 'Bedside - General';
+ }
+
+ $row = $this->db->table('ipd_item_type')
+ ->select('itype_id')
+ ->where('group_desc', $groupDesc)
+ ->get()
+ ->getRow();
+
+ if ($row) {
+ return (int) ($row->itype_id ?? 0);
+ }
+
+ $this->db->table('ipd_item_type')->insert([
+ 'group_desc' => $groupDesc,
+ ]);
+
+ return (int) $this->db->insertID();
+ }
+
+ private function resolveDoctorVisitItemTypeId(): int
+ {
+ $groupDesc = 'Doctor Visit';
+
+ $row = $this->db->table('ipd_item_type')
+ ->select('itype_id')
+ ->where('group_desc', $groupDesc)
+ ->get()
+ ->getRow();
+
+ if ($row) {
+ return (int) ($row->itype_id ?? 0);
+ }
+
+ $this->db->table('ipd_item_type')->insert([
+ 'group_desc' => $groupDesc,
+ ]);
+
+ return (int) $this->db->insertID();
+ }
+
+ private function insertNursingIpdChargeRow(array $data): int
+ {
+ if (! $this->db->tableExists('nursing_ipd_charges')) {
+ return 0;
+ }
+
+ $this->db->table('nursing_ipd_charges')->insert($data);
+
+ return (int) $this->db->insertID();
+ }
+
+ private function syncNursingChargesToInvoice(int $ipdId): void
+ {
+ if (! $this->db->tableExists('nursing_ipd_charges')) {
+ return;
+ }
+
+ $rows = $this->db->table('nursing_ipd_charges')
+ ->where('ipd_id', $ipdId)
+ ->where('include_in_bill', 1)
+ ->orderBy('charge_id', 'ASC')
+ ->get()
+ ->getResultArray();
+
+ if (empty($rows)) {
+ return;
+ }
+
+ $caseMeta = $this->ipdModel->getIpdCaseMeta($ipdId);
+ $insId = (int) ($caseMeta['insurance_id'] ?? 0);
+
+ foreach ($rows as $row) {
+ $chargeId = (int) ($row['charge_id'] ?? 0);
+ if ($chargeId <= 0) {
+ continue;
+ }
+
+ $marker = '[NURSING_CHARGE_ID:' . $chargeId . ']';
+ $existsByMarker = $this->db->table('ipd_invoice_item')
+ ->where('ipd_id', $ipdId)
+ ->like('comment', $marker)
+ ->countAllResults();
+ if ($existsByMarker > 0) {
+ continue;
+ }
+
+ $itemName = trim((string) ($row['item_name'] ?? ''));
+ $dateItem = (string) ($row['visit_date'] ?? date('Y-m-d'));
+ $qty = (float) ($row['qty'] ?? 1);
+ $rate = (float) ($row['rate'] ?? 0);
+ $amount = (float) ($row['amount'] ?? ($rate * $qty));
+ $docId = (int) ($row['doctor_id'] ?? 0);
+
+ $existsByValue = $this->db->table('ipd_invoice_item')
+ ->where('ipd_id', $ipdId)
+ ->where('item_name', $itemName)
+ ->where('date_item', $dateItem)
+ ->where('item_qty', $qty)
+ ->where('item_amount', $amount)
+ ->where('doc_id', $docId)
+ ->countAllResults();
+ if ($existsByValue > 0) {
+ continue;
+ }
+
+ $rawType = trim((string) ($row['item_type'] ?? ''));
+ $typeId = stripos($rawType, 'doctor') !== false
+ ? $this->resolveDoctorVisitItemTypeId()
+ : $this->resolveBedsideItemTypeId($rawType !== '' ? $rawType : 'General');
+
+ $remarks = trim((string) ($row['remarks'] ?? ''));
+ $comment = trim($remarks . ' ' . $marker);
+
+ $this->ipdEditModel->insertIpdItem([
+ 'ipd_id' => $ipdId,
+ 'item_type' => $typeId,
+ 'item_id' => (int) ($row['item_id'] ?? $chargeId),
+ 'item_name' => $itemName,
+ 'item_rate' => $rate,
+ 'item_added_date' => date('Y-m-d H:i:s'),
+ 'item_qty' => $qty,
+ 'item_amount' => $amount,
+ 'doc_id' => $docId,
+ 'doc_name' => (string) ($row['doctor_name'] ?? ''),
+ 'date_item' => $dateItem,
+ 'comment' => $comment,
+ 'ins_id' => $insId,
+ 'org_code' => '',
+ ]);
+ }
+ }
}
diff --git a/app/Controllers/Ipd_discharge.php b/app/Controllers/Ipd_discharge.php
index be4dc12..3eb3449 100644
--- a/app/Controllers/Ipd_discharge.php
+++ b/app/Controllers/Ipd_discharge.php
@@ -4,6 +4,8 @@ namespace App\Controllers;
use App\Controllers\BaseController;
use App\Models\IpdBillingModel;
+use Mpdf\Mpdf;
+use Mpdf\Output\Destination;
class Ipd_discharge extends BaseController
{
@@ -582,6 +584,23 @@ class Ipd_discharge extends BaseController
return date('d-m-Y', $ts);
}
+ private function normalizeRichText(string $raw): string
+ {
+ $value = trim($raw);
+ if ($value === '') {
+ return '';
+ }
+
+ $value = preg_replace('/<\s*br\s*\/?\s*>/i', "\n", $value) ?? $value;
+ $value = preg_replace('/<\s*\/\s*p\s*>/i', "\n", $value) ?? $value;
+ $value = preg_replace('/<\s*p\b[^>]*>/i', '', $value) ?? $value;
+ $value = html_entity_decode(strip_tags($value), ENT_QUOTES | ENT_HTML5, 'UTF-8');
+ $value = preg_replace('/\r\n?|\n/', "\n", $value) ?? $value;
+ $value = preg_replace('/\n{3,}/', "\n\n", $value) ?? $value;
+
+ return trim($value);
+ }
+
private function addListSection(array &$sections, string $title, array $rows): void
{
if (empty($rows)) {
@@ -624,13 +643,26 @@ class Ipd_discharge extends BaseController
));
$patientId = (int) ($person->id ?? 0);
$opdHistory = $this->getLatestOpdHistorySnapshot($patientId);
+ $instructionRowForMeta = $this->firstRowByIpd('ipd_discharge_instructions', $ipdId);
+ $instructionMetaForPreview = $this->parseInstructionMetaPayload((string) ($instructionRowForMeta['comp_report'] ?? ''));
+ if (empty($instructionMetaForPreview['food_ids'])
+ && $this->tableHasColumns('ipd_discharge_drug_food_interaction', ['ipd_id', 'food_id_list'])) {
+ $legacyFoodRow = $this->firstRowByIpd('ipd_discharge_drug_food_interaction', $ipdId);
+ $instructionMetaForPreview['food_ids'] = $this->parseFoodIdCsv((string) ($legacyFoodRow['food_id_list'] ?? ''));
+ }
+ $instructionNabh = is_array($instructionMetaForPreview['nabh'] ?? null) ? ($instructionMetaForPreview['nabh'] ?? []) : [];
+ foreach (['drug_allergy_status', 'drug_allergy_details', 'adr_history', 'current_medications', 'co_morbidities', 'hpi_note', 'women_lmp', 'women_last_baby', 'women_pregnancy_related', 'women_related_problems'] as $field) {
+ if (trim((string) ($opdHistory[$field] ?? '')) === '' && trim((string) ($instructionNabh[$field] ?? '')) !== '') {
+ $opdHistory[$field] = trim((string) ($instructionNabh[$field] ?? ''));
+ }
+ }
$sections = [];
$complaints = $this->byIpdRows('ipd_discharge_complaint', ['comp_report', 'comp_remark'], 'id ASC', $ipdId);
$this->addListSection($sections, 'Presenting Complaints and Reason for Admission', $complaints);
$complaintRemark = $this->firstRowByIpd('ipd_discharge_complaint_remark', $ipdId);
- $complaintRemarkText = trim((string) ($complaintRemark['comp_remark'] ?? ''));
+ $complaintRemarkText = $this->normalizeRichText((string) ($complaintRemark['comp_remark'] ?? ''));
if ($complaintRemarkText !== '') {
$sections[] = '
' . nl2br(esc($complaintRemarkText)) . '
';
}
@@ -700,13 +732,12 @@ class Ipd_discharge extends BaseController
$sysRows = $physicalExamRows['systemic'] ?? [];
$sysHtml = '';
foreach ($sysRows as $row) {
- $value = trim((string) ($row['value'] ?? ''));
+ $value = $this->normalizeRichText((string) ($row['value'] ?? ''));
if ($value === '') {
continue;
}
- $name = trim((string) ($row['name'] ?? 'Systemic Examination'));
- $sysHtml .= '' . nl2br(esc($value)) . '
';
}
if ($sysHtml !== '') {
$sections[] = '' . nl2br(esc($diagnosisRemarkText)) . '
';
}
$inhosRow = $this->firstRowByIpd('ipd_discharge_investigtions_inhos', $ipdId);
- $inhosRemark = trim((string) ($inhosRow['comp_remark'] ?? ''));
+ $inhosRemark = $this->normalizeRichText((string) ($inhosRow['comp_remark'] ?? ''));
if ($inhosRemark !== '') {
$sections[] = ''
. nl2br(esc($inhosRemark))
@@ -891,7 +922,7 @@ class Ipd_discharge extends BaseController
$course = $this->byIpdRows('ipd_discharge_course', ['comp_report', 'comp_remark'], 'id ASC', $ipdId);
$this->addListSection($sections, 'Course in the Hospital', $course);
$courseRemark = $this->firstRowByIpd('ipd_discharge_course_remark', $ipdId);
- $courseRemarkText = trim((string) ($courseRemark['comp_remark'] ?? ''));
+ $courseRemarkText = $this->normalizeRichText((string) ($courseRemark['comp_remark'] ?? ''));
if ($courseRemarkText !== '') {
$sections[] = '
' . nl2br(esc($courseRemarkText)) . '
';
}
@@ -1039,11 +1070,64 @@ class Ipd_discharge extends BaseController
}
}
- $instructions = $this->byIpdRows('ipd_discharge_instructions', ['comp_remark', 'review_after', 'footer_text'], 'id DESC', $ipdId);
+ $instructions = $this->byIpdRows('ipd_discharge_instructions', ['comp_report', 'comp_remark', 'review_after', 'footer_text'], 'id DESC', $ipdId);
if (! empty($instructions)) {
$first = $instructions[0];
$html = '
Discharge Advice/Instructions/Summary ';
+ $instructionMeta = $this->parseInstructionMetaPayload((string) ($first['comp_report'] ?? ''));
+ $foodIds = is_array($instructionMeta['food_ids'] ?? null) ? ($instructionMeta['food_ids'] ?? []) : [];
+ if (empty($foodIds)
+ && $this->tableHasColumns('ipd_discharge_drug_food_interaction', ['ipd_id', 'food_id_list'])) {
+ $legacyFoodRow = $this->firstRowByIpd('ipd_discharge_drug_food_interaction', $ipdId);
+ $foodIds = $this->parseFoodIdCsv((string) ($legacyFoodRow['food_id_list'] ?? ''));
+ }
+ $foodMap = [];
+ if (! empty($foodIds) && $this->tableHasColumns('ipd_discharge_master_food', ['id', 'food_short', 'food_desc'])) {
+ $rows = $this->db->table('ipd_discharge_master_food')
+ ->select('id,food_short,food_desc,food_desc_lang')
+ ->whereIn('id', array_map('intval', $foodIds))
+ ->get()
+ ->getResultArray();
+ foreach ($rows as $row) {
+ $foodMap[(int) ($row['id'] ?? 0)] = $row;
+ }
+ }
+
+ if (! empty($foodIds)) {
+ $html .= '
Dietary Advice:
';
+ $html .= '
';
+ foreach ($foodIds as $foodId) {
+ $id = (int) $foodId;
+ $row = $foodMap[$id] ?? null;
+ if (! is_array($row)) {
+ continue;
+ }
+
+ $heading = trim((string) ($row['food_short'] ?? ''));
+ $line = trim((string) ($row['food_desc_lang'] ?? ''));
+ if ($line === '') {
+ $line = trim((string) ($row['food_desc'] ?? ''));
+ }
+ if ($line === '' && $heading === '') {
+ continue;
+ }
+
+ $entry = '';
+ if ($heading !== '') {
+ $entry .= '' . esc($heading) . ': ';
+ }
+ $entry .= esc($line !== '' ? $line : $heading);
+ $html .= '' . $entry . ' ';
+ }
+ $html .= ' ';
+ }
+
+ $otherText = trim((string) ($instructionMeta['other_text'] ?? ''));
+ if ($otherText !== '') {
+ $html .= '
Other Advice: ' . nl2br(esc($otherText)) . '
';
+ }
+
$remark = trim((string) ($first['comp_remark'] ?? ''));
if ($remark !== '') {
$html .= '
' . nl2br(esc($remark)) . '
';
@@ -1212,6 +1296,17 @@ class Ipd_discharge extends BaseController
return false;
}
+ private function findFirstExistingTable(array $candidates): ?string
+ {
+ foreach ($candidates as $table) {
+ if ($this->db->tableExists($table)) {
+ return $table;
+ }
+ }
+
+ return null;
+ }
+
private function buildNabhAuditChecklist(int $ipdId, array $panelData): array
{
$person = $panelData['person_info'] ?? null;
@@ -1423,6 +1518,568 @@ class Ipd_discharge extends BaseController
return date('Y-m-d', $ts);
}
+ private function ensureDischargeSurgeryMasterTable(): bool
+ {
+ if ($this->db->tableExists('ipd_discharge_surgery_master')) {
+ return true;
+ }
+
+ try {
+ $sql = "CREATE TABLE IF NOT EXISTS ipd_discharge_surgery_master (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ term_type VARCHAR(20) NOT NULL DEFAULT 'surgery',
+ term_name VARCHAR(255) NOT NULL,
+ term_code VARCHAR(60) DEFAULT NULL,
+ icd_code VARCHAR(60) DEFAULT NULL,
+ is_active TINYINT(1) NOT NULL DEFAULT 1,
+ created_at DATETIME NULL,
+ updated_at DATETIME NULL,
+ UNIQUE KEY uniq_term_type_name (term_type, term_name),
+ INDEX idx_type_active_name (term_type, is_active, term_name)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
+ $this->db->query($sql);
+ } catch (\Throwable $e) {
+ return false;
+ }
+
+ return $this->db->tableExists('ipd_discharge_surgery_master');
+ }
+
+ private function ensureDischargeIcdMasterTable(): bool
+ {
+ if ($this->db->tableExists('ipd_discharge_icd_master')) {
+ return true;
+ }
+
+ try {
+ $sql = "CREATE TABLE IF NOT EXISTS ipd_discharge_icd_master (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ icd_code VARCHAR(30) NOT NULL,
+ diagnosis_text VARCHAR(255) NOT NULL,
+ is_active TINYINT(1) NOT NULL DEFAULT 1,
+ created_at DATETIME NULL,
+ updated_at DATETIME NULL,
+ UNIQUE KEY uniq_icd_code_text (icd_code, diagnosis_text),
+ INDEX idx_icd_active_code (is_active, icd_code),
+ INDEX idx_icd_active_text (is_active, diagnosis_text)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
+ $this->db->query($sql);
+ } catch (\Throwable $e) {
+ return false;
+ }
+
+ return $this->db->tableExists('ipd_discharge_icd_master');
+ }
+
+ private function ensureDischargeFoodMasterTable(): bool
+ {
+ if ($this->db->tableExists('ipd_discharge_master_food')) {
+ return true;
+ }
+
+ try {
+ $sql = "CREATE TABLE IF NOT EXISTS ipd_discharge_master_food (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ food_short VARCHAR(255) NOT NULL,
+ food_desc TEXT NULL,
+ food_desc_lang TEXT NULL
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
+ $this->db->query($sql);
+ } catch (\Throwable $e) {
+ return false;
+ }
+
+ return $this->db->tableExists('ipd_discharge_master_food');
+ }
+
+ public function dietary_master_list()
+ {
+ $q = trim((string) $this->request->getGet('q'));
+ if (! $this->ensureDischargeFoodMasterTable()) {
+ return $this->response->setJSON(['rows' => []]);
+ }
+
+ $builder = $this->db->table('ipd_discharge_master_food')
+ ->select('id,food_short,food_desc,food_desc_lang');
+
+ if ($q !== '') {
+ $builder->groupStart()
+ ->like('food_short', $q)
+ ->orLike('food_desc', $q)
+ ->orLike('food_desc_lang', $q)
+ ->groupEnd();
+ }
+
+ $rows = $builder
+ ->orderBy('id', 'ASC')
+ ->limit(500)
+ ->get()
+ ->getResultArray();
+
+ return $this->response->setJSON(['rows' => $rows]);
+ }
+
+ public function dietary_master_save()
+ {
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
+ }
+
+ if (! $this->ensureDischargeFoodMasterTable()) {
+ return $this->response->setJSON([
+ 'update' => 0,
+ 'error_text' => 'Unable to access dietary master table',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ $id = (int) $this->request->getPost('id');
+ $short = trim((string) $this->request->getPost('food_short'));
+ $desc = trim((string) $this->request->getPost('food_desc'));
+ $lang = trim((string) $this->request->getPost('food_desc_lang'));
+
+ if ($short === '') {
+ return $this->response->setJSON([
+ 'update' => 0,
+ 'error_text' => 'Short text is required',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ $payload = [
+ 'food_short' => $short,
+ 'food_desc' => $desc !== '' ? $desc : $short,
+ 'food_desc_lang' => $lang !== '' ? $lang : null,
+ ];
+
+ try {
+ $table = $this->db->table('ipd_discharge_master_food');
+ if ($id > 0) {
+ $ok = (bool) $table->where('id', $id)->update($payload);
+ } else {
+ $ok = (bool) $table->insert($payload);
+ $id = (int) ($this->db->insertID() ?: 0);
+ }
+
+ return $this->response->setJSON([
+ 'update' => $ok ? 1 : 0,
+ 'id' => $id,
+ 'error_text' => $ok ? 'Dietary master saved' : 'Unable to save dietary master',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ } catch (\Throwable $e) {
+ return $this->response->setJSON([
+ 'update' => 0,
+ 'error_text' => 'Save failed: ' . $e->getMessage(),
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+ }
+
+ public function dietary_master_delete()
+ {
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
+ }
+
+ if (! $this->ensureDischargeFoodMasterTable()) {
+ return $this->response->setJSON([
+ 'update' => 0,
+ 'error_text' => 'Unable to access dietary master table',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ $id = (int) $this->request->getPost('id');
+ if ($id <= 0) {
+ return $this->response->setJSON([
+ 'update' => 0,
+ 'error_text' => 'Invalid record id',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ $ok = (bool) $this->db->table('ipd_discharge_master_food')->where('id', $id)->delete();
+ return $this->response->setJSON([
+ 'update' => $ok ? 1 : 0,
+ 'error_text' => $ok ? 'Record deleted' : 'Unable to delete record',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ private function normalizeSurgeryTermType(string $type): string
+ {
+ $clean = strtolower(trim($type));
+ return in_array($clean, ['procedure', 'surgery'], true) ? $clean : 'surgery';
+ }
+
+ public function surgery_master_lookup()
+ {
+ $type = $this->normalizeSurgeryTermType((string) $this->request->getGet('type'));
+ $q = trim((string) $this->request->getGet('q'));
+
+ if (! $this->ensureDischargeSurgeryMasterTable()) {
+ return $this->response->setJSON(['rows' => []]);
+ }
+
+ $builder = $this->db->table('ipd_discharge_surgery_master')
+ ->select('id,term_type,term_name,term_code,icd_code')
+ ->where('is_active', 1)
+ ->where('term_type', $type);
+
+ if ($q !== '') {
+ $builder->groupStart()
+ ->like('term_name', $q)
+ ->orLike('term_code', $q)
+ ->orLike('icd_code', $q)
+ ->groupEnd();
+ }
+
+ $rows = $builder
+ ->orderBy('term_name', 'ASC')
+ ->limit(25)
+ ->get()
+ ->getResultArray();
+
+ return $this->response->setJSON(['rows' => $rows]);
+ }
+
+ public function surgery_master_list()
+ {
+ $type = $this->normalizeSurgeryTermType((string) $this->request->getGet('type'));
+ $q = trim((string) $this->request->getGet('q'));
+
+ if (! $this->ensureDischargeSurgeryMasterTable()) {
+ return $this->response->setJSON(['rows' => []]);
+ }
+
+ $builder = $this->db->table('ipd_discharge_surgery_master')
+ ->select('id,term_type,term_name,term_code,icd_code,is_active,updated_at')
+ ->where('term_type', $type);
+
+ if ($q !== '') {
+ $builder->groupStart()
+ ->like('term_name', $q)
+ ->orLike('term_code', $q)
+ ->orLike('icd_code', $q)
+ ->groupEnd();
+ }
+
+ $rows = $builder
+ ->orderBy('is_active', 'DESC')
+ ->orderBy('term_name', 'ASC')
+ ->limit(250)
+ ->get()
+ ->getResultArray();
+
+ return $this->response->setJSON(['rows' => $rows]);
+ }
+
+ public function surgery_master_save()
+ {
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
+ }
+
+ if (! $this->ensureDischargeSurgeryMasterTable()) {
+ return $this->response->setJSON([
+ 'update' => 0,
+ 'error_text' => 'Unable to access surgery master table',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ $id = (int) $this->request->getPost('id');
+ $type = $this->normalizeSurgeryTermType((string) $this->request->getPost('type'));
+ $name = trim((string) $this->request->getPost('name'));
+ $code = trim((string) $this->request->getPost('code'));
+ $icdCode = trim((string) $this->request->getPost('icd_code'));
+ $isActive = (int) $this->request->getPost('is_active') === 0 ? 0 : 1;
+
+ if ($name === '') {
+ return $this->response->setJSON([
+ 'update' => 0,
+ 'error_text' => 'Name is required',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ $now = date('Y-m-d H:i:s');
+ $payload = [
+ 'term_type' => $type,
+ 'term_name' => $name,
+ 'term_code' => $code !== '' ? $code : null,
+ 'icd_code' => $icdCode !== '' ? $icdCode : null,
+ 'is_active' => $isActive,
+ 'updated_at' => $now,
+ ];
+
+ try {
+ $table = $this->db->table('ipd_discharge_surgery_master');
+
+ if ($id > 0) {
+ $ok = (bool) $table->where('id', $id)->update($payload);
+ } else {
+ $payload['created_at'] = $now;
+ $ok = (bool) $table->insert($payload);
+ $id = (int) ($this->db->insertID() ?: 0);
+ }
+
+ if (! $ok) {
+ return $this->response->setJSON([
+ 'update' => 0,
+ 'error_text' => 'Unable to save record',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ return $this->response->setJSON([
+ 'update' => 1,
+ 'id' => $id,
+ 'error_text' => 'Master record saved',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ } catch (\Throwable $e) {
+ return $this->response->setJSON([
+ 'update' => 0,
+ 'error_text' => 'Save failed: ' . $e->getMessage(),
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+ }
+
+ public function surgery_master_delete()
+ {
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
+ }
+
+ if (! $this->ensureDischargeSurgeryMasterTable()) {
+ return $this->response->setJSON([
+ 'update' => 0,
+ 'error_text' => 'Unable to access surgery master table',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ $id = (int) $this->request->getPost('id');
+ if ($id <= 0) {
+ return $this->response->setJSON([
+ 'update' => 0,
+ 'error_text' => 'Invalid record id',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ $ok = (bool) $this->db->table('ipd_discharge_surgery_master')
+ ->where('id', $id)
+ ->delete();
+
+ return $this->response->setJSON([
+ 'update' => $ok ? 1 : 0,
+ 'error_text' => $ok ? 'Record deleted' : 'Unable to delete record',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ public function diagnosis_icd_lookup()
+ {
+ $q = trim((string) $this->request->getGet('q'));
+ $rows = [];
+
+ if ($q !== '') {
+ if ($this->ensureDischargeIcdMasterTable()) {
+ $icdRows = $this->db->table('ipd_discharge_icd_master')
+ ->select('id,icd_code,diagnosis_text')
+ ->where('is_active', 1)
+ ->groupStart()
+ ->like('icd_code', $q)
+ ->orLike('diagnosis_text', $q)
+ ->groupEnd()
+ ->orderBy('icd_code', 'ASC')
+ ->limit(20)
+ ->get()
+ ->getResultArray();
+
+ foreach ($icdRows as $row) {
+ $rows[] = [
+ 'id' => (int) ($row['id'] ?? 0),
+ 'name' => (string) ($row['diagnosis_text'] ?? ''),
+ 'icd_code' => (string) ($row['icd_code'] ?? ''),
+ 'source' => 'icd_master',
+ ];
+ }
+ }
+
+ if (count($rows) < 25 && $this->db->tableExists('complaints_master')) {
+ $fallbackRows = $this->db->table('complaints_master')
+ ->select('Code as id, Name as name, name_hinglish')
+ ->groupStart()
+ ->like('Name', $q)
+ ->orLike('name_hinglish', $q)
+ ->groupEnd()
+ ->orderBy('Name', 'ASC')
+ ->limit(25 - count($rows))
+ ->get()
+ ->getResultArray();
+
+ foreach ($fallbackRows as $row) {
+ $rows[] = [
+ 'id' => (int) ($row['id'] ?? 0),
+ 'name' => (string) ($row['name'] ?? ''),
+ 'icd_code' => '',
+ 'source' => 'complaints_master',
+ 'name_hinglish' => (string) ($row['name_hinglish'] ?? ''),
+ ];
+ }
+ }
+ }
+
+ return $this->response->setJSON(['rows' => $rows]);
+ }
+
+ public function diagnosis_icd_seed_starter()
+ {
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
+ }
+
+ if (! $this->ensureDischargeIcdMasterTable()) {
+ return $this->response->setJSON([
+ 'update' => 0,
+ 'error_text' => 'Unable to access ICD master table',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ $seedRows = [
+ ['icd_code' => 'I10', 'diagnosis_text' => 'Primary hypertension'],
+ ['icd_code' => 'E11.9', 'diagnosis_text' => 'Type 2 diabetes mellitus without complications'],
+ ['icd_code' => 'J45.909', 'diagnosis_text' => 'Asthma, unspecified, uncomplicated'],
+ ['icd_code' => 'K21.9', 'diagnosis_text' => 'Gastro-esophageal reflux disease without esophagitis'],
+ ['icd_code' => 'N39.0', 'diagnosis_text' => 'Urinary tract infection, site not specified'],
+ ['icd_code' => 'J18.9', 'diagnosis_text' => 'Pneumonia, unspecified organism'],
+ ['icd_code' => 'A09', 'diagnosis_text' => 'Infectious gastroenteritis and colitis, unspecified'],
+ ['icd_code' => 'D64.9', 'diagnosis_text' => 'Anemia, unspecified'],
+ ['icd_code' => 'M54.5', 'diagnosis_text' => 'Low back pain'],
+ ['icd_code' => 'R50.9', 'diagnosis_text' => 'Fever, unspecified'],
+ ['icd_code' => 'R07.9', 'diagnosis_text' => 'Chest pain, unspecified'],
+ ['icd_code' => 'R51.9', 'diagnosis_text' => 'Headache, unspecified'],
+ ['icd_code' => 'K59.0', 'diagnosis_text' => 'Constipation'],
+ ['icd_code' => 'K52.9', 'diagnosis_text' => 'Noninfective gastroenteritis and colitis, unspecified'],
+ ['icd_code' => 'J06.9', 'diagnosis_text' => 'Acute upper respiratory infection, unspecified'],
+ ];
+
+ // Add historical ICD-tagged diagnoses already used in this installation.
+ if ($this->tableHasColumns('ipd_discharge_diagnosis', ['comp_report'])) {
+ $historic = $this->db->table('ipd_discharge_diagnosis')
+ ->select('comp_report')
+ ->where('comp_report IS NOT NULL', null, false)
+ ->where('comp_report !=', '')
+ ->orderBy('id', 'DESC')
+ ->limit(1000)
+ ->get()
+ ->getResultArray();
+
+ foreach ($historic as $row) {
+ $report = trim((string) ($row['comp_report'] ?? ''));
+ if ($report === '') {
+ continue;
+ }
+
+ if (preg_match('/\[\s*ICD\s*:\s*([^\]]+)\]/i', $report, $matches) !== 1) {
+ continue;
+ }
+
+ $icdCode = strtoupper(trim((string) ($matches[1] ?? '')));
+ $diagnosis = trim((string) preg_replace('/\[\s*ICD\s*:[^\]]+\]/i', '', $report));
+ if ($icdCode === '' || $diagnosis === '') {
+ continue;
+ }
+
+ $seedRows[] = [
+ 'icd_code' => $icdCode,
+ 'diagnosis_text' => $diagnosis,
+ ];
+ }
+ }
+
+ $seen = [];
+ $inserted = 0;
+ $skipped = 0;
+ $table = $this->db->table('ipd_discharge_icd_master');
+ $now = date('Y-m-d H:i:s');
+
+ foreach ($seedRows as $row) {
+ $icdCode = strtoupper(trim((string) ($row['icd_code'] ?? '')));
+ $diagnosis = trim((string) ($row['diagnosis_text'] ?? ''));
+ if ($icdCode === '' || $diagnosis === '') {
+ $skipped++;
+ continue;
+ }
+
+ $key = $icdCode . '|' . strtoupper($diagnosis);
+ if (isset($seen[$key])) {
+ $skipped++;
+ continue;
+ }
+ $seen[$key] = true;
+
+ $exists = $table
+ ->select('id')
+ ->where('icd_code', $icdCode)
+ ->where('diagnosis_text', $diagnosis)
+ ->get(1)
+ ->getRowArray();
+
+ if (! empty($exists['id'])) {
+ $skipped++;
+ continue;
+ }
+
+ $ok = (bool) $table->insert([
+ 'icd_code' => $icdCode,
+ 'diagnosis_text' => $diagnosis,
+ 'is_active' => 1,
+ 'created_at' => $now,
+ 'updated_at' => $now,
+ ]);
+
+ if ($ok) {
+ $inserted++;
+ } else {
+ $skipped++;
+ }
+ }
+
+ return $this->response->setJSON([
+ 'update' => 1,
+ 'inserted' => $inserted,
+ 'skipped' => $skipped,
+ 'error_text' => $inserted > 0
+ ? ('ICD starter loaded. Added ' . $inserted . ' rows.')
+ : 'ICD starter already present. No new rows added.',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
private function upsertByComposite(
string $table,
string $ipdField,
@@ -2240,6 +2897,139 @@ class Ipd_discharge extends BaseController
return ['pain_value' => $painValue];
}
+ private function buildInstructionMetaPayload(array $meta): string
+ {
+ $ids = $meta['food_ids'] ?? [];
+ if (! is_array($ids)) {
+ $ids = [];
+ }
+
+ $cleanIds = [];
+ foreach ($ids as $id) {
+ $intId = (int) $id;
+ if ($intId > 0) {
+ $cleanIds[$intId] = $intId;
+ }
+ }
+
+ $otherText = trim((string) ($meta['other_text'] ?? ''));
+ if (empty($cleanIds) && $otherText === '') {
+ return '';
+ }
+
+ $nabh = $meta['nabh'] ?? [];
+ if (! is_array($nabh)) {
+ $nabh = [];
+ }
+ $nabhPayload = [
+ 'drug_allergy_status' => trim((string) ($nabh['drug_allergy_status'] ?? '')),
+ 'drug_allergy_details' => trim((string) ($nabh['drug_allergy_details'] ?? '')),
+ 'adr_history' => trim((string) ($nabh['adr_history'] ?? '')),
+ 'current_medications' => trim((string) ($nabh['current_medications'] ?? '')),
+ 'co_morbidities' => trim((string) ($nabh['co_morbidities'] ?? '')),
+ 'hpi_note' => trim((string) ($nabh['hpi_note'] ?? '')),
+ 'women_lmp' => trim((string) ($nabh['women_lmp'] ?? '')),
+ 'women_last_baby' => trim((string) ($nabh['women_last_baby'] ?? '')),
+ 'women_pregnancy_related' => trim((string) ($nabh['women_pregnancy_related'] ?? '')),
+ 'women_related_problems' => trim((string) ($nabh['women_related_problems'] ?? '')),
+ ];
+
+ $payload = [
+ 'food_ids' => array_values($cleanIds),
+ 'other_text' => $otherText,
+ 'nabh' => $nabhPayload,
+ ];
+
+ return json_encode($payload, JSON_UNESCAPED_UNICODE) ?: '';
+ }
+
+ private function parseInstructionMetaPayload(string $raw): array
+ {
+ $raw = trim($raw);
+ if ($raw === '') {
+ return ['food_ids' => [], 'other_text' => '', 'nabh' => []];
+ }
+
+ $decoded = json_decode($raw, true);
+ if (! is_array($decoded)) {
+ return ['food_ids' => [], 'other_text' => '', 'nabh' => []];
+ }
+
+ $ids = $decoded['food_ids'] ?? [];
+ if (! is_array($ids)) {
+ $ids = [];
+ }
+
+ $cleanIds = [];
+ foreach ($ids as $id) {
+ $intId = (int) $id;
+ if ($intId > 0) {
+ $cleanIds[$intId] = $intId;
+ }
+ }
+
+ $nabh = $decoded['nabh'] ?? [];
+ if (! is_array($nabh)) {
+ $nabh = [];
+ }
+
+ return [
+ 'food_ids' => array_values($cleanIds),
+ 'other_text' => trim((string) ($decoded['other_text'] ?? '')),
+ 'nabh' => [
+ 'drug_allergy_status' => trim((string) ($nabh['drug_allergy_status'] ?? '')),
+ 'drug_allergy_details' => trim((string) ($nabh['drug_allergy_details'] ?? '')),
+ 'adr_history' => trim((string) ($nabh['adr_history'] ?? '')),
+ 'current_medications' => trim((string) ($nabh['current_medications'] ?? '')),
+ 'co_morbidities' => trim((string) ($nabh['co_morbidities'] ?? '')),
+ 'hpi_note' => trim((string) ($nabh['hpi_note'] ?? '')),
+ 'women_lmp' => trim((string) ($nabh['women_lmp'] ?? '')),
+ 'women_last_baby' => trim((string) ($nabh['women_last_baby'] ?? '')),
+ 'women_pregnancy_related' => trim((string) ($nabh['women_pregnancy_related'] ?? '')),
+ 'women_related_problems' => trim((string) ($nabh['women_related_problems'] ?? '')),
+ ],
+ ];
+ }
+
+ private function parseFoodIdCsv(string $raw): array
+ {
+ $raw = trim($raw);
+ if ($raw === '') {
+ return [];
+ }
+
+ $ids = [];
+ foreach (explode(',', $raw) as $part) {
+ $id = (int) trim((string) $part);
+ if ($id > 0) {
+ $ids[$id] = $id;
+ }
+ }
+
+ return array_values($ids);
+ }
+
+ private function readInstructionFoodIdsFromRequest(): array
+ {
+ $posted = $this->request->getPost('instruction_food_ids');
+ if (! is_array($posted)) {
+ $posted = $this->request->getPost('instruction_food_ids[]');
+ }
+ if (! is_array($posted)) {
+ $posted = [];
+ }
+
+ $clean = [];
+ foreach ($posted as $id) {
+ $intId = (int) $id;
+ if ($intId > 0) {
+ $clean[$intId] = $intId;
+ }
+ }
+
+ return array_values($clean);
+ }
+
private function extractNabhFieldsFromRemarks(string $remarks): array
{
$extract = static function (string $pattern, string $source): string {
@@ -2460,6 +3250,60 @@ class Ipd_discharge extends BaseController
$action = (string) ($this->request->getPost('action') ?? 'save_main');
$savedAny = false;
+ if ((int) ($this->request->getPost('dietary_autosave') ?? 0) === 1) {
+ $instructionFoodIds = $this->readInstructionFoodIdsFromRequest();
+
+ $instructionRow = $this->firstRowByIpd('ipd_discharge_instructions', $ipdId);
+ $existingMeta = $this->parseInstructionMetaPayload((string) ($instructionRow['comp_report'] ?? ''));
+ $instructionOtherPosted = $this->request->getPost('instruction_other');
+ $instructionOther = is_string($instructionOtherPosted)
+ ? trim($instructionOtherPosted)
+ : trim((string) ($existingMeta['other_text'] ?? ''));
+
+ $instructionMeta = $this->buildInstructionMetaPayload([
+ 'food_ids' => $instructionFoodIds,
+ 'other_text' => $instructionOther,
+ 'nabh' => is_array($existingMeta['nabh'] ?? null) ? ($existingMeta['nabh'] ?? []) : [],
+ ]);
+
+ if ($this->tableHasColumns('ipd_discharge_instructions', ['ipd_id'])) {
+ $savedAny = $this->upsertByIpd('ipd_discharge_instructions', $ipdId, [
+ 'comp_report' => $instructionMeta,
+ 'comp_remark' => (string) ($instructionRow['comp_remark'] ?? ''),
+ 'review_after' => (string) ($instructionRow['review_after'] ?? ''),
+ 'footer_text' => (string) ($instructionRow['footer_text'] ?? ''),
+ 'footer_banner' => (string) ($instructionRow['footer_banner'] ?? '0'),
+ 'update_by' => $userLabel,
+ ]) || $savedAny;
+ }
+
+ if ($this->tableHasColumns('ipd_discharge_drug_food_interaction', ['ipd_id', 'food_id_list'])) {
+ $foodIds = [];
+ foreach ($instructionFoodIds as $foodId) {
+ $id = (int) $foodId;
+ if ($id > 0) {
+ $foodIds[$id] = $id;
+ }
+ }
+
+ $legacyData = [
+ 'food_id_list' => implode(',', array_values($foodIds)),
+ ];
+ if ($this->db->fieldExists('food_text', 'ipd_discharge_drug_food_interaction')) {
+ $legacyData['food_text'] = (string) ($instructionRow['comp_remark'] ?? '');
+ }
+
+ $savedAny = $this->upsertByIpd('ipd_discharge_drug_food_interaction', $ipdId, $legacyData) || $savedAny;
+ }
+
+ return $this->response->setJSON([
+ 'update' => $savedAny ? 1 : 0,
+ 'error_text' => $savedAny ? 'Dietary advice saved.' : 'Unable to save dietary advice.',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
if ($action === 'add_complaint') {
$complaintName = trim((string) ($this->request->getPost('new_complaint_name') ?? ''));
$complaintRemarkRow = trim((string) ($this->request->getPost('new_complaint_remark') ?? ''));
@@ -2524,6 +3368,7 @@ class Ipd_discharge extends BaseController
$name = trim((string) ($this->request->getPost('new_surgery_name') ?? ''));
$date = $this->parseInputDateToDb((string) ($this->request->getPost('new_surgery_date') ?? ''));
$remark = trim((string) ($this->request->getPost('new_surgery_remark') ?? ''));
+ $masterId = max(0, (int) ($this->request->getPost('new_surgery_master_id') ?? 0));
if ($name !== '' && $this->tableHasColumns('ipd_discharge_surgery', ['ipd_id', 'surgery_name'])) {
$insert = [
'ipd_id' => $ipdId,
@@ -2535,7 +3380,7 @@ class Ipd_discharge extends BaseController
$insert['surgery_date'] = $date;
}
if ($this->db->fieldExists('surgery_id', 'ipd_discharge_surgery')) {
- $insert['surgery_id'] = 0;
+ $insert['surgery_id'] = $masterId;
}
if ($this->db->fieldExists('surgery_by_doc_id', 'ipd_discharge_surgery')) {
$insert['surgery_by_doc_id'] = 0;
@@ -2566,6 +3411,7 @@ class Ipd_discharge extends BaseController
$name = trim((string) ($this->request->getPost('new_procedure_name') ?? ''));
$date = $this->parseInputDateToDb((string) ($this->request->getPost('new_procedure_date') ?? ''));
$remark = trim((string) ($this->request->getPost('new_procedure_remark') ?? ''));
+ $masterId = max(0, (int) ($this->request->getPost('new_procedure_master_id') ?? 0));
if ($name !== '' && $this->tableHasColumns('ipd_discharge_procedure', ['ipd_id', 'procedure_name'])) {
$insert = [
'ipd_id' => $ipdId,
@@ -2577,7 +3423,7 @@ class Ipd_discharge extends BaseController
$insert['procedure_date'] = $date;
}
if ($this->db->fieldExists('procedure_id', 'ipd_discharge_procedure')) {
- $insert['procedure_id'] = 0;
+ $insert['procedure_id'] = $masterId;
}
if ($this->db->fieldExists('procedure_by_doc_id', 'ipd_discharge_procedure')) {
$insert['procedure_by_doc_id'] = 0;
@@ -2714,15 +3560,55 @@ class Ipd_discharge extends BaseController
}
} elseif ($action === 'add_drug') {
$name = trim((string) ($this->request->getPost('new_drug_name') ?? ''));
+ $type = trim((string) ($this->request->getPost('new_drug_type') ?? ''));
$dose = trim((string) ($this->request->getPost('new_drug_dose') ?? ''));
+ $when = trim((string) ($this->request->getPost('new_drug_when') ?? ''));
+ $freq = trim((string) ($this->request->getPost('new_drug_freq') ?? ''));
$day = trim((string) ($this->request->getPost('new_drug_day') ?? ''));
- if ($name !== '' && $this->tableHasColumns('ipd_discharge_drug', ['ipd_id', 'drug_name'])) {
+ $qty = trim((string) ($this->request->getPost('new_drug_qty') ?? ''));
+ $remark = trim((string) ($this->request->getPost('new_drug_remark') ?? ''));
+
+ $legacyDrugTable = $this->findFirstExistingTable(['ipd_discharge_prescrption_prescribed', 'ipd_discharge_prescription_prescribed']);
+ if ($name !== '' && $legacyDrugTable !== null && $this->tableHasColumns($legacyDrugTable, ['ipd_id', 'med_name'])) {
+ $insert = [
+ 'ipd_id' => $ipdId,
+ 'med_id' => 0,
+ 'med_name' => $name,
+ 'med_type' => $type,
+ 'dosage' => $dose,
+ 'dosage_when' => $when,
+ 'dosage_freq' => $freq,
+ 'no_of_days' => $day,
+ 'qty' => $qty,
+ 'remark' => $remark,
+ 'update_by' => $userLabel,
+ ];
+ if ($this->db->fieldExists('order_id', $legacyDrugTable)) {
+ $insert['order_id'] = 0;
+ }
+
+ $allowed = [];
+ foreach ($insert as $field => $value) {
+ if ($this->db->fieldExists($field, $legacyDrugTable)) {
+ $allowed[$field] = $value;
+ }
+ }
+
+ $savedAny = ! empty($allowed)
+ ? (bool) $this->db->table($legacyDrugTable)->insert($allowed)
+ : false;
+ $notice = $savedAny ? 'Medicine row added.' : 'Unable to add medicine row.';
+ $noticeType = $savedAny ? 'success' : 'warning';
+ } elseif ($name !== '' && $this->tableHasColumns('ipd_discharge_drug', ['ipd_id', 'drug_name'])) {
+ $doseText = trim(implode(' ', array_filter([$type, $dose, $when, $freq], static fn ($v) => trim((string) $v) !== '')));
+ $dayText = trim(implode(' ', array_filter([$day, $qty !== '' ? ('Qty:' . $qty) : '', $remark], static fn ($v) => trim((string) $v) !== '')));
+
$insert = [
'ipd_id' => $ipdId,
'drug_code' => 0,
'drug_name' => $name,
- 'drug_dose' => $dose,
- 'drug_day' => $day,
+ 'drug_dose' => $doseText,
+ 'drug_day' => $dayText,
'update_by' => $userLabel,
];
if ($this->db->fieldExists('order_id', 'ipd_discharge_drug')) {
@@ -2737,13 +3623,135 @@ class Ipd_discharge extends BaseController
: 'Drug table/columns are missing in database.';
$noticeType = 'warning';
}
+ } elseif ($action === 'apply_rx_group') {
+ $rxGroupId = (int) ($this->request->getPost('selected_rx_group_id') ?? 0);
+ $templateTable = $this->findFirstExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']);
+ $legacyDrugTable = $this->findFirstExistingTable(['ipd_discharge_prescrption_prescribed', 'ipd_discharge_prescription_prescribed']);
+ $fallbackDrugTable = $this->tableHasColumns('ipd_discharge_drug', ['ipd_id', 'drug_name']) ? 'ipd_discharge_drug' : null;
+
+ if ($rxGroupId <= 0) {
+ $notice = 'Select an Rx Group first.';
+ $noticeType = 'warning';
+ } elseif ($templateTable === null) {
+ $notice = 'Rx Group medicine template table not found.';
+ $noticeType = 'warning';
+ } elseif ($legacyDrugTable === null && $fallbackDrugTable === null) {
+ $notice = 'No discharge medicine table found.';
+ $noticeType = 'warning';
+ } else {
+ $templateRows = $this->db->table($templateTable)
+ ->where('rx_group_id', $rxGroupId)
+ ->orderBy('id', 'ASC')
+ ->get()
+ ->getResultArray();
+
+ if (empty($templateRows)) {
+ $notice = 'No medicines found in selected Rx Group.';
+ $noticeType = 'warning';
+ } else {
+ $inserted = 0;
+
+ foreach ($templateRows as $row) {
+ $medName = trim((string) ($row['med_name'] ?? ''));
+ if ($medName === '') {
+ continue;
+ }
+
+ if ($legacyDrugTable !== null && $this->tableHasColumns($legacyDrugTable, ['ipd_id', 'med_name'])) {
+ $insert = [
+ 'ipd_id' => $ipdId,
+ 'med_id' => (int) ($row['med_id'] ?? 0),
+ 'med_name' => $medName,
+ 'med_type' => trim((string) ($row['med_type'] ?? '')),
+ 'dosage' => trim((string) ($row['dosage'] ?? '')),
+ 'dosage_when' => trim((string) ($row['dosage_when'] ?? '')),
+ 'dosage_freq' => trim((string) ($row['dosage_freq'] ?? '')),
+ 'dosage_where' => trim((string) ($row['dosage_where'] ?? '')),
+ 'no_of_days' => trim((string) ($row['no_of_days'] ?? '')),
+ 'qty' => trim((string) ($row['qty'] ?? '')),
+ 'remark' => trim((string) ($row['remark'] ?? '')),
+ 'genericname' => trim((string) ($row['genericname'] ?? '')),
+ 'update_by' => $userLabel,
+ ];
+ if ($this->db->fieldExists('order_id', $legacyDrugTable)) {
+ $insert['order_id'] = 0;
+ }
+
+ $allowed = [];
+ foreach ($insert as $field => $value) {
+ if ($this->db->fieldExists($field, $legacyDrugTable)) {
+ $allowed[$field] = $value;
+ }
+ }
+
+ if (! empty($allowed) && $this->db->table($legacyDrugTable)->insert($allowed)) {
+ $inserted++;
+ }
+ continue;
+ }
+
+ if ($fallbackDrugTable !== null) {
+ $doseText = trim(implode(' ', array_filter([
+ trim((string) ($row['med_type'] ?? '')),
+ trim((string) ($row['dosage'] ?? '')),
+ trim((string) ($row['dosage_when'] ?? '')),
+ trim((string) ($row['dosage_freq'] ?? '')),
+ ], static fn ($v) => trim((string) $v) !== '')));
+ $dayText = trim(implode(' ', array_filter([
+ trim((string) ($row['no_of_days'] ?? '')),
+ trim((string) ($row['qty'] ?? '')) !== '' ? ('Qty:' . trim((string) ($row['qty'] ?? ''))) : '',
+ trim((string) ($row['remark'] ?? '')),
+ ], static fn ($v) => trim((string) $v) !== '')));
+
+ $insert = [
+ 'ipd_id' => $ipdId,
+ 'drug_code' => (int) ($row['med_id'] ?? 0),
+ 'drug_name' => $medName,
+ 'drug_dose' => $doseText,
+ 'drug_day' => $dayText,
+ 'update_by' => $userLabel,
+ ];
+ if ($this->db->fieldExists('order_id', $fallbackDrugTable)) {
+ $insert['order_id'] = 0;
+ }
+
+ if ($this->db->table($fallbackDrugTable)->insert($insert)) {
+ $inserted++;
+ }
+ }
+ }
+
+ $savedAny = $inserted > 0;
+ $notice = $savedAny
+ ? ($inserted . ' medicine(s) added from Rx Group.')
+ : 'No medicines could be added from selected Rx Group.';
+ $noticeType = $savedAny ? 'success' : 'warning';
+ }
+ }
} elseif ($action === 'remove_drug') {
$removeId = (int) ($this->request->getPost('drug_remove_id') ?? 0);
- if ($removeId > 0 && $this->tableHasColumns('ipd_discharge_drug', ['id', 'ipd_id'])) {
- $savedAny = (bool) $this->db->table('ipd_discharge_drug')
- ->where('id', $removeId)
- ->where('ipd_id', $ipdId)
- ->delete();
+ $removeSource = strtolower(trim((string) ($this->request->getPost('drug_remove_source') ?? 'legacy')));
+ if ($removeId > 0) {
+ $deleted = false;
+
+ if ($removeSource === 'legacy') {
+ $legacyDrugTable = $this->findFirstExistingTable(['ipd_discharge_prescrption_prescribed', 'ipd_discharge_prescription_prescribed']);
+ if ($legacyDrugTable !== null && $this->tableHasColumns($legacyDrugTable, ['id', 'ipd_id'])) {
+ $deleted = (bool) $this->db->table($legacyDrugTable)
+ ->where('id', $removeId)
+ ->where('ipd_id', $ipdId)
+ ->delete();
+ }
+ }
+
+ if (! $deleted && $this->tableHasColumns('ipd_discharge_drug', ['id', 'ipd_id'])) {
+ $deleted = (bool) $this->db->table('ipd_discharge_drug')
+ ->where('id', $removeId)
+ ->where('ipd_id', $ipdId)
+ ->delete();
+ }
+
+ $savedAny = $deleted;
$notice = $savedAny ? 'Drug row removed.' : 'Unable to remove drug row.';
$noticeType = $savedAny ? 'success' : 'warning';
} else {
@@ -2782,6 +3790,24 @@ class Ipd_discharge extends BaseController
$courseRemark = trim((string) ($this->request->getPost('course_remark') ?? ''));
$instructionRemark = trim((string) ($this->request->getPost('instruction_remark') ?? ''));
$reviewAfter = trim((string) ($this->request->getPost('review_after') ?? ''));
+ $instructionOther = trim((string) ($this->request->getPost('instruction_other') ?? ''));
+ $instructionFoodIds = $this->readInstructionFoodIdsFromRequest();
+ $instructionMeta = $this->buildInstructionMetaPayload([
+ 'food_ids' => $instructionFoodIds,
+ 'other_text' => $instructionOther,
+ 'nabh' => [
+ 'drug_allergy_status' => trim((string) ($this->request->getPost('drug_allergy_status') ?? '')),
+ 'drug_allergy_details' => trim((string) ($this->request->getPost('drug_allergy_details') ?? '')),
+ 'adr_history' => trim((string) ($this->request->getPost('adr_history') ?? '')),
+ 'current_medications' => trim((string) ($this->request->getPost('current_medications') ?? '')),
+ 'co_morbidities' => trim((string) ($this->request->getPost('co_morbidities') ?? '')),
+ 'hpi_note' => trim((string) ($this->request->getPost('hpi_note') ?? '')),
+ 'women_lmp' => trim((string) ($this->request->getPost('women_lmp') ?? '')),
+ 'women_last_baby' => trim((string) ($this->request->getPost('women_last_baby') ?? '')),
+ 'women_pregnancy_related' => trim((string) ($this->request->getPost('women_pregnancy_related') ?? '')),
+ 'women_related_problems' => trim((string) ($this->request->getPost('women_related_problems') ?? '')),
+ ],
+ ]);
if ($complaintRemark !== '' || $this->tableHasColumns('ipd_discharge_complaint_remark', ['ipd_id'])) {
$savedAny = $this->upsertByIpd('ipd_discharge_complaint_remark', $ipdId, [
@@ -2807,9 +3833,9 @@ class Ipd_discharge extends BaseController
]) || $savedAny;
}
- if ($instructionRemark !== '' || $reviewAfter !== '' || $this->tableHasColumns('ipd_discharge_instructions', ['ipd_id'])) {
+ if ($instructionRemark !== '' || $reviewAfter !== '' || $instructionMeta !== '' || $this->tableHasColumns('ipd_discharge_instructions', ['ipd_id'])) {
$savedAny = $this->upsertByIpd('ipd_discharge_instructions', $ipdId, [
- 'comp_report' => '',
+ 'comp_report' => $instructionMeta,
'comp_remark' => $instructionRemark,
'review_after' => $reviewAfter,
'footer_text' => '',
@@ -2818,6 +3844,25 @@ class Ipd_discharge extends BaseController
]) || $savedAny;
}
+ if ($this->tableHasColumns('ipd_discharge_drug_food_interaction', ['ipd_id', 'food_id_list'])) {
+ $foodIds = [];
+ foreach ($instructionFoodIds as $foodId) {
+ $id = (int) $foodId;
+ if ($id > 0) {
+ $foodIds[$id] = $id;
+ }
+ }
+
+ $legacyData = [
+ 'food_id_list' => implode(',', array_values($foodIds)),
+ ];
+ if ($this->db->fieldExists('food_text', 'ipd_discharge_drug_food_interaction')) {
+ $legacyData['food_text'] = $instructionRemark;
+ }
+
+ $savedAny = $this->upsertByIpd('ipd_discharge_drug_food_interaction', $ipdId, $legacyData) || $savedAny;
+ }
+
// Examination on Admission (General Examination values).
if ($this->tableHasColumns('ipd_discharge_general_exam_col', ['id', 'col_name'])
&& $this->tableHasColumns('ipd_discharge_1_b', ['ipd_d_id', 'col_id', 'short_head', 'rdata'])) {
@@ -3119,6 +4164,17 @@ class Ipd_discharge extends BaseController
$notice = 'Requested action could not be completed. Please verify database table/columns for this section.';
$noticeType = 'warning';
}
+
+ $ajaxMode = strtolower(trim((string) ($this->request->getPost('ajax_mode') ?? '')));
+ if ($ajaxMode === 'json') {
+ return $this->response->setJSON([
+ 'update' => $savedAny ? 1 : 0,
+ 'error_text' => $notice,
+ 'notice_type' => $noticeType,
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
}
if ($reCreate > 0) {
@@ -3141,9 +4197,36 @@ class Ipd_discharge extends BaseController
$diagnosisRows = $this->byIpdRows('ipd_discharge_diagnosis', ['id', 'comp_report', 'comp_remark'], 'id ASC', $ipdId);
$courseRows = $this->byIpdRows('ipd_discharge_course', ['id', 'comp_report', 'comp_remark'], 'id ASC', $ipdId);
$drugRows = $this->byIpdRows('ipd_discharge_drug', ['id', 'drug_name', 'drug_dose', 'drug_day'], 'id ASC', $ipdId);
+ $legacyDrugRows = $this->byIpdRows('ipd_discharge_prescrption_prescribed', ['id', 'med_name', 'med_type', 'dosage', 'dosage_when', 'dosage_freq', 'no_of_days', 'qty', 'remark'], 'id ASC', $ipdId);
+ if (empty($legacyDrugRows)) {
+ $legacyDrugRows = $this->byIpdRows('ipd_discharge_prescription_prescribed', ['id', 'med_name', 'med_type', 'dosage', 'dosage_when', 'dosage_freq', 'no_of_days', 'qty', 'remark'], 'id ASC', $ipdId);
+ }
$diagnosisRemarkRow = $this->firstRowByIpd('ipd_discharge_diagnosis_remark', $ipdId);
$courseRemarkRow = $this->firstRowByIpd('ipd_discharge_course_remark', $ipdId);
$instructionRow = $this->firstRowByIpd('ipd_discharge_instructions', $ipdId);
+ $instructionMeta = $this->parseInstructionMetaPayload((string) ($instructionRow['comp_report'] ?? ''));
+ if (empty($instructionMeta['food_ids'])
+ && $this->tableHasColumns('ipd_discharge_drug_food_interaction', ['ipd_id', 'food_id_list'])) {
+ $legacyFoodRow = $this->firstRowByIpd('ipd_discharge_drug_food_interaction', $ipdId);
+ $instructionMeta['food_ids'] = $this->parseFoodIdCsv((string) ($legacyFoodRow['food_id_list'] ?? ''));
+ if (trim((string) ($instructionMeta['other_text'] ?? '')) === '' && $this->db->fieldExists('food_text', 'ipd_discharge_drug_food_interaction')) {
+ $instructionMeta['other_text'] = trim((string) ($legacyFoodRow['food_text'] ?? ''));
+ }
+ }
+ $instructionNabh = is_array($instructionMeta['nabh'] ?? null) ? ($instructionMeta['nabh'] ?? []) : [];
+
+ foreach (['drug_allergy_status', 'drug_allergy_details', 'adr_history', 'current_medications', 'co_morbidities', 'hpi_note', 'women_lmp', 'women_last_baby', 'women_pregnancy_related', 'women_related_problems'] as $field) {
+ if (trim((string) ($opdHistorySnapshot[$field] ?? '')) === '' && trim((string) ($instructionNabh[$field] ?? '')) !== '') {
+ $opdHistorySnapshot[$field] = trim((string) ($instructionNabh[$field] ?? ''));
+ }
+ }
+ $instructionFoodRows = [];
+ if ($this->tableHasColumns('ipd_discharge_master_food', ['id', 'food_short', 'food_desc'])) {
+ $builder = $this->db->table('ipd_discharge_master_food')
+ ->select('id,food_short,food_desc,food_desc_lang')
+ ->orderBy('id', 'ASC');
+ $instructionFoodRows = $builder->get()->getResultArray();
+ }
$inhosRow = $this->firstRowByIpd('ipd_discharge_investigtions_inhos', $ipdId);
$otherExamRow = [];
if ($this->tableHasColumns('ipd_discharge_2', ['ipd_d_id'])) {
@@ -3213,6 +4296,7 @@ class Ipd_discharge extends BaseController
'diagnosis_rows' => $diagnosisRows,
'course_rows' => $courseRows,
'drug_rows' => $drugRows,
+ 'legacy_drug_rows' => $legacyDrugRows,
'patient_history_row' => $patientHistoryRow,
'physical_exam_rows' => $physicalExamRows,
'manual_invest_rows' => $manualInvestRows,
@@ -3230,6 +4314,9 @@ class Ipd_discharge extends BaseController
'course_remark' => (string) ($courseRemarkRow['comp_remark'] ?? ''),
'instruction_remark' => (string) ($instructionRow['comp_remark'] ?? ''),
'review_after' => (string) ($instructionRow['review_after'] ?? ''),
+ 'instruction_food_rows' => $instructionFoodRows,
+ 'instruction_food_ids' => $instructionMeta['food_ids'] ?? [],
+ 'instruction_other' => (string) ($instructionMeta['other_text'] ?? ''),
'inhos_remark' => (string) ($inhosRow['comp_remark'] ?? ''),
'other_exam_text' => (string) ($otherExamParsed['text'] ?? ''),
]);
@@ -3324,11 +4411,66 @@ class Ipd_discharge extends BaseController
$requestedTemplateId = (int) ($this->request->getGet('tpl') ?? 0);
$templatePack = $this->applyDischargeTemplate($content, $panelData, $requestedTemplateId > 0 ? $requestedTemplateId : null);
+ $withHeader = $printType !== 0;
+ $renderedHtml = (string) ($templatePack['rendered_html'] ?? $content);
+ $templateName = (string) ($templatePack['selected_template_name'] ?? 'Discharge Template');
+
+ try {
+ $patient = $panelData['person_info'] ?? null;
+ $ipd = $panelData['ipd_info'] ?? null;
+
+ $patientName = trim((string) ($patient->p_fname ?? 'Patient'));
+ $ipdCode = trim((string) ($ipd->ipd_code ?? $ipdId));
+ $fileName = 'discharge_' . preg_replace('/[^A-Za-z0-9_-]+/', '_', $ipdCode !== '' ? $ipdCode : (string) $ipdId) . '.pdf';
+
+ $pdfHtml = $this->buildDischargePdfHtml($panelData, $renderedHtml, $withHeader, $templateName);
+
+ $mpdf = new Mpdf([
+ 'mode' => 'utf-8',
+ 'format' => 'A4',
+ 'margin_left' => 10,
+ 'margin_right' => 10,
+ 'margin_top' => $withHeader ? 28 : 12,
+ 'margin_bottom' => 12,
+ 'margin_header' => 8,
+ 'margin_footer' => 8,
+ 'default_font' => 'freeserif',
+ 'tempDir' => WRITEPATH . 'cache',
+ ]);
+
+ $mpdf->autoScriptToLang = true;
+ $mpdf->autoLangToFont = true;
+ $mpdf->SetTitle('Discharge Summary - ' . ($patientName !== '' ? $patientName : ('IPD ' . $ipdId)));
+ $mpdf->SetAuthor('Atria HMS');
+
+ if ($withHeader) {
+ $headerHtml = '
'
+ . '
Discharge Summary
'
+ . '
IPD: ' . esc($ipdCode) . ' | Template: ' . esc($templateName) . '
'
+ . '
';
+ $mpdf->SetHTMLHeader($headerHtml);
+ }
+
+ $mpdf->SetHTMLFooter('
Page {PAGENO}/{nbpg}
');
+ $mpdf->WriteHTML($pdfHtml);
+
+ $pdfBinary = $mpdf->Output($fileName, Destination::STRING_RETURN);
+ return $this->response
+ ->setHeader('Content-Type', 'application/pdf')
+ ->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
+ ->setBody($pdfBinary);
+ } catch (\Throwable $e) {
+ log_message('error', 'PDF generation failed for IPD {ipd}: {msg}', [
+ 'ipd' => $ipdId,
+ 'msg' => $e->getMessage(),
+ ]);
+ }
+
return view('billing/ipd/discharge_print', [
'ipd_id' => $ipdId,
'ipd_info' => $panelData['ipd_info'] ?? null,
'person_info' => $panelData['person_info'] ?? null,
- 'content' => (string) ($templatePack['rendered_html'] ?? $content),
+ 'content' => $renderedHtml,
'selected_template_name' => (string) ($templatePack['selected_template_name'] ?? ''),
'selected_template_id' => (int) ($templatePack['selected_template_id'] ?? 0),
'print_type' => $printType,
@@ -3336,6 +4478,65 @@ class Ipd_discharge extends BaseController
]);
}
+ private function buildDischargePdfHtml(array $panelData, string $renderedContent, bool $withHeader, string $templateName): string
+ {
+ $ipd = $panelData['ipd_info'] ?? null;
+ $person = $panelData['person_info'] ?? null;
+
+ $patientName = trim((string) ($person->p_fname ?? ''));
+ $patientCode = trim((string) (
+ $person->uhid
+ ?? $person->UHID
+ ?? $person->patient_code
+ ?? $person->p_code
+ ?? $person->reg_no
+ ?? ''
+ ));
+ $ipdCode = trim((string) ($ipd->ipd_code ?? ''));
+ $gender = trim((string) ($person->xgender ?? ''));
+ $age = get_age_1($person->dob ?? null, $person->age ?? '', $person->age_in_month ?? '', $person->estimate_dob ?? '');
+ $admitDate = trim((string) ($ipd->str_register_date ?? ''));
+ $dischargeDate = trim((string) ($ipd->str_discharge_date ?? ''));
+
+ $headerBlock = '';
+ if ($withHeader) {
+ $headerBlock = '';
+ }
+
+ return ''
+ . '
'
+ . ''
+ . $headerBlock
+ . '
'
+ . '
' . $renderedContent . '
'
+ . '';
+ }
+
public function show_file3(int $ipdId)
{
return $this->show_discharge($ipdId, 3);
diff --git a/app/Controllers/ItemIpd.php b/app/Controllers/ItemIpd.php
index 648f13d..b37c237 100644
--- a/app/Controllers/ItemIpd.php
+++ b/app/Controllers/ItemIpd.php
@@ -266,4 +266,5 @@ class ItemIpd extends BaseController
'csrfHash' => csrf_hash(),
]);
}
+
}
diff --git a/app/Controllers/Patient.php b/app/Controllers/Patient.php
index e7c56bb..a40ff50 100644
--- a/app/Controllers/Patient.php
+++ b/app/Controllers/Patient.php
@@ -37,11 +37,7 @@ class Patient extends BaseController
public function create()
{
- if (!$this->request->isAJAX()) {
- return $this->response
- ->setStatusCode(400)
- ->setJSON(['insertid' => 0, 'error_text' => 'Invalid request']);
- }
+ $isAjax = $this->request->isAJAX();
$chk_age = $this->request->getPost('chk_age');
$age_month = (string) $this->request->getPost('input_age_month');
@@ -67,10 +63,16 @@ class Patient extends BaseController
$validation->setRules($rules);
if (!$validation->withRequest($this->request)->run()) {
$errorText = implode("\n", $validation->getErrors());
- return $this->response->setJSON([
- 'insertid' => 0,
- 'error_text' => $errorText,
- ]);
+ if ($isAjax) {
+ return $this->response->setJSON([
+ 'insertid' => 0,
+ 'error_text' => $errorText,
+ ]);
+ }
+
+ return redirect()->to(base_url('billing/patient'))
+ ->withInput()
+ ->with('error', $errorText);
}
$data = [
@@ -100,12 +102,23 @@ class Patient extends BaseController
$patientModel = new PatientModel();
$insertId = $patientModel->insertPatient($data);
if ($insertId <= 0) {
- return $this->response->setJSON([
- 'insertid' => 0,
- 'error_text' => 'Unable to save patient.',
- ]);
+ if ($isAjax) {
+ return $this->response->setJSON([
+ 'insertid' => 0,
+ 'error_text' => 'Unable to save patient.',
+ ]);
+ }
+
+ return redirect()->to(base_url('billing/patient'))
+ ->withInput()
+ ->with('error', 'Unable to save patient.');
}
+ $this->saveNamesToNameList([
+ (string) $this->request->getPost('input_name'),
+ (string) $this->request->getPost('input_relative_name'),
+ ]);
+
// Check Multiple UHID
$relativeName = trim(strtoupper((string) $this->request->getPost('input_relative_name')));
$patientName = trim(strtoupper((string) $this->request->getPost('input_name')));
@@ -143,7 +156,11 @@ class Patient extends BaseController
$patientModel->insertDuplicateLog($dupData);
}
- return $this->response->setJSON(['insertid' => $insertId]);
+ if ($isAjax) {
+ return $this->response->setJSON(['insertid' => $insertId]);
+ }
+
+ return redirect()->to(base_url('billing/patient/person_record/' . $insertId));
}
public function search_adv()
@@ -416,6 +433,10 @@ class Patient extends BaseController
$pid = (int) $this->request->getPost('p_id');
$patientModel = new PatientModel();
$patientModel->updatePatient($data, $pid);
+ $this->saveNamesToNameList([
+ (string) $this->request->getPost('input_name'),
+ (string) $this->request->getPost('input_relative_name'),
+ ]);
return $this->response->setJSON([
'update' => 1,
@@ -778,19 +799,8 @@ class Patient extends BaseController
return $this->response->setJSON([]);
}
- $sql = "select * from name_list where name like '" . $this->db->escapeLikeString($q) . "%' order by name limit 20";
- $query = $this->db->query($sql);
- $rows = $query->getResultArray();
-
- $rowSet = [];
- foreach ($rows as $row) {
- $rowSet[] = [
- 'label' => trim($row['name']),
- 'value' => trim($row['name']),
- ];
- }
-
- return $this->response->setJSON($rowSet);
+ $patientModel = new PatientModel();
+ return $this->response->setJSON($patientModel->getNameSuggestions($q));
}
private function getProfileFilePath(int $fileId): string
@@ -922,10 +932,38 @@ class Patient extends BaseController
return null;
}
- $dt = \DateTime::createFromFormat('d/m/Y', $date);
+ $date = trim($date);
+
+ $dt = \DateTime::createFromFormat('Y-m-d', $date);
+ if (!$dt) {
+ $dt = \DateTime::createFromFormat('d/m/Y', $date);
+ }
return $dt ? $dt->format('Y-m-d') : null;
}
+ private function saveNamesToNameList(array $names): void
+ {
+ if (! $this->db->tableExists('name_list')) {
+ return;
+ }
+
+ $builder = $this->db->table('name_list');
+ foreach ($names as $rawName) {
+ $name = preg_replace('/\s+/', ' ', trim((string) $rawName));
+ if ($name === '' || strlen($name) < 2) {
+ continue;
+ }
+
+ $name = ucwords(strtolower($name));
+ $exists = $builder->select('id')->where('name', $name)->limit(1)->get()->getRowArray();
+ if ($exists) {
+ continue;
+ }
+
+ $builder->insert(['name' => $name]);
+ }
+ }
+
}
diff --git a/app/Controllers/Report.php b/app/Controllers/Report.php
index fb52af2..4caebc2 100644
--- a/app/Controllers/Report.php
+++ b/app/Controllers/Report.php
@@ -4,6 +4,11 @@ namespace App\Controllers;
class Report extends BaseController
{
+ public function index()
+ {
+ return view('report/index');
+ }
+
public function collection_report()
{
$employees = $this->db->table('users')
@@ -142,6 +147,189 @@ class Report extends BaseController
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('');
+ }
+
+ return $this->response->setBody($content);
+ }
+
public function diagnosis_report()
{
$itemTypes = $this->db->table('hc_item_type')
@@ -228,6 +416,48 @@ class Report extends BaseController
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')
@@ -494,4 +724,668 @@ class Report extends BaseController
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'], 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);
+ }
+
+ $rows = array_merge($ipdRows, $opdRows);
+
+ 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],
+ ];
+
+ $completionAccumulator = [
+ 'all' => 0.0,
+ 'ipd' => 0.0,
+ 'opd' => 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 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 : [];
+ }
}
diff --git a/app/Controllers/Setting/Template.php b/app/Controllers/Setting/Template.php
index 14aa756..e6ca6a0 100644
--- a/app/Controllers/Setting/Template.php
+++ b/app/Controllers/Setting/Template.php
@@ -15,6 +15,7 @@ class Template extends BaseController
'template.ct',
'template.mri',
'template.echo',
+ 'template.discharge',
];
public function __construct()
@@ -649,4 +650,197 @@ class Template extends BaseController
'mstTestKey' => $mstTestKey,
]);
}
+
+ private function ensureDischargeTemplateTable(): void
+ {
+ if ($this->db->tableExists('ipd_discharge_templates')) {
+ return;
+ }
+
+ $sql = "CREATE TABLE IF NOT EXISTS ipd_discharge_templates (
+ id INT NOT NULL AUTO_INCREMENT,
+ template_name VARCHAR(120) NOT NULL,
+ template_html LONGTEXT NOT NULL,
+ is_default TINYINT(1) NOT NULL DEFAULT 0,
+ status TINYINT(1) NOT NULL DEFAULT 1,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
+
+ $this->db->query($sql);
+ }
+
+ private function defaultDischargeTemplateHtml(): string
+ {
+ return '
Discharge Summary '
+ . '
'
+ . ''
+ . 'Patient : {{PATIENT_NAME}} '
+ . 'UHID : {{UHID}} '
+ . 'IPD : {{IPD_CODE}} '
+ . ' '
+ . ''
+ . 'Age/Gender : {{AGE_GENDER}} '
+ . 'Admit Date : {{ADMIT_DATE}} '
+ . 'Discharge Date : {{DISCHARGE_DATE}} '
+ . ' '
+ . '
'
+ . '
{{CONTENT}}
';
+ }
+
+ private function nabhDischargeTemplateHtml(): string
+ {
+ return '
DISCHARGE SUMMARY '
+ . '
'
+ . ''
+ . 'Patient Name : {{PATIENT_NAME}} '
+ . 'UHID : {{UHID}} '
+ . 'IPD No. : {{IPD_CODE}} '
+ . ' '
+ . ''
+ . 'Age/Gender : {{AGE_GENDER}} '
+ . 'Date of Admission : {{ADMIT_DATE}} '
+ . 'Date of Discharge : {{DISCHARGE_DATE}} '
+ . ' '
+ . ''
+ . 'Prepared On : {{CURRENT_DATE}} '
+ . ' '
+ . '
'
+ . '
'
+ . 'NABH guidance note: Ensure diagnosis, procedures, clinical course, condition at discharge, medication with dose/duration, follow-up advice, red-flag signs, and emergency contact are documented.'
+ . '
'
+ . '
{{CONTENT}}
'
+ . '
Counselling & Handover Confirmation '
+ . '
'
+ . 'Medication explained to patient/attendant Remarks: '
+ . 'Follow-up date and department explained Next Visit: __________________ '
+ . 'Red-flag symptoms explained Emergency Contact: __________________ '
+ . 'Diet and activity instructions explained '
+ . '
'
+ . '
'
+ . ''
+ . '____________________ Consultant Name/Signature '
+ . '____________________ Medical Officer Signature '
+ . '____________________ Patient/Attendant Signature & Date '
+ . ' '
+ . '
';
+ }
+
+ private function ensureDefaultDischargeTemplateSeeded(): void
+ {
+ $this->ensureDischargeTemplateTable();
+ if (! $this->db->tableExists('ipd_discharge_templates')) {
+ return;
+ }
+
+ $table = $this->db->table('ipd_discharge_templates');
+ $count = (int) ($table->countAllResults() ?? 0);
+ if ($count === 0) {
+ $table->insert([
+ 'template_name' => 'Default Discharge Template',
+ 'template_html' => $this->defaultDischargeTemplateHtml(),
+ 'is_default' => 1,
+ 'status' => 1,
+ ]);
+ }
+
+ $nabhExists = $this->db->table('ipd_discharge_templates')
+ ->where('template_name', 'NABH Compliant Discharge Summary')
+ ->get(1)
+ ->getRowArray();
+
+ if (empty($nabhExists)) {
+ $table->insert([
+ 'template_name' => 'NABH Compliant Discharge Summary',
+ 'template_html' => $this->nabhDischargeTemplateHtml(),
+ 'is_default' => 0,
+ 'status' => 1,
+ ]);
+ }
+ }
+
+ public function discharge_templates()
+ {
+ if ($resp = $this->requireAnyPermission(['template.discharge'])) {
+ return $resp;
+ }
+
+ $this->ensureDefaultDischargeTemplateSeeded();
+
+ $notice = '';
+ $noticeType = 'success';
+
+ if (strtolower($this->request->getMethod()) === 'post') {
+ $id = (int) ($this->request->getPost('id') ?? 0);
+ $templateName = trim((string) ($this->request->getPost('template_name') ?? ''));
+ $templateHtml = (string) ($this->request->getPost('template_html') ?? '');
+ $isDefault = (int) ($this->request->getPost('is_default') ?? 0) === 1 ? 1 : 0;
+ $status = (int) ($this->request->getPost('status') ?? 1) === 1 ? 1 : 0;
+
+ if ($templateName === '' || trim($templateHtml) === '') {
+ $notice = 'Template name and template HTML are required.';
+ $noticeType = 'danger';
+ } else {
+ $table = $this->db->table('ipd_discharge_templates');
+ $data = [
+ 'template_name' => $templateName,
+ 'template_html' => $templateHtml,
+ 'is_default' => $isDefault,
+ 'status' => $status,
+ ];
+
+ if ($id > 0) {
+ $table->where('id', $id)->update($data);
+ $notice = 'Template updated.';
+ } else {
+ $table->insert($data);
+ $id = (int) $this->db->insertID();
+ $notice = 'Template created.';
+ }
+
+ if ($isDefault === 1 && $id > 0) {
+ $this->db->table('ipd_discharge_templates')
+ ->where('id <>', $id)
+ ->update(['is_default' => 0]);
+ }
+ }
+ }
+
+ $editId = (int) ($this->request->getGet('edit') ?? 0);
+ $editRow = [];
+ if ($editId > 0) {
+ $editRow = $this->db->table('ipd_discharge_templates')
+ ->where('id', $editId)
+ ->get(1)
+ ->getRowArray() ?? [];
+ }
+
+ $rows = $this->db->table('ipd_discharge_templates')
+ ->orderBy('is_default', 'DESC')
+ ->orderBy('id', 'ASC')
+ ->get()
+ ->getResultArray();
+
+ return view('Setting/Template/discharge_templates', [
+ 'rows' => $rows,
+ 'edit_row' => $editRow,
+ 'notice' => $notice,
+ 'notice_type' => $noticeType,
+ ]);
+ }
+
+ public function discharge_template_delete(int $id)
+ {
+ if ($resp = $this->requireAnyPermission(['template.discharge'])) {
+ return $resp;
+ }
+
+ $id = (int) $id;
+ if ($id > 0 && $this->db->tableExists('ipd_discharge_templates')) {
+ $this->db->table('ipd_discharge_templates')->where('id', $id)->delete();
+ }
+
+ return redirect()->to(base_url('setting/template/discharge_templates'));
+ }
}
diff --git a/app/Database/Migrations/2026-03-07-000010_CreateIpdDischargeTemplatesTable.php b/app/Database/Migrations/2026-03-07-000010_CreateIpdDischargeTemplatesTable.php
new file mode 100644
index 0000000..ebaedbf
--- /dev/null
+++ b/app/Database/Migrations/2026-03-07-000010_CreateIpdDischargeTemplatesTable.php
@@ -0,0 +1,68 @@
+db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getRowArray();
+
+ return ! empty($row);
+ }
+
+ public function up()
+ {
+ if ($this->hasTable('ipd_discharge_templates')) {
+ return;
+ }
+
+ $this->forge->addField([
+ 'id' => [
+ 'type' => 'INT',
+ 'constraint' => 11,
+ 'unsigned' => true,
+ 'auto_increment' => true,
+ ],
+ 'template_name' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 120,
+ 'null' => false,
+ ],
+ 'template_html' => [
+ 'type' => 'LONGTEXT',
+ 'null' => false,
+ ],
+ 'is_default' => [
+ 'type' => 'TINYINT',
+ 'constraint' => 1,
+ 'default' => 0,
+ ],
+ 'status' => [
+ 'type' => 'TINYINT',
+ 'constraint' => 1,
+ 'default' => 1,
+ ],
+ 'created_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ 'updated_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ ]);
+
+ $this->forge->addKey('id', true);
+ $this->forge->addKey('is_default');
+ $this->forge->addKey('status');
+ $this->forge->createTable('ipd_discharge_templates', true);
+ }
+
+ public function down()
+ {
+ $this->forge->dropTable('ipd_discharge_templates', true);
+ }
+}
diff --git a/app/Database/Seeds/IndianNameListSeeder.php b/app/Database/Seeds/IndianNameListSeeder.php
new file mode 100644
index 0000000..7082770
--- /dev/null
+++ b/app/Database/Seeds/IndianNameListSeeder.php
@@ -0,0 +1,74 @@
+db->tableExists('name_list')) {
+ $forge = \Config\Database::forge();
+ $forge->addField([
+ 'id' => [
+ 'type' => 'INT',
+ 'constraint' => 11,
+ 'unsigned' => true,
+ 'auto_increment' => true,
+ ],
+ 'name' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 100,
+ 'null' => false,
+ ],
+ ]);
+ $forge->addKey('id', true);
+ $forge->addKey('name');
+ $forge->createTable('name_list', true);
+ }
+
+ $names = [
+ 'Aarav', 'Aarush', 'Aditi', 'Akanksha', 'Akash', 'Akhil', 'Alok', 'Aman', 'Amar', 'Amit',
+ 'Amrita', 'Anand', 'Ananya', 'Anil', 'Anjali', 'Ankit', 'Ankur', 'Ansh', 'Anshika', 'Anuj',
+ 'Arjun', 'Arnav', 'Arun', 'Aryan', 'Asha', 'Ashish', 'Ayush', 'Bhavna', 'Bhavesh', 'Bhavya',
+ 'Chandni', 'Charu', 'Chetan', 'Darpan', 'Deepa', 'Deepak', 'Dev', 'Devansh', 'Devesh', 'Dhruv',
+ 'Diksha', 'Divya', 'Gaurav', 'Geeta', 'Gopal', 'Govind', 'Harish', 'Harsh', 'Harshita', 'Himanshu',
+ 'Isha', 'Ishaan', 'Jatin', 'Jaya', 'Jyoti', 'Kabir', 'Kajal', 'Karan', 'Kartik', 'Kavita',
+ 'Khushi', 'Kiran', 'Komal', 'Krishna', 'Kunal', 'Kusum', 'Lakshmi', 'Lokesh', 'Madhav', 'Madhuri',
+ 'Mahesh', 'Manav', 'Manish', 'Meera', 'Megha', 'Mohan', 'Mohit', 'Mukesh', 'Nandini', 'Naveen',
+ 'Neeraj', 'Neha', 'Nikhil', 'Nilesh', 'Niraj', 'Nisha', 'Nitin', 'Pankaj', 'Pooja', 'Pradeep',
+ 'Prakash', 'Pranav', 'Prashant', 'Pratik', 'Preeti', 'Priya', 'Rahul', 'Raj', 'Rajat', 'Rajesh',
+ 'Rakesh', 'Rani', 'Ravi', 'Rekha', 'Ritika', 'Rohan', 'Rohit', 'Sahil', 'Sakshi', 'Sameer',
+ 'Sandeep', 'Sanjay', 'Sanjana', 'Sarthak', 'Satish', 'Seema', 'Shalini', 'Shankar', 'Shivam', 'Shreya',
+ 'Shruti', 'Simran', 'Sonali', 'Sourabh', 'Suhani', 'Suman', 'Sunil', 'Suraj', 'Suresh', 'Swati',
+ 'Tanvi', 'Tarun', 'Uday', 'Vaibhav', 'Varun', 'Vikas', 'Vinay', 'Vineet', 'Vishal', 'Vivek',
+ 'Yash', 'Yashika', 'Yogesh', 'Zoya',
+ 'Aggarwal', 'Agarwal', 'Arora', 'Bansal', 'Bhat', 'Bhatt', 'Bhardwaj', 'Chauhan', 'Chawla', 'Das',
+ 'Dubey', 'Dwivedi', 'Garg', 'Goswami', 'Gupta', 'Jain', 'Joshi', 'Kapoor', 'Khanna', 'Khan',
+ 'Kumar', 'Mishra', 'Nair', 'Pandey', 'Patel', 'Rao', 'Rawat', 'Saha', 'Saxena', 'Shah',
+ 'Sharma', 'Shukla', 'Singh', 'Srivastava', 'Thakur', 'Tripathi', 'Tyagi', 'Taygi', 'Varma', 'Verma',
+ 'Yadav'
+ ];
+
+ $existingRows = $this->db->table('name_list')->select('name')->get()->getResultArray();
+ $existing = [];
+ foreach ($existingRows as $row) {
+ $existing[strtolower(trim((string) ($row['name'] ?? '')))] = true;
+ }
+
+ $batch = [];
+ foreach ($names as $name) {
+ $key = strtolower($name);
+ if (isset($existing[$key])) {
+ continue;
+ }
+
+ $batch[] = ['name' => $name];
+ }
+
+ if (! empty($batch)) {
+ $this->db->table('name_list')->insertBatch($batch);
+ }
+ }
+}
diff --git a/app/Models/IpdBillingModel.php b/app/Models/IpdBillingModel.php
index 73c4195..c272b89 100644
--- a/app/Models/IpdBillingModel.php
+++ b/app/Models/IpdBillingModel.php
@@ -215,6 +215,101 @@ class IpdBillingModel extends Model
->getResult();
}
+ public function getDoctorVisitFeeTypes(): array
+ {
+ if (! $this->db->tableExists('doc_ipd_fee_type')) {
+ return [];
+ }
+
+ return $this->db->table('doc_ipd_fee_type')
+ ->select('id, fee_type')
+ ->orderBy('fee_type', 'ASC')
+ ->get()
+ ->getResultArray();
+ }
+
+ public function getDoctorVisitFeeMap(array $doctorIds): array
+ {
+ if (empty($doctorIds) || ! $this->db->tableExists('doc_ipd_fee') || ! $this->db->tableExists('doc_ipd_fee_type')) {
+ return [];
+ }
+
+ $builder = $this->db->table('doc_ipd_fee d')
+ ->select('d.doc_id, d.doc_fee_type as fee_type_id, d.doc_fee_desc, d.amount, t.fee_type')
+ ->join('doc_ipd_fee_type t', 't.id = d.doc_fee_type', 'left')
+ ->whereIn('d.doc_id', array_map('intval', $doctorIds));
+
+ if ($this->db->fieldExists('isdelete', 'doc_ipd_fee')) {
+ $builder->where('d.isdelete', 0);
+ }
+
+ $rows = $builder->orderBy('d.id', 'ASC')->get()->getResultArray();
+
+ $map = [];
+ foreach ($rows as $row) {
+ $docId = (int) ($row['doc_id'] ?? 0);
+ $feeTypeId = (int) ($row['fee_type_id'] ?? 0);
+ if ($docId <= 0 || $feeTypeId <= 0) {
+ continue;
+ }
+
+ $feeType = (string) ($row['fee_type'] ?? '');
+ $feeDesc = trim((string) ($row['doc_fee_desc'] ?? ''));
+ if ($feeDesc === '') {
+ $feeDesc = $feeType;
+ }
+
+ $map[$docId][$feeTypeId] = [
+ 'fee_type_id' => $feeTypeId,
+ 'fee_type' => $feeType,
+ 'fee_desc' => $feeDesc,
+ 'amount' => (float) ($row['amount'] ?? 0),
+ ];
+ }
+
+ return $map;
+ }
+
+ public function getDoctorVisitFeeDetail(int $docId, int $feeTypeId = 0): ?array
+ {
+ if ($docId <= 0 || ! $this->db->tableExists('doc_ipd_fee') || ! $this->db->tableExists('doc_ipd_fee_type')) {
+ return null;
+ }
+
+ $builder = $this->db->table('doc_ipd_fee d')
+ ->select('d.id, d.doc_id, d.doc_fee_type as fee_type_id, d.doc_fee_desc, d.amount, t.fee_type')
+ ->join('doc_ipd_fee_type t', 't.id = d.doc_fee_type', 'left')
+ ->where('d.doc_id', $docId);
+
+ if ($this->db->fieldExists('isdelete', 'doc_ipd_fee')) {
+ $builder->where('d.isdelete', 0);
+ }
+
+ if ($feeTypeId > 0) {
+ $builder->where('d.doc_fee_type', $feeTypeId);
+ }
+
+ $row = $builder->orderBy('d.id', 'ASC')->get()->getRowArray();
+ if (! is_array($row) || empty($row)) {
+ return null;
+ }
+
+ $feeType = (string) ($row['fee_type'] ?? '');
+ $feeDesc = trim((string) ($row['doc_fee_desc'] ?? ''));
+ if ($feeDesc === '') {
+ $feeDesc = $feeType;
+ }
+
+ return [
+ 'id' => (int) ($row['id'] ?? 0),
+ 'doc_id' => (int) ($row['doc_id'] ?? 0),
+ 'fee_type_id' => (int) ($row['fee_type_id'] ?? 0),
+ 'fee_type' => $feeType,
+ 'fee_desc' => $feeDesc,
+ 'amount' => (float) ($row['amount'] ?? 0),
+ ];
+ }
+
public function getBankPaymentSources(): array
{
return $this->db->table('hospital_bank m')
@@ -387,6 +482,26 @@ class IpdBillingModel extends Model
->getResult();
}
+ public function getNursingChargeHistory(int $ipdId): array
+ {
+ if (! $this->db->tableExists('nursing_ipd_charges')) {
+ return [];
+ }
+
+ $builder = $this->db->table('nursing_ipd_charges')
+ ->select('charge_id, item_name, item_type, doctor_name, visit_date, visit_time, qty, rate, amount, remarks, include_in_bill')
+ ->where('ipd_id', $ipdId);
+
+ if ($this->db->fieldExists('include_in_bill', 'nursing_ipd_charges')) {
+ $builder->where('include_in_bill', 1);
+ }
+
+ return $builder
+ ->orderBy('charge_id', 'DESC')
+ ->get()
+ ->getResultArray();
+ }
+
private function baseIpdListQuery()
{
$builder = $this->db->table('ipd_master i');
diff --git a/app/Models/IpdModel.php b/app/Models/IpdModel.php
index bedf0af..fb70753 100644
--- a/app/Models/IpdModel.php
+++ b/app/Models/IpdModel.php
@@ -296,11 +296,35 @@ class IpdModel extends Model
return 0;
}
+ $nursingChargeId = $this->extractNursingChargeIdFromComment((string) ($row['comment'] ?? ''));
+ if ($nursingChargeId > 0 && $this->db->tableExists('nursing_ipd_charges')) {
+ $nursingFields = $this->db->getFieldNames('nursing_ipd_charges');
+ $nursingUpdate = [];
+ if (in_array('include_in_bill', $nursingFields, true)) {
+ $nursingUpdate['include_in_bill'] = 0;
+ }
+ if (in_array('updated_by', $nursingFields, true)) {
+ $user = $this->getUserIdentity();
+ $nursingUpdate['updated_by'] = $user['name'] . ' [' . $user['id'] . '] D:' . date('Y-m-d H:i:s');
+ }
+ if (! empty($nursingUpdate)) {
+ $this->db->table('nursing_ipd_charges')
+ ->where('charge_id', $nursingChargeId)
+ ->update($nursingUpdate);
+ }
+ }
+
$user = $this->getUserIdentity();
$row['update_by_id'] = $user['id'];
$row['update_by'] = $user['name'];
$row['update_action'] = '1';
+ // Keep only columns that exist in the audit table to avoid schema drift failures.
+ $auditTableFields = $this->db->getFieldNames('ipd_invoice_item_update');
+ if (! empty($auditTableFields)) {
+ $row = array_intersect_key($row, array_flip($auditTableFields));
+ }
+
$inserted = $this->db->table('ipd_invoice_item_update')->insert($row);
if (! $inserted) {
return 0;
@@ -310,6 +334,10 @@ class IpdModel extends Model
->where('id', $id)
->delete();
+ if ($this->db->affectedRows() < 1) {
+ return 0;
+ }
+
return 1;
}
@@ -409,6 +437,19 @@ class IpdModel extends Model
];
}
+ private function extractNursingChargeIdFromComment(string $comment): int
+ {
+ if ($comment === '') {
+ return 0;
+ }
+
+ if (preg_match('/\[NURSING_CHARGE_ID:(\d+)\]/', $comment, $matches) !== 1) {
+ return 0;
+ }
+
+ return (int) ($matches[1] ?? 0);
+ }
+
private function getUserSignature(): string
{
$user = $this->getUserIdentity();
diff --git a/app/Models/OrganizationCaseModel.php b/app/Models/OrganizationCaseModel.php
index 38337e9..464190e 100644
--- a/app/Models/OrganizationCaseModel.php
+++ b/app/Models/OrganizationCaseModel.php
@@ -33,4 +33,54 @@ class OrganizationCaseModel extends Model
->where('id', $id)
->update($data);
}
+
+ public function getInvoiceRowsAndTotal(int $caseId): array
+ {
+ $sql1 = "select c.id AS id,c.case_id_code AS case_id_code,c.p_id AS p_id,
+ o.apointment_date AS s_Date,'OPD' AS Charge_type,0 AS Charge_type_id,
+ o.opd_id AS item_id,0 AS master_item_id,o.apointment_date AS Adate,
+ o.opd_fee_amount AS item_rate,1 AS item_qty,
+ date_format(o.apointment_date,'%d-%m-%Y') AS str_date,
+ concat('OPD Charge: Dr. ',o.doc_name) AS Description,
+ o.opd_code AS Code,o.opd_fee_amount AS Amount,'1' AS orgcode
+ from opd_master o
+ join organization_case_master c on o.insurance_case_id = c.id
+ where o.opd_status in (1,2) and c.id=" . $caseId .
+ " order by Adate";
+ $showinvoice1 = $this->db->query($sql1)->getResult();
+
+ $sql2 = "select c.id AS id,c.case_id_code AS case_id_code,c.p_id AS p_id,
+ i.inv_date AS s_Date,l.group_desc AS Charge_type,l.itype_id AS Charge_type_id,
+ t.id AS item_id,t.item_id AS master_item_id,i.inv_date AS Adate,
+ t.item_rate AS item_rate,t.item_qty AS item_qty,
+ date_format(i.inv_date,'%d-%m-%Y') AS str_date,
+ concat(t.item_name) AS Description,i.invoice_code AS Code,
+ t.item_amount AS Amount,t.org_code AS orgcode
+ from invoice_master i
+ join organization_case_master c on i.insurance_case_id = c.id
+ join invoice_item t on t.inv_master_id = i.id
+ join hc_item_type l on t.item_type = l.itype_id
+ left join hc_items_insurance it on t.item_id = it.hc_items_id and i.insurance_id = it.hc_insurance_id
+ where i.ipd_include = 1 and i.invoice_status = 1 and c.id=" . $caseId .
+ " order by Adate";
+ $showinvoice2 = $this->db->query($sql2)->getResult();
+
+ $rows = array_merge($showinvoice1, $showinvoice2);
+ usort($rows, static function ($a, $b): int {
+ $left = strtotime((string) ($a->s_Date ?? $a->Adate ?? ''));
+ $right = strtotime((string) ($b->s_Date ?? $b->Adate ?? ''));
+
+ return $left <=> $right;
+ });
+
+ $total = 0.0;
+ foreach ($rows as $row) {
+ $total += (float) ($row->Amount ?? 0);
+ }
+
+ return [
+ 'rows' => $rows,
+ 'total' => $total,
+ ];
+ }
}
diff --git a/app/Models/PatientModel.php b/app/Models/PatientModel.php
index 5b5304f..7e51a48 100644
--- a/app/Models/PatientModel.php
+++ b/app/Models/PatientModel.php
@@ -130,6 +130,87 @@ class PatientModel extends Model
return $rowSet;
}
+ public function getNameSuggestions(string $q): array
+ {
+ $term = strtolower(trim($q));
+ if ($term === '') {
+ return [];
+ }
+
+ $rows = [];
+ if ($this->db->tableExists('name_list')) {
+ try {
+ $rows = $this->db->table('name_list')
+ ->select('name')
+ ->like('name', $term, 'after')
+ ->orderBy('name', 'ASC')
+ ->limit(20)
+ ->get()
+ ->getResultArray();
+ } catch (\Throwable $e) {
+ $rows = [];
+ }
+ }
+
+ $rowSet = [];
+ foreach ($rows as $row) {
+ $name = trim((string) ($row['name'] ?? ''));
+ if ($name === '') {
+ continue;
+ }
+
+ $rowSet[] = [
+ 'label' => $name,
+ 'value' => $name,
+ ];
+ }
+
+ if (!empty($rowSet)) {
+ return $rowSet;
+ }
+
+ $fallback = [
+ 'Aarav', 'Aarush', 'Aditi', 'Akanksha', 'Akash', 'Akhil', 'Alok', 'Aman', 'Amar', 'Amit',
+ 'Amrita', 'Anand', 'Ananya', 'Anil', 'Anjali', 'Ankit', 'Ankur', 'Ansh', 'Anshika', 'Anuj',
+ 'Arjun', 'Arnav', 'Arun', 'Aryan', 'Asha', 'Ashish', 'Ayush', 'Bhavna', 'Bhavesh', 'Bhavya',
+ 'Chandni', 'Charu', 'Chetan', 'Darpan', 'Deepa', 'Deepak', 'Dev', 'Devansh', 'Devesh', 'Dhruv',
+ 'Diksha', 'Divya', 'Gaurav', 'Geeta', 'Gopal', 'Govind', 'Harish', 'Harsh', 'Harshita', 'Himanshu',
+ 'Isha', 'Ishaan', 'Jatin', 'Jaya', 'Jyoti', 'Kabir', 'Kajal', 'Karan', 'Kartik', 'Kavita',
+ 'Khushi', 'Kiran', 'Komal', 'Krishna', 'Kunal', 'Kusum', 'Lakshmi', 'Lokesh', 'Madhav', 'Madhuri',
+ 'Mahesh', 'Manav', 'Manish', 'Meera', 'Megha', 'Mohan', 'Mohit', 'Mukesh', 'Nandini', 'Naveen',
+ 'Neeraj', 'Neha', 'Nikhil', 'Nilesh', 'Niraj', 'Nisha', 'Nitin', 'Pankaj', 'Pooja', 'Pradeep',
+ 'Prakash', 'Pranav', 'Prashant', 'Pratik', 'Preeti', 'Priya', 'Rahul', 'Raj', 'Rajat', 'Rajesh',
+ 'Rakesh', 'Rani', 'Ravi', 'Rekha', 'Ritika', 'Rohan', 'Rohit', 'Sahil', 'Sakshi', 'Sameer',
+ 'Sandeep', 'Sanjay', 'Sanjana', 'Sarthak', 'Satish', 'Seema', 'Shalini', 'Shankar', 'Shivam', 'Shreya',
+ 'Shruti', 'Simran', 'Sonali', 'Sourabh', 'Suhani', 'Suman', 'Sunil', 'Suraj', 'Suresh', 'Swati',
+ 'Tanvi', 'Tarun', 'Uday', 'Vaibhav', 'Varun', 'Vikas', 'Vinay', 'Vineet', 'Vishal', 'Vivek',
+ 'Yash', 'Yashika', 'Yogesh', 'Zoya',
+ 'Aggarwal', 'Agarwal', 'Arora', 'Bansal', 'Bhat', 'Bhatt', 'Bhardwaj', 'Chauhan', 'Chawla', 'Das',
+ 'Dubey', 'Dwivedi', 'Garg', 'Goswami', 'Gupta', 'Jain', 'Joshi', 'Kapoor', 'Khanna', 'Khan',
+ 'Kumar', 'Mishra', 'Nair', 'Pandey', 'Patel', 'Rao', 'Rawat', 'Saha', 'Saxena', 'Shah',
+ 'Sharma', 'Shukla', 'Singh', 'Srivastava', 'Thakur', 'Tripathi', 'Tyagi', 'Taygi', 'Varma', 'Verma',
+ 'Yadav'
+ ];
+
+ $result = [];
+ foreach ($fallback as $name) {
+ if (stripos($name, $term) !== 0) {
+ continue;
+ }
+
+ $result[] = [
+ 'label' => $name,
+ 'value' => $name,
+ ];
+
+ if (count($result) >= 20) {
+ break;
+ }
+ }
+
+ return $result;
+ }
+
public function insertRemark(array $data): int
{
$builder = $this->db->table('patient_remark');
diff --git a/app/Views/Invoice/Org_Contingent_bill.php b/app/Views/Invoice/Org_Contingent_bill.php
index 8095eeb..44e9251 100644
--- a/app/Views/Invoice/Org_Contingent_bill.php
+++ b/app/Views/Invoice/Org_Contingent_bill.php
@@ -63,12 +63,9 @@ $('#updatereport').click(function(){
{
var csrf_name = '= csrf_token() ?>';
var csrf_value = $('input[name="= csrf_token() ?>"]').first().val() || '= csrf_hash() ?>';
- $.post('= base_url('billing/case/create_contingent_bill') ?>/'+caseid,{
- "case_id": caseid,
- [csrf_name]: csrf_value
- }, function(data){
- notify('success','Please Attention',data);
- load_form_div('= base_url('billing/case/contingent_bill') ?>/'+caseid,'org_CONTINGENT');
+ $.get('= base_url('Orgcase/create_contingent_bill') ?>/'+caseid, function(data){
+ notify('success','Please Attention',data);
+ load_form_div('= base_url('Orgcase/contingent_bill') ?>/'+caseid,'org_CONTINGENT');
});
}
@@ -76,12 +73,13 @@ $('#updatereport').click(function(){
$('#showreport').click(function(){
var caseid = $('#caseid').val();
- load_report_div('= base_url('billing/case/contingent_bill') ?>/'+caseid+'/1','org_CONTINGENT');
+ var finalUrl = '= base_url('Orgcase/contingent_bill') ?>/' + caseid + '/1';
+ window.open(finalUrl, '_blank');
});
$('#editreport').click(function(){
var caseid = $('#caseid').val();
- load_form_div('= base_url('billing/case/contingent_bill') ?>/'+caseid+'/0','org_CONTINGENT');
+ load_form_div('= base_url('Orgcase/contingent_bill') ?>/'+caseid+'/0','org_CONTINGENT');
});
diff --git a/app/Views/Setting/Charges/IPD_Charges/item_search_V.php b/app/Views/Setting/Charges/IPD_Charges/item_search_V.php
index 5d1f875..249725d 100644
--- a/app/Views/Setting/Charges/IPD_Charges/item_search_V.php
+++ b/app/Views/Setting/Charges/IPD_Charges/item_search_V.php
@@ -6,6 +6,10 @@
Charge Groups
+
+
+ Clinical Procedure/Test Master
+
Add New Charge
diff --git a/app/Views/Setting/Charges/index.php b/app/Views/Setting/Charges/index.php
index a0411a1..562cdcc 100644
--- a/app/Views/Setting/Charges/index.php
+++ b/app/Views/Setting/Charges/index.php
@@ -62,6 +62,12 @@