diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 40a1360..4275dd2 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -223,6 +223,21 @@ $routes->post('Payment_Medical/change_user', 'Payment_Medical::change_user');
service('auth')->routes($routes);
+// Legacy compatibility: some IPD charge screens still refresh via old IpdNew URL.
+$routes->get('IpdNew/show_ipd_items/(:num)', 'Billing\\Ipd::panelTab/$1/ipd-charges');
+$routes->get('IpdNew/show_ipd_items/(:num)/(:num)', 'Billing\\Ipd::panelTab/$1/ipd-charges');
+
+// Legacy compatibility: old discharge summary links should open current discharge tab.
+$routes->match(['get', 'post'], 'Ipd_discharge/ipd_select/(:num)', 'Ipd_discharge::ipd_select/$1');
+$routes->match(['get', 'post'], 'Ipd_discharge/ipd_select/(:num)/(:num)', 'Ipd_discharge::ipd_select/$1/$2');
+$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->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');
+$routes->get('Ipd_discharge/show_file3/(:num)', 'Ipd_discharge::show_file3/$1');
+
$routes->get('setup/db-tools', 'Setup::dbTools');
$routes->post('setup/db-tools/generate', 'Setup::generateMigrations');
$routes->post('setup/db-tools/export-master-schema', 'Setup::exportMasterSchema');
@@ -244,6 +259,10 @@ $routes->group('billing', function($routes) {
$routes->post('ipd/charge/add/(:num)', 'Billing\\Ipd::addIpdCharge/$1');
$routes->post('ipd/charge/update/(:num)', 'Billing\\Ipd::updateIpdCharge/$1');
$routes->post('ipd/charge/delete/(:num)', 'Billing\\Ipd::deleteIpdCharge/$1');
+ $routes->post('ipd/bedside-charge/add/(:num)', 'Billing\\Ipd::addBedsideCharge/$1');
+ $routes->post('ipd/doctor-visit/add/(:num)', 'Billing\\Ipd::addDoctorVisitCharge/$1');
+ $routes->post('ipd/nursing-charge/update/(:num)/(:num)', 'Billing\\Ipd::updateNursingCharge/$1/$2');
+ $routes->post('ipd/nursing-charge/delete/(:num)/(:num)', 'Billing\\Ipd::deleteNursingCharge/$1/$2');
$routes->post('ipd/charge/package/(:num)', 'Billing\\Ipd::updateIpdChargePackage/$1');
$routes->post('ipd/package/add/(:num)', 'Billing\\Ipd::addIpdPackage/$1');
$routes->post('ipd/package/update/(:num)', 'Billing\\Ipd::updateIpdPackage/$1');
@@ -319,6 +338,21 @@ $routes->group('billing', function($routes) {
$routes->post('charges/update-discount', 'Billing\\Charges::updateDiscount');
});
+$routes->get('ipd/patient', 'IpdPatient::index');
+$routes->get('ipd/patient/workspace/(:num)', 'IpdPatient::workspace/$1');
+$routes->post('ipd/patient/nursing/save/(:num)', 'IpdPatient::saveNursingEntry/$1');
+$routes->post('ipd/patient/history/save/(:num)', 'IpdPatient::saveAdmissionHistory/$1');
+$routes->post('ipd/patient/nursing/scan/(:num)', 'IpdPatient::scanNursingPaper/$1');
+$routes->post('ipd/patient/nursing/scan-save/(:num)', 'IpdPatient::saveScannedNursingEntries/$1');
+$routes->get('ipd/patient/nursing/print/(:num)', 'IpdPatient::nursingChartPrint/$1');
+
+$routes->get('nursing-bedside-items', 'Setting\\NursingBedsideItems::index');
+$routes->get('nursing-bedside-items/(:num)', 'Setting\\NursingBedsideItems::get/$1');
+$routes->post('nursing-bedside-items/save', 'Setting\\NursingBedsideItems::save');
+$routes->get('nursing-bedside-items/insurance/(:num)', 'Setting\\NursingBedsideItems::insuranceList/$1');
+$routes->post('nursing-bedside-items/insurance/add', 'Setting\\NursingBedsideItems::insuranceAdd');
+$routes->post('nursing-bedside-items/insurance/remove', 'Setting\\NursingBedsideItems::insuranceRemove');
+
$routes->get('Opd/addopd/(:num)', 'Opd::addopd/$1');
$routes->get('Opd/addopd/(:num)/(:num)', 'Opd::addopd/$1/$2');
$routes->get('opd/appointment', 'Opd::appointment');
@@ -479,6 +513,9 @@ $routes->post('Lab_Admin/add_test_repo/(:num)/(:num)', 'Setting\Template::add_te
$routes->post('Lab_Admin/remove_test_item/(:num)/(:num)', 'Setting\Template::remove_test_item/$1/$2');
$routes->post('Lab_Admin/test_item_search', 'Setting\Template::test_item_search');
+$routes->match(['get', 'post'], 'setting/template/discharge_templates', 'Setting\Template::discharge_templates');
+$routes->get('setting/template/discharge_templates/delete/(:num)', 'Setting\Template::discharge_template_delete/$1');
+
$routes->get('Opdcase/addopd/(:num)', 'Opdcase::addopd/$1');
$routes->post('Opdcase/showfee', 'Opdcase::showfee');
$routes->post('Opdcase/confirm_opd', 'Opdcase::confirm_opd');
@@ -496,6 +533,10 @@ $routes->get('Report/diagnosis_report_data/(:segment)/(:segment)', 'Report::diag
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)/(:segment)', 'Report::diagnosis_report_data/$1/$2/$3');
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::diagnosis_report_data/$1/$2/$3/$4');
+$routes->get('Report/nabh_audit_report', 'Report::nabh_audit_report');
+$routes->get('Report/nabh_audit_report_data/(:segment)/(:segment)/(:segment)', 'Report::nabh_audit_report_data/$1/$2/$3');
+$routes->get('Report/nabh_audit_report_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::nabh_audit_report_data/$1/$2/$3/$4');
+
// Insurance Credit Reports
$routes->get('Report/insurance_credit_main', 'Report::insurance_credit_main');
$routes->get('Report/insurance_opd_report', 'Report::insurance_opd_report');
diff --git a/app/Controllers/IpdPatient.php b/app/Controllers/IpdPatient.php
new file mode 100644
index 0000000..c98981a
--- /dev/null
+++ b/app/Controllers/IpdPatient.php
@@ -0,0 +1,825 @@
+ipdBillingModel = new IpdBillingModel();
+ $this->ipdNursingEntryModel = new IpdNursingEntryModel();
+ $this->nursingBedsideItemModel = new NursingBedsideItemModel();
+ $this->nursingScanExtractionService = new NursingScanExtractionService();
+ helper(['common', 'form', 'age']);
+ }
+
+ public function index()
+ {
+ $permission = $this->requireAnyPermission([
+ 'billing.access',
+ 'billing.ipd.current-admission',
+ 'billing.ipd.invoice',
+ ]);
+ if ($permission) {
+ return $permission;
+ }
+
+ return view('ipd_nursing/patient_list', [
+ 'records' => $this->ipdBillingModel->getCurrentAdmissions(),
+ ]);
+ }
+
+ public function workspace(int $ipdId)
+ {
+ $permission = $this->requireAnyPermission([
+ 'billing.access',
+ 'billing.ipd.current-admission',
+ 'billing.ipd.invoice',
+ ]);
+ if ($permission) {
+ return $permission;
+ }
+
+ $panelData = $this->ipdBillingModel->getIpdPanelInfo($ipdId);
+ if (empty($panelData)) {
+ return $this->response->setStatusCode(404)->setBody('IPD not found');
+ }
+
+ $caseMeta = $this->ipdBillingModel->getIpdCaseMeta($ipdId);
+ $insCompId = (int) ($caseMeta['insurance_id'] ?? 0);
+ if ($insCompId <= 0) {
+ $insCompId = 1;
+ }
+
+ $docList = $this->ipdBillingModel->getIpdDoctorList();
+ $doctorIds = array_map(static fn ($doc) => (int) ($doc->id ?? 0), $docList);
+ $doctorIds = array_values(array_filter($doctorIds, static fn ($id) => $id > 0));
+ $person = $panelData['person_info'] ?? null;
+ $patientId = max(0, (int) ($person->id ?? 0));
+ if ($patientId <= 0) {
+ $patientId = $this->getPatientIdFromIpd($ipdId);
+ }
+
+ $historyFields = [
+ 'is_smoking' => 'Smoking',
+ 'is_alcohol' => 'Alcohol',
+ 'is_tobacoo' => 'Tobacco',
+ 'is_drug_abuse' => 'Drug abuse',
+ 'is_hypertesion' => 'Hypertension',
+ 'is_niddm' => 'Type 2 diabetes mellitus (DM)',
+ 'is_hbsag' => 'HBsAg',
+ 'is_hcv' => 'HCV',
+ 'is_hiv_I_II' => 'HIV I & II',
+ ];
+ $patientHistoryRow = $this->getPatientHistoryFlags($patientId, array_keys($historyFields));
+ $opdHistorySnapshot = $this->getLatestOpdHistorySnapshot($patientId);
+ $isFemalePatient = strtolower((string) ($person->xgender ?? '')) === 'female';
+
+ return view('ipd_nursing/workspace', [
+ 'ipd_info' => $panelData['ipd_info'] ?? null,
+ 'person_info' => $person,
+ 'nursing_entries' => $this->ipdNursingEntryModel->getByIpd($ipdId),
+ 'nursing_charge_history' => $this->ipdBillingModel->getNursingChargeHistory($ipdId),
+ 'bedside_items_by_category' => $this->nursingBedsideItemModel->getBillableGroupedByCategory($insCompId),
+ 'doctor_visit_fee_types' => $this->ipdBillingModel->getDoctorVisitFeeTypes(),
+ 'doctor_visit_fee_map' => $this->ipdBillingModel->getDoctorVisitFeeMap($doctorIds),
+ 'doc_list' => $docList,
+ 'history_fields' => $historyFields,
+ 'patient_history_row' => $patientHistoryRow,
+ 'opd_history_snapshot' => $opdHistorySnapshot,
+ 'is_female_patient' => $isFemalePatient,
+ ]);
+ }
+
+ public function saveAdmissionHistory(int $ipdId)
+ {
+ $permission = $this->requireAnyPermission([
+ 'billing.access',
+ 'billing.ipd.current-admission',
+ 'billing.ipd.invoice',
+ ]);
+ if ($permission) {
+ return $permission;
+ }
+
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Invalid request']);
+ }
+
+ $patientId = $this->getPatientIdFromIpd($ipdId);
+ if ($patientId <= 0) {
+ return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Patient not found']);
+ }
+
+ $historyFieldKeys = [
+ 'is_smoking',
+ 'is_alcohol',
+ 'is_tobacoo',
+ 'is_drug_abuse',
+ 'is_hypertesion',
+ 'is_niddm',
+ 'is_hbsag',
+ 'is_hcv',
+ 'is_hiv_I_II',
+ ];
+
+ $saved = false;
+ if ($this->db->tableExists('patient_master')) {
+ $patientFields = $this->db->getFieldNames('patient_master') ?? [];
+ if (in_array('id', $patientFields, true)) {
+ $historyUpdate = [];
+ foreach ($historyFieldKeys as $field) {
+ if (in_array($field, $patientFields, true)) {
+ $historyUpdate[$field] = (int) ($this->request->getPost($field) ?? 0) === 1 ? 1 : 0;
+ }
+ }
+
+ if (! empty($historyUpdate)) {
+ $saved = (bool) $this->db->table('patient_master')
+ ->where('id', $patientId)
+ ->update($historyUpdate) || $saved;
+ }
+ }
+ }
+
+ $opdPayload = [
+ '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') ?? '')),
+ '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') ?? '')),
+ 'hpi_note' => trim((string) ($this->request->getPost('hpi_note') ?? '')),
+ ];
+
+ if ($this->db->tableExists('opd_prescription')) {
+ $rxFields = $this->db->getFieldNames('opd_prescription') ?? [];
+ if (in_array('p_id', $rxFields, true) && in_array('id', $rxFields, true)) {
+ $latest = $this->db->table('opd_prescription')
+ ->select('id,Prescriber_Remarks')
+ ->where('p_id', $patientId)
+ ->orderBy('id', 'DESC')
+ ->get(1)
+ ->getRowArray();
+
+ if (is_array($latest) && ! empty($latest['id'])) {
+ $update = [];
+ foreach (['drug_allergy_status', 'drug_allergy_details', 'adr_history', 'current_medications', 'women_lmp', 'women_last_baby', 'women_pregnancy_related', 'women_related_problems'] as $field) {
+ if (in_array($field, $rxFields, true)) {
+ $update[$field] = (string) ($opdPayload[$field] ?? '');
+ }
+ }
+
+ if (in_array('Prescriber_Remarks', $rxFields, true)) {
+ $remarks = (string) ($latest['Prescriber_Remarks'] ?? '');
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'Drug Allergy Status', (string) ($opdPayload['drug_allergy_status'] ?? ''));
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'Drug Allergy Details', (string) ($opdPayload['drug_allergy_details'] ?? ''));
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'ADR History', (string) ($opdPayload['adr_history'] ?? ''));
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'Current Medications', (string) ($opdPayload['current_medications'] ?? ''));
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'Co-Morbidities', (string) ($opdPayload['co_morbidities'] ?? ''));
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'HPI Note', (string) ($opdPayload['hpi_note'] ?? ''));
+ $update['Prescriber_Remarks'] = $remarks;
+ }
+
+ if (! empty($update)) {
+ $saved = (bool) $this->db->table('opd_prescription')
+ ->where('id', (int) $latest['id'])
+ ->update($update) || $saved;
+ }
+ }
+ }
+ }
+
+ return $this->response->setJSON([
+ 'status' => $saved ? 1 : 0,
+ 'message' => $saved ? 'Admission history updated' : 'No data updated',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ public function saveNursingEntry(int $ipdId)
+ {
+ $permission = $this->requireAnyPermission([
+ 'billing.access',
+ 'billing.ipd.current-admission',
+ 'billing.ipd.invoice',
+ ]);
+ if ($permission) {
+ return $permission;
+ }
+
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Invalid request']);
+ }
+
+ $entryType = (string) ($this->request->getPost('entry_type') ?? '');
+ if (! in_array($entryType, ['vitals', 'fluid', 'treatment', 'admission'], true)) {
+ return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Invalid nursing entry type']);
+ }
+
+ $recordedAtInput = (string) ($this->request->getPost('recorded_at') ?? '');
+ $recordedAt = $recordedAtInput !== ''
+ ? str_replace('T', ' ', $recordedAtInput) . (strlen($recordedAtInput) === 16 ? ':00' : '')
+ : date('Y-m-d H:i:s');
+
+ $user = auth()->user();
+ $recordedBy = $user->username ?? $user->email ?? 'User';
+ $recordedById = $user->id ?? null;
+
+ $data = [
+ 'ipd_id' => $ipdId,
+ 'entry_type' => $entryType,
+ 'recorded_at' => $recordedAt,
+ 'shift_name' => $this->resolveShiftName($recordedAt),
+ 'temperature_c' => $this->resolveTemperatureCFromPost(),
+ 'pulse_rate' => $this->request->getPost('pulse_rate') !== null && $this->request->getPost('pulse_rate') !== '' ? (int) $this->request->getPost('pulse_rate') : null,
+ 'resp_rate' => $this->request->getPost('resp_rate') !== null && $this->request->getPost('resp_rate') !== '' ? (int) $this->request->getPost('resp_rate') : null,
+ 'bp_systolic' => $this->request->getPost('bp_systolic') !== null && $this->request->getPost('bp_systolic') !== '' ? (int) $this->request->getPost('bp_systolic') : null,
+ 'bp_diastolic' => $this->request->getPost('bp_diastolic') !== null && $this->request->getPost('bp_diastolic') !== '' ? (int) $this->request->getPost('bp_diastolic') : null,
+ 'spo2' => $this->request->getPost('spo2') !== null && $this->request->getPost('spo2') !== '' ? (int) $this->request->getPost('spo2') : null,
+ 'weight_kg' => $this->request->getPost('weight_kg') !== null && $this->request->getPost('weight_kg') !== '' ? (float) $this->request->getPost('weight_kg') : null,
+ 'fluid_direction' => (string) ($this->request->getPost('fluid_direction') ?? ''),
+ 'fluid_route' => (string) ($this->request->getPost('fluid_route') ?? ''),
+ 'fluid_amount_ml' => $this->request->getPost('fluid_amount_ml') !== null && $this->request->getPost('fluid_amount_ml') !== '' ? (int) $this->request->getPost('fluid_amount_ml') : null,
+ 'treatment_text' => (string) ($this->request->getPost('treatment_text') ?? ''),
+ 'general_note' => (string) ($this->request->getPost('general_note') ?? ''),
+ 'recorded_by' => $recordedBy,
+ 'recorded_by_id' => $recordedById,
+ 'created_at' => date('Y-m-d H:i:s'),
+ 'updated_at' => date('Y-m-d H:i:s'),
+ ];
+
+ if ($entryType === 'vitals') {
+ $hasVitals = $data['temperature_c'] !== null
+ || $data['pulse_rate'] !== null
+ || $data['resp_rate'] !== null
+ || $data['bp_systolic'] !== null
+ || $data['bp_diastolic'] !== null
+ || $data['spo2'] !== null
+ || $data['weight_kg'] !== null;
+ if (! $hasVitals) {
+ return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Enter at least one vital value']);
+ }
+ }
+
+ if ($entryType === 'fluid' && $data['fluid_amount_ml'] === null) {
+ return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Fluid amount is required']);
+ }
+
+ if ($entryType === 'treatment' && trim((string) $data['treatment_text']) === '') {
+ return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Treatment note is required']);
+ }
+
+ if ($entryType === 'admission') {
+ $hasVitals = $data['temperature_c'] !== null
+ || $data['pulse_rate'] !== null
+ || $data['resp_rate'] !== null
+ || $data['bp_systolic'] !== null
+ || $data['bp_diastolic'] !== null
+ || $data['spo2'] !== null
+ || $data['weight_kg'] !== null;
+ $hasComplaint = trim((string) $data['treatment_text']) !== '';
+
+ if (! $hasVitals && ! $hasComplaint) {
+ return $this->response->setStatusCode(422)->setJSON([
+ 'status' => 0,
+ 'message' => 'Enter admission complaints or at least one vital value',
+ ]);
+ }
+ }
+
+ $this->ipdNursingEntryModel->insert($data);
+
+ return $this->response->setJSON([
+ 'status' => 1,
+ 'message' => 'Nursing entry saved',
+ ]);
+ }
+
+ public function scanNursingPaper(int $ipdId)
+ {
+ $permission = $this->requireAnyPermission([
+ 'billing.access',
+ 'billing.ipd.current-admission',
+ 'billing.ipd.invoice',
+ ]);
+ if ($permission) {
+ return $permission;
+ }
+
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Invalid request']);
+ }
+
+ $docType = strtolower(trim((string) ($this->request->getPost('document_type') ?? 'auto')));
+ if (! in_array($docType, ['auto', 'vitals', 'fluid', 'treatment'], true)) {
+ $docType = 'auto';
+ }
+
+ $manualText = trim((string) ($this->request->getPost('ocr_text') ?? ''));
+ $file = $this->request->getFile('scan_file');
+ $uploadedMeta = null;
+
+ if ($file && $file->isValid() && ! $file->hasMoved()) {
+ $allowed = ['pdf', 'png', 'jpg', 'jpeg', 'webp', 'txt'];
+ $ext = strtolower((string) $file->getExtension());
+ if (! in_array($ext, $allowed, true)) {
+ return $this->response->setStatusCode(422)->setJSON([
+ 'status' => 0,
+ 'message' => 'Only PDF, image, or text files are allowed',
+ ]);
+ }
+
+ $scanRef = 'NSCAN-' . date('YmdHis') . '-' . substr(md5((string) mt_rand()), 0, 6);
+ $targetDir = WRITEPATH . 'uploads/nursing_scans/' . $ipdId;
+ if (! is_dir($targetDir)) {
+ @mkdir($targetDir, 0775, true);
+ }
+ $storedName = $scanRef . '.' . $ext;
+ $file->move($targetDir, $storedName, true);
+
+ $uploadedMeta = [
+ 'scan_ref' => $scanRef,
+ 'path' => rtrim($targetDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $storedName,
+ 'mime' => (string) $file->getClientMimeType(),
+ ];
+ }
+
+ if ($manualText === '' && $uploadedMeta === null) {
+ return $this->response->setStatusCode(422)->setJSON([
+ 'status' => 0,
+ 'message' => 'Upload a scan file or paste OCR text',
+ ]);
+ }
+
+ $parsed = $this->nursingScanExtractionService->extract($docType, $manualText, $uploadedMeta);
+ $rows = is_array($parsed['rows'] ?? null) ? $parsed['rows'] : [];
+
+ return $this->response->setJSON([
+ 'status' => 1,
+ 'message' => empty($rows)
+ ? 'Scan processed. No structured rows detected; please review and edit manually.'
+ : 'Scan processed. Review extracted rows before saving.',
+ 'scan_ref' => (string) ($parsed['scan_ref'] ?? ''),
+ 'document_type' => $docType,
+ 'rows' => $rows,
+ 'warnings' => $parsed['warnings'] ?? [],
+ 'nabh_requirements' => [
+ 'Each row must have recorded date/time.',
+ 'Each row must include nursing note or measurable values.',
+ 'Vitals rows require at least one vital parameter.',
+ 'Fluid rows require direction and amount in ml.',
+ 'All AI/OCR rows must be nurse-reviewed before save.',
+ ],
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ public function saveScannedNursingEntries(int $ipdId)
+ {
+ $permission = $this->requireAnyPermission([
+ 'billing.access',
+ 'billing.ipd.current-admission',
+ 'billing.ipd.invoice',
+ ]);
+ if ($permission) {
+ return $permission;
+ }
+
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Invalid request']);
+ }
+
+ $rowsJson = (string) ($this->request->getPost('rows_json') ?? '[]');
+ $rows = json_decode($rowsJson, true);
+ if (! is_array($rows) || empty($rows)) {
+ return $this->response->setStatusCode(422)->setJSON([
+ 'status' => 0,
+ 'message' => 'No reviewed rows to save',
+ ]);
+ }
+
+ $scanRef = trim((string) ($this->request->getPost('scan_ref') ?? ''));
+
+ $user = auth()->user();
+ $recordedBy = $user->username ?? $user->email ?? 'User';
+ $recordedById = $user->id ?? null;
+
+ $savedCount = 0;
+ $errors = [];
+
+ foreach ($rows as $idx => $row) {
+ if (! is_array($row)) {
+ $errors[] = 'Row ' . ($idx + 1) . ': Invalid payload.';
+ continue;
+ }
+
+ $entryType = strtolower(trim((string) ($row['entry_type'] ?? '')));
+ if (! in_array($entryType, ['vitals', 'fluid', 'treatment'], true)) {
+ $errors[] = 'Row ' . ($idx + 1) . ': Unsupported entry type.';
+ continue;
+ }
+
+ $recordedAt = $this->normalizeScannedRecordedAt((string) ($row['recorded_at'] ?? ''));
+ if ($recordedAt === '') {
+ $errors[] = 'Row ' . ($idx + 1) . ': Recorded At is required.';
+ continue;
+ }
+
+ $temperatureF = $this->toNullableFloat($row['temperature_f'] ?? null);
+ $temperatureC = $temperatureF !== null ? round(($temperatureF - 32) * 5 / 9, 2) : null;
+ $pulse = $this->toNullableInt($row['pulse_rate'] ?? null);
+ $resp = $this->toNullableInt($row['resp_rate'] ?? null);
+ $sbp = $this->toNullableInt($row['bp_systolic'] ?? null);
+ $dbp = $this->toNullableInt($row['bp_diastolic'] ?? null);
+ $spo2 = $this->toNullableInt($row['spo2'] ?? null);
+ $weight = $this->toNullableFloat($row['weight_kg'] ?? null);
+ $fluidAmount = $this->toNullableInt($row['fluid_amount_ml'] ?? null);
+ $fluidDirection = strtolower(trim((string) ($row['fluid_direction'] ?? '')));
+ $fluidRoute = trim((string) ($row['fluid_route'] ?? ''));
+ $treatmentText = trim((string) ($row['treatment_text'] ?? ''));
+ $generalNote = trim((string) ($row['general_note'] ?? ''));
+ $confidence = $this->toNullableFloat($row['confidence'] ?? null);
+ $isCorrected = (int) ($row['is_corrected'] ?? 0) === 1;
+
+ $sourceParts = ['Source: Scanned Paper'];
+ if ($scanRef !== '') {
+ $sourceParts[] = 'ScanRef: ' . $scanRef;
+ }
+ $sourceParts[] = 'ReviewedBy: ' . $recordedBy;
+ $sourceParts[] = 'ReviewedAt: ' . date('Y-m-d H:i:s');
+ if ($confidence !== null) {
+ $sourceParts[] = 'AIConfidence: ' . max(0, min(100, (int) round($confidence * 100))) . '%';
+ }
+ $sourceParts[] = 'Corrected: ' . ($isCorrected ? 'Yes' : 'No');
+ $sourceTag = ' [' . implode(' | ', $sourceParts) . ']';
+ $generalNote = trim($generalNote . $sourceTag);
+
+ if ($entryType === 'vitals') {
+ $hasVitals = $temperatureC !== null || $pulse !== null || $resp !== null || $sbp !== null || $dbp !== null || $spo2 !== null || $weight !== null;
+ if (! $hasVitals) {
+ $errors[] = 'Row ' . ($idx + 1) . ': Vitals row requires at least one vital parameter.';
+ continue;
+ }
+ }
+
+ if ($entryType === 'fluid') {
+ if (! in_array($fluidDirection, ['intake', 'output'], true)) {
+ $errors[] = 'Row ' . ($idx + 1) . ': Fluid direction must be intake/output.';
+ continue;
+ }
+ if ($fluidAmount === null || $fluidAmount <= 0) {
+ $errors[] = 'Row ' . ($idx + 1) . ': Fluid amount is required.';
+ continue;
+ }
+ }
+
+ if ($entryType === 'treatment' && $treatmentText === '' && $generalNote === $sourceTag) {
+ $errors[] = 'Row ' . ($idx + 1) . ': Treatment text or note is required.';
+ continue;
+ }
+
+ $insert = [
+ 'ipd_id' => $ipdId,
+ 'entry_type' => $entryType,
+ 'recorded_at' => $recordedAt,
+ 'shift_name' => $this->resolveShiftName($recordedAt),
+ 'temperature_c' => $temperatureC,
+ 'pulse_rate' => $pulse,
+ 'resp_rate' => $resp,
+ 'bp_systolic' => $sbp,
+ 'bp_diastolic' => $dbp,
+ 'spo2' => $spo2,
+ 'weight_kg' => $weight,
+ 'fluid_direction' => $entryType === 'fluid' ? $fluidDirection : '',
+ 'fluid_route' => $entryType === 'fluid' ? $fluidRoute : '',
+ 'fluid_amount_ml' => $entryType === 'fluid' ? $fluidAmount : null,
+ 'treatment_text' => $entryType === 'treatment' ? $treatmentText : '',
+ 'general_note' => $generalNote,
+ 'recorded_by' => $recordedBy,
+ 'recorded_by_id' => $recordedById,
+ 'created_at' => date('Y-m-d H:i:s'),
+ 'updated_at' => date('Y-m-d H:i:s'),
+ ];
+
+ $this->ipdNursingEntryModel->insert($insert);
+ $savedCount++;
+ }
+
+ return $this->response->setJSON([
+ 'status' => $savedCount > 0 ? 1 : 0,
+ 'message' => $savedCount > 0
+ ? ('Saved ' . $savedCount . ' scanned nursing row(s).')
+ : 'No rows saved. Please fix validation errors.',
+ 'saved_count' => $savedCount,
+ 'errors' => $errors,
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ public function nursingChartPrint(int $ipdId)
+ {
+ $permission = $this->requireAnyPermission([
+ 'billing.access',
+ 'billing.ipd.current-admission',
+ 'billing.ipd.invoice',
+ ]);
+ if ($permission) {
+ return $permission;
+ }
+
+ $panelData = $this->ipdBillingModel->getIpdPanelInfo($ipdId);
+ if (empty($panelData)) {
+ return $this->response->setStatusCode(404)->setBody('IPD not found');
+ }
+
+ $chartDate = (string) ($this->request->getGet('date') ?? date('Y-m-d'));
+ if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $chartDate)) {
+ $chartDate = date('Y-m-d');
+ }
+
+ $nurse = trim((string) ($this->request->getGet('nurse') ?? ''));
+ $startDateTime = $chartDate . ' 00:00:00';
+ $endDateTime = $chartDate . ' 23:59:59';
+
+ $entries = $this->ipdNursingEntryModel->getByIpdPeriod($ipdId, $startDateTime, $endDateTime, $nurse);
+
+ return view('billing/ipd/nursing_chart_print', [
+ 'ipd_info' => $panelData['ipd_info'] ?? null,
+ 'person_info' => $panelData['person_info'] ?? null,
+ 'nursing_entries' => $entries,
+ 'chart_date' => $chartDate,
+ 'chart_nurse' => $nurse,
+ ]);
+ }
+
+ private function requireAnyPermission(array $permissions)
+ {
+ if (! function_exists('auth')) {
+ return null;
+ }
+
+ $user = auth()->user();
+ if (! $user || ! method_exists($user, 'can')) {
+ return $this->response->setStatusCode(403)->setBody('Access denied');
+ }
+
+ foreach ($permissions as $permission) {
+ if ($user->can($permission)) {
+ return null;
+ }
+ }
+
+ return $this->response->setStatusCode(403)->setBody('Access denied');
+ }
+
+ private function resolveShiftName(string $recordedAt): string
+ {
+ $timePart = substr($recordedAt, 11, 5);
+ if (! preg_match('/^\d{2}:\d{2}$/', $timePart)) {
+ return 'Morning';
+ }
+
+ [$hour, $minute] = array_map('intval', explode(':', $timePart));
+ $minutesFromMidnight = ($hour * 60) + $minute;
+
+ if ($minutesFromMidnight >= 360 && $minutesFromMidnight < 840) {
+ return 'Morning';
+ }
+
+ if ($minutesFromMidnight >= 840 && $minutesFromMidnight < 1320) {
+ return 'Evening';
+ }
+
+ return 'Night';
+ }
+
+ private function resolveTemperatureCFromPost(): ?float
+ {
+ $temperatureF = $this->request->getPost('temperature_f');
+ if ($temperatureF !== null && $temperatureF !== '') {
+ return round((((float) $temperatureF) - 32) * 5 / 9, 2);
+ }
+
+ $temperatureC = $this->request->getPost('temperature_c');
+ if ($temperatureC !== null && $temperatureC !== '') {
+ return (float) $temperatureC;
+ }
+
+ return null;
+ }
+
+ private function normalizeScannedRecordedAt(string $value): string
+ {
+ $value = trim($value);
+ if ($value === '') {
+ return '';
+ }
+
+ $candidate = str_replace('T', ' ', $value);
+ if (preg_match('/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}(:\d{2})?$/', $candidate) === 1) {
+ return strlen($candidate) === 16 ? $candidate . ':00' : $candidate;
+ }
+
+ $ts = strtotime($value);
+ if ($ts === false) {
+ return '';
+ }
+
+ return date('Y-m-d H:i:s', $ts);
+ }
+
+ private function toNullableInt($value): ?int
+ {
+ if ($value === null || $value === '') {
+ return null;
+ }
+
+ if (! is_numeric($value)) {
+ return null;
+ }
+
+ return (int) $value;
+ }
+
+ private function toNullableFloat($value): ?float
+ {
+ if ($value === null || $value === '') {
+ return null;
+ }
+
+ if (! is_numeric($value)) {
+ return null;
+ }
+
+ return (float) $value;
+ }
+
+ private function getPatientIdFromIpd(int $ipdId): int
+ {
+ if ($ipdId <= 0 || ! $this->db->tableExists('ipd_master')) {
+ return 0;
+ }
+
+ $fields = $this->db->getFieldNames('ipd_master') ?? [];
+ if (! in_array('id', $fields, true) || ! in_array('p_id', $fields, true)) {
+ return 0;
+ }
+
+ $row = $this->db->table('ipd_master')
+ ->select('p_id')
+ ->where('id', $ipdId)
+ ->get(1)
+ ->getRowArray();
+
+ return max(0, (int) ($row['p_id'] ?? 0));
+ }
+
+ private function getPatientHistoryFlags(int $patientId, array $keys): array
+ {
+ $result = [];
+ foreach ($keys as $key) {
+ $result[$key] = 0;
+ }
+
+ if ($patientId <= 0 || ! $this->db->tableExists('patient_master')) {
+ return $result;
+ }
+
+ $fields = $this->db->getFieldNames('patient_master') ?? [];
+ if (! in_array('id', $fields, true)) {
+ return $result;
+ }
+
+ $select = ['id'];
+ foreach ($keys as $key) {
+ if (in_array($key, $fields, true)) {
+ $select[] = $key;
+ }
+ }
+
+ $row = $this->db->table('patient_master')
+ ->select(implode(',', $select))
+ ->where('id', $patientId)
+ ->get(1)
+ ->getRowArray() ?? [];
+
+ foreach ($keys as $key) {
+ if (isset($row[$key])) {
+ $result[$key] = (int) $row[$key] === 1 ? 1 : 0;
+ }
+ }
+
+ return $result;
+ }
+
+ private function extractLabeledField(string $remarks, string $label): string
+ {
+ if (preg_match('/^\s*' . preg_quote($label, '/') . '\s*:\s*(.+)$/im', $remarks, $m) !== 1) {
+ return '';
+ }
+
+ return trim((string) ($m[1] ?? ''));
+ }
+
+ private function getLatestOpdHistorySnapshot(int $patientId): array
+ {
+ $empty = [
+ 'drug_allergy_status' => '',
+ 'drug_allergy_details' => '',
+ 'adr_history' => '',
+ 'current_medications' => '',
+ 'co_morbidities' => '',
+ 'women_lmp' => '',
+ 'women_last_baby' => '',
+ 'women_pregnancy_related' => '',
+ 'women_related_problems' => '',
+ 'hpi_note' => '',
+ ];
+
+ if ($patientId <= 0 || ! $this->db->tableExists('opd_prescription')) {
+ return $empty;
+ }
+
+ $fields = $this->db->getFieldNames('opd_prescription') ?? [];
+ if (! in_array('p_id', $fields, true)) {
+ return $empty;
+ }
+
+ $row = $this->db->table('opd_prescription')
+ ->where('p_id', $patientId)
+ ->orderBy('id', 'DESC')
+ ->get(1)
+ ->getRowArray() ?? [];
+
+ if (empty($row)) {
+ return $empty;
+ }
+
+ $remarks = trim((string) ($row['Prescriber_Remarks'] ?? ''));
+ foreach (['drug_allergy_status', 'drug_allergy_details', 'adr_history', 'current_medications', 'women_lmp', 'women_last_baby', 'women_pregnancy_related', 'women_related_problems'] as $field) {
+ $value = trim((string) ($row[$field] ?? ''));
+ if ($value !== '') {
+ $empty[$field] = $value;
+ }
+ }
+
+ if ($empty['co_morbidities'] === '') {
+ $empty['co_morbidities'] = $this->extractLabeledField($remarks, 'Co-Morbidities');
+ }
+ if ($empty['hpi_note'] === '') {
+ $empty['hpi_note'] = $this->extractLabeledField($remarks, 'HPI Note');
+ }
+
+ if ($empty['drug_allergy_status'] === '') {
+ $empty['drug_allergy_status'] = $this->extractLabeledField($remarks, 'Drug Allergy Status');
+ }
+ if ($empty['drug_allergy_details'] === '') {
+ $empty['drug_allergy_details'] = $this->extractLabeledField($remarks, 'Drug Allergy Details');
+ }
+ if ($empty['adr_history'] === '') {
+ $empty['adr_history'] = $this->extractLabeledField($remarks, 'ADR History');
+ }
+ if ($empty['current_medications'] === '') {
+ $empty['current_medications'] = $this->extractLabeledField($remarks, 'Current Medications');
+ }
+
+ return $empty;
+ }
+
+ private function upsertLabeledLineInRemarks(string $remarks, string $label, string $value): string
+ {
+ $remarks = trim($remarks);
+ $pattern = '/^\s*' . preg_quote($label, '/') . '\s*:\s*.*$/im';
+ $remarks = preg_replace($pattern, '', $remarks) ?? $remarks;
+
+ $lines = array_filter(array_map(static function (string $line): string {
+ return trim($line);
+ }, preg_split('/\R/', $remarks) ?: []), static function (string $line): bool {
+ return $line !== '';
+ });
+
+ $value = trim($value);
+ if ($value !== '') {
+ $lines[] = $label . ': ' . $value;
+ }
+
+ return trim(implode(PHP_EOL, $lines));
+ }
+}
diff --git a/app/Controllers/Ipd_discharge.php b/app/Controllers/Ipd_discharge.php
index cdbc0e0..be4dc12 100644
--- a/app/Controllers/Ipd_discharge.php
+++ b/app/Controllers/Ipd_discharge.php
@@ -55,6 +55,203 @@ class Ipd_discharge extends BaseController
return (string) ($row['content'] ?? '');
}
+ private function ensureDischargeTemplateTable(): void
+ {
+ if ($this->db->tableExists('ipd_discharge_templates')) {
+ return;
+ }
+
+ $sql = "CREATE TABLE IF NOT EXISTS ipd_discharge_templates (
+ id INT NOT NULL AUTO_INCREMENT,
+ template_name VARCHAR(120) NOT NULL,
+ template_html LONGTEXT NOT NULL,
+ is_default TINYINT(1) NOT NULL DEFAULT 0,
+ status TINYINT(1) NOT NULL DEFAULT 1,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
+
+ $this->db->query($sql);
+ }
+
+ private function defaultDischargeTemplateHtml(): string
+ {
+ return '
Discharge Summary
'
+ . ''
+ . ''
+ . '| Patient: {{PATIENT_NAME}} | '
+ . 'UHID: {{UHID}} | '
+ . 'IPD: {{IPD_CODE}} | '
+ . '
'
+ . ''
+ . '| Age/Gender: {{AGE_GENDER}} | '
+ . 'Admit Date: {{ADMIT_DATE}} | '
+ . 'Discharge Date: {{DISCHARGE_DATE}} | '
+ . '
'
+ . '
'
+ . '{{CONTENT}}
';
+ }
+
+ private function nabhDischargeTemplateHtml(): string
+ {
+ return 'DISCHARGE SUMMARY
'
+ . ''
+ . ''
+ . '| Patient Name: {{PATIENT_NAME}} | '
+ . 'UHID: {{UHID}} | '
+ . 'IPD No.: {{IPD_CODE}} | '
+ . '
'
+ . ''
+ . '| Age/Gender: {{AGE_GENDER}} | '
+ . 'Date of Admission: {{ADMIT_DATE}} | '
+ . 'Date of Discharge: {{DISCHARGE_DATE}} | '
+ . '
'
+ . ''
+ . '| Prepared On: {{CURRENT_DATE}} | '
+ . '
'
+ . '
'
+ . ''
+ . 'NABH guidance note: Ensure diagnosis, procedures, clinical course, condition at discharge, medication with dose/duration, follow-up advice, red-flag signs, and emergency contact are documented.'
+ . '
'
+ . '{{CONTENT}}
'
+ . 'Counselling & Handover Confirmation
'
+ . ''
+ . '| Medication explained to patient/attendant | | Remarks: |
'
+ . '| Follow-up date and department explained | | Next Visit: __________________ |
'
+ . '| Red-flag symptoms explained | | Emergency Contact: __________________ |
'
+ . '| Diet and activity instructions explained | | |
'
+ . '
'
+ . ''
+ . ''
+ . '____________________ Consultant Name/Signature | '
+ . '____________________ Medical Officer Signature | '
+ . '____________________ Patient/Attendant Signature & Date | '
+ . '
'
+ . '
';
+ }
+
+ private function ensureDefaultDischargeTemplateSeeded(): void
+ {
+ $this->ensureDischargeTemplateTable();
+ if (! $this->db->tableExists('ipd_discharge_templates')) {
+ return;
+ }
+
+ $table = $this->db->table('ipd_discharge_templates');
+ $count = (int) ($table->countAllResults() ?? 0);
+ if ($count === 0) {
+ $table->insert([
+ 'template_name' => 'Default Discharge Template',
+ 'template_html' => $this->defaultDischargeTemplateHtml(),
+ 'is_default' => 1,
+ 'status' => 1,
+ ]);
+ }
+
+ $nabhExists = $this->db->table('ipd_discharge_templates')
+ ->where('template_name', 'NABH Compliant Discharge Summary')
+ ->get(1)
+ ->getRowArray();
+
+ if (empty($nabhExists)) {
+ $table->insert([
+ 'template_name' => 'NABH Compliant Discharge Summary',
+ 'template_html' => $this->nabhDischargeTemplateHtml(),
+ 'is_default' => 0,
+ 'status' => 1,
+ ]);
+ }
+ }
+
+ private function getDischargeTemplateRows(): array
+ {
+ $this->ensureDefaultDischargeTemplateSeeded();
+ if (! $this->db->tableExists('ipd_discharge_templates')) {
+ return [];
+ }
+
+ return $this->db->table('ipd_discharge_templates')
+ ->select('id,template_name,template_html,is_default,status')
+ ->where('status', 1)
+ ->orderBy('is_default', 'DESC')
+ ->orderBy('id', 'ASC')
+ ->get()
+ ->getResultArray();
+ }
+
+ private function applyDischargeTemplate(string $content, array $panelData, ?int $requestedTemplateId = null): array
+ {
+ $templates = $this->getDischargeTemplateRows();
+
+ $selectedTemplate = null;
+ if ($requestedTemplateId !== null && $requestedTemplateId > 0) {
+ foreach ($templates as $row) {
+ if ((int) ($row['id'] ?? 0) === $requestedTemplateId) {
+ $selectedTemplate = $row;
+ break;
+ }
+ }
+ }
+
+ if ($selectedTemplate === null) {
+ foreach ($templates as $row) {
+ if ((int) ($row['is_default'] ?? 0) === 1) {
+ $selectedTemplate = $row;
+ break;
+ }
+ }
+ }
+
+ if ($selectedTemplate === null && ! empty($templates)) {
+ $selectedTemplate = $templates[0];
+ }
+
+ $templateHtml = (string) ($selectedTemplate['template_html'] ?? '{{CONTENT}}');
+ if (trim($templateHtml) === '') {
+ $templateHtml = '{{CONTENT}}';
+ }
+
+ if (strpos($templateHtml, '{{CONTENT}}') === false) {
+ $templateHtml .= "\n{{CONTENT}}";
+ }
+
+ $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
+ ?? ''
+ ));
+
+ $age = get_age_1($person->dob ?? null, $person->age ?? '', $person->age_in_month ?? '', $person->estimate_dob ?? '');
+ $ageGender = trim($age . ' / ' . ((string) ($person->xgender ?? '')));
+
+ $replacements = [
+ '{{CONTENT}}' => $content,
+ '{{PATIENT_NAME}}' => esc($patientName),
+ '{{UHID}}' => esc($patientCode),
+ '{{IPD_CODE}}' => esc((string) ($ipd->ipd_code ?? '')),
+ '{{AGE_GENDER}}' => esc($ageGender),
+ '{{ADMIT_DATE}}' => esc((string) ($ipd->str_register_date ?? '')),
+ '{{DISCHARGE_DATE}}' => esc((string) ($ipd->str_discharge_date ?? '')),
+ '{{CURRENT_DATE}}' => esc(date('d-m-Y')),
+ ];
+
+ $rendered = strtr($templateHtml, $replacements);
+
+ return [
+ 'rendered_html' => $rendered,
+ 'templates' => $templates,
+ 'selected_template_id' => (int) ($selectedTemplate['id'] ?? 0),
+ 'selected_template_name' => (string) ($selectedTemplate['template_name'] ?? ''),
+ ];
+ }
+
private function currentUserLabel(): string
{
if (! function_exists('auth')) {
@@ -146,6 +343,209 @@ class Ipd_discharge extends BaseController
return true;
}
+ private function getCurrentUserId(): int
+ {
+ if (! function_exists('auth')) {
+ return 0;
+ }
+
+ $user = auth()->user();
+ if (! $user) {
+ return 0;
+ }
+
+ return max(0, (int) ($user->id ?? 0));
+ }
+
+ private function isNarrativeTemplateSectionAllowed(string $section): bool
+ {
+ return in_array($section, ['diagnosis_remark', 'course_remark'], true);
+ }
+
+ private function narrativeSectionTable(string $section): ?string
+ {
+ $map = [
+ 'diagnosis_remark' => 'ipd_discharge_diagnosis_remark',
+ 'course_remark' => 'ipd_discharge_course_remark',
+ ];
+
+ return $map[$section] ?? null;
+ }
+
+ private function ensureDischargeNarrativeTemplateTable(): bool
+ {
+ if ($this->db->tableExists('ipd_discharge_narrative_templates')) {
+ return true;
+ }
+
+ try {
+ $sql = "CREATE TABLE IF NOT EXISTS ipd_discharge_narrative_templates (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ doc_id INT NOT NULL DEFAULT 0,
+ section_key VARCHAR(80) NOT NULL,
+ template_name VARCHAR(120) NOT NULL,
+ template_text LONGTEXT NOT NULL,
+ is_active TINYINT(1) NOT NULL DEFAULT 1,
+ created_at DATETIME NULL,
+ updated_at DATETIME NULL,
+ UNIQUE KEY uniq_doc_section_name (doc_id, section_key, template_name),
+ INDEX idx_section_doc (section_key, doc_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
+ $this->db->query($sql);
+ } catch (\Throwable $e) {
+ return false;
+ }
+
+ return $this->db->tableExists('ipd_discharge_narrative_templates');
+ }
+
+ private function getPatientIdFromIpd(int $ipdId): int
+ {
+ if ($ipdId <= 0 || ! $this->tableHasColumns('ipd_master', ['id', 'p_id'])) {
+ return 0;
+ }
+
+ $row = $this->db->table('ipd_master')
+ ->select('p_id')
+ ->where('id', $ipdId)
+ ->get(1)
+ ->getRowArray();
+
+ return max(0, (int) ($row['p_id'] ?? 0));
+ }
+
+ public function section_past_data()
+ {
+ $section = trim((string) $this->request->getGet('section'));
+ $ipdId = (int) $this->request->getGet('ipd_id');
+
+ if ($ipdId <= 0 || ! $this->isNarrativeTemplateSectionAllowed($section)) {
+ return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Past data not found']);
+ }
+
+ $table = $this->narrativeSectionTable($section);
+ if ($table === null || ! $this->tableHasColumns($table, ['ipd_id', 'comp_remark'])) {
+ return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Past data column missing']);
+ }
+
+ $patientId = $this->getPatientIdFromIpd($ipdId);
+ if ($patientId <= 0) {
+ return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Patient not found']);
+ }
+
+ $row = $this->db->table($table . ' r')
+ ->select('r.comp_remark as section_text,r.ipd_id')
+ ->join('ipd_master m', 'm.id = r.ipd_id', 'inner')
+ ->where('m.p_id', $patientId)
+ ->where('r.ipd_id !=', $ipdId)
+ ->where('r.comp_remark IS NOT NULL', null, false)
+ ->where('r.comp_remark !=', '')
+ ->orderBy('r.id', 'DESC')
+ ->get(1)
+ ->getRowArray();
+
+ if (empty($row)) {
+ return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'No past data found']);
+ }
+
+ return $this->response->setJSON([
+ 'update' => 1,
+ 'past_text' => (string) ($row['section_text'] ?? ''),
+ 'past_ipd_id' => (int) ($row['ipd_id'] ?? 0),
+ 'error_text' => 'Past data loaded',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ public function section_template_save()
+ {
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
+ }
+
+ $section = trim((string) $this->request->getPost('section'));
+ $templateName = trim((string) $this->request->getPost('template_name'));
+ $templateText = trim((string) $this->request->getPost('template_text'));
+ $templateScope = strtolower(trim((string) $this->request->getPost('template_scope')));
+
+ if ($templateName === '' || $templateText === '') {
+ return $this->response->setJSON(['update' => 0, 'error_text' => 'Template name and text are required']);
+ }
+
+ if (! $this->isNarrativeTemplateSectionAllowed($section)) {
+ return $this->response->setJSON(['update' => 0, 'error_text' => 'Unsupported section']);
+ }
+
+ if (! $this->ensureDischargeNarrativeTemplateTable()) {
+ return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to access template storage']);
+ }
+
+ $docId = $templateScope === 'master' ? 0 : $this->getCurrentUserId();
+ $table = $this->db->table('ipd_discharge_narrative_templates');
+ $existing = $table
+ ->where('doc_id', $docId)
+ ->where('section_key', $section)
+ ->where('template_name', $templateName)
+ ->where('is_active', 1)
+ ->get(1)
+ ->getRowArray();
+
+ $now = date('Y-m-d H:i:s');
+ if (! empty($existing)) {
+ $table->where('id', (int) ($existing['id'] ?? 0))->update([
+ 'template_text' => $templateText,
+ 'updated_at' => $now,
+ ]);
+ } else {
+ $table->insert([
+ 'doc_id' => $docId,
+ 'section_key' => $section,
+ 'template_name' => $templateName,
+ 'template_text' => $templateText,
+ 'is_active' => 1,
+ 'created_at' => $now,
+ 'updated_at' => $now,
+ ]);
+ }
+
+ return $this->response->setJSON([
+ 'update' => 1,
+ 'error_text' => $docId === 0 ? 'Master template saved' : 'My template saved',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ public function section_template_list()
+ {
+ $section = trim((string) $this->request->getGet('section'));
+ if (! $this->isNarrativeTemplateSectionAllowed($section)) {
+ return $this->response->setJSON(['rows' => []]);
+ }
+
+ if (! $this->ensureDischargeNarrativeTemplateTable()) {
+ return $this->response->setJSON(['rows' => []]);
+ }
+
+ $docId = $this->getCurrentUserId();
+ $rows = $this->db->table('ipd_discharge_narrative_templates')
+ ->select('id,template_name,template_text,doc_id,section_key')
+ ->where('section_key', $section)
+ ->where('is_active', 1)
+ ->groupStart()
+ ->where('doc_id', $docId)
+ ->orWhere('doc_id', 0)
+ ->groupEnd()
+ ->orderBy('doc_id', 'DESC')
+ ->orderBy('template_name', 'ASC')
+ ->limit(100)
+ ->get()
+ ->getResultArray();
+
+ return $this->response->setJSON(['rows' => $rows]);
+ }
+
private function byIpdRows(string $table, array $columns = ['*'], string $orderBy = 'id ASC', int $ipdId = 0): array
{
if ($ipdId <= 0 || ! $this->tableHasColumns($table, ['ipd_id'])) {
@@ -223,23 +623,10 @@ class Ipd_discharge extends BaseController
?? ''
));
$patientId = (int) ($person->id ?? 0);
+ $opdHistory = $this->getLatestOpdHistorySnapshot($patientId);
$sections = [];
- $sections[] = 'IPD Discharge Summary
';
- $sections[] = ''
- . ''
- . '| IPD: ' . esc((string) ($ipd->ipd_code ?? $ipdId)) . ' | '
- . 'UHID: ' . esc($patientCode) . ' | '
- . 'Date: ' . esc(date('d-m-Y')) . ' | '
- . '
'
- . ''
- . '| Patient: ' . esc($patientName) . ' | '
- . 'Admit Date: ' . esc((string) ($ipd->str_register_date ?? '')) . ' | '
- . 'Discharge Date: ' . esc((string) ($ipd->str_discharge_date ?? '')) . ' | '
- . '
'
- . '
';
-
$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);
@@ -248,11 +635,30 @@ class Ipd_discharge extends BaseController
$sections[] = '' . nl2br(esc($complaintRemarkText)) . '
';
}
+ $complaintMeta = $this->parseComplaintMetaPayload((string) ($complaintRemark['comp_report'] ?? ''));
+ $painValue = (string) ($complaintMeta['pain_value'] ?? '');
+ $painLabel = $this->painScaleLabel($painValue);
+ if ($painLabel !== '') {
+ $sections[] = 'Pain Measurement Scale: ' . esc($painLabel) . ' (' . esc($painValue) . ')
';
+ } elseif ((string) ($opdHistory['pain_label'] ?? '') !== '') {
+ $opdPainLabel = (string) ($opdHistory['pain_label'] ?? '');
+ $opdPainValue = (string) ($opdHistory['pain_value'] ?? '');
+ $sections[] = 'Pain Measurement Scale: ' . esc($opdPainLabel)
+ . ($opdPainValue !== '' ? ' (' . esc($opdPainValue) . ')' : '')
+ . '
';
+ }
+
$physicalExamRows = $this->getPhysicalExamRows($ipdId);
- $generalRows = array_merge(
- $physicalExamRows['general_group_1'] ?? [],
- $physicalExamRows['general_group_2'] ?? []
- );
+ $generalRowsRaw = $physicalExamRows['general_all'] ?? [];
+ $generalRows = [];
+ foreach ($generalRowsRaw as $row) {
+ $value = trim((string) ($row['value'] ?? ''));
+ if ($value === '') {
+ continue;
+ }
+ $generalRows[] = $row;
+ }
+
if (! empty($generalRows)) {
$html = 'General Examination on Admission
'
. '';
@@ -266,7 +672,12 @@ class Ipd_discharge extends BaseController
$label = (string) ($row['label'] ?? '');
$value = trim((string) ($row['value'] ?? ''));
- $html .= '| ' . esc($label) . ': ' . esc($value) . ' | ';
+ $unit = trim((string) ($row['unit'] ?? ''));
+ $html .= '' . esc($label) . ': ' . esc($value);
+ if ($unit !== '') {
+ $html .= ' ' . esc($unit);
+ }
+ $html .= ' | ';
if ($col % $perRow === $perRow - 1) {
$html .= '';
@@ -328,6 +739,28 @@ class Ipd_discharge extends BaseController
$sections[] = 'Personal History
' . esc(implode(', ', $personalHistory)) . '
';
}
+ $allergyLines = [];
+ if ((string) ($opdHistory['drug_allergy_status'] ?? '') !== '') {
+ $allergyLines[] = 'Drug Allergy Status: ' . esc((string) ($opdHistory['drug_allergy_status'] ?? '')) . '
';
+ }
+ if ((string) ($opdHistory['drug_allergy_details'] ?? '') !== '') {
+ $allergyLines[] = 'Drug Allergy Details: ' . esc((string) ($opdHistory['drug_allergy_details'] ?? '')) . '
';
+ }
+ if ((string) ($opdHistory['adr_history'] ?? '') !== '') {
+ $allergyLines[] = 'ADR History: ' . esc((string) ($opdHistory['adr_history'] ?? '')) . '
';
+ }
+ if ((string) ($opdHistory['current_medications'] ?? '') !== '') {
+ $allergyLines[] = 'Current Medications: ' . esc((string) ($opdHistory['current_medications'] ?? '')) . '
';
+ }
+ if (! empty($allergyLines)) {
+ $sections[] = 'Drug Allergy / ADR
' . implode('', $allergyLines);
+ }
+
+ $coMorbText = trim((string) ($opdHistory['co_morbidities'] ?? ''));
+ if ($coMorbText !== '') {
+ $sections[] = 'Co-Morbidities
' . esc($coMorbText) . '
';
+ }
+
$admitDate = $this->normalizeDateValue((string) ($ipd->register_date ?? '')) ?? '';
$dischargeDate = $this->normalizeDateValue((string) ($ipd->discharge_date ?? '')) ?? date('Y-m-d');
$savedClinicalDates = $this->getSavedClinicalLabDates($ipdId);
@@ -463,7 +896,21 @@ class Ipd_discharge extends BaseController
$sections[] = '' . nl2br(esc($courseRemarkText)) . '
';
}
- $dischargeExamRows = $this->getMappedColRows('ipd_discharge_general_exam_col', 'ipd_discharge_1_b_final', $ipdId, 'Discharge Condition', null);
+ $nursingTrendSection = $this->buildNursingTrendSection($ipdId);
+ if ($nursingTrendSection !== '') {
+ $sections[] = $nursingTrendSection;
+ }
+
+ $dischargeExamRowsRaw = $this->getMappedColRows('ipd_discharge_general_exam_col', 'ipd_discharge_1_b_final', $ipdId, 'Discharge Condition', null);
+ $dischargeExamRows = [];
+ foreach ($dischargeExamRowsRaw as $row) {
+ $value = trim((string) ($row['value'] ?? ''));
+ if ($value === '') {
+ continue;
+ }
+ $dischargeExamRows[] = $row;
+ }
+
if (! empty($dischargeExamRows)) {
$html = 'Examination on Discharge
'
. '';
@@ -477,7 +924,12 @@ class Ipd_discharge extends BaseController
$label = (string) ($row['label'] ?? '');
$value = trim((string) ($row['value'] ?? ''));
- $html .= '| ' . esc($label) . ': ' . esc($value) . ' | ';
+ $unit = trim((string) ($row['unit'] ?? ''));
+ $html .= '' . esc($label) . ': ' . esc($value);
+ if ($unit !== '') {
+ $html .= ' ' . esc($unit);
+ }
+ $html .= ' | ';
if ($col % $perRow === $perRow - 1) {
$html .= '';
@@ -621,6 +1073,267 @@ class Ipd_discharge extends BaseController
return trim(implode("\n", $sections));
}
+ private function buildNursingTrendSection(int $ipdId): string
+ {
+ if ($ipdId <= 0 || ! $this->db->tableExists('ipd_nursing_entries')) {
+ return '';
+ }
+
+ foreach (['ipd_id', 'entry_type', 'recorded_at'] as $field) {
+ if (! $this->db->fieldExists($field, 'ipd_nursing_entries')) {
+ return '';
+ }
+ }
+
+ $rows = $this->db->table('ipd_nursing_entries')
+ ->select('entry_type,recorded_at,temperature_c,pulse_rate,resp_rate,bp_systolic,bp_diastolic,spo2,weight_kg,fluid_direction,fluid_amount_ml,treatment_text,general_note')
+ ->where('ipd_id', $ipdId)
+ ->orderBy('recorded_at', 'ASC')
+ ->orderBy('id', 'ASC')
+ ->get()
+ ->getResultArray();
+
+ if (empty($rows)) {
+ return '';
+ }
+
+ $sinceTs = strtotime('-24 hours');
+ $since = $sinceTs === false ? date('Y-m-d H:i:s') : date('Y-m-d H:i:s', $sinceTs);
+
+ $vitalsCount = 0;
+ $fluidIn = 0;
+ $fluidOut = 0;
+ $treatments = [];
+ $scannedCount = 0;
+ $latestVitalsLine = '';
+
+ foreach ($rows as $row) {
+ $recordedAt = trim((string) ($row['recorded_at'] ?? ''));
+ if ($recordedAt === '' || $recordedAt < $since) {
+ continue;
+ }
+
+ $entryType = strtolower(trim((string) ($row['entry_type'] ?? '')));
+ $note = trim((string) ($row['general_note'] ?? ''));
+ if (stripos($note, 'Source: Scanned Paper') !== false) {
+ $scannedCount++;
+ }
+
+ if ($entryType === 'vitals' || $entryType === 'admission') {
+ $vitalsCount++;
+ $parts = [];
+ if ($row['temperature_c'] !== null && $row['temperature_c'] !== '') {
+ $tempF = (((float) $row['temperature_c']) * 9 / 5) + 32;
+ $parts[] = 'Temp ' . rtrim(rtrim(number_format($tempF, 1, '.', ''), '0'), '.') . ' F';
+ }
+ if ($row['pulse_rate'] !== null && $row['pulse_rate'] !== '') {
+ $parts[] = 'Pulse ' . (string) $row['pulse_rate'];
+ }
+ if ($row['resp_rate'] !== null && $row['resp_rate'] !== '') {
+ $parts[] = 'Resp ' . (string) $row['resp_rate'];
+ }
+ if ($row['bp_systolic'] !== null && $row['bp_systolic'] !== '') {
+ $parts[] = 'BP ' . (string) $row['bp_systolic'] . '/' . (string) ($row['bp_diastolic'] ?? '');
+ }
+ if ($row['spo2'] !== null && $row['spo2'] !== '') {
+ $parts[] = 'SpO2 ' . (string) $row['spo2'] . '%';
+ }
+ if ($row['weight_kg'] !== null && $row['weight_kg'] !== '') {
+ $parts[] = 'Wt ' . (string) $row['weight_kg'] . ' kg';
+ }
+ if (! empty($parts)) {
+ $latestVitalsLine = '[' . $recordedAt . '] ' . implode(', ', $parts);
+ }
+ }
+
+ if ($entryType === 'fluid') {
+ $amount = (int) ($row['fluid_amount_ml'] ?? 0);
+ $dir = strtolower(trim((string) ($row['fluid_direction'] ?? '')));
+ if ($dir === 'output') {
+ $fluidOut += max(0, $amount);
+ } else {
+ $fluidIn += max(0, $amount);
+ }
+ }
+
+ if ($entryType === 'treatment') {
+ $text = trim((string) ($row['treatment_text'] ?? ''));
+ if ($text !== '') {
+ $treatments[] = '[' . $recordedAt . '] ' . $text;
+ }
+ }
+ }
+
+ if ($vitalsCount === 0 && $fluidIn === 0 && $fluidOut === 0 && empty($treatments) && $scannedCount === 0) {
+ return '';
+ }
+
+ $html = 'Nursing Trend Summary (Last 24 Hours)
';
+ $html .= '';
+ if ($vitalsCount > 0) {
+ $html .= '- Vitals charted entries: ' . esc((string) $vitalsCount) . '
';
+ if ($latestVitalsLine !== '') {
+ $html .= '- Latest vitals: ' . esc($latestVitalsLine) . '
';
+ }
+ }
+ if ($fluidIn > 0 || $fluidOut > 0) {
+ $html .= '- Fluid balance (approx): Intake ' . esc((string) $fluidIn) . ' ml, Output ' . esc((string) $fluidOut) . ' ml, Net ' . esc((string) ($fluidIn - $fluidOut)) . ' ml
';
+ }
+ if (! empty($treatments)) {
+ $html .= '- Key nursing treatments:
- ';
+ $html .= '
';
+ foreach (array_slice($treatments, -5) as $line) {
+ $html .= '- ' . esc($line) . '
';
+ }
+ $html .= '
';
+ }
+ if ($scannedCount > 0) {
+ $html .= '- Scanned paper-derived entries reviewed and saved: ' . esc((string) $scannedCount) . '
';
+ }
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function hasAnyNonEmptyValue(array $rows, array $fields): bool
+ {
+ foreach ($rows as $row) {
+ if (! is_array($row)) {
+ continue;
+ }
+ foreach ($fields as $field) {
+ $value = trim((string) ($row[$field] ?? ''));
+ if ($value !== '') {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private function buildNabhAuditChecklist(int $ipdId, array $panelData): array
+ {
+ $person = $panelData['person_info'] ?? null;
+ $patientId = (int) ($person->id ?? 0);
+
+ $complaintRows = $this->byIpdRows('ipd_discharge_complaint', ['comp_report', 'comp_remark'], 'id ASC', $ipdId);
+ $complaintRemark = $this->firstRowByIpd('ipd_discharge_complaint_remark', $ipdId);
+ $diagnosisRows = $this->byIpdRows('ipd_discharge_diagnosis', ['comp_report', 'comp_remark'], 'id ASC', $ipdId);
+ $diagnosisRemark = $this->firstRowByIpd('ipd_discharge_diagnosis_remark', $ipdId);
+ $courseRows = $this->byIpdRows('ipd_discharge_course', ['comp_report', 'comp_remark'], 'id ASC', $ipdId);
+ $courseRemark = $this->firstRowByIpd('ipd_discharge_course_remark', $ipdId);
+ $surgeryRows = $this->byIpdRows('ipd_discharge_surgery', ['surgery_name', 'surgery_remark'], 'id ASC', $ipdId);
+ $procedureRows = $this->byIpdRows('ipd_discharge_procedure', ['procedure_name', 'procedure_remark'], 'id ASC', $ipdId);
+ $conditionRows = $this->getMappedColRows('ipd_discharge_general_exam_col', 'ipd_discharge_1_b_final', $ipdId, 'Discharge Condition', null);
+ $drugRows = $this->byIpdRows('ipd_discharge_drug', ['drug_name', 'drug_dose', 'drug_day'], 'id ASC', $ipdId);
+ $legacyMedRows = $this->byIpdRows('ipd_discharge_prescrption_prescribed', ['med_name', 'med_type', 'qty', 'no_of_days', 'remark'], 'id ASC', $ipdId);
+ $instructionRow = $this->firstRowByIpd('ipd_discharge_instructions', $ipdId);
+ $opdSnapshot = $this->getLatestOpdHistorySnapshot($patientId);
+
+ $instructionText = trim((string) ($instructionRow['comp_remark'] ?? ''));
+ $reviewAfter = trim((string) ($instructionRow['review_after'] ?? ''));
+ $instructionLower = strtolower($instructionText);
+
+ $hasAdmissionReason = $this->hasAnyNonEmptyValue($complaintRows, ['comp_report', 'comp_remark'])
+ || trim((string) ($complaintRemark['comp_remark'] ?? '')) !== '';
+ $hasDiagnosis = $this->hasAnyNonEmptyValue($diagnosisRows, ['comp_report', 'comp_remark'])
+ || trim((string) ($diagnosisRemark['comp_remark'] ?? '')) !== '';
+ $hasCourse = $this->hasAnyNonEmptyValue($courseRows, ['comp_report', 'comp_remark'])
+ || trim((string) ($courseRemark['comp_remark'] ?? '')) !== '';
+ $hasProcedure = $this->hasAnyNonEmptyValue($surgeryRows, ['surgery_name', 'surgery_remark'])
+ || $this->hasAnyNonEmptyValue($procedureRows, ['procedure_name', 'procedure_remark']);
+ $hasCondition = $this->hasAnyNonEmptyValue($conditionRows, ['value']);
+ $hasMedication = $this->hasAnyNonEmptyValue($drugRows, ['drug_name'])
+ || $this->hasAnyNonEmptyValue($legacyMedRows, ['med_name']);
+ $hasFollowUp = $reviewAfter !== '' || $instructionText !== '';
+ $hasAllergyAdr = trim((string) ($opdSnapshot['drug_allergy_status'] ?? '')) !== ''
+ || trim((string) ($opdSnapshot['drug_allergy_details'] ?? '')) !== ''
+ || trim((string) ($opdSnapshot['adr_history'] ?? '')) !== '';
+ $hasRedFlags = strpos($instructionLower, 'emergency') !== false
+ || strpos($instructionLower, 'warning') !== false
+ || strpos($instructionLower, 'red flag') !== false
+ || strpos($instructionLower, 'immediately') !== false
+ || strpos($instructionLower, 'return if') !== false;
+
+ $items = [
+ [
+ 'key' => 'admission_reason',
+ 'label' => 'Reason for admission / presenting complaints',
+ 'ok' => $hasAdmissionReason,
+ 'critical' => true,
+ ],
+ [
+ 'key' => 'final_diagnosis',
+ 'label' => 'Final diagnosis documented',
+ 'ok' => $hasDiagnosis,
+ 'critical' => true,
+ ],
+ [
+ 'key' => 'hospital_course',
+ 'label' => 'Course / treatment in hospital documented',
+ 'ok' => $hasCourse,
+ 'critical' => true,
+ ],
+ [
+ 'key' => 'procedure_documentation',
+ 'label' => 'Surgery/procedure documented (if applicable)',
+ 'ok' => $hasProcedure,
+ 'critical' => false,
+ ],
+ [
+ 'key' => 'condition_at_discharge',
+ 'label' => 'Condition at discharge documented',
+ 'ok' => $hasCondition,
+ 'critical' => true,
+ ],
+ [
+ 'key' => 'discharge_medication',
+ 'label' => 'Discharge medication documented',
+ 'ok' => $hasMedication,
+ 'critical' => true,
+ ],
+ [
+ 'key' => 'follow_up_plan',
+ 'label' => 'Follow-up instructions / review plan documented',
+ 'ok' => $hasFollowUp,
+ 'critical' => true,
+ ],
+ [
+ 'key' => 'allergy_adr',
+ 'label' => 'Drug allergy / ADR history documented',
+ 'ok' => $hasAllergyAdr,
+ 'critical' => false,
+ ],
+ [
+ 'key' => 'red_flags',
+ '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 [
+ 'items' => $items,
+ 'ok_count' => $okCount,
+ 'total_count' => count($items),
+ 'critical_missing' => $criticalMissing,
+ 'critical_missing_count' => count($criticalMissing),
+ ];
+ }
+
private function firstRowByIpd(string $table, int $ipdId): array
{
if ($ipdId <= 0 || ! $this->tableHasColumns($table, ['ipd_id'])) {
@@ -755,14 +1468,19 @@ class Ipd_discharge extends BaseController
{
$group1 = [];
$group2 = [];
+ $generalAll = [];
$sysRows = [];
if ($this->tableHasColumns('ipd_discharge_general_exam_col', ['id', 'col_description'])) {
- $generalCols = $this->db->table('ipd_discharge_general_exam_col')
+ $generalBuilder = $this->db->table('ipd_discharge_general_exam_col')
->select('id,col_description,col_name,col_pre_value,cat_group')
- ->orderBy('id', 'ASC')
- ->get()
- ->getResultArray();
+ ->orderBy('id', 'ASC');
+
+ if ($this->db->fieldExists('col_unit', 'ipd_discharge_general_exam_col')) {
+ $generalBuilder->select('col_unit');
+ }
+
+ $generalCols = $generalBuilder->get()->getResultArray();
$rdataMap = [];
if ($this->tableHasColumns('ipd_discharge_1_b', ['ipd_d_id', 'col_id', 'rdata'])) {
@@ -785,8 +1503,11 @@ class Ipd_discharge extends BaseController
'id' => $colId,
'label' => (string) ($row['col_description'] ?? $row['col_name'] ?? ('Exam ' . $colId)),
'value' => (string) ($rdataMap[$colId] ?? (string) ($row['col_pre_value'] ?? '')),
+ 'unit' => trim((string) ($row['col_unit'] ?? '')),
];
+ $generalAll[] = $item;
+
if ((int) ($row['cat_group'] ?? 1) === 2) {
$group2[] = $item;
} else {
@@ -827,13 +1548,121 @@ class Ipd_discharge extends BaseController
}
}
+ $nursingAdmission = $this->getNursingAdmissionSnapshot($ipdId);
+ if (! empty($nursingAdmission)) {
+ $group1 = $this->applyAdmissionVitalsToGeneralRows($group1, $nursingAdmission);
+ $group2 = $this->applyAdmissionVitalsToGeneralRows($group2, $nursingAdmission);
+ $generalAll = $this->applyAdmissionVitalsToGeneralRows($generalAll, $nursingAdmission);
+ }
+
return [
'general_group_1' => $group1,
'general_group_2' => $group2,
+ 'general_all' => $generalAll,
'systemic' => $sysRows,
];
}
+ private function getNursingAdmissionSnapshot(int $ipdId): array
+ {
+ if ($ipdId <= 0 || ! $this->db->tableExists('ipd_nursing_entries')) {
+ return [];
+ }
+
+ $required = ['ipd_id', 'entry_type', 'recorded_at'];
+ foreach ($required as $field) {
+ if (! $this->db->fieldExists($field, 'ipd_nursing_entries')) {
+ return [];
+ }
+ }
+
+ $builder = $this->db->table('ipd_nursing_entries')
+ ->where('ipd_id', $ipdId)
+ ->whereIn('entry_type', ['admission', 'vitals'])
+ ->orderBy('entry_type', 'DESC')
+ ->orderBy('recorded_at', 'ASC')
+ ->orderBy('id', 'ASC');
+
+ $row = $builder->get(1)->getRowArray();
+ return is_array($row) ? $row : [];
+ }
+
+ private function celsiusToFahrenheit(?float $value): string
+ {
+ if ($value === null) {
+ return '';
+ }
+
+ $f = ($value * 9 / 5) + 32;
+ return rtrim(rtrim(number_format($f, 1, '.', ''), '0'), '.');
+ }
+
+ private function applyAdmissionVitalsToGeneralRows(array $rows, array $snapshot): array
+ {
+ if (empty($rows) || empty($snapshot)) {
+ return $rows;
+ }
+
+ $pulse = trim((string) ($snapshot['pulse_rate'] ?? ''));
+ $resp = trim((string) ($snapshot['resp_rate'] ?? ''));
+ $spo2 = trim((string) ($snapshot['spo2'] ?? ''));
+ $weight = trim((string) ($snapshot['weight_kg'] ?? ''));
+ $sbp = trim((string) ($snapshot['bp_systolic'] ?? ''));
+ $dbp = trim((string) ($snapshot['bp_diastolic'] ?? ''));
+ $bp = $sbp !== '' || $dbp !== '' ? trim($sbp . ($dbp !== '' ? '/' . $dbp : '')) : '';
+
+ $tempC = $snapshot['temperature_c'] ?? null;
+ $temp = '';
+ if ($tempC !== null && $tempC !== '') {
+ $temp = $this->celsiusToFahrenheit((float) $tempC);
+ }
+
+ foreach ($rows as &$row) {
+ $current = trim((string) ($row['value'] ?? ''));
+ if ($current !== '') {
+ continue;
+ }
+
+ $label = strtolower(trim((string) ($row['label'] ?? '')));
+ if ($label === '') {
+ continue;
+ }
+
+ if (strpos($label, 'pulse') !== false && $pulse !== '') {
+ $row['value'] = $pulse;
+ continue;
+ }
+ if ((strpos($label, 'resp') !== false || strpos($label, 'rr') !== false) && $resp !== '') {
+ $row['value'] = $resp;
+ continue;
+ }
+ if ((strpos($label, 'spo2') !== false || strpos($label, 'spo 2') !== false) && $spo2 !== '') {
+ $row['value'] = $spo2;
+ continue;
+ }
+ if ((strpos($label, 'temp') !== false || strpos($label, 'temperature') !== false) && $temp !== '') {
+ $row['value'] = $temp;
+ continue;
+ }
+ if (strpos($label, 'weight') !== false && $weight !== '') {
+ $row['value'] = $weight;
+ continue;
+ }
+ if ((strpos($label, 'bp') !== false || strpos($label, 'blood pressure') !== false) && $bp !== '') {
+ if (strpos($label, 'diastolic') !== false && $dbp !== '') {
+ $row['value'] = $dbp;
+ } elseif ((strpos($label, 'systolic') !== false || strpos($label, 'sys') !== false) && $sbp !== '') {
+ $row['value'] = $sbp;
+ } else {
+ $row['value'] = $bp;
+ }
+ }
+ }
+ unset($row);
+
+ return $rows;
+ }
+
private function getMappedColRows(
string $masterTable,
string $valueTable,
@@ -848,6 +1677,10 @@ class Ipd_discharge extends BaseController
$builder = $this->db->table($masterTable)
->select('id,col_name,col_description,col_pre_value');
+ if ($this->db->fieldExists('col_unit', $masterTable)) {
+ $builder->select('col_unit');
+ }
+
if ($catGroup !== null && $this->db->fieldExists('cat_group', $masterTable)) {
$builder->where('cat_group', $catGroup);
}
@@ -879,6 +1712,7 @@ class Ipd_discharge extends BaseController
'name' => (string) ($row['col_name'] ?? ''),
'label' => (string) ($row['col_description'] ?? $row['col_name'] ?? ($prefix . ' ' . $colId)),
'value' => (string) ($valueMap[$colId] ?? (string) ($row['col_pre_value'] ?? '')),
+ 'unit' => trim((string) ($row['col_unit'] ?? '')),
];
}
@@ -1363,6 +2197,238 @@ class Ipd_discharge extends BaseController
return $ts === false ? null : date('Y-m-d', $ts);
}
+ private function painScaleLabel(string $value): string
+ {
+ $map = [
+ '0' => 'No Pain',
+ '1' => 'Mild Pain',
+ '2' => 'Moderate',
+ '3' => 'Intense',
+ '4' => 'Worst Pain Possible',
+ ];
+
+ return $map[$value] ?? '';
+ }
+
+ private function buildComplaintMetaPayload(array $meta): string
+ {
+ $painValue = trim((string) ($meta['pain_value'] ?? ''));
+ if (! in_array($painValue, ['0', '1', '2', '3', '4'], true)) {
+ return '';
+ }
+
+ return json_encode(['pain_value' => $painValue], JSON_UNESCAPED_UNICODE) ?: '';
+ }
+
+ private function parseComplaintMetaPayload(string $raw): array
+ {
+ $raw = trim($raw);
+ if ($raw === '') {
+ return [];
+ }
+
+ $decoded = json_decode($raw, true);
+ if (! is_array($decoded)) {
+ return [];
+ }
+
+ $painValue = trim((string) ($decoded['pain_value'] ?? ''));
+ if (! in_array($painValue, ['0', '1', '2', '3', '4'], true)) {
+ $painValue = '';
+ }
+
+ return ['pain_value' => $painValue];
+ }
+
+ private function extractNabhFieldsFromRemarks(string $remarks): array
+ {
+ $extract = static function (string $pattern, string $source): string {
+ if (preg_match($pattern, $source, $m) !== 1) {
+ return '';
+ }
+
+ return trim((string) ($m[1] ?? ''));
+ };
+
+ return [
+ 'drug_allergy_status' => $extract('/^\s*Drug\s*Allergy\s*Status\s*:\s*(.+)$/im', $remarks),
+ 'drug_allergy_details' => $extract('/^\s*Drug\s*Allergy\s*Details\s*:\s*(.+)$/im', $remarks),
+ 'adr_history' => $extract('/^\s*ADR\s*History\s*:\s*(.+)$/im', $remarks),
+ 'current_medications' => $extract('/^\s*Current\s*Medications\s*:\s*(.+)$/im', $remarks),
+ 'co_morbidities' => $extract('/^\s*Co-Morbidities\s*:\s*(.+)$/im', $remarks),
+ 'women_lmp' => $extract('/^\s*Women\s*Related\s*LMP\s*:\s*(.+)$/im', $remarks),
+ 'women_last_baby' => $extract('/^\s*Women\s*Related\s*Last\s*Baby\s*:\s*(.+)$/im', $remarks),
+ 'women_pregnancy_related' => $extract('/^\s*Women\s*Related\s*Pregnancy\s*Related\s*:\s*(.+)$/im', $remarks),
+ 'women_related_problems' => $extract('/^\s*Women\s*Related\s*Problems\s*:\s*(.+)$/im', $remarks),
+ 'hpi_note' => $extract('/^\s*HPI\s*Note\s*:\s*(.+)$/im', $remarks),
+ ];
+ }
+
+ private function hydrateNabhFieldsFromOpdRow(array $row): array
+ {
+ $pick = static function (array $candidates) use ($row): string {
+ foreach ($candidates as $field) {
+ $value = trim((string) ($row[$field] ?? ''));
+ if ($value !== '') {
+ return $value;
+ }
+ }
+
+ return '';
+ };
+
+ $remarks = trim((string) ($row['Prescriber_Remarks'] ?? ''));
+ $parsed = $this->extractNabhFieldsFromRemarks($remarks);
+
+ $result = [
+ 'drug_allergy_status' => $pick(['drug_allergy_status', 'allergy_status', 'drug_allergy']),
+ 'drug_allergy_details' => $pick(['drug_allergy_details', 'allergy_details', 'drug_allergy_note', 'allergy_note']),
+ 'adr_history' => $pick(['adr_history', 'adverse_drug_reaction', 'adr_details', 'adverse_reaction_history']),
+ 'current_medications' => $pick(['current_medications', 'current_medication', 'current_medication_history', 'ongoing_medications']),
+ 'co_morbidities' => (string) ($parsed['co_morbidities'] ?? ''),
+ 'women_lmp' => $pick(['women_lmp']),
+ 'women_last_baby' => $pick(['women_last_baby']),
+ 'women_pregnancy_related' => $pick(['women_pregnancy_related']),
+ 'women_related_problems' => $pick(['women_related_problems']),
+ 'hpi_note' => (string) ($parsed['hpi_note'] ?? ''),
+ 'pain_value' => trim((string) ($row['pain_value'] ?? '')),
+ ];
+
+ foreach (['drug_allergy_status', 'drug_allergy_details', 'adr_history', 'current_medications', 'women_lmp', 'women_last_baby', 'women_pregnancy_related', 'women_related_problems'] as $key) {
+ if ($result[$key] === '' && ! empty($parsed[$key])) {
+ $result[$key] = (string) $parsed[$key];
+ }
+ }
+
+ if (! in_array($result['pain_value'], ['0', '1', '2', '3', '4'], true)) {
+ $result['pain_value'] = '';
+ }
+
+ return $result;
+ }
+
+ private function getLatestOpdHistorySnapshot(int $patientId): array
+ {
+ $empty = [
+ 'drug_allergy_status' => '',
+ 'drug_allergy_details' => '',
+ 'adr_history' => '',
+ 'current_medications' => '',
+ 'co_morbidities' => '',
+ 'women_lmp' => '',
+ 'women_last_baby' => '',
+ 'women_pregnancy_related' => '',
+ 'women_related_problems' => '',
+ 'hpi_note' => '',
+ 'pain_value' => '',
+ 'pain_label' => '',
+ ];
+
+ if ($patientId <= 0 || ! $this->db->tableExists('opd_prescription')) {
+ return $empty;
+ }
+
+ $row = $this->db->table('opd_prescription')
+ ->where('p_id', $patientId)
+ ->orderBy('id', 'DESC')
+ ->get(1)
+ ->getRowArray() ?? [];
+
+ if (empty($row)) {
+ return $empty;
+ }
+
+ $hydrated = $this->hydrateNabhFieldsFromOpdRow($row);
+
+ return [
+ 'drug_allergy_status' => (string) ($hydrated['drug_allergy_status'] ?? ''),
+ 'drug_allergy_details' => (string) ($hydrated['drug_allergy_details'] ?? ''),
+ 'adr_history' => (string) ($hydrated['adr_history'] ?? ''),
+ 'current_medications' => (string) ($hydrated['current_medications'] ?? ''),
+ 'co_morbidities' => (string) ($hydrated['co_morbidities'] ?? ''),
+ 'women_lmp' => (string) ($hydrated['women_lmp'] ?? ''),
+ 'women_last_baby' => (string) ($hydrated['women_last_baby'] ?? ''),
+ 'women_pregnancy_related' => (string) ($hydrated['women_pregnancy_related'] ?? ''),
+ 'women_related_problems' => (string) ($hydrated['women_related_problems'] ?? ''),
+ 'hpi_note' => (string) ($hydrated['hpi_note'] ?? ''),
+ 'pain_value' => (string) ($hydrated['pain_value'] ?? ''),
+ 'pain_label' => $this->painScaleLabel((string) ($hydrated['pain_value'] ?? '')),
+ ];
+ }
+
+ private function upsertLabeledLineInRemarks(string $remarks, string $label, string $value): string
+ {
+ $remarks = trim($remarks);
+ $pattern = '/^\s*' . preg_quote($label, '/') . '\s*:\s*.*$/im';
+ $remarks = preg_replace($pattern, '', $remarks) ?? $remarks;
+
+ $lines = array_filter(array_map(static function (string $line): string {
+ return trim($line);
+ }, preg_split('/\R/', $remarks) ?: []), static function (string $line): bool {
+ return $line !== '';
+ });
+
+ $value = trim($value);
+ if ($value !== '') {
+ $lines[] = $label . ': ' . $value;
+ }
+
+ return trim(implode(PHP_EOL, $lines));
+ }
+
+ private function saveNabhHistoryFromDischarge(int $patientId, array $payload): bool
+ {
+ if ($patientId <= 0 || ! $this->db->tableExists('opd_prescription')) {
+ return false;
+ }
+
+ $fields = $this->db->getFieldNames('opd_prescription') ?? [];
+ if (! in_array('p_id', $fields, true) || ! in_array('id', $fields, true)) {
+ return false;
+ }
+
+ $latest = $this->db->table('opd_prescription')
+ ->select('id,Prescriber_Remarks')
+ ->where('p_id', $patientId)
+ ->orderBy('id', 'DESC')
+ ->get(1)
+ ->getRowArray();
+
+ if (! is_array($latest) || empty($latest['id'])) {
+ return false;
+ }
+
+ $update = [];
+ foreach (['drug_allergy_status', 'drug_allergy_details', 'adr_history', 'current_medications', 'women_lmp', 'women_last_baby', 'women_pregnancy_related', 'women_related_problems'] as $field) {
+ if (in_array($field, $fields, true)) {
+ $update[$field] = trim((string) ($payload[$field] ?? ''));
+ }
+ }
+
+ if (in_array('Prescriber_Remarks', $fields, true)) {
+ $remarks = (string) ($latest['Prescriber_Remarks'] ?? '');
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'Drug Allergy Status', (string) ($payload['drug_allergy_status'] ?? ''));
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'Drug Allergy Details', (string) ($payload['drug_allergy_details'] ?? ''));
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'ADR History', (string) ($payload['adr_history'] ?? ''));
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'Current Medications', (string) ($payload['current_medications'] ?? ''));
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'Co-Morbidities', (string) ($payload['co_morbidities'] ?? ''));
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'Women Related LMP', (string) ($payload['women_lmp'] ?? ''));
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'Women Related Last Baby', (string) ($payload['women_last_baby'] ?? ''));
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'Women Related Pregnancy Related', (string) ($payload['women_pregnancy_related'] ?? ''));
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'Women Related Problems', (string) ($payload['women_related_problems'] ?? ''));
+ $remarks = $this->upsertLabeledLineInRemarks($remarks, 'HPI Note', (string) ($payload['hpi_note'] ?? ''));
+ $update['Prescriber_Remarks'] = $remarks;
+ }
+
+ if ($update === []) {
+ return false;
+ }
+
+ return (bool) $this->db->table('opd_prescription')
+ ->where('id', (int) $latest['id'])
+ ->update($update);
+ }
+
public function ipd_select(int $ipdId, int $reCreate = 0)
{
$permission = $this->requireAnyPermission([
@@ -1397,6 +2463,11 @@ class Ipd_discharge extends BaseController
if ($action === 'add_complaint') {
$complaintName = trim((string) ($this->request->getPost('new_complaint_name') ?? ''));
$complaintRemarkRow = trim((string) ($this->request->getPost('new_complaint_remark') ?? ''));
+ $complaintRemarkText = trim((string) ($this->request->getPost('complaint_remark') ?? ''));
+ $painValue = trim((string) ($this->request->getPost('pain_value') ?? ''));
+ if (! in_array($painValue, ['0', '1', '2', '3', '4'], true)) {
+ $painValue = '';
+ }
if ($complaintName !== '' && $this->tableHasColumns('ipd_discharge_complaint', ['ipd_id'])) {
$insert = [
@@ -1414,12 +2485,25 @@ class Ipd_discharge extends BaseController
$savedAny = (bool) $this->db->table('ipd_discharge_complaint')->insert($insert);
$notice = $savedAny ? 'Complaint row added.' : 'Unable to add complaint row.';
$noticeType = $savedAny ? 'success' : 'warning';
+
+ if ($this->tableHasColumns('ipd_discharge_complaint_remark', ['ipd_id'])) {
+ $savedAny = $this->upsertByIpd('ipd_discharge_complaint_remark', $ipdId, [
+ 'comp_report' => $this->buildComplaintMetaPayload(['pain_value' => $painValue]),
+ 'comp_remark' => $complaintRemarkText,
+ 'update_by' => $userLabel,
+ ]) || $savedAny;
+ }
} else {
$notice = 'Enter complaint name before adding.';
$noticeType = 'warning';
}
} elseif ($action === 'remove_complaint') {
$removeId = (int) ($this->request->getPost('complaint_remove_id') ?? 0);
+ $complaintRemarkText = trim((string) ($this->request->getPost('complaint_remark') ?? ''));
+ $painValue = trim((string) ($this->request->getPost('pain_value') ?? ''));
+ if (! in_array($painValue, ['0', '1', '2', '3', '4'], true)) {
+ $painValue = '';
+ }
if ($removeId > 0 && $this->tableHasColumns('ipd_discharge_complaint', ['id', 'ipd_id'])) {
$savedAny = (bool) $this->db->table('ipd_discharge_complaint')
->where('id', $removeId)
@@ -1427,6 +2511,14 @@ class Ipd_discharge extends BaseController
->delete();
$notice = $savedAny ? 'Complaint row removed.' : 'Unable to remove complaint row.';
$noticeType = $savedAny ? 'success' : 'warning';
+
+ if ($this->tableHasColumns('ipd_discharge_complaint_remark', ['ipd_id'])) {
+ $savedAny = $this->upsertByIpd('ipd_discharge_complaint_remark', $ipdId, [
+ 'comp_report' => $this->buildComplaintMetaPayload(['pain_value' => $painValue]),
+ 'comp_remark' => $complaintRemarkText,
+ 'update_by' => $userLabel,
+ ]) || $savedAny;
+ }
}
} elseif ($action === 'add_surgery') {
$name = trim((string) ($this->request->getPost('new_surgery_name') ?? ''));
@@ -1515,6 +2607,7 @@ class Ipd_discharge extends BaseController
} elseif ($action === 'add_diagnosis') {
$name = trim((string) ($this->request->getPost('new_diagnosis_name') ?? ''));
$remark = trim((string) ($this->request->getPost('new_diagnosis_remark') ?? ''));
+ $diagnosisRemarkText = trim((string) ($this->request->getPost('diagnosis_remark') ?? ''));
if ($name !== '' && $this->tableHasColumns('ipd_discharge_diagnosis', ['ipd_id', 'comp_report'])) {
$insert = [
'ipd_id' => $ipdId,
@@ -1529,6 +2622,14 @@ class Ipd_discharge extends BaseController
$savedAny = (bool) $this->db->table('ipd_discharge_diagnosis')->insert($insert);
$notice = $savedAny ? 'Diagnosis row added.' : 'Unable to add diagnosis row.';
$noticeType = $savedAny ? 'success' : 'warning';
+
+ if ($this->tableHasColumns('ipd_discharge_diagnosis_remark', ['ipd_id'])) {
+ $savedAny = $this->upsertByIpd('ipd_discharge_diagnosis_remark', $ipdId, [
+ 'comp_report' => '',
+ 'comp_remark' => $diagnosisRemarkText,
+ 'update_by' => $userLabel,
+ ]) || $savedAny;
+ }
} else {
$notice = $name === ''
? 'Enter diagnosis before adding.'
@@ -1537,6 +2638,7 @@ class Ipd_discharge extends BaseController
}
} elseif ($action === 'remove_diagnosis') {
$removeId = (int) ($this->request->getPost('diagnosis_remove_id') ?? 0);
+ $diagnosisRemarkText = trim((string) ($this->request->getPost('diagnosis_remark') ?? ''));
if ($removeId > 0 && $this->tableHasColumns('ipd_discharge_diagnosis', ['id', 'ipd_id'])) {
$savedAny = (bool) $this->db->table('ipd_discharge_diagnosis')
->where('id', $removeId)
@@ -1544,6 +2646,14 @@ class Ipd_discharge extends BaseController
->delete();
$notice = $savedAny ? 'Diagnosis row removed.' : 'Unable to remove diagnosis row.';
$noticeType = $savedAny ? 'success' : 'warning';
+
+ if ($this->tableHasColumns('ipd_discharge_diagnosis_remark', ['ipd_id'])) {
+ $savedAny = $this->upsertByIpd('ipd_discharge_diagnosis_remark', $ipdId, [
+ 'comp_report' => '',
+ 'comp_remark' => $diagnosisRemarkText,
+ 'update_by' => $userLabel,
+ ]) || $savedAny;
+ }
} else {
$notice = 'Select a valid diagnosis row to remove.';
$noticeType = 'warning';
@@ -1551,6 +2661,7 @@ class Ipd_discharge extends BaseController
} elseif ($action === 'add_course') {
$name = trim((string) ($this->request->getPost('new_course_name') ?? ''));
$remark = trim((string) ($this->request->getPost('new_course_remark') ?? ''));
+ $courseRemarkText = trim((string) ($this->request->getPost('course_remark') ?? ''));
if ($name !== '' && $this->tableHasColumns('ipd_discharge_course', ['ipd_id', 'comp_report'])) {
$insert = [
'ipd_id' => $ipdId,
@@ -1565,6 +2676,14 @@ class Ipd_discharge extends BaseController
$savedAny = (bool) $this->db->table('ipd_discharge_course')->insert($insert);
$notice = $savedAny ? 'Course row added.' : 'Unable to add course row.';
$noticeType = $savedAny ? 'success' : 'warning';
+
+ if ($this->tableHasColumns('ipd_discharge_course_remark', ['ipd_id'])) {
+ $savedAny = $this->upsertByIpd('ipd_discharge_course_remark', $ipdId, [
+ 'comp_report' => '',
+ 'comp_remark' => $courseRemarkText,
+ 'update_by' => $userLabel,
+ ]) || $savedAny;
+ }
} else {
$notice = $name === ''
? 'Enter course/treatment text before adding.'
@@ -1573,6 +2692,7 @@ class Ipd_discharge extends BaseController
}
} elseif ($action === 'remove_course') {
$removeId = (int) ($this->request->getPost('course_remove_id') ?? 0);
+ $courseRemarkText = trim((string) ($this->request->getPost('course_remark') ?? ''));
if ($removeId > 0 && $this->tableHasColumns('ipd_discharge_course', ['id', 'ipd_id'])) {
$savedAny = (bool) $this->db->table('ipd_discharge_course')
->where('id', $removeId)
@@ -1580,6 +2700,14 @@ class Ipd_discharge extends BaseController
->delete();
$notice = $savedAny ? 'Course row removed.' : 'Unable to remove course row.';
$noticeType = $savedAny ? 'success' : 'warning';
+
+ if ($this->tableHasColumns('ipd_discharge_course_remark', ['ipd_id'])) {
+ $savedAny = $this->upsertByIpd('ipd_discharge_course_remark', $ipdId, [
+ 'comp_report' => '',
+ 'comp_remark' => $courseRemarkText,
+ 'update_by' => $userLabel,
+ ]) || $savedAny;
+ }
} else {
$notice = 'Select a valid course row to remove.';
$noticeType = 'warning';
@@ -1646,6 +2774,10 @@ class Ipd_discharge extends BaseController
}
$complaintRemark = trim((string) ($this->request->getPost('complaint_remark') ?? ''));
+ $painValue = trim((string) ($this->request->getPost('pain_value') ?? ''));
+ if (! in_array($painValue, ['0', '1', '2', '3', '4'], true)) {
+ $painValue = '';
+ }
$diagnosisRemark = trim((string) ($this->request->getPost('diagnosis_remark') ?? ''));
$courseRemark = trim((string) ($this->request->getPost('course_remark') ?? ''));
$instructionRemark = trim((string) ($this->request->getPost('instruction_remark') ?? ''));
@@ -1653,7 +2785,7 @@ class Ipd_discharge extends BaseController
if ($complaintRemark !== '' || $this->tableHasColumns('ipd_discharge_complaint_remark', ['ipd_id'])) {
$savedAny = $this->upsertByIpd('ipd_discharge_complaint_remark', $ipdId, [
- 'comp_report' => '',
+ 'comp_report' => $this->buildComplaintMetaPayload(['pain_value' => $painValue]),
'comp_remark' => $complaintRemark,
'update_by' => $userLabel,
]) || $savedAny;
@@ -1727,26 +2859,53 @@ class Ipd_discharge extends BaseController
->get()
->getResultArray();
- foreach ($sysMaster as $sys) {
- $sid = (int) ($sys['id'] ?? 0);
- if ($sid <= 0) {
- continue;
+ $singleSystemicTextRaw = $this->request->getPost('systemic_exam_text');
+ if ($singleSystemicTextRaw !== null) {
+ $singleSystemicText = trim((string) $singleSystemicTextRaw);
+ $primarySysId = (int) ($sysMaster[0]['id'] ?? 0);
+
+ foreach ($sysMaster as $sys) {
+ $sid = (int) ($sys['id'] ?? 0);
+ if ($sid <= 0) {
+ continue;
+ }
+
+ $rowText = $sid === $primarySysId ? $singleSystemicText : '';
+ $savedAny = $this->upsertByComposite(
+ 'ipd_discharge_1_a',
+ 'ipd_d_id',
+ $ipdId,
+ 'head_id',
+ $sid,
+ [
+ 'short_head' => (string) ($sys['sys_exam_name'] ?? ('Systemic Exam ' . $sid)),
+ 'rdata' => $rowText,
+ ]
+ ) || $savedAny;
}
- $posted = $this->request->getPost('sys_exam_' . $sid);
- if ($posted === null) {
- continue;
+
+ } else {
+ foreach ($sysMaster as $sys) {
+ $sid = (int) ($sys['id'] ?? 0);
+ if ($sid <= 0) {
+ continue;
+ }
+ $posted = $this->request->getPost('sys_exam_' . $sid);
+ if ($posted === null) {
+ continue;
+ }
+ $savedAny = $this->upsertByComposite(
+ 'ipd_discharge_1_a',
+ 'ipd_d_id',
+ $ipdId,
+ 'head_id',
+ $sid,
+ [
+ 'short_head' => (string) ($sys['sys_exam_name'] ?? ('Systemic Exam ' . $sid)),
+ 'rdata' => trim((string) $posted),
+ ]
+ ) || $savedAny;
}
- $savedAny = $this->upsertByComposite(
- 'ipd_discharge_1_a',
- 'ipd_d_id',
- $ipdId,
- 'head_id',
- $sid,
- [
- 'short_head' => (string) ($sys['sys_exam_name'] ?? ('Systemic Exam ' . $sid)),
- 'rdata' => trim((string) $posted),
- ]
- ) || $savedAny;
}
}
@@ -1933,6 +3092,21 @@ class Ipd_discharge extends BaseController
}
}
+ // Save editable NABH history fields back to latest OPD history row for this patient.
+ $nabhHistoryPayload = [
+ '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') ?? '')),
+ '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') ?? '')),
+ 'hpi_note' => trim((string) ($this->request->getPost('hpi_note') ?? '')),
+ ];
+ $savedAny = $this->saveNabhHistoryFromDischarge($patientId, $nabhHistoryPayload) || $savedAny;
+
if ($savedAny) {
$notice = 'Discharge form data saved. You can now preview or regenerate summary.';
} else {
@@ -1960,6 +3134,7 @@ class Ipd_discharge extends BaseController
: [];
$complaintRemarkRow = $this->firstRowByIpd('ipd_discharge_complaint_remark', $ipdId);
+ $complaintMeta = $this->parseComplaintMetaPayload((string) ($complaintRemarkRow['comp_report'] ?? ''));
$complaintRows = $this->byIpdRows('ipd_discharge_complaint', ['id', 'comp_report', 'comp_remark'], 'id ASC', $ipdId);
$surgeryRows = $this->byIpdRows('ipd_discharge_surgery', ['id', 'surgery_name', 'surgery_date', 'surgery_remark'], 'id ASC', $ipdId);
$procedureRows = $this->byIpdRows('ipd_discharge_procedure', ['id', 'procedure_name', 'procedure_date', 'procedure_remark'], 'id ASC', $ipdId);
@@ -1983,6 +3158,8 @@ class Ipd_discharge extends BaseController
$patientHistoryRow = $patientId > 0 && $this->db->tableExists('patient_master')
? ($this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? [])
: [];
+ $opdHistorySnapshot = $this->getLatestOpdHistorySnapshot($patientId);
+ $nursingAdmissionSnapshot = $this->getNursingAdmissionSnapshot($ipdId);
$physicalExamRows = $this->getPhysicalExamRows($ipdId);
$manualInvestRows = $this->getMappedColRows('ipd_discharge_investigation_during_admit', 'ipd_discharge_1_d', $ipdId, 'Manual Exam', 1);
$specialInvestRows = $this->getMappedColRows('ipd_discharge_special_investigation', 'ipd_discharge_1_e', $ipdId, 'Special Exam', 1);
@@ -1999,6 +3176,28 @@ class Ipd_discharge extends BaseController
$labInvestigationList = implode(',', $savedClinicalDates);
$nonPathInvestigationList = implode(',', $savedNonPathIds);
+ $complaintRemarkText = (string) ($complaintRemarkRow['comp_remark'] ?? '');
+ if (trim($complaintRemarkText) === '' && ! empty($nursingAdmissionSnapshot)) {
+ $parts = [];
+ $nursingComplaint = trim((string) ($nursingAdmissionSnapshot['treatment_text'] ?? ''));
+ $nursingNote = trim((string) ($nursingAdmissionSnapshot['general_note'] ?? ''));
+ if ($nursingComplaint !== '') {
+ $parts[] = $nursingComplaint;
+ }
+ if ($nursingNote !== '') {
+ $parts[] = $nursingNote;
+ }
+ if (! empty($parts)) {
+ $complaintRemarkText = implode(PHP_EOL, $parts);
+ }
+ }
+ if (trim($complaintRemarkText) === '') {
+ $hpiFallback = trim((string) ($opdHistorySnapshot['hpi_note'] ?? ''));
+ if ($hpiFallback !== '') {
+ $complaintRemarkText = $hpiFallback;
+ }
+ }
+
return view('billing/ipd/discharge_create', [
'ipd_id' => $ipdId,
'ipd_info' => $panelData['ipd_info'] ?? null,
@@ -2023,7 +3222,10 @@ class Ipd_discharge extends BaseController
'lab_investigation_list' => $labInvestigationList,
'non_path_investigation_list' => $nonPathInvestigationList,
'discharge_condition_rows' => $dischargeConditionRows,
- 'complaint_remark' => (string) ($complaintRemarkRow['comp_remark'] ?? ''),
+ 'complaint_remark' => $complaintRemarkText,
+ 'pain_value' => (string) ($complaintMeta['pain_value'] ?? ''),
+ 'opd_history_snapshot' => $opdHistorySnapshot,
+ 'nursing_admission_snapshot' => $nursingAdmissionSnapshot,
'diagnosis_remark' => (string) ($diagnosisRemarkRow['comp_remark'] ?? ''),
'course_remark' => (string) ($courseRemarkRow['comp_remark'] ?? ''),
'instruction_remark' => (string) ($instructionRow['comp_remark'] ?? ''),
@@ -2057,17 +3259,9 @@ class Ipd_discharge extends BaseController
$noticeType = 'success';
$content = $this->getDischargeContent($ipdId);
$shouldRegenerate = (int) ($this->request->getGet('regen') ?? 0) === 1;
+ $requestedTemplateId = (int) ($this->request->getGet('tpl') ?? 0);
- if (strtolower($this->request->getMethod()) === 'post') {
- $content = (string) ($this->request->getPost('editor_Discharge_Preview') ?? '');
- if ($this->saveDischargeContent($ipdId, $content)) {
- $notice = 'Discharge summary saved.';
- $noticeType = 'success';
- } else {
- $notice = 'Unable to save discharge summary in ipd_discharge table. Please check schema/permissions.';
- $noticeType = 'danger';
- }
- } elseif ($shouldRegenerate || trim(strip_tags($content)) === '') {
+ if ($shouldRegenerate || trim(strip_tags($content)) === '') {
$generated = $this->buildAutoDischargeContent($ipdId, $panelData);
if (trim(strip_tags($generated)) !== '') {
$content = $generated;
@@ -2081,11 +3275,19 @@ class Ipd_discharge extends BaseController
}
}
+ $templatePack = $this->applyDischargeTemplate($content, $panelData, $requestedTemplateId > 0 ? $requestedTemplateId : null);
+ $nabhAudit = $this->buildNabhAuditChecklist($ipdId, $panelData);
+
return view('billing/ipd/discharge_preview', [
'ipd_id' => $ipdId,
'ipd_info' => $panelData['ipd_info'] ?? null,
'person_info' => $panelData['person_info'] ?? null,
'content' => $content,
+ 'rendered_content' => (string) ($templatePack['rendered_html'] ?? $content),
+ 'template_rows' => $templatePack['templates'] ?? [],
+ 'selected_template_id' => (int) ($templatePack['selected_template_id'] ?? 0),
+ 'selected_template_name' => (string) ($templatePack['selected_template_name'] ?? ''),
+ 'nabh_audit' => $nabhAudit,
'notice' => $notice,
'notice_type' => $noticeType,
]);
@@ -2119,11 +3321,16 @@ class Ipd_discharge extends BaseController
}
}
+ $requestedTemplateId = (int) ($this->request->getGet('tpl') ?? 0);
+ $templatePack = $this->applyDischargeTemplate($content, $panelData, $requestedTemplateId > 0 ? $requestedTemplateId : null);
+
return view('billing/ipd/discharge_print', [
'ipd_id' => $ipdId,
'ipd_info' => $panelData['ipd_info'] ?? null,
'person_info' => $panelData['person_info'] ?? null,
- 'content' => $content,
+ 'content' => (string) ($templatePack['rendered_html'] ?? $content),
+ 'selected_template_name' => (string) ($templatePack['selected_template_name'] ?? ''),
+ 'selected_template_id' => (int) ($templatePack['selected_template_id'] ?? 0),
'print_type' => $printType,
'print_mode' => 'standard',
]);
diff --git a/app/Controllers/Setting/NursingBedsideItems.php b/app/Controllers/Setting/NursingBedsideItems.php
new file mode 100644
index 0000000..c811c82
--- /dev/null
+++ b/app/Controllers/Setting/NursingBedsideItems.php
@@ -0,0 +1,167 @@
+model = new NursingBedsideItemModel();
+ }
+
+ public function index()
+ {
+ return view('Setting/Charges/Nursing_Bedside/item_search_V', [
+ 'rows' => $this->model->getMasterList(),
+ 'insurance_list' => $this->model->getInsuranceList(),
+ ]);
+ }
+
+ public function get(int $itemId)
+ {
+ return $this->response->setJSON([
+ 'ok' => 1,
+ 'row' => $this->model->find($itemId),
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ public function save()
+ {
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'message' => 'Invalid request']);
+ }
+
+ $itemId = (int) ($this->request->getPost('item_id') ?? 0);
+ $itemName = trim((string) ($this->request->getPost('item_name') ?? ''));
+ $itemType = trim((string) ($this->request->getPost('item_type') ?? ''));
+
+ if ($itemName === '' || $itemType === '') {
+ return $this->response->setStatusCode(422)->setJSON([
+ 'ok' => 0,
+ 'message' => 'Item Name and Item Type are required',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ $userId = null;
+ if (function_exists('auth')) {
+ $user = auth()->user();
+ if ($user) {
+ $userId = (int) ($user->id ?? 0);
+ }
+ }
+
+ $data = [
+ 'item_code' => trim((string) ($this->request->getPost('item_code') ?? '')) ?: null,
+ 'item_name' => $itemName,
+ 'item_type' => $itemType,
+ 'category' => trim((string) ($this->request->getPost('category') ?? '')) ?: null,
+ 'default_rate' => (float) ($this->request->getPost('default_rate') ?? 0),
+ 'unit' => trim((string) ($this->request->getPost('unit') ?? 'Unit')),
+ 'description' => trim((string) ($this->request->getPost('description') ?? '')) ?: null,
+ 'is_active' => (int) ($this->request->getPost('is_active') ?? 0) === 1 ? 1 : 0,
+ 'is_billable' => (int) ($this->request->getPost('is_billable') ?? 0) === 1 ? 1 : 0,
+ 'updated_by' => $userId,
+ ];
+
+ if ($itemId > 0) {
+ $saved = $this->model->update($itemId, $data);
+ $savedItemId = $itemId;
+ } else {
+ $data['created_by'] = $userId;
+ $savedItemId = (int) $this->model->insert($data);
+ $saved = $savedItemId > 0;
+ }
+
+ return $this->response->setJSON([
+ 'ok' => $saved ? 1 : 0,
+ 'message' => $saved ? 'Saved successfully' : 'Unable to save',
+ 'item_id' => $saved ? $savedItemId : 0,
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ public function insuranceList(int $itemId)
+ {
+ return $this->response->setJSON([
+ 'ok' => 1,
+ 'html' => view('Setting/Charges/Nursing_Bedside/item_insurance_list', [
+ 'rows' => $this->model->getInsuranceRatesForItem($itemId),
+ ]),
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ public function insuranceAdd()
+ {
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'message' => 'Invalid request']);
+ }
+
+ $itemId = (int) ($this->request->getPost('item_id') ?? 0);
+ $insuranceId = (int) ($this->request->getPost('insurance_id') ?? 0);
+ $amount = (float) ($this->request->getPost('amount') ?? 0);
+ $code = trim((string) ($this->request->getPost('code') ?? ''));
+
+ if ($itemId <= 0 || $insuranceId <= 0) {
+ return $this->response->setStatusCode(422)->setJSON([
+ 'ok' => 0,
+ 'message' => 'Select item and insurance company',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ $saved = $this->model->upsertInsuranceRate($itemId, $insuranceId, $amount, $code);
+
+ return $this->response->setJSON([
+ 'ok' => $saved ? 1 : 0,
+ 'message' => $saved ? 'Insurance rate saved' : 'Unable to save insurance rate',
+ 'html' => view('Setting/Charges/Nursing_Bedside/item_insurance_list', [
+ 'rows' => $this->model->getInsuranceRatesForItem($itemId),
+ ]),
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ public function insuranceRemove()
+ {
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'message' => 'Invalid request']);
+ }
+
+ $mappingId = (int) ($this->request->getPost('mapping_id') ?? 0);
+ $itemId = (int) ($this->request->getPost('item_id') ?? 0);
+ if ($mappingId <= 0 || $itemId <= 0) {
+ return $this->response->setStatusCode(422)->setJSON([
+ 'ok' => 0,
+ 'message' => 'Invalid insurance mapping',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ $removed = $this->model->removeInsuranceRate($mappingId);
+
+ return $this->response->setJSON([
+ 'ok' => $removed ? 1 : 0,
+ 'message' => $removed ? 'Insurance rate removed' : 'Unable to remove insurance rate',
+ 'html' => view('Setting/Charges/Nursing_Bedside/item_insurance_list', [
+ 'rows' => $this->model->getInsuranceRatesForItem($itemId),
+ ]),
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+}
diff --git a/app/Database/Migrations/2026-03-05-000003_CreateIpdNursingEntriesTable.php b/app/Database/Migrations/2026-03-05-000003_CreateIpdNursingEntriesTable.php
new file mode 100644
index 0000000..1b4c3fc
--- /dev/null
+++ b/app/Database/Migrations/2026-03-05-000003_CreateIpdNursingEntriesTable.php
@@ -0,0 +1,131 @@
+db->tableExists('ipd_nursing_entries')) {
+ return;
+ }
+
+ $this->forge->addField([
+ 'id' => [
+ 'type' => 'INT',
+ 'constraint' => 11,
+ 'unsigned' => true,
+ 'auto_increment' => true,
+ ],
+ 'ipd_id' => [
+ 'type' => 'INT',
+ 'constraint' => 11,
+ 'unsigned' => true,
+ 'null' => false,
+ ],
+ 'entry_type' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 20,
+ 'null' => false,
+ ],
+ 'recorded_at' => [
+ 'type' => 'DATETIME',
+ 'null' => false,
+ ],
+ 'shift_name' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 20,
+ 'null' => true,
+ ],
+ 'temperature_c' => [
+ 'type' => 'DECIMAL',
+ 'constraint' => '5,2',
+ 'null' => true,
+ ],
+ 'pulse_rate' => [
+ 'type' => 'SMALLINT',
+ 'constraint' => 5,
+ 'null' => true,
+ ],
+ 'resp_rate' => [
+ 'type' => 'SMALLINT',
+ 'constraint' => 5,
+ 'null' => true,
+ ],
+ 'bp_systolic' => [
+ 'type' => 'SMALLINT',
+ 'constraint' => 5,
+ 'null' => true,
+ ],
+ 'bp_diastolic' => [
+ 'type' => 'SMALLINT',
+ 'constraint' => 5,
+ 'null' => true,
+ ],
+ 'spo2' => [
+ 'type' => 'SMALLINT',
+ 'constraint' => 5,
+ 'null' => true,
+ ],
+ 'weight_kg' => [
+ 'type' => 'DECIMAL',
+ 'constraint' => '6,2',
+ 'null' => true,
+ ],
+ 'fluid_direction' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 10,
+ 'null' => true,
+ ],
+ 'fluid_route' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 30,
+ 'null' => true,
+ ],
+ 'fluid_amount_ml' => [
+ 'type' => 'INT',
+ 'constraint' => 11,
+ 'null' => true,
+ ],
+ 'treatment_text' => [
+ 'type' => 'TEXT',
+ 'null' => true,
+ ],
+ 'general_note' => [
+ 'type' => 'TEXT',
+ 'null' => true,
+ ],
+ 'recorded_by' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 100,
+ 'null' => true,
+ ],
+ 'recorded_by_id' => [
+ 'type' => 'INT',
+ 'constraint' => 11,
+ 'null' => true,
+ ],
+ 'created_at' => [
+ 'type' => 'DATETIME',
+ 'null' => false,
+ ],
+ 'updated_at' => [
+ 'type' => 'DATETIME',
+ 'null' => false,
+ ],
+ ]);
+
+ $this->forge->addKey('id', true);
+ $this->forge->addKey('ipd_id');
+ $this->forge->addKey('entry_type');
+ $this->forge->addKey('recorded_at');
+ $this->forge->createTable('ipd_nursing_entries', true);
+ }
+
+ public function down()
+ {
+ $this->forge->dropTable('ipd_nursing_entries', true);
+ }
+}
diff --git a/app/Database/Migrations/2026-03-06-000001_CreateNursingBedsideItemsInsuranceTable.php b/app/Database/Migrations/2026-03-06-000001_CreateNursingBedsideItemsInsuranceTable.php
new file mode 100644
index 0000000..06475cc
--- /dev/null
+++ b/app/Database/Migrations/2026-03-06-000001_CreateNursingBedsideItemsInsuranceTable.php
@@ -0,0 +1,72 @@
+db->tableExists('nursing_bedside_items_insurance')) {
+ return;
+ }
+
+ $this->forge->addField([
+ 'id' => [
+ 'type' => 'INT',
+ 'constraint' => 11,
+ 'unsigned' => true,
+ 'auto_increment' => true,
+ ],
+ 'bedside_item_id' => [
+ 'type' => 'INT',
+ 'constraint' => 11,
+ 'unsigned' => true,
+ 'null' => false,
+ ],
+ 'hc_insurance_id' => [
+ 'type' => 'INT',
+ 'constraint' => 11,
+ 'unsigned' => true,
+ 'null' => false,
+ ],
+ 'amount1' => [
+ 'type' => 'DECIMAL',
+ 'constraint' => '10,2',
+ 'null' => false,
+ 'default' => '0.00',
+ ],
+ 'code' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 100,
+ 'null' => true,
+ ],
+ 'isdelete' => [
+ 'type' => 'TINYINT',
+ 'constraint' => 1,
+ 'null' => false,
+ 'default' => 0,
+ ],
+ 'created_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ 'updated_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ ]);
+
+ $this->forge->addKey('id', true);
+ $this->forge->addKey('bedside_item_id');
+ $this->forge->addKey('hc_insurance_id');
+ $this->forge->addKey(['bedside_item_id', 'hc_insurance_id']);
+ $this->forge->createTable('nursing_bedside_items_insurance', true);
+ }
+
+ public function down()
+ {
+ $this->forge->dropTable('nursing_bedside_items_insurance', true);
+ }
+}
diff --git a/app/Libraries/NursingScanExtractionService.php b/app/Libraries/NursingScanExtractionService.php
new file mode 100644
index 0000000..f5a5b38
--- /dev/null
+++ b/app/Libraries/NursingScanExtractionService.php
@@ -0,0 +1,297 @@
+extractTextUsingAzureDocumentIntelligence($uploadedPath, $uploaded['mime'] ?? '');
+ if ($azureText !== '') {
+ $text = $azureText;
+ } else {
+ $warnings[] = 'No OCR text extracted. You can paste OCR text manually and rescan.';
+ }
+ }
+
+ $rows = $this->parseRows($docType, $text);
+
+ return [
+ 'scan_ref' => $scanRef,
+ 'source_text' => $text,
+ 'rows' => $rows,
+ 'warnings' => $warnings,
+ ];
+ }
+
+ private function parseRows(string $docType, string $text): array
+ {
+ $normalized = strtolower(trim($docType));
+ if (! in_array($normalized, ['vitals', 'fluid', 'treatment', 'auto'], true)) {
+ $normalized = 'auto';
+ }
+
+ if ($normalized === 'auto') {
+ $hint = strtolower($text);
+ if (preg_match('/\b(intake|output|i\/?o|fluid)\b/i', $hint) === 1) {
+ $normalized = 'fluid';
+ } elseif (preg_match('/\b(pulse|bp|spo2|temperature|temp|resp|rr)\b/i', $hint) === 1) {
+ $normalized = 'vitals';
+ } else {
+ $normalized = 'treatment';
+ }
+ }
+
+ if ($normalized === 'vitals') {
+ return [$this->parseVitalsRow($text)];
+ }
+
+ if ($normalized === 'fluid') {
+ return [$this->parseFluidRow($text)];
+ }
+
+ return [$this->parseTreatmentRow($text)];
+ }
+
+ private function parseVitalsRow(string $text): array
+ {
+ return [
+ 'entry_type' => 'vitals',
+ 'recorded_at' => $this->extractDateTime($text),
+ 'temperature_f' => $this->extractNumber($text, '/(?:temp|temperature)\s*[:=-]?\s*(\d{2,3}(?:\.\d+)?)/i'),
+ 'pulse_rate' => $this->extractNumber($text, '/(?:pulse|pr)\s*[:=-]?\s*(\d{2,3})/i'),
+ 'resp_rate' => $this->extractNumber($text, '/(?:resp|rr)\s*[:=-]?\s*(\d{1,3})/i'),
+ 'bp_systolic' => $this->extractBloodPressure($text, 0),
+ 'bp_diastolic' => $this->extractBloodPressure($text, 1),
+ 'spo2' => $this->extractNumber($text, '/(?:spo2|spo\s*2)\s*[:=-]?\s*(\d{2,3})/i'),
+ 'weight_kg' => $this->extractNumber($text, '/(?:weight|wt)\s*[:=-]?\s*(\d{1,3}(?:\.\d+)?)/i'),
+ 'general_note' => trim($text),
+ 'confidence' => 0.65,
+ ];
+ }
+
+ private function parseFluidRow(string $text): array
+ {
+ $direction = 'intake';
+ if (preg_match('/\b(output|urine|drain|vomit|stool)\b/i', $text) === 1) {
+ $direction = 'output';
+ }
+
+ return [
+ 'entry_type' => 'fluid',
+ 'recorded_at' => $this->extractDateTime($text),
+ 'fluid_direction' => $direction,
+ 'fluid_route' => $this->extractWord($text, '/(?:route)\s*[:=-]?\s*([a-zA-Z\s\/]{2,40})/i'),
+ 'fluid_amount_ml' => $this->extractNumber($text, '/(\d{1,5})\s*(?:ml|mL|cc)\b/i'),
+ 'general_note' => trim($text),
+ 'confidence' => 0.65,
+ ];
+ }
+
+ private function parseTreatmentRow(string $text): array
+ {
+ return [
+ 'entry_type' => 'treatment',
+ 'recorded_at' => $this->extractDateTime($text),
+ 'treatment_text' => trim($text),
+ 'general_note' => trim($text),
+ 'confidence' => 0.6,
+ ];
+ }
+
+ private function extractDateTime(string $text): string
+ {
+ if (preg_match('/(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2})/', $text, $m) === 1) {
+ return $m[1] . 'T' . $m[2];
+ }
+
+ if (preg_match('/(\d{2}[\/\-]\d{2}[\/\-]\d{4})\s+(\d{1,2}:\d{2})/', $text, $m) === 1) {
+ $date = str_replace('/', '-', $m[1]);
+ $ts = strtotime($date . ' ' . $m[2]);
+ if ($ts !== false) {
+ return date('Y-m-d\TH:i', $ts);
+ }
+ }
+
+ return date('Y-m-d\TH:i');
+ }
+
+ private function extractNumber(string $text, string $pattern): ?string
+ {
+ if (preg_match($pattern, $text, $m) !== 1) {
+ return null;
+ }
+
+ return trim((string) ($m[1] ?? ''));
+ }
+
+ private function extractWord(string $text, string $pattern): string
+ {
+ if (preg_match($pattern, $text, $m) !== 1) {
+ return '';
+ }
+
+ return trim((string) ($m[1] ?? ''));
+ }
+
+ private function extractBloodPressure(string $text, int $index): ?string
+ {
+ if (preg_match('/(?:bp|blood\s*pressure)\s*[:=-]?\s*(\d{2,3})\s*[\/\\-]\s*(\d{2,3})/i', $text, $m) !== 1) {
+ return null;
+ }
+
+ return $index === 0 ? (string) ($m[1] ?? '') : (string) ($m[2] ?? '');
+ }
+
+ private function extractTextUsingAzureDocumentIntelligence(string $path, string $mime): string
+ {
+ $endpoint = rtrim((string) env('azure.docintel.endpoint', ''), '/');
+ $apiKey = trim((string) env('azure.docintel.api_key', ''));
+ $model = trim((string) env('azure.docintel.model', 'prebuilt-read'));
+ $apiVersion = trim((string) env('azure.docintel.api_version', '2024-11-30'));
+
+ if ($endpoint === '' || $apiKey === '') {
+ return '';
+ }
+
+ $content = @file_get_contents($path);
+ if (! is_string($content) || $content === '') {
+ return '';
+ }
+
+ $url = $endpoint . '/documentintelligence/documentModels/' . rawurlencode($model) . ':analyze?api-version=' . rawurlencode($apiVersion);
+
+ $headers = [
+ 'Ocp-Apim-Subscription-Key: ' . $apiKey,
+ 'Content-Type: ' . ($mime !== '' ? $mime : 'application/octet-stream'),
+ ];
+
+ $result = $this->sendCurlRequest($url, 'POST', $headers, $content, true);
+ if ($result['http'] < 200 || $result['http'] >= 300) {
+ return '';
+ }
+
+ $operation = '';
+ foreach ($result['headers'] as $key => $value) {
+ if (strtolower($key) === 'operation-location') {
+ $operation = $value;
+ break;
+ }
+ }
+
+ if ($operation === '') {
+ return '';
+ }
+
+ $readText = '';
+ for ($i = 0; $i < 10; $i++) {
+ usleep(700000);
+ $poll = $this->sendCurlRequest($operation, 'GET', [
+ 'Ocp-Apim-Subscription-Key: ' . $apiKey,
+ ], null, false);
+
+ if ($poll['http'] < 200 || $poll['http'] >= 300) {
+ continue;
+ }
+
+ $body = json_decode((string) $poll['body'], true);
+ if (! is_array($body)) {
+ continue;
+ }
+
+ $status = strtolower((string) ($body['status'] ?? ''));
+ if ($status === 'succeeded') {
+ $lines = [];
+ $items = $body['analyzeResult']['content'] ?? '';
+ if (is_string($items) && trim($items) !== '') {
+ $readText = trim($items);
+ } elseif (isset($body['analyzeResult']['pages']) && is_array($body['analyzeResult']['pages'])) {
+ foreach ($body['analyzeResult']['pages'] as $page) {
+ foreach (($page['lines'] ?? []) as $line) {
+ $txt = trim((string) ($line['content'] ?? ''));
+ if ($txt !== '') {
+ $lines[] = $txt;
+ }
+ }
+ }
+ $readText = trim(implode("\n", $lines));
+ }
+ break;
+ }
+
+ if ($status === 'failed') {
+ break;
+ }
+ }
+
+ return $readText;
+ }
+
+ private function sendCurlRequest(string $url, string $method, array $headers, ?string $body, bool $includeHeaders): array
+ {
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 45);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
+ curl_setopt($ch, CURLOPT_HEADER, $includeHeaders);
+
+ if ($body !== null) {
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
+ }
+
+ $raw = curl_exec($ch);
+ $http = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
+ $headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE);
+ curl_close($ch);
+
+ $responseHeaders = [];
+ $responseBody = is_string($raw) ? $raw : '';
+ if ($includeHeaders && is_string($raw)) {
+ $headerPart = substr($raw, 0, $headerSize);
+ $responseBody = substr($raw, $headerSize);
+ $lines = preg_split('/\r\n|\n|\r/', $headerPart) ?: [];
+ foreach ($lines as $line) {
+ $parts = explode(':', $line, 2);
+ if (count($parts) !== 2) {
+ continue;
+ }
+ $name = trim($parts[0]);
+ $value = trim($parts[1]);
+ if ($name !== '') {
+ $responseHeaders[$name] = $value;
+ }
+ }
+ }
+
+ return [
+ 'http' => $http,
+ 'headers' => $responseHeaders,
+ 'body' => $responseBody,
+ ];
+ }
+}
diff --git a/app/Models/IpdNursingEntryModel.php b/app/Models/IpdNursingEntryModel.php
new file mode 100644
index 0000000..acb70d0
--- /dev/null
+++ b/app/Models/IpdNursingEntryModel.php
@@ -0,0 +1,62 @@
+builder()
+ ->where('ipd_id', $ipdId)
+ ->orderBy('recorded_at', 'DESC')
+ ->orderBy('id', 'DESC')
+ ->limit($limit)
+ ->get()
+ ->getResultArray();
+ }
+
+ public function getByIpdPeriod(int $ipdId, string $startDateTime, string $endDateTime, string $nurse = ''): array
+ {
+ $builder = $this->builder()
+ ->where('ipd_id', $ipdId)
+ ->where('recorded_at >=', $startDateTime)
+ ->where('recorded_at <=', $endDateTime)
+ ->orderBy('recorded_at', 'ASC')
+ ->orderBy('id', 'ASC');
+
+ if ($nurse !== '') {
+ $builder->where('recorded_by', $nurse);
+ }
+
+ return $builder->get()->getResultArray();
+ }
+}
diff --git a/app/Models/NursingBedsideItemModel.php b/app/Models/NursingBedsideItemModel.php
new file mode 100644
index 0000000..ad7756d
--- /dev/null
+++ b/app/Models/NursingBedsideItemModel.php
@@ -0,0 +1,199 @@
+db->table('nursing_bedside_items b')
+ ->select('b.item_id, b.item_code, b.item_name, b.item_type, b.category, b.default_rate, b.unit')
+ ->where('b.is_active', 1)
+ ->where('b.is_billable', 1)
+ ->orderBy('b.category', 'ASC')
+ ->orderBy('b.item_name', 'ASC');
+
+ if ($insuranceId > 0 && $this->insuranceTableExists()) {
+ $builder->select('i.amount1 as insurance_rate, i.code as insurance_code');
+ $builder->join(
+ 'nursing_bedside_items_insurance i',
+ 'i.bedside_item_id = b.item_id and i.hc_insurance_id = ' . (int) $insuranceId . ' and i.isdelete = 0',
+ 'left'
+ );
+ }
+
+ $rows = $builder->get()->getResultArray();
+
+ foreach ($rows as &$row) {
+ if (isset($row['insurance_rate']) && $row['insurance_rate'] !== null) {
+ $row['default_rate'] = (float) $row['insurance_rate'];
+ }
+ if (! isset($row['insurance_code'])) {
+ $row['insurance_code'] = null;
+ }
+ }
+ unset($row);
+
+ $grouped = [];
+ foreach ($rows as $row) {
+ $category = trim((string) ($row['category'] ?? ''));
+ if ($category === '') {
+ $category = 'General';
+ }
+ if (! isset($grouped[$category])) {
+ $grouped[$category] = [];
+ }
+ $grouped[$category][] = $row;
+ }
+
+ return $grouped;
+ }
+
+ public function getMasterList(): array
+ {
+ return $this->builder()
+ ->select('*')
+ ->orderBy('category', 'ASC')
+ ->orderBy('item_name', 'ASC')
+ ->get()
+ ->getResultArray();
+ }
+
+ public function getInsuranceList(): array
+ {
+ return $this->db->table('hc_insurance')
+ ->select('id, ins_company_name')
+ ->where('id >', 1)
+ ->orderBy('ins_company_name', 'ASC')
+ ->get()
+ ->getResultArray();
+ }
+
+ public function getInsuranceRatesForItem(int $itemId): array
+ {
+ if (! $this->insuranceTableExists()) {
+ return [];
+ }
+
+ $latestSubQuery = $this->db->table('nursing_bedside_items_insurance')
+ ->select('MAX(id) AS id')
+ ->where('bedside_item_id', $itemId)
+ ->where('isdelete', 0)
+ ->groupBy('hc_insurance_id')
+ ->getCompiledSelect();
+
+ return $this->db->table('nursing_bedside_items_insurance i')
+ ->select('i.id, i.bedside_item_id, i.hc_insurance_id, i.amount1, i.code, c.ins_company_name')
+ ->join('hc_insurance c', 'c.id = i.hc_insurance_id', 'left')
+ ->where('i.id IN (' . $latestSubQuery . ')', null, false)
+ ->orderBy('c.ins_company_name', 'ASC')
+ ->get()
+ ->getResultArray();
+ }
+
+ public function upsertInsuranceRate(int $itemId, int $insuranceId, float $amount, string $code): bool
+ {
+ if (! $this->insuranceTableExists()) {
+ return false;
+ }
+
+ $existing = $this->db->table('nursing_bedside_items_insurance')
+ ->where('bedside_item_id', $itemId)
+ ->where('hc_insurance_id', $insuranceId)
+ ->where('isdelete', 0)
+ ->get()
+ ->getRowArray();
+
+ if (! empty($existing)) {
+ return (bool) $this->db->table('nursing_bedside_items_insurance')
+ ->where('id', (int) $existing['id'])
+ ->update([
+ 'amount1' => $amount,
+ 'code' => $code,
+ 'updated_at' => date('Y-m-d H:i:s'),
+ ]);
+ }
+
+ return (bool) $this->db->table('nursing_bedside_items_insurance')->insert([
+ 'bedside_item_id' => $itemId,
+ 'hc_insurance_id' => $insuranceId,
+ 'amount1' => $amount,
+ 'code' => $code,
+ 'isdelete' => 0,
+ 'created_at' => date('Y-m-d H:i:s'),
+ 'updated_at' => date('Y-m-d H:i:s'),
+ ]);
+ }
+
+ public function removeInsuranceRate(int $mappingId): bool
+ {
+ if (! $this->insuranceTableExists()) {
+ return false;
+ }
+
+ return (bool) $this->db->table('nursing_bedside_items_insurance')
+ ->where('id', $mappingId)
+ ->update([
+ 'isdelete' => 1,
+ 'updated_at' => date('Y-m-d H:i:s'),
+ ]);
+ }
+
+ public function getBillableItemForInsurance(int $itemId, int $insuranceId): ?array
+ {
+ $builder = $this->db->table('nursing_bedside_items b');
+ $builder->select([
+ 'b.item_id',
+ 'b.item_code',
+ 'b.item_name',
+ 'b.item_type',
+ 'b.category',
+ 'b.default_rate',
+ 'b.unit',
+ 'b.is_active',
+ 'b.is_billable',
+ ]);
+ if ($insuranceId > 0 && $this->insuranceTableExists()) {
+ $builder->select('i.amount1 as insurance_rate, i.code as insurance_code');
+ $builder->join(
+ 'nursing_bedside_items_insurance i',
+ 'i.bedside_item_id = b.item_id and i.hc_insurance_id = ' . (int) $insuranceId . ' and i.isdelete = 0',
+ 'left'
+ );
+ } else {
+ $builder->select('NULL as insurance_rate, NULL as insurance_code', false);
+ }
+ $builder->where('b.item_id', $itemId);
+ $builder->where('b.is_active', 1);
+ $builder->where('b.is_billable', 1);
+
+ $row = $builder->get()->getRowArray();
+
+ return empty($row) ? null : $row;
+ }
+
+ private function insuranceTableExists(): bool
+ {
+ return $this->db->tableExists('nursing_bedside_items_insurance');
+ }
+}
diff --git a/app/Views/Setting/Charges/Nursing_Bedside/item_insurance_list.php b/app/Views/Setting/Charges/Nursing_Bedside/item_insurance_list.php
new file mode 100644
index 0000000..a5b63ef
--- /dev/null
+++ b/app/Views/Setting/Charges/Nursing_Bedside/item_insurance_list.php
@@ -0,0 +1,27 @@
+
+
+
+
+ | Company Name |
+ Rate |
+ Code |
+ |
+
+
+
+
+ | No insurance-specific rate added. |
+
+
+
+ | = esc((string) ($row['ins_company_name'] ?? '')) ?> |
+ = esc(number_format((float) ($row['amount1'] ?? 0), 2)) ?> |
+ = esc((string) ($row['code'] ?? '')) ?> |
+
+
+ |
+
+
+
+
+
diff --git a/app/Views/Setting/Charges/Nursing_Bedside/item_search_V.php b/app/Views/Setting/Charges/Nursing_Bedside/item_search_V.php
new file mode 100644
index 0000000..ca00b6c
--- /dev/null
+++ b/app/Views/Setting/Charges/Nursing_Bedside/item_search_V.php
@@ -0,0 +1,296 @@
+
+
+
+
+
+
+
+
+ | Code |
+ Item Name |
+ Type |
+ Category |
+ Rate |
+ Unit |
+ Billable |
+ Active |
+ |
+
+
+
+
+
+ | = esc((string) ($row['item_code'] ?? '')) ?> |
+ = esc((string) ($row['item_name'] ?? '')) ?> |
+ = esc((string) ($row['item_type'] ?? '')) ?> |
+ = esc((string) ($row['category'] ?? '')) ?> |
+ = esc(number_format((float) ($row['default_rate'] ?? 0), 2)) ?> |
+ = esc((string) ($row['unit'] ?? '')) ?> |
+ = (int) ($row['is_billable'] ?? 0) === 1 ? 'Yes' : 'No' ?> |
+ = (int) ($row['is_active'] ?? 0) === 1 ? 'Yes' : 'No' ?> |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Insurance-specific Rate & Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= view('Setting/Charges/Nursing_Bedside/item_insurance_list', ['rows' => []]) ?>
+
+
+
+
+
+
+
+
+
diff --git a/app/Views/billing/ipd/discharge_create.php b/app/Views/billing/ipd/discharge_create.php
index c6eb96b..57dbf1f 100644
--- a/app/Views/billing/ipd/discharge_create.php
+++ b/app/Views/billing/ipd/discharge_create.php
@@ -45,6 +45,27 @@ $physicalExamRows = $physical_exam_rows ?? [];
$generalExamGroup1 = $physicalExamRows['general_group_1'] ?? [];
$generalExamGroup2 = $physicalExamRows['general_group_2'] ?? [];
$systemicExamRows = $physicalExamRows['systemic'] ?? [];
+$systemicExamText = '';
+if (! empty($systemicExamRows)) {
+ $systemicParts = [];
+ foreach ($systemicExamRows as $row) {
+ $value = trim((string) ($row['value'] ?? ''));
+ if ($value === '') {
+ continue;
+ }
+
+ $name = trim((string) ($row['name'] ?? ''));
+ if ($name !== '' && stripos($value, $name . ':') !== 0) {
+ $systemicParts[] = $name . ': ' . $value;
+ } else {
+ $systemicParts[] = $value;
+ }
+ }
+
+ if (! empty($systemicParts)) {
+ $systemicExamText = implode(PHP_EOL, $systemicParts);
+ }
+}
$manualInvestRows = $manual_invest_rows ?? [];
$specialInvestRows = $special_invest_rows ?? [];
$clinicalLabRows = $clinical_lab_rows ?? [];
@@ -59,6 +80,86 @@ $courseRows = $course_rows ?? [];
$drugRows = $drug_rows ?? [];
$inhosRemark = (string) ($inhos_remark ?? '');
$otherExamText = (string) ($other_exam_text ?? '');
+$painValue = (string) ($pain_value ?? '');
+$opdHistorySnapshot = $opd_history_snapshot ?? [];
+$nursingAdmissionSnapshot = $nursing_admission_snapshot ?? [];
+$drugAllergyStatus = trim((string) ($opdHistorySnapshot['drug_allergy_status'] ?? ''));
+$drugAllergyDetails = trim((string) ($opdHistorySnapshot['drug_allergy_details'] ?? ''));
+$adrHistory = trim((string) ($opdHistorySnapshot['adr_history'] ?? ''));
+$currentMedications = trim((string) ($opdHistorySnapshot['current_medications'] ?? ''));
+$coMorbiditiesText = trim((string) ($opdHistorySnapshot['co_morbidities'] ?? ''));
+$womenLmp = trim((string) ($opdHistorySnapshot['women_lmp'] ?? ''));
+$womenLastBaby = trim((string) ($opdHistorySnapshot['women_last_baby'] ?? ''));
+$womenPregnancyRelated = trim((string) ($opdHistorySnapshot['women_pregnancy_related'] ?? ''));
+$womenRelatedProblems = trim((string) ($opdHistorySnapshot['women_related_problems'] ?? ''));
+$hpiNote = trim((string) ($opdHistorySnapshot['hpi_note'] ?? ''));
+$nursingHistoryRecordedAt = trim((string) ($nursingAdmissionSnapshot['recorded_at'] ?? ''));
+$isFemalePatient = strtolower(trim((string) ($person->xgender ?? ''))) === 'female';
+$hasNursingHistoryPrefill = $nursingHistoryRecordedAt !== ''
+ && ($hpiNote !== ''
+ || $drugAllergyStatus !== ''
+ || $coMorbiditiesText !== ''
+ || $womenLmp !== ''
+ || $womenLastBaby !== ''
+ || $womenPregnancyRelated !== ''
+ || $womenRelatedProblems !== '');
+if ($coMorbiditiesText === '') {
+ $coMorbidFallback = [];
+ if ((int) ($patientHistory['is_niddm'] ?? 0) === 1) {
+ $coMorbidFallback[] = 'Diabetes mellitus (DM)';
+ }
+ if ((int) ($patientHistory['is_hypertesion'] ?? 0) === 1) {
+ $coMorbidFallback[] = 'Hypertension';
+ }
+ if (! empty($coMorbidFallback)) {
+ $coMorbiditiesText = implode(', ', $coMorbidFallback);
+ }
+}
+
+$coMorbidityOptions = [
+ 'dm' => ['label' => 'Diabetes mellitus (DM)', 'keywords' => ['diabetes mellitus', 'diabetes', 'dm']],
+ 'htn' => ['label' => 'high blood pressure (HTn)', 'keywords' => ['high blood pressure', 'hypertension', 'htn']],
+ 'cad' => ['label' => 'Coronary artery disease (CAD)', 'keywords' => ['coronary artery disease', 'cad']],
+ 'copd' => ['label' => 'Chronic Obstructive Pulmonary Disease', 'keywords' => ['chronic obstructive pulmonary disease', 'copd']],
+ 'cva' => ['label' => 'cerebral vascular accident (CVA)', 'keywords' => ['cerebral vascular accident', 'stroke', 'cva']],
+];
+$coMorbiditySelected = array_fill_keys(array_keys($coMorbidityOptions), false);
+$coMorbidityOtherText = '';
+if ($coMorbiditiesText !== '') {
+ $coMorbidityParts = preg_split('/[,;\n]+/', $coMorbiditiesText);
+ if (is_array($coMorbidityParts)) {
+ $otherParts = [];
+ foreach ($coMorbidityParts as $part) {
+ $cleanPart = trim((string) $part);
+ if ($cleanPart === '') {
+ continue;
+ }
+ $normalizedPart = strtolower($cleanPart);
+ $matched = false;
+ foreach ($coMorbidityOptions as $key => $opt) {
+ foreach (($opt['keywords'] ?? []) as $kw) {
+ if ($kw !== '' && strpos($normalizedPart, strtolower((string) $kw)) !== false) {
+ $coMorbiditySelected[$key] = true;
+ $matched = true;
+ break;
+ }
+ }
+ if ($matched) {
+ break;
+ }
+ }
+ if (! $matched) {
+ $otherParts[] = $cleanPart;
+ }
+ }
+ $coMorbidityOtherText = implode(', ', $otherParts);
+ }
+}
+
+$allergyStatusNormalized = strtolower($drugAllergyStatus);
+$allergyStatusKnown = in_array($allergyStatusNormalized, ['known'], true);
+$allergyStatusUnknown = in_array($allergyStatusNormalized, ['allergies not known', 'not known', 'unknown'], true);
+$allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug allergy', 'none', 'no known allergy'], true);
$historyFields = [
'is_smoking' => 'Smoking',
@@ -74,10 +175,15 @@ $otherExamText = (string) ($other_exam_text ?? '');
?>
-
+
Name :
= esc($patientName) ?>
/ Gender :
= esc((string) ($person->xgender ?? '')) ?>
@@ -174,7 +451,8 @@ $otherExamText = (string) ($other_exam_text ?? '');
@@ -209,56 +488,169 @@ $otherExamText = (string) ($other_exam_text ?? '');
+
+
+
+
+
Admission snapshot time: = esc($nursingHistoryRecordedAt) ?>
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
| Complaint Name |
- Remarks |
+ Remark |
Action |
| No complaint rows yet. |
-
-
-
- | = esc((string) ($row['comp_report'] ?? '')) ?> |
- = esc((string) ($row['comp_remark'] ?? '')) ?> |
-
-
- |
-
-
-
+
+
+ | = esc((string) ($row['comp_report'] ?? '')) ?> |
+ = esc((string) ($row['comp_remark'] ?? '')) ?> |
+
+
+ |
+
+
-
-
-
-
-
+
+
Pain Measurement Scale
+
+
+ >
+ >
+ >
+ >
+ >
-
-
-
+
+
+
+
+
+
Drug Allergy / ADR (NABH)
+
Required: Drug Allergy Status is mandatory. If status is Known, Drug Allergy Details are mandatory.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
Co-Morbidities
+
+ $mOpt): ?>
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
@@ -301,16 +693,8 @@ $otherExamText = (string) ($other_exam_text ?? '');
-
-
No systemic examination master rows found.
-
-
-
-
-
-
-
-
+
+
@@ -408,6 +792,43 @@ $otherExamText = (string) ($other_exam_text ?? '');
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -465,6 +886,16 @@ $otherExamText = (string) ($other_exam_text ?? '');
+
+
+
+
+
+
+
+
+
+
| Diagnosis | Remark | Action |
@@ -481,10 +912,25 @@ $otherExamText = (string) ($other_exam_text ?? '');
+
+
+
+
+
+
+
+
+
@@ -498,6 +944,16 @@ $otherExamText = (string) ($other_exam_text ?? '');
+
+
+
+
+
+
+
+
+
+
| Course | Remark | Action |
@@ -514,10 +970,25 @@ $otherExamText = (string) ($other_exam_text ?? '');
+
+
+
+
+
+
+
+
+
@@ -563,55 +1034,11 @@ $otherExamText = (string) ($other_exam_text ?? '');
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
-
-
-
-
-
-
-
@@ -634,8 +1061,64 @@ $otherExamText = (string) ($other_exam_text ?? '');
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/Views/ipd_nursing/workspace.php b/app/Views/ipd_nursing/workspace.php
new file mode 100644
index 0000000..9c6297f
--- /dev/null
+++ b/app/Views/ipd_nursing/workspace.php
@@ -0,0 +1,1442 @@
+id ?? 0);
+$saveUrl = site_url('ipd/patient/nursing/save/' . $ipdId);
+$historySaveUrl = site_url('ipd/patient/history/save/' . $ipdId);
+$scanExtractUrl = site_url('ipd/patient/nursing/scan/' . $ipdId);
+$scanSaveUrl = site_url('ipd/patient/nursing/scan-save/' . $ipdId);
+$reloadUrl = site_url('ipd/patient/workspace/' . $ipdId);
+$printUrl = site_url('ipd/patient/nursing/print/' . $ipdId);
+$dischargeSummaryUrl = site_url('Ipd_discharge/preview_discharge_report/' . $ipdId);
+$dischargeSummaryCreateUrl = site_url('Ipd_discharge/ipd_select/' . $ipdId);
+$bedsideChargeUrl = site_url('billing/ipd/bedside-charge/add/' . $ipdId);
+$doctorVisitChargeUrl = site_url('billing/ipd/doctor-visit/add/' . $ipdId);
+$today = date('Y-m-d');
+$csrfName = csrf_token();
+$csrfHash = csrf_hash();
+$bedsideItemsByCategory = $bedside_items_by_category ?? [];
+$doctorVisitFeeTypes = $doctor_visit_fee_types ?? [];
+$doctorVisitFeeMap = $doctor_visit_fee_map ?? [];
+$docList = $doc_list ?? [];
+$nursingChargeHistory = $nursing_charge_history ?? [];
+$historyFields = $history_fields ?? [];
+$patientHistoryRow = $patient_history_row ?? [];
+$opdHistorySnapshot = $opd_history_snapshot ?? [];
+$isFemalePatient = (bool) ($is_female_patient ?? false);
+$coMorbidityOptions = ['Hypertension', 'DM', 'COPD', 'Asthma', 'IHD', 'CKD', 'Hypothyroidism'];
+$selectedCoMorbidities = array_filter(array_map('trim', explode(',', (string) ($opdHistorySnapshot['co_morbidities'] ?? ''))), static fn ($v) => $v !== '');
+$otherCoMorbidities = array_values(array_filter($selectedCoMorbidities, static fn ($item) => ! in_array($item, $coMorbidityOptions, true)));
+$toFahrenheit = static function ($temperatureC): string {
+ if ($temperatureC === null || $temperatureC === '') {
+ return '';
+ }
+
+ $value = (((float) $temperatureC) * 9 / 5) + 32;
+
+ return rtrim(rtrim(number_format($value, 1, '.', ''), '0'), '.');
+};
+$nurseNames = [];
+$scannedAuditRows = [];
+foreach ($entries as $entryRow) {
+ $name = trim((string) ($entryRow['recorded_by'] ?? ''));
+ if ($name === '') {
+ continue;
+ }
+ $nurseNames[$name] = $name;
+
+ $note = trim((string) ($entryRow['general_note'] ?? ''));
+ if (stripos($note, 'Source: Scanned Paper') !== false) {
+ $scanRef = '';
+ $confidence = '';
+ $corrected = '';
+ if (preg_match('/ScanRef\s*:\s*([^\]|]+)/i', $note, $m) === 1) {
+ $scanRef = trim((string) ($m[1] ?? ''));
+ }
+ if (preg_match('/AIConfidence\s*:\s*([^\]|]+)/i', $note, $m) === 1) {
+ $confidence = trim((string) ($m[1] ?? ''));
+ }
+ if (preg_match('/Corrected\s*:\s*([^\]|]+)/i', $note, $m) === 1) {
+ $corrected = trim((string) ($m[1] ?? ''));
+ }
+
+ $scannedAuditRows[] = [
+ 'entry_id' => (int) ($entryRow['id'] ?? 0),
+ 'recorded_at' => (string) ($entryRow['recorded_at'] ?? ''),
+ 'entry_type' => (string) ($entryRow['entry_type'] ?? ''),
+ 'recorded_by' => (string) ($entryRow['recorded_by'] ?? ''),
+ 'scan_ref' => $scanRef,
+ 'confidence' => $confidence,
+ 'corrected' => $corrected,
+ 'note' => $note,
+ ];
+ }
+}
+ksort($nurseNames);
+?>
+
+
+
+
+
+
+
+ IPD: = esc((string) ($ipd->ipd_code ?? '')) ?> |
+ Patient: = esc((string) ($person->p_fname ?? '')) ?> |
+ UHID: = esc((string) ($person->p_code ?? '')) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ OCR/AI extraction is assistive only. Nurse review is mandatory before save (NABH documentation requirement).
+
+
+
+
+
+
+
+
+
+
+
+
+ | # |
+ Type |
+ Recorded At |
+ Values / Notes |
+ Confidence |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Item |
+ Type |
+ Doctor |
+ Date |
+ Time |
+ Qty |
+ Rate |
+ Amount |
+ In Bill |
+ Comment |
+ Action |
+
+
+
+
+
+ | No charge history found. |
+
+
+
+
+
+ | = esc((string) ($chargeRow['charge_id'] ?? '')) ?> |
+ = esc((string) ($chargeRow['item_name'] ?? '')) ?> |
+ = esc((string) ($chargeRow['item_type'] ?? '')) ?> |
+ = esc((string) ($chargeRow['doctor_name'] ?? '')) ?> |
+ = esc((string) ($chargeRow['visit_date'] ?? '')) ?> |
+ = esc((string) ($chargeRow['visit_time'] ?? '')) ?> |
+ = esc(number_format((float) ($chargeRow['qty'] ?? 0), 2)) ?> |
+ = esc(number_format((float) ($chargeRow['rate'] ?? 0), 2)) ?> |
+ = esc(number_format((float) ($chargeRow['amount'] ?? 0), 2)) ?> |
+ = (int) ($chargeRow['include_in_bill'] ?? 0) === 1 ? 'Yes' : 'No' ?> |
+ = esc((string) ($chargeRow['remarks'] ?? '')) ?> |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Time | Type | Shift | Details | By |
+
+
+ | No nursing entries yet. |
+
+
+
+ | = esc((string) ($entry['recorded_at'] ?? '')) ?> |
+ = esc((string) ($entry['entry_type'] ?? '')) ?> |
+ = esc((string) ($entry['shift_name'] ?? '')) ?> |
+
+
+ Temp: = esc($toFahrenheit($entry['temperature_c'] ?? null)) ?> °F, Pulse: = esc((string) ($entry['pulse_rate'] ?? '')) ?>, Resp: = esc((string) ($entry['resp_rate'] ?? '')) ?>, BP: = esc((string) ($entry['bp_systolic'] ?? '')) ?>/= esc((string) ($entry['bp_diastolic'] ?? '')) ?>, SpO2: = esc((string) ($entry['spo2'] ?? '')) ?>, Wt: = esc((string) ($entry['weight_kg'] ?? '')) ?>
+
+ = esc((string) ($entry['fluid_direction'] ?? '')) ?>, Route: = esc((string) ($entry['fluid_route'] ?? '')) ?>, Amount: = esc((string) ($entry['fluid_amount_ml'] ?? '')) ?> ml
+
+ = esc((string) ($entry['treatment_text'] ?? '')) ?>
+
+ Note: = esc((string) $entry['general_note']) ?>
+ |
+ = esc((string) ($entry['recorded_by'] ?? '')) ?> |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Entry ID |
+ Recorded At |
+ Type |
+ Scan Ref |
+ AI Confidence |
+ Corrected |
+ Reviewed By |
+ Audit Note |
+
+
+
+
+ | No scanned-entry audit rows yet. |
+
+
+
+ | = esc((string) ($audit['entry_id'] ?? '')) ?> |
+ = esc((string) ($audit['recorded_at'] ?? '')) ?> |
+ = esc((string) ($audit['entry_type'] ?? '')) ?> |
+ = esc((string) ($audit['scan_ref'] ?? '')) ?> |
+ = esc((string) ($audit['confidence'] ?? '')) ?> |
+ = esc((string) ($audit['corrected'] ?? '')) ?> |
+ = esc((string) ($audit['recorded_by'] ?? '')) ?> |
+ = esc((string) ($audit['note'] ?? '')) ?> |
+
+
+
+
+
+
+
+
+
+
+