diff --git a/app/Config/Routes.php b/app/Config/Routes.php index a33de71..499ff7d 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1378,6 +1378,15 @@ $routes->group('setting', static function($routes) { $routes->post('admin/reffer/types/create', 'Setting\\Reffer::storeType'); $routes->post('admin/reffer/types/update', 'Setting\\Reffer::updateType'); $routes->post('admin/reffer/types/delete', 'Setting\\Reffer::deleteType'); + $routes->get('admin/nurse', 'Setting\\Nurse::index'); + $routes->post('admin/nurse/save', 'Setting\\Nurse::save'); + $routes->post('admin/nurse/delete', 'Setting\\Nurse::delete'); + $routes->post('admin/nurse/delete/(:num)', 'Setting\\Nurse::delete/$1'); + $routes->get('admin/nurse/list_json', 'Setting\\Nurse::list_json'); + $routes->get('admin/nursing-station', 'Setting\\NursingStation::index'); + $routes->post('admin/nursing-station/save', 'Setting\\NursingStation::save'); + $routes->post('admin/nursing-station/delete', 'Setting\\NursingStation::delete'); + $routes->get('admin/nursing-station/list_json', 'Setting\\NursingStation::list_json'); $routes->get('admin/bed-management', 'Setting\\BedManagement::index', ['filter' => $settingsBedFilter]); $routes->get('admin/departments', 'Setting\\BedManagement::departments', ['filter' => $settingsBedFilter]); $routes->post('admin/departments/save', 'Setting\\BedManagement::saveDepartment', ['filter' => $settingsBedFilter]); diff --git a/app/Controllers/IpdPatient.php b/app/Controllers/IpdPatient.php index 1173d39..f4259b6 100644 --- a/app/Controllers/IpdPatient.php +++ b/app/Controllers/IpdPatient.php @@ -44,8 +44,131 @@ class IpdPatient extends BaseController return $permission; } + $records = $this->ipdBillingModel->getCurrentAdmissions(); + + $stats = [ + 'total_beds' => 0, + 'available' => 0, + 'occupied' => 0, + 'reserved' => 0, + 'maintenance' => 0, + 'cleaning' => 0, + 'total_admitted' => count($records), + 'nursing_stations' => 0, + ]; + $floors = []; + $wards = []; + + $stationModel = new \App\Models\NursingStationModel(); + $nursingStations = $stationModel->getActiveStations(); + $stats['nursing_stations'] = count($nursingStations); + + if ($this->db->tableExists('bed_master') && $this->db->tableExists('ward_master')) { + $builder = $this->db->table('bed_master b') + ->select('b.id as bed_id, b.bed_number, b.bed_code, b.bed_status, b.current_ipd_id') + ->select('b.has_oxygen, b.has_monitor, b.has_ventilator, b.is_isolation_bed') + ->select('w.id as ward_id, w.ward_name, w.floor_number, w.ward_type, w.nursing_station_id') + ->join('ward_master w', 'w.id = b.ward_id', 'left') + ->where('b.status', 'active') + ->orderBy('w.floor_number', 'ASC') + ->orderBy('w.ward_name', 'ASC') + ->orderBy('b.bed_number', 'ASC'); + + if ($this->db->tableExists('nursing_station_master')) { + $builder->select('ns.station_name, ns.station_code') + ->join('nursing_station_master ns', 'ns.id = w.nursing_station_id', 'left'); + } else { + $builder->select('NULL as station_name, NULL as station_code'); + } + + if ($this->db->tableExists('ipd_master')) { + $builder + ->select('i.id as ipd_id, i.ipd_code, i.register_date, i.r_doc_name') + ->join('ipd_master i', 'i.id = b.current_ipd_id AND i.ipd_status = 0', 'left', false); + + if ($this->db->tableExists('patient_master')) { + $builder + ->select('p.p_code, p.p_fname, p.p_lname') + ->join('patient_master p', 'p.id = i.p_id', 'left'); + } + } + + $today = new \DateTime(); + + foreach ($builder->get()->getResult() as $row) { + $floorNo = (string) ($row->floor_number !== null && $row->floor_number !== '' ? $row->floor_number : 'Ground Floor'); + $wardId = (int) ($row->ward_id ?? 0); + $status = (string) ($row->bed_status ?? 'available'); + + $stats['total_beds']++; + if (array_key_exists($status, $stats)) { + $stats[$status]++; + } elseif (strpos($status, 'maintenance') !== false) { + $stats['maintenance']++; + } + + $daysAdmitted = null; + if (! empty($row->register_date)) { + try { + $admitDate = new \DateTime((string) $row->register_date); + $daysAdmitted = (int) $today->diff($admitDate)->days; + } catch (\Throwable $e) { + $daysAdmitted = null; + } + } + + $patientName = trim(((string) ($row->p_fname ?? '')) . ' ' . ((string) ($row->p_lname ?? ''))); + + if (! isset($floors[$floorNo])) { + $floors[$floorNo] = []; + } + if (! isset($floors[$floorNo][$wardId])) { + $floors[$floorNo][$wardId] = [ + 'ward_name' => (string) ($row->ward_name ?? 'Unknown Ward'), + 'ward_type' => (string) ($row->ward_type ?? ''), + 'station_name' => (string) ($row->station_name ?? 'Unassigned Station'), + 'station_code' => (string) ($row->station_code ?? ''), + 'beds' => [], + 'count' => ['total' => 0, 'available' => 0, 'occupied' => 0], + ]; + $wards[$wardId] = (string) ($row->ward_name ?? 'Unknown Ward'); + } + + $floors[$floorNo][$wardId]['beds'][] = [ + 'bed_id' => (int) ($row->bed_id ?? 0), + 'bed_number' => (string) ($row->bed_number !== '' && $row->bed_number !== null + ? $row->bed_number : ($row->bed_code ?? '-')), + 'bed_code' => (string) ($row->bed_code ?? ''), + 'bed_status' => $status, + 'patient_name' => $patientName, + 'patient_code' => (string) ($row->p_code ?? ''), + 'doctor_name' => trim((string) ($row->r_doc_name ?? '')), + 'ipd_code' => (string) ($row->ipd_code ?? ''), + 'ipd_id' => (int) ($row->ipd_id ?? 0), + 'admit_date' => (string) ($row->register_date ?? ''), + 'days_admitted' => $daysAdmitted, + 'has_oxygen' => (bool) ($row->has_oxygen ?? false), + 'has_monitor' => (bool) ($row->has_monitor ?? false), + 'has_ventilator' => (bool) ($row->has_ventilator ?? false), + 'is_isolation' => (bool) ($row->is_isolation_bed ?? false), + 'station_name' => (string) ($row->station_name ?? ''), + ]; + + $floors[$floorNo][$wardId]['count']['total']++; + if ($status === 'available') { + $floors[$floorNo][$wardId]['count']['available']++; + } elseif ($status === 'occupied') { + $floors[$floorNo][$wardId]['count']['occupied']++; + } + } + } + return view('ipd_nursing/patient_list', [ - 'records' => $this->ipdBillingModel->getCurrentAdmissions(), + 'records' => $records, + 'stats' => $stats, + 'floors' => $floors, + 'wards' => $wards, + 'nursingStations' => $nursingStations, ]); } @@ -108,6 +231,9 @@ class IpdPatient extends BaseController // Get bed assignment history $bedHistory = $this->bedAssignmentModel->getByIpd($ipdId); + $nurseModel = new \App\Models\NurseModel(); + $nurseList = $nurseModel->getActiveNurses(); + return view('ipd_nursing/workspace', [ 'ipd_info' => $panelData['ipd_info'] ?? null, 'person_info' => $person, @@ -117,6 +243,7 @@ class IpdPatient extends BaseController 'doctor_visit_fee_types' => $this->ipdBillingModel->getDoctorVisitFeeTypes(), 'doctor_visit_fee_map' => $this->ipdBillingModel->getDoctorVisitFeeMap($doctorIds), 'doc_list' => $docList, + 'nurse_list' => $nurseList, 'history_fields' => $historyFields, 'patient_history_row' => $patientHistoryRow, 'opd_history_snapshot' => $opdHistorySnapshot, @@ -268,11 +395,21 @@ class IpdPatient extends BaseController $recordedBy = $user->username ?? $user->email ?? 'User'; $recordedById = $user->id ?? null; + $nurseIdPost = (int) ($this->request->getPost('recorded_by_nurse_id') ?? 0); + if ($nurseIdPost > 0) { + $nurseModel = new \App\Models\NurseModel(); + $nurseRow = $nurseModel->getNurseById($nurseIdPost); + if ($nurseRow) { + $recordedBy = ($nurseRow['nurse_code'] ? '[' . $nurseRow['nurse_code'] . '] ' : '') . $nurseRow['name']; + $recordedById = $nurseRow['id']; + } + } + $data = [ 'ipd_id' => $ipdId, 'entry_type' => $entryType, 'recorded_at' => $recordedAt, - 'shift_name' => $this->resolveShiftName($recordedAt), + 'shift_name' => '', '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, diff --git a/app/Controllers/Ipd_discharge.php b/app/Controllers/Ipd_discharge.php index 5af824a..73c301d 100644 --- a/app/Controllers/Ipd_discharge.php +++ b/app/Controllers/Ipd_discharge.php @@ -3525,33 +3525,89 @@ class Ipd_discharge extends BaseController public function surgery_master_lookup() { + session_write_close(); + $type = $this->normalizeSurgeryTermType((string) $this->request->getGet('type')); $q = trim((string) $this->request->getGet('q')); + $masterOnly = (int) $this->request->getGet('master_only') === 1; - if (! $this->ensureDischargeSurgeryMasterTable()) { + if ($q === '') { return $this->response->setJSON(['rows' => []]); } - $builder = $this->db->table('ipd_discharge_surgery_master') - ->select('id,term_type,term_name,term_code,icd_code') - ->where('is_active', 1) - ->where('term_type', $type); + $cacheKey = 'surg_lkp_v4_' . md5($type . '_' . ($masterOnly ? 'm_' : 'all_') . mb_strtolower($q)); + try { + if ($cached = cache($cacheKey)) { + if (is_array($cached)) { + return $this->response->setJSON(['rows' => $cached]); + } + } + } catch (\Throwable $e) {} - if ($q !== '') { - $builder->groupStart() - ->like('term_name', $q) - ->orLike('term_code', $q) - ->orLike('icd_code', $q) - ->groupEnd(); + $rows = []; + $seen = []; + $maxLimit = 12; + + if ($this->ensureDischargeSurgeryMasterTable()) { + $masterRows = $this->db->table('ipd_discharge_surgery_master') + ->select('id,term_type,term_name,term_code,icd_code') + ->where('is_active', 1) + ->where('term_type', $type) + ->groupStart() + ->like('term_name', $q) + ->orLike('term_code', $q) + ->orLike('icd_code', $q) + ->groupEnd() + ->orderBy('term_name', 'ASC') + ->limit($maxLimit) + ->get() + ->getResultArray(); + + foreach ($masterRows as $row) { + $name = (string) ($row['term_name'] ?? ''); + if ($name === '') continue; + $k = mb_strtoupper($name); + if (isset($seen[$k])) continue; + $seen[$k] = true; + $rows[] = [ + 'id' => (int) ($row['id'] ?? 0), + 'term_type' => (string) ($row['term_type'] ?? $type), + 'term_name' => $name, + 'term_code' => (string) ($row['term_code'] ?? ''), + 'icd_code' => (string) ($row['icd_code'] ?? ''), + 'source' => 'master', + ]; + } } - $rows = $builder - ->orderBy('term_name', 'ASC') - ->limit(25) - ->get() - ->getResultArray(); + // SNOMED CT Procedure / Surgery Fallback (only if not master_only) + if (! $masterOnly && count($rows) < $maxLimit) { + $snomedRows = (new \App\Libraries\CsnotkTerminologyService())->searchProcedure($q, $maxLimit); + foreach ($snomedRows as $row) { + $name = (string) ($row['term'] ?? $row['fsn'] ?? ''); + if ($name === '') continue; + $conceptId = (string) ($row['concept_id'] ?? ''); + $k = mb_strtoupper($name); + if (isset($seen[$k])) continue; + $seen[$k] = true; + $rows[] = [ + 'id' => 0, + 'term_type' => $type, + 'term_name' => $name, + 'term_code' => $conceptId, + 'icd_code' => '', + 'source' => 'snomed_local', + ]; + if (count($rows) >= $maxLimit) break; + } + } - return $this->response->setJSON(['rows' => $rows]); + $res = array_slice($rows, 0, $maxLimit); + try { + cache()->save($cacheKey, $res, 86400 * 30); + } catch (\Throwable $e) {} + + return $this->response->setJSON(['rows' => $res]); } public function surgery_master_list() @@ -3730,6 +3786,7 @@ class Ipd_discharge extends BaseController public function course_master_lookup() { $q = trim((string) $this->request->getGet('q')); + $masterOnly = (int) ($this->request->getGet('master_only') ?? 0); $rows = []; if ($q !== '') { @@ -3753,6 +3810,20 @@ class Ipd_discharge extends BaseController 'term_name' => (string) ($row['term_name'] ?? ''), 'term_code' => (string) ($row['term_code'] ?? ''), 'icd_code' => (string) ($row['icd_code'] ?? ''), + 'source' => 'master', + ]; + } + } + + if ($masterOnly === 0 && count($rows) < 10 && method_exists($this, 'searchSnomedTerms')) { + $snomedRows = $this->searchSnomedTerms($q, 10 - count($rows)); + foreach ($snomedRows as $s) { + $rows[] = [ + 'id' => 0, + 'term_name' => (string) ($s['term'] ?? ''), + 'term_code' => (string) ($s['concept_id'] ?? ''), + 'icd_code' => '', + 'source' => 'snomed', ]; } } @@ -6131,6 +6202,24 @@ class Ipd_discharge extends BaseController $notice = 'Select a valid diagnosis row to remove.'; $noticeType = 'warning'; } + } + + if ($this->request->isAJAX() && in_array($action, ['add_surgery', 'remove_surgery', 'add_procedure', 'remove_procedure', 'add_diagnosis', 'remove_diagnosis', 'add_course', 'remove_course'], true)) { + $surgeryRows = $this->byIpdRows('ipd_discharge_surgery', ['id', 'surgery_name', 'surgery_date', 'surgery_remark'], 'id ASC', $ipdId); + $procedureRows = $this->byIpdRows('ipd_discharge_procedure', ['id', 'procedure_name', 'procedure_date', 'procedure_remark'], 'id ASC', $ipdId); + $diagnosisRows = $this->byIpdRows('ipd_discharge_diagnosis', ['id', 'comp_report', 'comp_remark'], 'id ASC', $ipdId); + $courseRows = $this->byIpdRows('ipd_discharge_course', ['id', 'comp_report', 'comp_remark'], 'id ASC', $ipdId); + return $this->response->setJSON([ + 'update' => ($savedAny ?? false) ? 1 : 0, + 'notice' => $notice ?? '', + 'noticeType' => $noticeType ?? 'info', + 'surgeryRows' => $surgeryRows, + 'procedureRows' => $procedureRows, + 'diagnosisRows' => $diagnosisRows, + 'courseRows' => $courseRows, + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); } elseif ($action === 'add_course') { $name = trim((string) ($this->request->getPost('new_course_name') ?? '')); $remark = trim((string) ($this->request->getPost('new_course_remark') ?? '')); @@ -8195,6 +8284,29 @@ class Ipd_discharge extends BaseController } } + $chiefComplaintsList = []; + if ($this->tableHasColumns('ipd_discharge_complaint', ['ipd_id', 'comp_report'])) { + $complaintRows = $this->byIpdRows('ipd_discharge_complaint', ['comp_report', 'comp_remark'], 'id ASC', $ipdId); + foreach ($complaintRows as $row) { + $term = trim((string) ($row['comp_report'] ?? '')); + $duration = trim((string) ($row['comp_remark'] ?? '')); + if ($term !== '') { + $text = $term . ($duration !== '' ? (' - ' . $duration) : ''); + $chiefComplaintsList[] = ['text' => $text, 'code' => '']; + } + } + } + + $complaintRemarkText = ''; + if ($this->tableHasColumns('ipd_discharge_complaint_remark', ['ipd_id', 'comp_remark'])) { + $complaintRemarkRow = $this->firstRowByIpd('ipd_discharge_complaint_remark', $ipdId); + $complaintRemarkText = trim((string) ($complaintRemarkRow['comp_remark'] ?? '')); + } + + if (empty($chiefComplaintsList) && $complaintRemarkText !== '') { + $chiefComplaintsList[] = ['text' => $complaintRemarkText, 'code' => '']; + } + $procedureRows = []; foreach ($this->byIpdRows('ipd_discharge_surgery', ['surgery_name', 'surgery_date'], 'id ASC', $ipdId) as $row) { $text = trim((string) ($row['surgery_name'] ?? '')); @@ -8297,6 +8409,8 @@ class Ipd_discharge extends BaseController 'start' => $admissionIso, 'end' => $dischargeIso, ], + 'chief_complaints' => $chiefComplaintsList, + 'chief_complaint_narrative' => $complaintRemarkText, 'conditions' => $conditionRows, 'procedures' => $procedureRows, 'medications' => $medicationRows, diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php index 006920b..41e19a6 100644 --- a/app/Controllers/Opd_prescription.php +++ b/app/Controllers/Opd_prescription.php @@ -2709,17 +2709,28 @@ class Opd_prescription extends BaseController */ public function opd_medicince_snomed_lookup() { + session_write_close(); + if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'rows' => []]); } $term = trim((string) $this->request->getGet('term')); - $limit = max(1, min(20, (int) ($this->request->getGet('limit') ?: 10))); + $limit = max(1, min(12, (int) ($this->request->getGet('limit') ?: 10))); if ($term === '') { return $this->response->setJSON(['update' => 1, 'rows' => [], 'message' => 'No term provided']); } + $cacheKey = 'med_snomed_loc_' . md5(mb_strtolower($term) . '_' . $limit); + try { + if ($cached = cache($cacheKey)) { + if (is_array($cached)) { + return $this->response->setJSON($cached); + } + } + } catch (\Throwable $e) {} + try { $svc = new \App\Libraries\CsnotkTerminologyService(); $rows = $svc->searchSubstance($term, $limit); @@ -2754,12 +2765,17 @@ class Opd_prescription extends BaseController ]; } - return $this->response->setJSON([ + $payload = [ 'update' => 1, 'rows' => $out, 'count' => count($out), 'message' => count($out) === 0 ? 'No SNOMED CT substance concepts found for "' . $term . '"' : '', - ]); + ]; + try { + cache()->save($cacheKey, $payload, 86400 * 30); + } catch (\Throwable $e) {} + + return $this->response->setJSON($payload); } public function opd_medicince_save() @@ -3928,12 +3944,29 @@ class Opd_prescription extends BaseController public function opd_invest_snomed_search() { + session_write_close(); + $q = trim((string) $this->request->getGet('q')); if ($q === '' || strlen($q) < 2) { return $this->response->setJSON(['rows' => []]); } + + $cacheKey = 'opd_invest_snomed_v3_' . md5(mb_strtolower($q)); + try { + if ($cached = cache($cacheKey)) { + if (is_array($cached)) { + return $this->response->setJSON(['rows' => $cached]); + } + } + } catch (\Throwable $e) {} + $svc = new \App\Libraries\CsnotkTerminologyService(); - $rows = $svc->searchProcedure($q, 20); + $rows = $svc->searchProcedure($q, 12); + + try { + cache()->save($cacheKey, $rows, 86400 * 30); + } catch (\Throwable $e) {} + return $this->response->setJSON(['rows' => $rows]); } @@ -6029,41 +6062,28 @@ class Opd_prescription extends BaseController public function complaints_search() { + session_write_close(); + $q = trim((string) $this->request->getGet('q')); if ($q === '') { return $this->response->setJSON(['rows' => []]); } + $needle = mb_strtolower($this->normalizeAutocompleteSuggestionText($q)); + $cacheKey = 'complaint_search_v3_' . md5($needle); + try { + if ($cached = cache($cacheKey)) { + if (is_array($cached)) { + return $this->response->setJSON(['rows' => $cached]); + } + } + } catch (\Throwable $e) {} + $out = []; $seen = []; - $needle = mb_strtolower($this->normalizeAutocompleteSuggestionText($q)); + $maxLimit = 12; - // 1. SNOMED CT via CSNOtk (finding / disorder semantic tags) - $csnotkRows = (new CsnotkTerminologyService())->searchFinding($q, 20); - foreach ($csnotkRows as $row) { - $name = $this->normalizeAutocompleteSuggestionText((string) ($row['term'] ?? '')); - if ($name === '') { - continue; - } - $conceptId = trim((string) ($row['concept_id'] ?? '')); - $hierarchy = trim((string) ($row['hierarchy'] ?? '')); - $k = mb_strtoupper($name) . '|' . $conceptId; - if (isset($seen[$k])) { - continue; - } - $seen[$k] = true; - $out[] = [ - 'name' => $name, - 'concept_id' => $conceptId, - 'hierarchy' => $hierarchy, - 'source' => 'snomed', - ]; - if (count($out) >= 20) { - return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); - } - } - - // 2. Local complaints_master fallback + // 1. Local complaints_master (Primary hospital master list - ~1ms response) if ($this->db->tableExists('complaints_master')) { $localRows = $this->db->table('complaints_master') ->select('Code as code, Name as name, name_hinglish, show_in_short, ai_hint') @@ -6075,7 +6095,7 @@ class Opd_prescription extends BaseController ->groupEnd() ->orderBy('show_in_short', 'DESC') ->orderBy('Name', 'ASC') - ->limit(20) + ->limit($maxLimit) ->get() ->getResultArray(); @@ -6096,41 +6116,72 @@ class Opd_prescription extends BaseController 'source' => 'local', 'name_hinglish' => (string) ($row['name_hinglish'] ?? ''), ]; - if (count($out) >= 20) { - return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); + } + } + + // 2. Hospital frequent autotype keywords + if (count($out) < $maxLimit) { + $keywordRows = $this->fetchAutotypeKeywords( + 'complaints', + $q, + max(0, (int) ($this->getCurrentUserId() ?? 0)), + 8 + ); + foreach ($keywordRows as $keyword) { + $name = $this->normalizeAutocompleteSuggestionText((string) $keyword); + if ($name === '' || ($needle !== '' && mb_stripos($name, $needle) === false)) { + continue; + } + $k = mb_strtoupper($name) . '|'; + if (isset($seen[$k])) { + continue; + } + $seen[$k] = true; + $out[] = [ + 'name' => $name, + 'concept_id' => '', + 'hierarchy' => '', + 'source' => 'keyword', + ]; + if (count($out) >= $maxLimit) { + break; } } } - // 3. User autotype keywords - $keywordRows = $this->fetchAutotypeKeywords( - 'complaints', - $q, - max(0, (int) ($this->getCurrentUserId() ?? 0)), - 10 - ); - foreach ($keywordRows as $keyword) { - $name = $this->normalizeAutocompleteSuggestionText((string) $keyword); - if ($name === '' || ($needle !== '' && mb_stripos($name, $needle) === false)) { - continue; - } - $k = mb_strtoupper($name) . '|'; - if (isset($seen[$k])) { - continue; - } - $seen[$k] = true; - $out[] = [ - 'name' => $name, - 'concept_id' => '', - 'hierarchy' => '', - 'source' => 'keyword', - ]; - if (count($out) >= 20) { - break; + // 3. Local 1.7M SNOMED CT database (100% offline local DB query) + if (count($out) < $maxLimit) { + $csnotkRows = (new CsnotkTerminologyService())->searchFinding($q, $maxLimit); + foreach ($csnotkRows as $row) { + $name = $this->normalizeAutocompleteSuggestionText((string) ($row['term'] ?? '')); + if ($name === '') { + continue; + } + $conceptId = trim((string) ($row['concept_id'] ?? '')); + $hierarchy = trim((string) ($row['hierarchy'] ?? '')); + $k = mb_strtoupper($name) . '|' . $conceptId; + if (isset($seen[$k])) { + continue; + } + $seen[$k] = true; + $out[] = [ + 'name' => $name, + 'concept_id' => $conceptId, + 'hierarchy' => $hierarchy, + 'source' => 'snomed_local', + ]; + if (count($out) >= $maxLimit) { + break; + } } } - return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); + $res = array_slice($out, 0, $maxLimit); + try { + cache()->save($cacheKey, $res, 86400 * 30); // 30-day server cache for repeat queries + } catch (\Throwable $e) {} + + return $this->response->setJSON(['rows' => $res]); } /** @@ -6241,142 +6292,153 @@ class Opd_prescription extends BaseController public function provisional_diagnosis_search() { + session_write_close(); + $q = trim((string) $this->request->getGet('q')); if ($q === '') { return $this->response->setJSON(['rows' => []]); } + $needle = mb_strtolower($this->normalizeAutocompleteSuggestionText($q)); + $cacheKey = 'prov_diag_v3_' . md5($needle); + try { + if ($cached = cache($cacheKey)) { + if (is_array($cached)) { + return $this->response->setJSON(['rows' => $cached]); + } + } + } catch (\Throwable $e) {} + $out = []; $seen = []; - $needle = mb_strtolower($this->normalizeAutocompleteSuggestionText($q)); - $hasLocalSnomedRows = false; + $maxLimit = 12; - $csnotkRows = (new CsnotkTerminologyService())->searchDiagnosis($q, 20); - foreach ($csnotkRows as $row) { - $name = $this->normalizeAutocompleteSuggestionText((string) ($row['term'] ?? '')); - if ($name === '') { - continue; - } - if ($needle !== '' && mb_stripos($name, $needle) === false) { - continue; - } + // 1. Local disease_master hospital master list (Primary ~1ms) + if ($this->db->tableExists('disease_master')) { + $fields = $this->db->getFieldNames('disease_master') ?? []; + $nameField = in_array('Name', $fields, true) ? 'Name' : (in_array('name', $fields, true) ? 'name' : null); + if ($nameField !== null) { + $diseaseRows = $this->db->table('disease_master') + ->select($nameField . ' as name') + ->like($nameField, $q) + ->orderBy($nameField, 'ASC') + ->limit($maxLimit) + ->get() + ->getResultArray(); - $conceptId = trim((string) ($row['concept_id'] ?? '')); - $source = trim((string) ($row['source'] ?? 'csnotk')); - if (in_array($source, ['local', 'snomed'], true)) { - $hasLocalSnomedRows = true; - } - $k = mb_strtoupper($name) . '|' . $conceptId; - if (isset($seen[$k])) { - continue; - } - $seen[$k] = true; - - $out[] = [ - 'name' => $name, - 'snomed_concept_id' => $conceptId, - 'snomed_term' => $name, - 'source' => $source, - ]; - - if (count($out) >= 20) { - return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); + foreach ($diseaseRows as $row) { + $name = $this->normalizeAutocompleteSuggestionText((string) ($row['name'] ?? '')); + if ($name === '' || ($needle !== '' && mb_stripos($name, $needle) === false)) { + continue; + } + $k = mb_strtoupper($name); + if (isset($seen[$k])) { + continue; + } + $seen[$k] = true; + $out[] = [ + 'name' => $name, + 'snomed_concept_id' => '', + 'snomed_term' => '', + 'source' => 'disease_master', + ]; + } } } - $snomedRows = ($hasLocalSnomedRows || mb_strlen($needle) < 3) ? [] : $this->fetchSnomedDiagnosisRows($q, 20); - foreach ($snomedRows as $row) { - $name = $this->normalizeAutocompleteSuggestionText((string) ($row['term'] ?? '')); - if ($name === '') { - continue; - } - if ($needle !== '' && mb_stripos($name, $needle) === false) { - continue; - } - - $k = mb_strtoupper($name); - if (isset($seen[$k])) { - continue; - } - $seen[$k] = true; - $out[] = [ - 'name' => $name, - 'snomed_concept_id' => (string) ($row['concept_id'] ?? ''), - 'snomed_term' => $name, - 'source' => 'snomed', - ]; - - if (count($out) >= 20) { - return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); + // 2. Hospital frequent autotype keywords + if (count($out) < $maxLimit) { + $keywordRows = $this->fetchAutotypeKeywords( + 'provisional_diagnosis', + $q, + max(0, (int) ($this->getCurrentUserId() ?? 0)), + 8 + ); + foreach ($keywordRows as $keyword) { + $name = $this->normalizeAutocompleteSuggestionText((string) $keyword); + if ($name === '' || ($needle !== '' && mb_stripos($name, $needle) === false)) { + continue; + } + $k = mb_strtoupper($name); + if (isset($seen[$k])) { + continue; + } + $seen[$k] = true; + $out[] = [ + 'name' => $name, + 'snomed_concept_id' => '', + 'snomed_term' => '', + 'source' => 'keyword', + ]; + if (count($out) >= $maxLimit) { + break; + } } } - if (! $this->db->tableExists('disease_master')) { - return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); + // 3. Local 1.7M SNOMED CT Database (100% offline local DB query) + if (count($out) < $maxLimit) { + $csnotkRows = (new CsnotkTerminologyService())->searchDiagnosis($q, $maxLimit); + foreach ($csnotkRows as $row) { + $name = $this->normalizeAutocompleteSuggestionText((string) ($row['term'] ?? '')); + if ($name === '' || ($needle !== '' && mb_stripos($name, $needle) === false)) { + continue; + } + + $conceptId = trim((string) ($row['concept_id'] ?? '')); + $source = trim((string) ($row['source'] ?? 'snomed_local')); + $k = mb_strtoupper($name) . '|' . $conceptId; + if (isset($seen[$k])) { + continue; + } + $seen[$k] = true; + + $out[] = [ + 'name' => $name, + 'snomed_concept_id' => $conceptId, + 'snomed_term' => $name, + 'source' => $source, + ]; + + if (count($out) >= $maxLimit) { + break; + } + } } - $fields = $this->db->getFieldNames('disease_master') ?? []; - $nameField = in_array('Name', $fields, true) ? 'Name' : (in_array('name', $fields, true) ? 'name' : null); - if ($nameField === null) { - return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); + // 4. Secondary local SNOMED fallback if still below limit + if (count($out) < $maxLimit) { + $snomedRows = $this->fetchSnomedDiagnosisRows($q, $maxLimit); + foreach ($snomedRows as $row) { + $name = $this->normalizeAutocompleteSuggestionText((string) ($row['term'] ?? '')); + if ($name === '' || ($needle !== '' && mb_stripos($name, $needle) === false)) { + continue; + } + + $k = mb_strtoupper($name); + if (isset($seen[$k])) { + continue; + } + $seen[$k] = true; + $out[] = [ + 'name' => $name, + 'snomed_concept_id' => (string) ($row['concept_id'] ?? ''), + 'snomed_term' => $name, + 'source' => 'snomed_local', + ]; + + if (count($out) >= $maxLimit) { + break; + } + } } - $rows = $this->db->table('disease_master') - ->select($nameField . ' as name') - ->like($nameField, $q) - ->orderBy($nameField, 'ASC') - ->limit(20) - ->get() - ->getResultArray(); + $res = array_slice($out, 0, $maxLimit); + try { + cache()->save($cacheKey, $res, 86400 * 30); // 30-day server cache + } catch (\Throwable $e) {} - $keywordRows = $this->fetchAutotypeKeywords( - 'provisional_diagnosis', - $q, - max(0, (int) ($this->getCurrentUserId() ?? 0)), - 20 - ); - foreach ($rows as $row) { - $name = $this->normalizeAutocompleteSuggestionText((string) ($row['name'] ?? '')); - if ($name === '') { - continue; - } - if ($needle !== '' && mb_stripos($name, $needle) === false) { - continue; - } - $k = mb_strtoupper($name); - if (isset($seen[$k])) { - continue; - } - $seen[$k] = true; - $out[] = [ - 'name' => $name, - 'snomed_concept_id' => '', - 'snomed_term' => '', - 'source' => 'disease_master', - ]; - } - foreach ($keywordRows as $keyword) { - $name = $this->normalizeAutocompleteSuggestionText((string) $keyword); - if ($name === '') { - continue; - } - if ($needle !== '' && mb_stripos($name, $needle) === false) { - continue; - } - $k = mb_strtoupper($name); - if (isset($seen[$k])) { - continue; - } - $seen[$k] = true; - $out[] = [ - 'name' => $name, - 'snomed_concept_id' => '', - 'snomed_term' => '', - 'source' => 'keyword', - ]; - } - - return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); + return $this->response->setJSON(['rows' => $res]); } private function fetchSnomedDiagnosisRows(string $query, int $limit = 20): array diff --git a/app/Controllers/Setting/BedManagement.php b/app/Controllers/Setting/BedManagement.php index 4d78353..95a05bf 100644 --- a/app/Controllers/Setting/BedManagement.php +++ b/app/Controllers/Setting/BedManagement.php @@ -74,14 +74,24 @@ class BedManagement extends BaseController $departments = array_merge($departments, $departmentRows); } + $nursingStationModel = new \App\Models\NursingStationModel(); + $nursingStations = $nursingStationModel->getActiveStations(); + $wardBuilder = $this->db->table('ward_master w') ->orderBy('w.ward_name', 'ASC'); + if ($this->db->tableExists('nursing_station_master')) { + $wardBuilder->select('w.*, ns.station_name, ns.station_code') + ->join('nursing_station_master ns', 'ns.id = w.nursing_station_id', 'left'); + } else { + $wardBuilder->select('w.*, NULL as station_name, NULL as station_code'); + } + if ($this->db->tableExists('hc_department')) { - $wardBuilder->select('w.*, COALESCE(d.vName, "All Department") AS department_name', false) + $wardBuilder->select('COALESCE(d.vName, "All Department") AS department_name', false) ->join('hc_department d', 'd.iId = w.department_id', 'left'); } else { - $wardBuilder->select('w.*, "All Department" AS department_name', false); + $wardBuilder->select('"All Department" AS department_name', false); } $wardRows = $wardBuilder->get()->getResult(); @@ -89,6 +99,7 @@ class BedManagement extends BaseController return view('Setting/Bed/ward_list', [ 'wards' => $wardRows, 'departments' => $departments, + 'nursingStations' => $nursingStations, ]); } @@ -103,6 +114,7 @@ class BedManagement extends BaseController $wardName = trim((string) $this->request->getPost('ward_name')); $departmentIdRaw = $this->request->getPost('department_id'); $departmentId = $departmentIdRaw === null || $departmentIdRaw === '' ? 0 : (int) $departmentIdRaw; + $nursingStationId = (int) ($this->request->getPost('nursing_station_id') ?? 0); if ($wardCode === '' || $wardName === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Ward code and ward name are required.']); @@ -127,6 +139,7 @@ class BedManagement extends BaseController 'ward_code' => $wardCode, 'ward_name' => $wardName, 'department_id' => $departmentId, + 'nursing_station_id' => $nursingStationId > 0 ? $nursingStationId : null, 'building_id' => $this->request->getPost('building_id') ?: null, 'floor_number' => $this->request->getPost('floor_number') ?: null, 'ward_type' => $this->request->getPost('ward_type') ?: 'General', diff --git a/app/Controllers/Setting/Nurse.php b/app/Controllers/Setting/Nurse.php new file mode 100644 index 0000000..54ba2e8 --- /dev/null +++ b/app/Controllers/Setting/Nurse.php @@ -0,0 +1,108 @@ +nurseModel = new NurseModel(); + } + + public function index(): string + { + $q = trim((string) ($this->request->getGet('q') ?? '')); + $nurses = $this->nurseModel->getNurses($q); + + return view('Setting/Nurse/Nurse_Search_V', [ + 'nurses' => $nurses, + 'searchQuery' => $q, + ]); + } + + public function save() + { + if (! $this->request->isAJAX()) { + return $this->response->setStatusCode(400)->setJSON(['ok' => false, 'error' => 'Invalid request']); + } + + $id = (int) ($this->request->getPost('id') ?? 0); + $nurseCode = trim((string) ($this->request->getPost('nurse_code') ?? '')); + $name = trim((string) ($this->request->getPost('name') ?? '')); + $hprId = trim((string) ($this->request->getPost('hpr_id') ?? '')); + $registrationNo = trim((string) ($this->request->getPost('registration_no') ?? '')); + $gender = trim((string) ($this->request->getPost('gender') ?? 'Female')); + $designation = trim((string) ($this->request->getPost('designation') ?? 'Staff Nurse')); + $qualification = trim((string) ($this->request->getPost('qualification') ?? '')); + $contactNo = trim((string) ($this->request->getPost('contact_no') ?? '')); + $email = trim((string) ($this->request->getPost('email') ?? '')); + $department = trim((string) ($this->request->getPost('department') ?? 'Nursing')); + $isActive = (int) ($this->request->getPost('is_active') ?? 1); + + if ($name === '') { + return $this->response->setJSON(['ok' => false, 'error' => 'Nurse Name is required.']); + } + + if ($nurseCode === '') { + // Auto generate code if empty + $nurseCode = 'NUR-' . str_pad((string) (rand(100, 999)), 3, '0', STR_PAD_LEFT); + } + + $payload = [ + 'nurse_code' => $nurseCode, + 'name' => $name, + 'hpr_id' => $hprId, + 'registration_no' => $registrationNo, + 'gender' => $gender, + 'designation' => $designation, + 'qualification' => $qualification, + 'contact_no' => $contactNo, + 'email' => $email, + 'department' => $department, + 'is_active' => $isActive, + ]; + + if ($id > 0) { + $this->nurseModel->updateNurse($id, $payload); + $savedId = $id; + } else { + $savedId = $this->nurseModel->insertNurse($payload); + } + + return $this->response->setJSON([ + 'ok' => true, + 'message' => $id > 0 ? 'Nurse updated successfully.' : 'Nurse created successfully.', + 'id' => $savedId, + 'nurse' => $this->nurseModel->getNurseById($savedId), + ]); + } + + public function delete($id = null) + { + $id = (int) ($id ?? $this->request->getPost('id') ?? 0); + if ($id <= 0) { + return $this->response->setJSON(['ok' => false, 'error' => 'Invalid nurse ID.']); + } + + $this->nurseModel->deleteNurse($id); + + return $this->response->setJSON([ + 'ok' => true, + 'message' => 'Nurse record deleted successfully.', + ]); + } + + public function list_json() + { + $nurses = $this->nurseModel->getActiveNurses(); + return $this->response->setJSON([ + 'ok' => true, + 'nurses' => $nurses, + ]); + } +} diff --git a/app/Controllers/Setting/NursingStation.php b/app/Controllers/Setting/NursingStation.php new file mode 100644 index 0000000..72cb9c5 --- /dev/null +++ b/app/Controllers/Setting/NursingStation.php @@ -0,0 +1,104 @@ +stationModel = new NursingStationModel(); + $this->nurseModel = new NurseModel(); + } + + public function index() + { + $q = trim((string) ($this->request->getGet('q') ?? '')); + $stations = $this->stationModel->getStations($q); + $nurses = $this->nurseModel->getActiveNurses(); + + return view('Setting/NursingStation/nursing_station_index', [ + 'stations' => $stations, + 'nurses' => $nurses, + 'searchQuery' => $q, + ]); + } + + public function save() + { + if (! $this->request->isAJAX()) { + return $this->response->setStatusCode(400)->setJSON(['ok' => false, 'error' => 'Invalid request']); + } + + $id = (int) ($this->request->getPost('id') ?? 0); + $stationCode = trim((string) ($this->request->getPost('station_code') ?? '')); + $stationName = trim((string) ($this->request->getPost('station_name') ?? '')); + $floorNumber = trim((string) ($this->request->getPost('floor_number') ?? '')); + $inchargeNurseId = (int) ($this->request->getPost('incharge_nurse_id') ?? 0); + $contactNo = trim((string) ($this->request->getPost('contact_no') ?? '')); + $status = trim((string) ($this->request->getPost('status') ?? 'active')); + $remarks = trim((string) ($this->request->getPost('remarks') ?? '')); + + if ($stationName === '') { + return $this->response->setJSON(['ok' => false, 'error' => 'Station Name is required.']); + } + + if ($stationCode === '') { + $stationCode = 'NS-' . str_pad((string) (rand(100, 999)), 3, '0', STR_PAD_LEFT); + } + + $inchargeNurseName = ''; + if ($inchargeNurseId > 0) { + $nurseRow = $this->nurseModel->getNurseById($inchargeNurseId); + if ($nurseRow) { + $inchargeNurseName = trim(($nurseRow['nurse_code'] ? '[' . $nurseRow['nurse_code'] . '] ' : '') . $nurseRow['name']); + } + } + + $payload = [ + 'station_code' => $stationCode, + 'station_name' => $stationName, + 'floor_number' => $floorNumber, + 'incharge_nurse_id' => $inchargeNurseId, + 'incharge_nurse_name' => $inchargeNurseName, + 'contact_no' => $contactNo, + 'status' => $status, + 'remarks' => $remarks, + ]; + + if ($id > 0) { + $ok = $this->stationModel->updateStation($id, $payload); + return $this->response->setJSON(['ok' => $ok, 'error' => $ok ? '' : 'Failed to update nursing station']); + } + + $newId = $this->stationModel->insertStation($payload); + return $this->response->setJSON(['ok' => $newId > 0, 'id' => $newId, 'error' => $newId > 0 ? '' : 'Failed to insert nursing station']); + } + + public function delete() + { + if (! $this->request->isAJAX()) { + return $this->response->setStatusCode(400)->setJSON(['ok' => false, 'error' => 'Invalid request']); + } + + $id = (int) ($this->request->getPost('id') ?? 0); + if ($id <= 0) { + return $this->response->setJSON(['ok' => false, 'error' => 'Invalid Station ID']); + } + + $ok = $this->stationModel->deleteStation($id); + return $this->response->setJSON(['ok' => $ok, 'error' => $ok ? '' : 'Failed to delete station']); + } + + public function list_json() + { + $stations = $this->stationModel->getActiveStations(); + return $this->response->setJSON(['ok' => true, 'stations' => $stations]); + } +} diff --git a/app/Libraries/CsnotkTerminologyService.php b/app/Libraries/CsnotkTerminologyService.php index f9134db..add94cc 100644 --- a/app/Libraries/CsnotkTerminologyService.php +++ b/app/Libraries/CsnotkTerminologyService.php @@ -28,10 +28,10 @@ class CsnotkTerminologyService $baseUrl = trim((string) env('CSNOTK_BASE_URL', 'https://csnotk.e-atria.in')); } $this->baseUrl = rtrim($baseUrl, '/'); - $this->timeoutSec = max(1, (int) env('snomed.csnotk.timeoutSec', 5)); + $this->timeoutSec = 1; - $enabledRaw = strtolower(trim((string) env('snomed.csnotk.enabled', env('CSNOTK_ENABLED', '')))); - $this->apiEnabled = in_array($enabledRaw, ['1', 'true', 'yes', 'on'], true) || $this->baseUrl !== ''; + // Force 100% Local DB mode (No external HTTP calls) + $this->apiEnabled = false; try { $db = db_connect(); @@ -41,63 +41,83 @@ class CsnotkTerminologyService $this->hasFtIndex = $this->checkFtIndex($db); } } catch (\Throwable $e) { - // Local DB not available — will fall through to API + // Local DB not available } } public function isEnabled(): bool { - return $this->localAvailable || ($this->apiEnabled && $this->baseUrl !== ''); + return $this->localAvailable; } - // ─── Public search API ──────────────────────────────────────────────── + // ─── Public search API ─────────────────────────────────────────────────── /** * General diagnosis search (findings + disorders). * @return array */ - public function searchDiagnosis(string $query, int $limit = 20): array + public function searchDiagnosis(string $query, int $limit = 12): array { $q = trim($query); if ($q === '') { return []; } - $limit = max(1, min(50, $limit)); + $limit = max(1, min(12, $limit)); - // Local DB is primary — ~17ms vs ~330ms for external API from server + $cacheKey = 'snomed_diag_loc_' . md5(mb_strtolower($q) . '_' . $limit); + try { + if ($cached = cache($cacheKey)) { + if (is_array($cached)) { + return $cached; + } + } + } catch (\Throwable $e) {} + + // Local DB is 100% primary (<20ms) $rows = $this->localSearch($q, $limit, ['finding', 'disorder', 'morphologic abnormality', 'disease']); - - if (count($rows) < $limit && mb_strlen($q) >= 3 && $this->apiEnabled && $this->baseUrl !== '') { - $rows = $this->mergeFallback($rows, $this->apiSearchDiagnosis($q, $limit), $limit); - } - - return array_map(fn ($r) => [ + $res = array_map(fn ($r) => [ 'concept_id' => (string) ($r['concept_id'] ?? ''), 'term' => (string) ($r['term'] ?? ''), - 'source' => (string) ($r['source'] ?? 'local'), + 'source' => 'snomed_local', ], array_slice($rows, 0, $limit)); + + try { + cache()->save($cacheKey, $res, 86400 * 30); // Cache for 30 days + } catch (\Throwable $e) {} + + return $res; } /** * Search for clinical findings / symptoms / disorders (for chief complaints autocomplete). * @return array */ - public function searchFinding(string $query, int $limit = 20): array + public function searchFinding(string $query, int $limit = 12): array { $q = trim($query); if ($q === '') { return []; } - $limit = max(1, min(50, $limit)); + $limit = max(1, min(12, $limit)); - // Local DB is primary — ~17ms vs ~330ms for external API from server + $cacheKey = 'snomed_find_loc_' . md5(mb_strtolower($q) . '_' . $limit); + try { + if ($cached = cache($cacheKey)) { + if (is_array($cached)) { + return $cached; + } + } + } catch (\Throwable $e) {} + + // Local DB is 100% primary (<20ms) $rows = $this->localSearch($q, $limit, ['finding', 'disorder']); + $res = array_slice($rows, 0, $limit); - if (count($rows) < $limit && mb_strlen($q) >= 3 && $this->apiEnabled && $this->baseUrl !== '') { - $rows = $this->mergeFallback($rows, $this->apiSearchBySemTag($q, $limit, ['finding', 'disorder']), $limit); - } + try { + cache()->save($cacheKey, $res, 86400 * 30); // Cache for 30 days + } catch (\Throwable $e) {} - return array_slice($rows, 0, $limit); + return $res; } /** @@ -201,16 +221,15 @@ class CsnotkTerminologyService } /** - * FULLTEXT MATCH ... AGAINST (Boolean Mode) search. + * FULLTEXT MATCH ... AGAINST (Boolean Mode) search on local 1.7M snomed_description table. * @return array> */ private function localFtSearch(string $q, int $limit): array { $words = preg_split('/\s+/', mb_strtolower(trim($q))) ?: [$q]; - $ftWords = array_filter($words, fn ($w) => mb_strlen($w) >= 3); + $ftWords = array_filter($words, fn ($w) => mb_strlen($w) >= 2); if (empty($ftWords)) { - // FT cannot handle words shorter than innodb_ft_min_token_size (default 3) return $this->localPrefixSearch($q, $limit); } @@ -218,19 +237,19 @@ class CsnotkTerminologyService try { return (array) $this->db->query( - "SELECT d.concept_id, d.term + "SELECT d.concept_id, d.term, + (SELECT d2.term FROM snomed_description d2 WHERE d2.concept_id = d.concept_id AND d2.type_id = ? AND d2.active = 1 LIMIT 1) AS fsn FROM snomed_description d INNER JOIN snomed_concept c ON c.concept_id = d.concept_id AND c.active = 1 WHERE d.active = 1 AND d.language_code = 'en' - AND d.type_id = ? AND MATCH(d.term) AGAINST(? IN BOOLEAN MODE) - ORDER BY d.term ASC + ORDER BY LENGTH(d.term) ASC LIMIT ?", [self::FSN_TYPE_ID, $ftQuery, $limit] )->getResultArray(); } catch (\Throwable $e) { - $this->hasFtIndex = false; // Index may not exist yet — disable for this request + $this->hasFtIndex = false; return $this->localPrefixSearch($q, $limit); } } @@ -244,14 +263,14 @@ class CsnotkTerminologyService $norm = mb_strtolower(trim($q)); try { return (array) $this->db->query( - "SELECT d.concept_id, d.term + "SELECT d.concept_id, d.term, + (SELECT d2.term FROM snomed_description d2 WHERE d2.concept_id = d.concept_id AND d2.type_id = ? AND d2.active = 1 LIMIT 1) AS fsn FROM snomed_description d INNER JOIN snomed_concept c ON c.concept_id = d.concept_id AND c.active = 1 WHERE d.active = 1 AND d.language_code = 'en' - AND d.type_id = ? AND d.term_normalized LIKE ? - ORDER BY d.term ASC + ORDER BY LENGTH(d.term) ASC LIMIT ?", [self::FSN_TYPE_ID, $norm . '%', $limit] )->getResultArray(); @@ -261,7 +280,7 @@ class CsnotkTerminologyService } /** - * Substring search: term_normalized LIKE '%query%' (full scan — slower). + * Substring search: term_normalized LIKE '%query%'. * @return array> */ private function localSubstringSearch(string $q, int $limit): array @@ -269,14 +288,14 @@ class CsnotkTerminologyService $norm = mb_strtolower(trim($q)); try { return (array) $this->db->query( - "SELECT d.concept_id, d.term + "SELECT d.concept_id, d.term, + (SELECT d2.term FROM snomed_description d2 WHERE d2.concept_id = d.concept_id AND d2.type_id = ? AND d2.active = 1 LIMIT 1) AS fsn FROM snomed_description d INNER JOIN snomed_concept c ON c.concept_id = d.concept_id AND c.active = 1 WHERE d.active = 1 AND d.language_code = 'en' - AND d.type_id = ? AND d.term_normalized LIKE ? - ORDER BY d.term ASC + ORDER BY LENGTH(d.term) ASC LIMIT ?", [self::FSN_TYPE_ID, '%' . $norm . '%', $limit] )->getResultArray(); @@ -304,8 +323,9 @@ class CsnotkTerminologyService } $conceptId = trim((string) ($row['concept_id'] ?? '')); - $fsn = trim((string) ($row['term'] ?? '')); - if ($conceptId === '' || $fsn === '') { + $term = trim((string) ($row['term'] ?? '')); + $fsn = trim((string) ($row['fsn'] ?? '')); + if ($conceptId === '' || $term === '') { continue; } @@ -313,16 +333,17 @@ class CsnotkTerminologyService continue; } - // Extract semantic tag from FSN suffix: "Fever (finding)" → "finding" - $hierarchy = ''; - $displayTerm = $fsn; - if (preg_match('/^(.*)\(([^)]+)\)$/', $fsn, $m)) { - $hierarchy = trim($m[2]); - $displayTerm = trim($m[1]); + // Extract semantic tag from FSN or term suffix + $hierarchy = ''; + if ($fsn !== '' && preg_match('/^(.*)\(([^)]+)\)$/', $fsn, $m)) { + $hierarchy = trim($m[2]); + } elseif (preg_match('/^(.*)\(([^)]+)\)$/', $term, $m)) { + $hierarchy = trim($m[2]); + $term = trim($m[1]); } - // Apply semantic tag filter - if (! empty($tagsLower)) { + // Apply semantic tag filter if tags specified and hierarchy identified + if (! empty($tagsLower) && $hierarchy !== '') { if (! in_array(mb_strtolower($hierarchy), $tagsLower, true)) { continue; } @@ -331,17 +352,17 @@ class CsnotkTerminologyService $seen[$conceptId] = true; $out[] = [ 'concept_id' => $conceptId, - 'term' => $displayTerm !== '' ? $displayTerm : $fsn, - 'fsn' => $fsn, + 'term' => $term, + 'fsn' => $fsn !== '' ? $fsn : $term, 'hierarchy' => $hierarchy, - 'source' => 'local', + 'source' => 'snomed_local', ]; } return $out; } - // ─── External API fallbacks ─────────────────────────────────────────── + // ─── External API fallbacks ─────────────────────────────────────────────── /** * @return array diff --git a/app/Models/NurseModel.php b/app/Models/NurseModel.php new file mode 100644 index 0000000..096a050 --- /dev/null +++ b/app/Models/NurseModel.php @@ -0,0 +1,108 @@ +db = db_connect(); + $this->ensureNurseMasterTable(); + } + + public function ensureNurseMasterTable(): bool + { + if (! $this->db->tableExists('nurse_master')) { + $sql = "CREATE TABLE IF NOT EXISTS nurse_master ( + id INT AUTO_INCREMENT PRIMARY KEY, + nurse_code VARCHAR(50) NOT NULL, + name VARCHAR(255) NOT NULL, + hpr_id VARCHAR(100) NULL, + registration_no VARCHAR(100) NULL, + gender VARCHAR(20) NULL, + designation VARCHAR(100) NULL, + qualification VARCHAR(255) NULL, + contact_no VARCHAR(50) NULL, + email VARCHAR(100) NULL, + department VARCHAR(100) DEFAULT 'Nursing', + is_active TINYINT DEFAULT 1, + created_at DATETIME NULL, + updated_at DATETIME NULL, + INDEX idx_nurse_code (nurse_code), + INDEX idx_hpr_id (hpr_id), + INDEX idx_is_active (is_active) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; + $this->db->query($sql); + } + + $fields = $this->db->getFieldNames('nurse_master') ?? []; + if (! in_array('hpr_id', $fields, true)) { + $this->db->query("ALTER TABLE nurse_master ADD COLUMN hpr_id VARCHAR(100) NULL AFTER name"); + } + if (! in_array('registration_no', $fields, true)) { + $this->db->query("ALTER TABLE nurse_master ADD COLUMN registration_no VARCHAR(100) NULL AFTER hpr_id"); + } + if (! in_array('gender', $fields, true)) { + $this->db->query("ALTER TABLE nurse_master ADD COLUMN gender VARCHAR(20) NULL AFTER registration_no"); + } + if (! in_array('designation', $fields, true)) { + $this->db->query("ALTER TABLE nurse_master ADD COLUMN designation VARCHAR(100) NULL AFTER gender"); + } + + return true; + } + + public function getNurses(string $q = ''): array + { + $builder = $this->db->table('nurse_master'); + if ($q !== '') { + $builder->groupStart() + ->like('name', $q) + ->orLike('nurse_code', $q) + ->orLike('hpr_id', $q) + ->orLike('registration_no', $q) + ->orLike('qualification', $q) + ->orLike('contact_no', $q) + ->groupEnd(); + } + return $builder->orderBy('name', 'ASC')->get()->getResultArray(); + } + + public function getActiveNurses(): array + { + return $this->db->table('nurse_master') + ->where('is_active', 1) + ->orderBy('name', 'ASC') + ->get() + ->getResultArray(); + } + + public function getNurseById(int $id): ?array + { + $row = $this->db->table('nurse_master')->where('id', $id)->get()->getRowArray(); + return $row ?: null; + } + + public function insertNurse(array $data): int + { + $data['created_at'] = date('Y-m-d H:i:s'); + $data['updated_at'] = date('Y-m-d H:i:s'); + $this->db->table('nurse_master')->insert($data); + return (int) $this->db->insertID(); + } + + public function updateNurse(int $id, array $data): bool + { + $data['updated_at'] = date('Y-m-d H:i:s'); + return (bool) $this->db->table('nurse_master')->where('id', $id)->update($data); + } + + public function deleteNurse(int $id): bool + { + return (bool) $this->db->table('nurse_master')->where('id', $id)->delete(); + } +} diff --git a/app/Models/NursingStationModel.php b/app/Models/NursingStationModel.php new file mode 100644 index 0000000..286c9e3 --- /dev/null +++ b/app/Models/NursingStationModel.php @@ -0,0 +1,95 @@ +db = db_connect(); + $this->ensureNursingStationTable(); + } + + public function ensureNursingStationTable(): bool + { + if (! $this->db->tableExists('nursing_station_master')) { + $sql = "CREATE TABLE IF NOT EXISTS nursing_station_master ( + id INT AUTO_INCREMENT PRIMARY KEY, + station_code VARCHAR(50) NOT NULL, + station_name VARCHAR(255) NOT NULL, + floor_number VARCHAR(50) NULL, + incharge_nurse_id INT NULL, + incharge_nurse_name VARCHAR(255) NULL, + contact_no VARCHAR(50) NULL, + status ENUM('active', 'inactive') DEFAULT 'active', + remarks TEXT NULL, + created_at DATETIME NULL, + updated_at DATETIME NULL, + INDEX idx_station_code (station_code), + INDEX idx_status (status) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; + $this->db->query($sql); + } + + if ($this->db->tableExists('ward_master')) { + $fields = $this->db->getFieldNames('ward_master') ?? []; + if (! in_array('nursing_station_id', $fields, true)) { + $this->db->query("ALTER TABLE ward_master ADD COLUMN nursing_station_id INT NULL AFTER department_id"); + } + } + + return true; + } + + public function getStations(string $q = ''): array + { + $builder = $this->db->table('nursing_station_master'); + if ($q !== '') { + $builder->groupStart() + ->like('station_name', $q) + ->orLike('station_code', $q) + ->orLike('floor_number', $q) + ->orLike('incharge_nurse_name', $q) + ->groupEnd(); + } + return $builder->orderBy('station_name', 'ASC')->get()->getResultArray(); + } + + public function getActiveStations(): array + { + return $this->db->table('nursing_station_master') + ->where('status', 'active') + ->orderBy('station_name', 'ASC') + ->get() + ->getResultArray(); + } + + public function getStationById(int $id): ?array + { + $row = $this->db->table('nursing_station_master')->where('id', $id)->get()->getRowArray(); + return $row ?: null; + } + + public function insertStation(array $data): int + { + $data['created_at'] = date('Y-m-d H:i:s'); + $data['updated_at'] = date('Y-m-d H:i:s'); + $this->db->table('nursing_station_master')->insert($data); + return (int) $this->db->insertID(); + } + + public function updateStation(int $id, array $data): bool + { + $data['updated_at'] = date('Y-m-d H:i:s'); + return (bool) $this->db->table('nursing_station_master')->where('id', $id)->update($data); + } + + public function deleteStation(int $id): bool + { + return (bool) $this->db->table('nursing_station_master')->where('id', $id)->delete(); + } +} diff --git a/app/Views/Setting/Admin/index.php b/app/Views/Setting/Admin/index.php index a4cf7af..3b67281 100644 --- a/app/Views/Setting/Admin/index.php +++ b/app/Views/Setting/Admin/index.php @@ -1,10 +1,10 @@
-
+ + diff --git a/app/Views/Setting/Bed/ward_list.php b/app/Views/Setting/Bed/ward_list.php index 7a5104d..efbe6c2 100644 --- a/app/Views/Setting/Bed/ward_list.php +++ b/app/Views/Setting/Bed/ward_list.php @@ -35,6 +35,15 @@ +
+ + +
@@ -129,6 +138,7 @@ Code Name Department + Nursing Station Type Capacity Status @@ -143,6 +153,13 @@ ward_code ?? '') ?> ward_name ?? '') ?> department_name ?? 'All Department') ?> + + station_name)): ?> + station_name) ?> + + - + + ward_type ?? '') ?> total_capacity ?? 0) ?> status ?? '') ?> @@ -180,6 +197,7 @@ function clearForm() { form.reset(); $('#ward_id').val(0); + $('#nursing_station_id').val(0); $('#total_capacity').val(0); clearAlert(); } @@ -213,6 +231,7 @@ $('#ward_code').val(item.ward_code || ''); $('#ward_name').val(item.ward_name || ''); $('#department_id').val(item.department_id !== undefined ? item.department_id : 0); + $('#nursing_station_id').val(item.nursing_station_id !== undefined && item.nursing_station_id !== null ? item.nursing_station_id : 0); $('#floor_number').val(item.floor_number || ''); $('#total_capacity').val(item.total_capacity || 0); $('#ward_type').val(item.ward_type || 'General'); diff --git a/app/Views/Setting/Nurse/Nurse_Search_V.php b/app/Views/Setting/Nurse/Nurse_Search_V.php new file mode 100644 index 0000000..d5fa4e5 --- /dev/null +++ b/app/Views/Setting/Nurse/Nurse_Search_V.php @@ -0,0 +1,309 @@ +
+
+
+
Nursing Staff Master
+ Manage nursing staff records and credentials for hospital workspace +
+ +
+ +
+
+
+
+
+ + +
+
+
+ +
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + $nurse): ?> + + + + + + + + + + + + + + + +
#Staff IDFull NameABDM HPR IDCouncil Reg No.DesignationQualificationContact / MobileStatusActions
+ + No nursing staff records found. Click "Add New Nurse" to create one. +
+ + + () + + + + + + - + + + + Active + + Inactive + + + + +
+
+
+
+
+ + + + + diff --git a/app/Views/Setting/NursingStation/nursing_station_index.php b/app/Views/Setting/NursingStation/nursing_station_index.php new file mode 100644 index 0000000..dc17d27 --- /dev/null +++ b/app/Views/Setting/NursingStation/nursing_station_index.php @@ -0,0 +1,279 @@ +
+
+
+
Nursing Station Master
+ Manage hospital nursing stations and assign in-charge staff & floors +
+ +
+ + +
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + $st): ?> + + + + + + + + + + + + + +
#Station CodeStation NameFloor / LocationIn-charge NurseContact NoStatusActions
+ + No nursing stations found. Click "Add Nursing Station" to create one. +
+ + + + - Not Assigned - + + + + Active + + Inactive + + + + +
+
+
+
+
+ + + + + diff --git a/app/Views/billing/ipd/discharge_create.php b/app/Views/billing/ipd/discharge_create.php index 9c689ab..c1763a3 100644 --- a/app/Views/billing/ipd/discharge_create.php +++ b/app/Views/billing/ipd/discharge_create.php @@ -583,8 +583,8 @@ $historyFields = [
+
+