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 .= '
' . esc($name) . ': ' . nl2br(esc($value)) . '
'; + $sysHtml .= '
' . nl2br(esc($value)) . '
'; } if ($sysHtml !== '') { $sections[] = '

Other / Systemic Examinations

' . $sysHtml; @@ -864,7 +895,7 @@ class Ipd_discharge extends BaseController $html .= ''; } - $otherExamText = trim((string) ($otherExamParsed['text'] ?? '')); + $otherExamText = $this->normalizeRichText((string) ($otherExamParsed['text'] ?? '')); if ($otherExamText !== '') { $html .= '
Other Examinations / Provisional Diagnosis:
' . nl2br(esc($otherExamText)) . '
'; } @@ -875,13 +906,13 @@ class Ipd_discharge extends BaseController $diagnosis = $this->byIpdRows('ipd_discharge_diagnosis', ['comp_report', 'comp_remark'], 'id ASC', $ipdId); $this->addListSection($sections, 'Final Diagnosis', $diagnosis); $diagnosisRemark = $this->firstRowByIpd('ipd_discharge_diagnosis_remark', $ipdId); - $diagnosisRemarkText = trim((string) ($diagnosisRemark['comp_remark'] ?? '')); + $diagnosisRemarkText = $this->normalizeRichText((string) ($diagnosisRemark['comp_remark'] ?? '')); if ($diagnosisRemarkText !== '') { $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[] = '

Summary of key investigations during Hospitalization

' . 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 .= '
  1. ' . $entry . '
  2. '; + } + $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 = '
