Add nursing scan-to-structured workflow and discharge UI polish
This commit is contained in:
parent
13e058db95
commit
2127063c7f
|
|
@ -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');
|
||||
|
|
|
|||
825
app/Controllers/IpdPatient.php
Normal file
825
app/Controllers/IpdPatient.php
Normal 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
167
app/Controllers/Setting/NursingBedsideItems.php
Normal file
167
app/Controllers/Setting/NursingBedsideItems.php
Normal 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
297
app/Libraries/NursingScanExtractionService.php
Normal file
297
app/Libraries/NursingScanExtractionService.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
62
app/Models/IpdNursingEntryModel.php
Normal file
62
app/Models/IpdNursingEntryModel.php
Normal 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();
|
||||
}
|
||||
}
|
||||
199
app/Models/NursingBedsideItemModel.php
Normal file
199
app/Models/NursingBedsideItemModel.php
Normal 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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
296
app/Views/Setting/Charges/Nursing_Bedside/item_search_V.php
Normal file
296
app/Views/Setting/Charges/Nursing_Bedside/item_search_V.php
Normal 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
111
app/Views/billing/ipd/nursing_chart_print.php
Normal file
111
app/Views/billing/ipd/nursing_chart_print.php
Normal 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>
|
||||
47
app/Views/ipd_nursing/patient_list.php
Normal file
47
app/Views/ipd_nursing/patient_list.php
Normal 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>
|
||||
1442
app/Views/ipd_nursing/workspace.php
Normal file
1442
app/Views/ipd_nursing/workspace.php
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user