Add nursing scan-to-structured workflow and discharge UI polish

This commit is contained in:
DevSoftTech Server 2026-03-09 00:12:04 +05:30
parent 13e058db95
commit 2127063c7f
15 changed files with 6489 additions and 145 deletions

View File

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

View File

@ -0,0 +1,825 @@
<?php
namespace App\Controllers;
use App\Libraries\NursingScanExtractionService;
use App\Models\IpdBillingModel;
use App\Models\IpdNursingEntryModel;
use App\Models\NursingBedsideItemModel;
class IpdPatient extends BaseController
{
protected IpdBillingModel $ipdBillingModel;
protected IpdNursingEntryModel $ipdNursingEntryModel;
protected NursingBedsideItemModel $nursingBedsideItemModel;
protected NursingScanExtractionService $nursingScanExtractionService;
public function __construct()
{
$this->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));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,167 @@
<?php
namespace App\Controllers\Setting;
use App\Controllers\BaseController;
use App\Models\NursingBedsideItemModel;
class NursingBedsideItems extends BaseController
{
protected NursingBedsideItemModel $model;
public function __construct()
{
$this->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(),
]);
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateIpdNursingEntriesTable extends Migration
{
public function up()
{
if ($this->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);
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateNursingBedsideItemsInsuranceTable extends Migration
{
public function up()
{
if ($this->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);
}
}

View File

@ -0,0 +1,297 @@
<?php
namespace App\Libraries;
class NursingScanExtractionService
{
public function extract(string $docType, string $manualText = '', ?array $uploaded = null): array
{
$warnings = [];
$text = trim($manualText);
$scanRef = '';
$uploadedPath = '';
if (is_array($uploaded) && ! empty($uploaded['path'])) {
$scanRef = (string) ($uploaded['scan_ref'] ?? '');
$uploadedPath = (string) $uploaded['path'];
}
if ($text === '' && $uploadedPath !== '') {
$ext = strtolower(pathinfo($uploadedPath, PATHINFO_EXTENSION));
if (in_array($ext, ['txt', 'csv', 'log'], true)) {
$fileText = @file_get_contents($uploadedPath);
if (is_string($fileText)) {
$text = trim($fileText);
}
}
}
if ($text === '' && $uploadedPath !== '') {
$azureText = $this->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,
];
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class IpdNursingEntryModel extends Model
{
protected $table = 'ipd_nursing_entries';
protected $primaryKey = 'id';
protected $returnType = 'array';
protected $allowedFields = [
'ipd_id',
'entry_type',
'recorded_at',
'shift_name',
'temperature_c',
'pulse_rate',
'resp_rate',
'bp_systolic',
'bp_diastolic',
'spo2',
'weight_kg',
'fluid_direction',
'fluid_route',
'fluid_amount_ml',
'treatment_text',
'general_note',
'recorded_by',
'recorded_by_id',
'created_at',
'updated_at',
];
protected $useTimestamps = false;
public function getByIpd(int $ipdId, int $limit = 100): array
{
return $this->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();
}
}

View File

@ -0,0 +1,199 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class NursingBedsideItemModel extends Model
{
protected $table = 'nursing_bedside_items';
protected $primaryKey = 'item_id';
protected $returnType = 'array';
protected $allowedFields = [
'item_code',
'item_name',
'item_type',
'category',
'default_rate',
'unit',
'description',
'is_active',
'is_billable',
'created_by',
'updated_by',
];
protected $useTimestamps = false;
public function getBillableGroupedByCategory(int $insuranceId = 0): array
{
$builder = $this->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');
}
}

View File

@ -0,0 +1,27 @@
<?php $rows = $rows ?? []; ?>
<table class="table table-bordered table-striped table-sm mb-0">
<thead>
<tr>
<th>Company Name</th>
<th style="width: 120px;" class="text-end">Rate</th>
<th style="width: 120px;">Code</th>
<th style="width: 70px;"></th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)) : ?>
<tr><td colspan="4" class="text-center text-muted">No insurance-specific rate added.</td></tr>
<?php else : ?>
<?php foreach ($rows as $row) : ?>
<tr data-insurance-id="<?= (int) ($row['hc_insurance_id'] ?? 0) ?>">
<td><?= esc((string) ($row['ins_company_name'] ?? '')) ?></td>
<td class="text-end"><?= esc(number_format((float) ($row['amount1'] ?? 0), 2)) ?></td>
<td><?= esc((string) ($row['code'] ?? '')) ?></td>
<td>
<button type="button" class="btn btn-danger btn-sm" onclick="nursingInsuranceRemove(<?= (int) ($row['id'] ?? 0) ?>)">Del</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>

View File

@ -0,0 +1,296 @@
<?php
$rows = $rows ?? [];
$insuranceList = $insurance_list ?? [];
$csrfName = csrf_token();
$csrfHash = csrf_hash();
?>
<div class="card">
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
<h3 class="card-title mb-0">Clinical Procedure / Test Master</h3>
<div class="card-tools ms-auto">
<button type="button" class="btn btn-primary" id="btn_nursing_item_add">Add New</button>
</div>
</div>
<div class="card-body table-responsive">
<table class="table table-bordered table-striped table-sm align-middle" id="nursing_bedside_table">
<thead>
<tr>
<th style="width: 90px;">Code</th>
<th>Item Name</th>
<th style="width: 130px;">Type</th>
<th style="width: 130px;">Category</th>
<th style="width: 100px;" class="text-end">Rate</th>
<th style="width: 80px;">Unit</th>
<th style="width: 85px;">Billable</th>
<th style="width: 70px;">Active</th>
<th style="width: 70px;"></th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row) : ?>
<tr>
<td><?= esc((string) ($row['item_code'] ?? '')) ?></td>
<td><?= esc((string) ($row['item_name'] ?? '')) ?></td>
<td><?= esc((string) ($row['item_type'] ?? '')) ?></td>
<td><?= esc((string) ($row['category'] ?? '')) ?></td>
<td class="text-end"><?= esc(number_format((float) ($row['default_rate'] ?? 0), 2)) ?></td>
<td><?= esc((string) ($row['unit'] ?? '')) ?></td>
<td><?= (int) ($row['is_billable'] ?? 0) === 1 ? 'Yes' : 'No' ?></td>
<td><?= (int) ($row['is_active'] ?? 0) === 1 ? 'Yes' : 'No' ?></td>
<td>
<button type="button" class="btn btn-primary btn-sm" onclick="nursingBedsideEdit(<?= (int) ($row['item_id'] ?? 0) ?>)">Edit</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="modal fade" id="nursing_item_modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="nursing_item_modal_title">Add Clinical Procedure/Test</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="nursing_item_form" class="row g-2">
<input type="hidden" id="nursing_item_id" value="0">
<div class="col-md-3"><label class="form-label">Item Code</label><input type="text" class="form-control" id="item_code"></div>
<div class="col-md-5"><label class="form-label">Item Name *</label><input type="text" class="form-control" id="item_name" required></div>
<div class="col-md-4"><label class="form-label">Item Type *</label><input type="text" class="form-control" id="item_type" required placeholder="Investigation / Procedure"></div>
<div class="col-md-4"><label class="form-label">Category</label><input type="text" class="form-control" id="category" placeholder="Lab Test / Cardiac"></div>
<div class="col-md-2"><label class="form-label">Rate</label><input type="number" step="0.01" class="form-control" id="default_rate" value="0"></div>
<div class="col-md-2"><label class="form-label">Unit</label><input type="text" class="form-control" id="unit" value="Unit"></div>
<div class="col-md-2"><label class="form-label">Billable</label><select class="form-select" id="is_billable"><option value="1">Yes</option><option value="0">No</option></select></div>
<div class="col-md-2"><label class="form-label">Active</label><select class="form-select" id="is_active"><option value="1">Yes</option><option value="0">No</option></select></div>
<div class="col-md-12"><label class="form-label">Description</label><textarea class="form-control" id="description" rows="2"></textarea></div>
</form>
<hr>
<h6 class="mb-2">Insurance-specific Rate & Code</h6>
<div class="row g-2 align-items-end">
<div class="col-md-5">
<label class="form-label">Insurance Company</label>
<select class="form-select" id="insurance_company_id">
<?php foreach ($insuranceList as $insRow) : ?>
<option value="<?= esc($insRow['id'] ?? '') ?>"><?= esc($insRow['ins_company_name'] ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Rate</label>
<input type="number" step="0.01" class="form-control" id="insurance_rate" value="0">
</div>
<div class="col-md-2">
<label class="form-label">Code</label>
<input type="text" class="form-control" id="insurance_code" placeholder="Code">
</div>
<div class="col-md-2">
<button type="button" class="btn btn-outline-primary w-100" id="btn_insurance_add">Add/Update</button>
</div>
<div class="col-md-12">
<div id="nursing_insurance_list_wrap"><?= view('Setting/Charges/Nursing_Bedside/item_insurance_list', ['rows' => []]) ?></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="btn_nursing_item_save">Save</button>
</div>
</div>
</div>
</div>
<script>
(function() {
var saveUrl = '<?= base_url('nursing-bedside-items/save') ?>';
var getUrlBase = '<?= base_url('nursing-bedside-items') ?>';
var insuranceListUrlBase = '<?= base_url('nursing-bedside-items/insurance') ?>';
var insuranceAddUrl = '<?= base_url('nursing-bedside-items/insurance/add') ?>';
var insuranceRemoveUrl = '<?= base_url('nursing-bedside-items/insurance/remove') ?>';
var csrfName = '<?= esc($csrfName) ?>';
var csrfHash = '<?= esc($csrfHash) ?>';
function resetForm() {
$('#nursing_item_id').val('0');
$('#item_code').val('');
$('#item_name').val('');
$('#item_type').val('');
$('#category').val('');
$('#default_rate').val('0');
$('#unit').val('Unit');
$('#description').val('');
$('#is_billable').val('1');
$('#is_active').val('1');
$('#insurance_rate').val('0');
$('#insurance_code').val('');
$('#nursing_insurance_list_wrap').html('Save item first, then add insurance rate/code.');
}
function applyCsrf(resp) {
if (!resp) {
return;
}
csrfName = resp.csrfName || csrfName;
csrfHash = resp.csrfHash || csrfHash;
}
function loadInsuranceList(itemId) {
if (!itemId || Number(itemId) <= 0) {
$('#nursing_insurance_list_wrap').html('Save item first, then add insurance rate/code.');
return;
}
$.get(insuranceListUrlBase + '/' + itemId, function(resp) {
applyCsrf(resp);
$('#nursing_insurance_list_wrap').html((resp && resp.html) ? resp.html : 'No insurance-specific rate added.');
refreshInsuranceSelectState();
}, 'json').fail(function() {
$('#nursing_insurance_list_wrap').html('Unable to load insurance rates.');
refreshInsuranceSelectState();
});
}
function refreshInsuranceSelectState() {
var mappedIds = {};
$('#nursing_insurance_list_wrap tr[data-insurance-id]').each(function() {
var id = Number($(this).attr('data-insurance-id') || 0);
if (id > 0) {
mappedIds[id] = true;
}
});
$('#insurance_company_id option').each(function() {
var optVal = Number($(this).val() || 0);
var disabled = !!mappedIds[optVal];
$(this).prop('disabled', disabled);
});
if ($('#insurance_company_id option:selected').prop('disabled')) {
var firstEnabled = $('#insurance_company_id option:not(:disabled)').first().val();
$('#insurance_company_id').val(firstEnabled || '');
}
}
$('#btn_nursing_item_add').on('click', function() {
resetForm();
$('#nursing_item_modal_title').text('Add Clinical Procedure/Test');
$('#nursing_item_modal').modal('show');
refreshInsuranceSelectState();
});
window.nursingBedsideEdit = function(itemId) {
$.get(getUrlBase + '/' + itemId, function(resp) {
var row = resp && resp.row ? resp.row : null;
if (!row) {
alert('Item not found');
return;
}
applyCsrf(resp);
$('#nursing_item_id').val(row.item_id || 0);
$('#item_code').val(row.item_code || '');
$('#item_name').val(row.item_name || '');
$('#item_type').val(row.item_type || '');
$('#category').val(row.category || '');
$('#default_rate').val(row.default_rate || 0);
$('#unit').val(row.unit || 'Unit');
$('#description').val(row.description || '');
$('#is_billable').val(String(row.is_billable || 0));
$('#is_active').val(String(row.is_active || 0));
$('#nursing_item_modal_title').text('Edit Clinical Procedure/Test');
$('#nursing_item_modal').modal('show');
loadInsuranceList(Number(row.item_id || 0));
}, 'json');
};
$('#btn_nursing_item_save').on('click', function() {
var payload = {};
payload[csrfName] = csrfHash;
payload.item_id = $('#nursing_item_id').val() || 0;
payload.item_code = $('#item_code').val() || '';
payload.item_name = $('#item_name').val() || '';
payload.item_type = $('#item_type').val() || '';
payload.category = $('#category').val() || '';
payload.default_rate = $('#default_rate').val() || 0;
payload.unit = $('#unit').val() || 'Unit';
payload.description = $('#description').val() || '';
payload.is_billable = $('#is_billable').val() || 1;
payload.is_active = $('#is_active').val() || 1;
$.post(saveUrl, payload, function(resp) {
applyCsrf(resp);
if (resp && Number(resp.ok || 0) === 1) {
var itemId = Number(resp.item_id || $('#nursing_item_id').val() || 0);
$('#nursing_item_id').val(itemId);
loadInsuranceList(itemId);
alert('Saved successfully');
return;
}
alert((resp && resp.message) ? resp.message : 'Unable to save');
}, 'json').fail(function(xhr) {
var message = (xhr && xhr.responseJSON && xhr.responseJSON.message) ? xhr.responseJSON.message : 'Unable to save';
alert(message);
});
});
$('#btn_insurance_add').on('click', function() {
var itemId = Number($('#nursing_item_id').val() || 0);
if (itemId <= 0) {
alert('Save item first, then add insurance rate/code.');
return;
}
var payload = {};
payload[csrfName] = csrfHash;
payload.item_id = itemId;
payload.insurance_id = $('#insurance_company_id').val() || 0;
payload.amount = $('#insurance_rate').val() || 0;
payload.code = $('#insurance_code').val() || '';
$.post(insuranceAddUrl, payload, function(resp) {
applyCsrf(resp);
if (resp && Number(resp.ok || 0) === 1) {
$('#nursing_insurance_list_wrap').html(resp.html || '');
refreshInsuranceSelectState();
return;
}
alert((resp && resp.message) ? resp.message : 'Unable to save insurance rate');
}, 'json').fail(function(xhr) {
var message = (xhr && xhr.responseJSON && xhr.responseJSON.message) ? xhr.responseJSON.message : 'Unable to save insurance rate';
alert(message);
});
});
window.nursingInsuranceRemove = function(mappingId) {
var itemId = Number($('#nursing_item_id').val() || 0);
if (itemId <= 0 || !mappingId) {
return;
}
var payload = {};
payload[csrfName] = csrfHash;
payload.mapping_id = mappingId;
payload.item_id = itemId;
$.post(insuranceRemoveUrl, payload, function(resp) {
applyCsrf(resp);
if (resp && Number(resp.ok || 0) === 1) {
$('#nursing_insurance_list_wrap').html(resp.html || '');
refreshInsuranceSelectState();
return;
}
alert((resp && resp.message) ? resp.message : 'Unable to remove insurance rate');
}, 'json');
};
$('#nursing_item_modal').on('hidden.bs.modal', function() {
load_form_div('<?= base_url('nursing-bedside-items') ?>', 'maindiv', 'Clinical Procedure / Test Master');
});
})();
</script>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,111 @@
<?php
$ipd = $ipd_info ?? null;
$person = $person_info ?? null;
$entries = $nursing_entries ?? [];
$chartDate = (string) ($chart_date ?? date('Y-m-d'));
$chartNurse = (string) ($chart_nurse ?? '');
$toFahrenheit = static function ($temperatureC): string {
if ($temperatureC === null || $temperatureC === '') {
return '';
}
$value = (((float) $temperatureC) * 9 / 5) + 32;
return rtrim(rtrim(number_format($value, 1, '.', ''), '0'), '.');
};
$grouped = [
'Morning' => [],
'Evening' => [],
'Night' => [],
'Other' => [],
];
foreach ($entries as $entry) {
$shift = trim((string) ($entry['shift_name'] ?? ''));
if ($shift === '' || ! isset($grouped[$shift])) {
$shift = 'Other';
}
$grouped[$shift][] = $entry;
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Nursing Chart - <?= esc((string) ($ipd->ipd_code ?? '')) ?></title>
<style>
body { font-family: Arial, sans-serif; font-size: 12px; margin: 14px; }
h2, h3 { margin: 6px 0; }
.meta { margin-bottom: 10px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 14px; }
th, td { border: 1px solid #333; padding: 6px; vertical-align: top; }
th { background: #f3f3f3; text-align: left; }
.muted { color: #666; }
@media print {
.print-btn { display: none; }
}
</style>
</head>
<body>
<button class="print-btn" onclick="window.print()">Print</button>
<h2>24-Hour Nursing Chart</h2>
<div class="meta">
<strong>IPD:</strong> <?= esc((string) ($ipd->ipd_code ?? '')) ?> |
<strong>Patient:</strong> <?= esc((string) ($person->p_fname ?? '')) ?> |
<strong>UHID:</strong> <?= esc((string) ($person->p_code ?? '')) ?> |
<strong>Date:</strong> <?= esc($chartDate) ?>
<?php if ($chartNurse !== '') : ?>
| <strong>Nurse:</strong> <?= esc($chartNurse) ?>
<?php endif; ?>
</div>
<?php foreach ($grouped as $shift => $rows) : ?>
<h3><?= esc($shift) ?> Shift</h3>
<table>
<thead>
<tr>
<th style="width: 140px;">Time</th>
<th style="width: 90px;">Type</th>
<th>Details</th>
<th style="width: 130px;">Recorded By</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)) : ?>
<tr>
<td colspan="4" class="muted">No entries.</td>
</tr>
<?php else : ?>
<?php foreach ($rows as $entry) : ?>
<tr>
<td><?= esc((string) ($entry['recorded_at'] ?? '')) ?></td>
<td><?= esc((string) ($entry['entry_type'] ?? '')) ?></td>
<td>
<?php if (($entry['entry_type'] ?? '') === 'vitals') : ?>
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'] ?? '')) ?>
<?php elseif (($entry['entry_type'] ?? '') === 'fluid') : ?>
<?= esc((string) ($entry['fluid_direction'] ?? '')) ?>,
Route: <?= esc((string) ($entry['fluid_route'] ?? '')) ?>,
Amount: <?= esc((string) ($entry['fluid_amount_ml'] ?? '')) ?> ml
<?php else : ?>
<?= esc((string) ($entry['treatment_text'] ?? '')) ?>
<?php endif; ?>
<?php if (! empty($entry['general_note'])) : ?>
<br><span class="muted">Note: <?= esc((string) $entry['general_note']) ?></span>
<?php endif; ?>
</td>
<td><?= esc((string) ($entry['recorded_by'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<?php endforeach; ?>
</body>
</html>

View File

@ -0,0 +1,47 @@
<section class="content">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Admitted IPD Patient List (Nursing)</h5>
</div>
<div class="card-body table-responsive">
<table class="table table-striped table-bordered" id="ipdNursingListTable">
<thead>
<tr>
<th>IPD Code</th>
<th>UHID</th>
<th>Patient</th>
<th>Bed</th>
<th>Doctor</th>
<th>Admit Date</th>
<th>Days</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php foreach (($records ?? []) as $row) : ?>
<tr>
<td><?= esc($row->ipd_code ?? '') ?></td>
<td><?= esc($row->p_code ?? '') ?></td>
<td><?= esc(trim((string) (($row->p_fname ?? '') . ' ' . ($row->p_rname ?? '')))) ?></td>
<td><?= esc($row->Bed_Desc ?? '') ?></td>
<td><?= esc($row->doc_name ?? '') ?></td>
<td><?= esc($row->str_register_date ?? '') ?></td>
<td><?= esc($row->no_days ?? '') ?></td>
<td>
<button type="button" class="btn btn-primary btn-sm" onclick="load_form('<?= base_url('ipd/patient/workspace/' . (int) ($row->id ?? 0)) ?>','Nursing Workspace');">Open</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</section>
<script>
(function () {
if (window.jQuery && $.fn.DataTable) {
$('#ipdNursingListTable').DataTable();
}
})();
</script>

File diff suppressed because it is too large Load Diff