' + . '
Discharge Summary
' + . '
Template: ' . esc($templateName) . '
' + . '
'; + } + + return '' + . '' + . '' + . $headerBlock + . '
' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
Patient: ' . esc($patientName) . 'UHID: ' . esc($patientCode) . 'IPD: ' . esc($ipdCode) . '
Age/Gender: ' . esc(trim($age . ' / ' . $gender)) . 'Admit Date: ' . esc($admitDate) . 'Discharge Date: ' . esc($dischargeDate) . '
' + . '
' . $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/attendantRemarks:
Follow-up date and department explainedNext Visit: __________________
Red-flag symptoms explainedEmergency 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 = ''; var csrf_value = $('input[name=""]').first().val() || ''; - $.post('/'+caseid,{ - "case_id": caseid, - [csrf_name]: csrf_value - }, function(data){ - notify('success','Please Attention',data); - load_form_div('/'+caseid,'org_CONTINGENT'); + $.get('/'+caseid, function(data){ + notify('success','Please Attention',data); + load_form_div('/'+caseid,'org_CONTINGENT'); }); } @@ -76,12 +73,13 @@ $('#updatereport').click(function(){ $('#showreport').click(function(){ var caseid = $('#caseid').val(); - load_report_div('/'+caseid+'/1','org_CONTINGENT'); + var finalUrl = '/' + caseid + '/1'; + window.open(finalUrl, '_blank'); }); $('#editreport').click(function(){ var caseid = $('#caseid').val(); - load_form_div('/'+caseid+'/0','org_CONTINGENT'); + load_form_div('/'+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 +
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 @@ IPD Charges +
+ + + Clinical Procedure/Test + +
diff --git a/app/Views/Setting/Template/discharge_templates.php b/app/Views/Setting/Template/discharge_templates.php new file mode 100644 index 0000000..3ffdc3f --- /dev/null +++ b/app/Views/Setting/Template/discharge_templates.php @@ -0,0 +1,160 @@ +{{CONTENT}}
'); +$isDefault = (int) ($edit['is_default'] ?? 0); +$status = (int) ($edit['status'] ?? 1); +?> + +
+
+ +
+ +
+ + +
+ Available placeholders: {{CONTENT}}, {{PATIENT_NAME}}, {{UHID}}, {{IPD_CODE}}, + {{AGE_GENDER}}, {{ADMIT_DATE}}, {{DISCHARGE_DATE}}, {{CURRENT_DATE}}. +
+ +
+ NABH drafting checklist: include reason for admission, significant findings, diagnosis, procedures, course in hospital, + condition at discharge, discharge medication with dose/duration, follow-up plan, and warning signs/emergency contact. +
+ +
+ + + +
+
+ + +
+
+ + +
+
+
+ > + +
+
+
+ + + Use the editor to format layout. Use the Source button for raw HTML. Keep {{CONTENT}} in the template. +
+
+ +
+ + 0): ?> + Cancel Edit + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
IDTemplate NameDefaultStatusAction
No templates found.
+ Edit + Delete +
+
+
+
+
+ + diff --git a/app/Views/Setting/Template/index.php b/app/Views/Setting/Template/index.php index d724ba1..571479b 100644 --- a/app/Views/Setting/Template/index.php +++ b/app/Views/Setting/Template/index.php @@ -6,13 +6,14 @@ $canXray = $user && method_exists($user, 'can') ? $user->can('template.xray') : $canCt = $user && method_exists($user, 'can') ? $user->can('template.ct') : false; $canMri = $user && method_exists($user, 'can') ? $user->can('template.mri') : false; $canEcho = $user && method_exists($user, 'can') ? $user->can('template.echo') : false; +$canDischarge = $user && method_exists($user, 'can') ? $user->can('template.discharge') : false; $canOpdPrintTemplate = $user && method_exists($user, 'can') ? $user->can('billing.opd.edit') : false; if (! $canOpdPrintTemplate && $user && method_exists($user, 'inGroup')) { $canOpdPrintTemplate = $user->inGroup('OPDEdit', 'admin', 'superadmin', 'developer'); } -$hasTemplateAccess = $canPathology || $canUltrasound || $canXray || $canCt || $canMri || $canEcho; +$hasTemplateAccess = $canPathology || $canUltrasound || $canXray || $canCt || $canMri || $canEcho || $canDischarge; ?>
@@ -194,6 +195,15 @@ $hasTemplateAccess = $canPathology || $canUltrasound || $canXray || $canCt || $c
+ +
+ + + IPD Discharge Template + +
+ +
diff --git a/app/Views/billing/Patient_V.php b/app/Views/billing/Patient_V.php index 3f399e0..e3f1805 100644 --- a/app/Views/billing/Patient_V.php +++ b/app/Views/billing/Patient_V.php @@ -259,7 +259,7 @@ notify('error', 'Please Attention', data.error_text); $("#RegisterPatient").prop("disabled", false); } else { - load_form('/' + data.insertid); + load_form('/' + data.insertid, 'Patient Record'); } }, 'json'); }); @@ -273,7 +273,7 @@ }); $("#input_city").autocomplete({ - source: "", + source: "", minLength: 3, autofocus: true, select: function(event, ui) { @@ -325,7 +325,7 @@ }) .autocomplete({ source: function(request, response) { - $.getJSON("", { + $.getJSON("", { term: extractLast(request.term) }, response); }, @@ -363,7 +363,7 @@ }) .autocomplete({ source: function(request, response) { - $.getJSON("", { + $.getJSON("", { term: extractLast(request.term) }, response); }, diff --git a/app/Views/billing/Person_Edit_V.php b/app/Views/billing/Person_Edit_V.php index a44c528..310d9de 100644 --- a/app/Views/billing/Person_Edit_V.php +++ b/app/Views/billing/Person_Edit_V.php @@ -104,7 +104,11 @@ $checkbox_checked=""; $age_input_1='style="display: none;"'; $age_input_2=''; - $DateofBirth=MysqlDate_to_str($data[0]->dob); + $DateofBirth=''; + if (!empty($data[0]->dob)) { + $ts = strtotime((string) $data[0]->dob); + $DateofBirth = $ts ? date('Y-m-d', $ts) : ''; + } } ?>
@@ -138,14 +142,8 @@
>
-
-
- -
- -
+
diff --git a/app/Views/billing/ipd/discharge_create.php b/app/Views/billing/ipd/discharge_create.php index 57dbf1f..5486d58 100644 --- a/app/Views/billing/ipd/discharge_create.php +++ b/app/Views/billing/ipd/discharge_create.php @@ -54,12 +54,8 @@ if (! empty($systemicExamRows)) { continue; } - $name = trim((string) ($row['name'] ?? '')); - if ($name !== '' && stripos($value, $name . ':') !== 0) { - $systemicParts[] = $name . ': ' . $value; - } else { - $systemicParts[] = $value; - } + // Keep saved systemic text as-is; do not auto-prefix master labels. + $systemicParts[] = $value; } if (! empty($systemicParts)) { @@ -78,11 +74,56 @@ $procedureRows = $procedure_rows ?? []; $diagnosisRows = $diagnosis_rows ?? []; $courseRows = $course_rows ?? []; $drugRows = $drug_rows ?? []; +$legacyDrugRows = $legacy_drug_rows ?? []; +$medicineRows = []; +if (! empty($legacyDrugRows)) { + foreach ($legacyDrugRows as $row) { + $medicineRows[] = [ + 'id' => (int) ($row['id'] ?? 0), + 'source' => 'legacy', + 'med_name' => (string) ($row['med_name'] ?? ''), + 'med_type' => (string) ($row['med_type'] ?? ''), + 'dosage' => (string) ($row['dosage'] ?? ''), + 'dosage_when' => (string) ($row['dosage_when'] ?? ''), + 'dosage_freq' => (string) ($row['dosage_freq'] ?? ''), + 'no_of_days' => (string) ($row['no_of_days'] ?? ''), + 'qty' => (string) ($row['qty'] ?? ''), + 'remark' => (string) ($row['remark'] ?? ''), + ]; + } +} else { + foreach ($drugRows as $row) { + $medicineRows[] = [ + 'id' => (int) ($row['id'] ?? 0), + 'source' => 'classic', + 'med_name' => (string) ($row['drug_name'] ?? ''), + 'med_type' => '', + 'dosage' => (string) ($row['drug_dose'] ?? ''), + 'dosage_when' => '', + 'dosage_freq' => '', + 'no_of_days' => (string) ($row['drug_day'] ?? ''), + 'qty' => '', + 'remark' => '', + ]; + } +} $inhosRemark = (string) ($inhos_remark ?? ''); $otherExamText = (string) ($other_exam_text ?? ''); $painValue = (string) ($pain_value ?? ''); $opdHistorySnapshot = $opd_history_snapshot ?? []; $nursingAdmissionSnapshot = $nursing_admission_snapshot ?? []; +$instructionFoodRows = $instruction_food_rows ?? []; +$instructionFoodIdsRaw = $instruction_food_ids ?? []; +$instructionFoodIds = []; +if (is_array($instructionFoodIdsRaw)) { + foreach ($instructionFoodIdsRaw as $fid) { + $fidInt = (int) $fid; + if ($fidInt > 0) { + $instructionFoodIds[$fidInt] = true; + } + } +} +$instructionOther = (string) ($instruction_other ?? ''); $drugAllergyStatus = trim((string) ($opdHistorySnapshot['drug_allergy_status'] ?? '')); $drugAllergyDetails = trim((string) ($opdHistorySnapshot['drug_allergy_details'] ?? '')); $adrHistory = trim((string) ($opdHistorySnapshot['adr_history'] ?? '')); @@ -201,21 +242,99 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller padding: 1rem 1rem 1.15rem; } + @media (min-width: 992px) { + .discharge-page { + display: flex; + flex-direction: column; + height: calc(100vh - 64px); + overflow: hidden; + } + + .discharge-page-title { + flex: 0 0 auto; + margin-bottom: 10px; + padding-bottom: 2px; + background: #f0f2f7; + } + + .discharge-main-card { + flex: 1 1 auto; + min-height: 0; + margin-bottom: 0; + } + + .discharge-main-card > .card-body { + /* Let notices consume natural height; keep form row as flexible remainder */ + height: 100%; + display: flex; + flex-direction: column; + max-height: none; + overflow: hidden; + } + + .discharge-main-card > .card-body > .row { + flex: 1 1 auto; + min-height: 0; + } + + .discharge-side-panel { + height: 100%; + margin-bottom: 0; + overflow: hidden; + } + + .discharge-form-area { + height: 100%; + max-height: none; + overflow-y: auto; + overflow-x: hidden; + padding-right: 6px; + } + + #discharge_section_nav { + top: 0; + max-height: none; + } + } + .discharge-side-panel { border: 1px solid var(--dc-border); background: var(--dc-muted-bg); border-radius: 10px; padding: 10px; margin-bottom: 12px; + display: flex; + flex-direction: column; + gap: 10px; + min-height: 0; + } + + .discharge-side-actions { + border-top: 1px solid #d8e2ef; + margin-top: 0; + margin-bottom: 2px; + padding-top: 12px; + display: grid; + gap: 6px; + flex: 0 0 auto; + position: relative; + z-index: 2; + } + + .discharge-side-actions .btn { + width: 100%; } #discharge_section_nav { - position: sticky; - top: 72px; - max-height: calc(100vh - 88px); + position: static; + top: auto; + max-height: none; overflow-y: auto; padding-right: 4px; margin-bottom: 0; + flex: 1 1 auto; + min-height: 0; + padding-bottom: 6px; } .discharge-nav-link { @@ -411,7 +530,34 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller min-height: 18px; } + .rx-quick-btn { + border: 1px solid #b8c5d6; + background: #fff; + color: #486485; + border-radius: 0.35rem; + padding: 0.2rem 0.6rem; + font-size: 0.88rem; + margin-right: 0.35rem; + margin-bottom: 0.35rem; + } + @media (max-width: 991.98px) { + .discharge-page { + height: auto; + overflow: visible; + } + + .discharge-main-card > .card-body { + max-height: none; + overflow: visible; + } + + .discharge-form-area { + max-height: none; + overflow: visible; + padding-right: 0; + } + #discharge_section_nav { position: static; max-height: none; @@ -465,14 +611,18 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller + +
+ +
-
+ -
+
Personal History
@@ -485,10 +635,13 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
+
+ +
-
+
From Nursing History & Physical Assessment @@ -529,6 +682,9 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
+
+ +
@@ -690,11 +846,15 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller -
+
Other / Systemic Examinations
+
+
+ +
@@ -827,11 +987,17 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
+
+ +
-
Surgery / Procedure / delivery if any
+
+ Surgery / Procedure / delivery if any + +
Surgery
@@ -850,12 +1016,14 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
+
-
+
+
Procedure
@@ -874,28 +1042,24 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
+
-
+
+ +
-
Final Diagnosis
+
+ Final Diagnosis + +
-
- -
- - -
- -
-
- @@ -912,10 +1076,13 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
DiagnosisRemarkAction
-
+
+ + Tip: if ICD match exists, selected value appends code in diagnosis text. +
Course / Treatment in the hospital
-
- -
- - -
- -
-
- @@ -970,10 +1130,12 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
CourseRemarkAction
-
+
+ +
+
+ +
Discharge Medicine Prescribed
+ + + + +
+
+ + No Rx-Group selected +
+
Select group and preview medicines before add.
+
+ - + - - - + + + - - - - + + + + + + + + +
DrugDoseDayAction
MedicineTypeDoseWhenFreqDaysQtyRemarkAction
No discharge drug rows.
No medicine added
-
-
-
-
-
+ +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + +
+ + + + + + + + + + + + + + + +
-
Discharge Instructions / Advice
+
+ Discharge Instructions / Advice + +
+
+ +
+ +
No dietary advice master found.
+ + + +
+ > + +
+ + +
+
+ + +
+
+ +
+
+
Selected Dietary Advice (Hindi print preview)
+
No dietary advice selected.
+
+
+ +
+ + +
+
- +
+
+ +
-
- - - -
+ + + + -
- -
- + +
+ +
- -
- - - - - - +
+
+ + +
+
+ NABH Audit Mode + Completed: / +
+
+ 0): ?> +
+ Critical sections missing (): + +
+ +
All critical NABH checklist sections are present.
+ + +
+ + + + + + + + + + + + + + + + + + +
Checklist ItemTypeStatus
+
+
+
+ + +
+ +
+ +
+ Back to Create Discharge Summary + + + +
@@ -72,59 +137,88 @@ if ($person) { } function loadOrRedirect(url, title) { + if (typeof load_form === 'function') { + load_form(url, title || 'Discharge'); + return; + } if (typeof load_form_div === 'function') { load_form_div(url, 'maindiv', title || 'Discharge'); return; } - window.location.href = url; - } - - if (window.CKEDITOR) { - CKEDITOR.config.versionCheck = false; - // Global layout removes notification plugins; reset here to avoid dependency errors. - CKEDITOR.config.removePlugins = ''; - if (CKEDITOR.instances.editor_Discharge_Preview) { - CKEDITOR.instances.editor_Discharge_Preview.destroy(true); - } - CKEDITOR.replace('editor_Discharge_Preview', { - height: 360 - }); + window.location.assign(url); } var ipdId = ; + var nabhCriticalMissingCount = ; + var nabhCriticalMissingItems = ; var btnCreate = document.getElementById('btn_create'); - var btnRegen = document.getElementById('btn_regen'); var btnShow = document.getElementById('btn_show'); var btnShow2 = document.getElementById('btn_show2'); var btnShow3 = document.getElementById('btn_show3'); - if (btnCreate) { - btnCreate.addEventListener('click', function() { - loadOrRedirect('/' + ipdId, 'Create Discharge Summary'); - }); + function confirmNabhPrint() { + if (nabhCriticalMissingCount <= 0) { + return true; + } + + var detail = Array.isArray(nabhCriticalMissingItems) ? nabhCriticalMissingItems.join(' | ') : ''; + var message = 'NABH Audit: critical sections are missing.\n\n' + detail + '\n\nDo you still want to print?'; + return window.confirm(message); } - if (btnRegen) { - btnRegen.addEventListener('click', function() { - loadOrRedirect('/' + ipdId + '?regen=1', 'Discharge Preview'); + if (btnCreate) { + btnCreate.addEventListener('click', function(e) { + if (typeof load_form !== 'function' && typeof load_form_div !== 'function') { + return; + } + // Prevent default only when SPA loader exists; otherwise native link works. + e.preventDefault(); + loadOrRedirect('/' + ipdId, 'Create Discharge Summary'); }); } if (btnShow) { btnShow.addEventListener('click', function() { - openUrl('/' + ipdId + '/1'); + if (!confirmNabhPrint()) { + return; + } + var tpl = document.getElementById('tpl_selector'); + var tplId = tpl ? parseInt(tpl.value || '0', 10) : 0; + var url = '/' + ipdId + '/1'; + if (tplId > 0) { + url += '?tpl=' + tplId; + } + openUrl(url); }); } if (btnShow2) { btnShow2.addEventListener('click', function() { - openUrl('/' + ipdId + '/0'); + if (!confirmNabhPrint()) { + return; + } + var tpl = document.getElementById('tpl_selector'); + var tplId = tpl ? parseInt(tpl.value || '0', 10) : 0; + var url = '/' + ipdId + '/0'; + if (tplId > 0) { + url += '?tpl=' + tplId; + } + openUrl(url); }); } if (btnShow3) { btnShow3.addEventListener('click', function() { - openUrl('/' + ipdId); + if (!confirmNabhPrint()) { + return; + } + var tpl = document.getElementById('tpl_selector'); + var tplId = tpl ? parseInt(tpl.value || '0', 10) : 0; + var url = '/' + ipdId; + if (tplId > 0) { + url += '?tpl=' + tplId; + } + openUrl(url); }); } })(); diff --git a/app/Views/billing/ipd/discharge_print.php b/app/Views/billing/ipd/discharge_print.php index 62c257b..a591ce8 100644 --- a/app/Views/billing/ipd/discharge_print.php +++ b/app/Views/billing/ipd/discharge_print.php @@ -5,10 +5,15 @@ $ipdId = (int) ($ipd_id ?? 0); $printType = (int) ($print_type ?? 1); $previewHtml = (string) ($content ?? ''); -$patientName = trim((string) (($person->p_fname ?? '') . ' ' . ($person->p_lname ?? ''))); -if ($patientName === '') { - $patientName = (string) ($person->p_fname ?? ''); -} +$patientName = trim((string) ($person->p_fname ?? '')); +$patientCode = trim((string) ( + $person->uhid + ?? $person->UHID + ?? $person->patient_code + ?? $person->p_code + ?? $person->reg_no + ?? '' +)); $title = 'IPD Discharge Preview'; if ($printType === 0) { @@ -72,7 +77,7 @@ if ($printType === 0) {
IPD: ipd_code ?? $ipdId) ?>
Patient:
-
UHID: p_code ?? '') ?>
+
UHID:
Date:
diff --git a/app/Views/billing/ipd/panel_ipd_charges.php b/app/Views/billing/ipd/panel_ipd_charges.php index 5019363..53b2664 100644 --- a/app/Views/billing/ipd/panel_ipd_charges.php +++ b/app/Views/billing/ipd/panel_ipd_charges.php @@ -7,6 +7,9 @@ $defaultPackageId = $hasPackage ? (int) ($packages[0]->id ?? 0) : 0; $csrfName = csrf_token(); $csrfHash = csrf_hash(); $today = date('Y-m-d'); +$bedsideItemsByCategory = $bedside_items_by_category ?? []; +$doctorVisitFeeTypes = $doctor_visit_fee_types ?? []; +$doctorVisitFeeMap = $doctor_visit_fee_map ?? []; ?> + + diff --git a/app/Views/partials/sidebar.php b/app/Views/partials/sidebar.php index 5cba00d..09c6860 100644 --- a/app/Views/partials/sidebar.php +++ b/app/Views/partials/sidebar.php @@ -80,6 +80,12 @@ if (! $canPharmacy && $user && method_exists($user, 'inGroup')) { IPD Patient List + +