feat(nursing): add Nursing Station Master, ABDM credentials to Nurse Master, and Visual Bed Grid at /ipd/patient

This commit is contained in:
DevSoftTech Server 2026-08-28 02:43:37 +05:30
parent 7c04828e99
commit 69f0e1fa45
18 changed files with 4024 additions and 825 deletions

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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',

View File

@ -0,0 +1,108 @@
<?php
namespace App\Controllers\Setting;
use App\Controllers\BaseController;
use App\Models\NurseModel;
class Nurse extends BaseController
{
private NurseModel $nurseModel;
public function __construct()
{
$this->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,
]);
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace App\Controllers\Setting;
use App\Controllers\BaseController;
use App\Models\NurseModel;
use App\Models\NursingStationModel;
class NursingStation extends BaseController
{
protected NursingStationModel $stationModel;
protected NurseModel $nurseModel;
public function __construct()
{
$this->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]);
}
}

View File

@ -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<int, array{concept_id:string,term:string,source:string}>
*/
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<int, array{concept_id:string,term:string,fsn:string,hierarchy:string,source:string}>
*/
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<int, array<string, string>>
*/
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<int, array<string, string>>
*/
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<int, array{concept_id:string,term:string,fsn:string,hierarchy:string,source:string}>

108
app/Models/NurseModel.php Normal file
View File

@ -0,0 +1,108 @@
<?php
namespace App\Models;
use CodeIgniter\Database\BaseConnection;
class NurseModel
{
private BaseConnection $db;
public function __construct()
{
$this->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();
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace App\Models;
use CodeIgniter\Database\BaseConnection;
class NursingStationModel
{
private BaseConnection $db;
public function __construct()
{
$this->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();
}
}

View File

@ -1,10 +1,10 @@
<section class="content">
<div class="pagetitle">
<h1>Admin Panel</h1>
<div class="pagetitle mb-3">
<h1 class="mb-0">Admin Panel</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item">Dashboard</li>
<li class="breadcrumb-item active">Admin</li>
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="javascript:showAdminTiles()">Dashboard Admin</a></li>
<li class="breadcrumb-item active d-none" id="admin_breadcrumb_sub">Sub Page</li>
</ol>
</nav>
</div>
@ -49,100 +49,160 @@
}
</style>
<div class="row g-3 admin-tiles">
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/doctor') ?>','maindiv','Doctor Master');">
<i class="bi bi-person-badge"></i>
<span>Doctor Master</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/user-management') ?>','maindiv','User Management');">
<i class="bi bi-people"></i>
<span>User Management</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/ai-settings') ?>','maindiv','AI Settings');">
<i class="bi bi-cpu"></i>
<span>AI Settings</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/healthplix-settings') ?>','maindiv','HealthPlix Integration');">
<i class="bi bi-link-45deg"></i>
<span>HealthPlix Integration</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/abdm-gateway') ?>','maindiv','ABDM Gateway Config');">
<i class="bi bi-hdd-network" style="color:#0d6efd"></i>
<span>ABDM Gateway</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/abdm-report-doctors') ?>','maindiv','ABDM Report Doctors');">
<i class="bi bi-person-vcard" style="color:#0d6efd"></i>
<span>ABDM Report Doctors</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/hospital-profile') ?>','maindiv','Hospital Profile');">
<i class="bi bi-building"></i>
<span>Hospital Profile</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/insurance') ?>','maindiv','Insurance Master');">
<i class="bi bi-shield-check"></i>
<span>Insurance Master</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/reffer') ?>','maindiv','Referral Admin');">
<i class="bi bi-share"></i>
<span>Referral Admin</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/bank') ?>','maindiv','Bank & Payment Sources');">
<i class="bi bi-bank"></i>
<span>Bank & Payment</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/medical-bank') ?>','maindiv','Medical Bank & Payment Sources');">
<i class="bi bi-credit-card-2-front"></i>
<span>Medical Bank</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/bed-management') ?>','maindiv','Bed Management');">
<i class="bi bi-hospital"></i>
<span>Bed Management</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/ipd-examination-fields') ?>','maindiv','IPD Examination Fields');">
<i class="bi bi-clipboard2-pulse"></i>
<span>IPD Examination Fields</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('Storestock') ?>','maindiv','Hospital Stock');">
<i class="bi bi-box-seam"></i>
<span>Hospital Stock</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/system-ops') ?>','maindiv','System Operations');">
<i class="bi bi-server"></i>
<span>System Operations</span>
</a>
<!-- Tiles Grid View -->
<div id="admin_tiles_view">
<div class="row g-3 admin-tiles">
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/doctor') ?>','Doctor Master');">
<i class="bi bi-person-badge"></i>
<span>Doctor Master</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/nurse') ?>','Nursing Staff Master');">
<i class="bi bi-person-heart" style="color:#0d6efd"></i>
<span>Nursing Staff Master</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/nursing-station') ?>','Nursing Station Master');">
<i class="bi bi-hospital" style="color:#0d6efd"></i>
<span>Nursing Station Master</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/user-management') ?>','User Management');">
<i class="bi bi-people"></i>
<span>User Management</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/ai-settings') ?>','AI Settings');">
<i class="bi bi-cpu"></i>
<span>AI Settings</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/healthplix-settings') ?>','HealthPlix Integration');">
<i class="bi bi-link-45deg"></i>
<span>HealthPlix Integration</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/abdm-gateway') ?>','ABDM Gateway Config');">
<i class="bi bi-hdd-network" style="color:#0d6efd"></i>
<span>ABDM Gateway</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/abdm-report-doctors') ?>','ABDM Report Doctors');">
<i class="bi bi-person-vcard" style="color:#0d6efd"></i>
<span>ABDM Report Doctors</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/hospital-profile') ?>','Hospital Profile');">
<i class="bi bi-building"></i>
<span>Hospital Profile</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/insurance') ?>','Insurance Master');">
<i class="bi bi-shield-check"></i>
<span>Insurance Master</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/reffer') ?>','Referral Admin');">
<i class="bi bi-share"></i>
<span>Referral Admin</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/bank') ?>','Bank & Payment Sources');">
<i class="bi bi-bank"></i>
<span>Bank & Payment</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/medical-bank') ?>','Medical Bank & Payment Sources');">
<i class="bi bi-credit-card-2-front"></i>
<span>Medical Bank</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/bed-management') ?>','Bed Management');">
<i class="bi bi-hospital"></i>
<span>Bed Management</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/ipd-examination-fields') ?>','IPD Examination Fields');">
<i class="bi bi-clipboard2-pulse"></i>
<span>IPD Examination Fields</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('Storestock') ?>','Hospital Stock');">
<i class="bi bi-box-seam"></i>
<span>Hospital Stock</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:openAdminSubPage('<?= base_url('setting/admin/system-ops') ?>','System Operations');">
<i class="bi bi-server"></i>
<span>System Operations</span>
</a>
</div>
</div>
</div>
<hr/>
<div class="row mt-4">
<div class="col-12" id="maindiv"></div>
<!-- Admin Sub-page View Container -->
<div id="admin_subpage_view" class="d-none">
<div class="card shadow-sm border-0 mb-3">
<div class="card-header bg-light d-flex justify-content-between align-items-center py-2">
<strong class="text-primary fs-6 mb-0" id="admin_subpage_title"><i class="bi bi-gear me-1"></i> Admin Module</strong>
<button type="button" class="btn btn-outline-primary btn-sm py-1 px-3" onclick="showAdminTiles()">
<i class="bi bi-arrow-left me-1"></i> Back to Admin Panel
</button>
</div>
<div class="card-body p-3" id="maindiv"></div>
</div>
</div>
</section>
<script>
function openAdminSubPage(url, title) {
$('#admin_tiles_view').addClass('d-none');
$('#admin_subpage_view').removeClass('d-none');
if (title) {
$('#admin_subpage_title').html('<i class="bi bi-gear me-1"></i> ' + title);
$('#admin_breadcrumb_sub').removeClass('d-none').text(title);
}
load_form_div(url, 'maindiv', title);
}
function showAdminTiles() {
$('#maindiv').empty();
$('#admin_subpage_view').addClass('d-none');
$('#admin_tiles_view').removeClass('d-none');
$('#admin_breadcrumb_sub').addClass('d-none').text('');
}
(function() {
var rawLoadFormDiv = window.load_form_div;
window.load_form_div = function(ourl, xdiv, top_title) {
if (xdiv === 'maindiv' && $('#admin_tiles_view').length > 0) {
$('#admin_tiles_view').addClass('d-none');
$('#admin_subpage_view').removeClass('d-none');
if (top_title) {
$('#admin_subpage_title').html('<i class="bi bi-gear me-1"></i> ' + top_title);
$('#admin_breadcrumb_sub').removeClass('d-none').text(top_title);
}
}
if (typeof rawLoadFormDiv === 'function') {
rawLoadFormDiv(ourl, xdiv, top_title);
}
};
})();
</script>

View File

@ -35,6 +35,15 @@
<label class="form-label">Floor</label>
<input class="form-control" id="floor_number" name="floor_number" type="number">
</div>
<div class="col-md-3">
<label class="form-label">Nursing Station</label>
<select class="form-select" id="nursing_station_id" name="nursing_station_id">
<option value="0">-- Select Nursing Station --</option>
<?php foreach (($nursingStations ?? []) as $st) { ?>
<option value="<?= (int)$st['id'] ?>"><?= esc($st['station_name']) ?> (<?= esc($st['floor_number'] ?: 'Ground Floor') ?>)</option>
<?php } ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Capacity</label>
<input class="form-control" id="total_capacity" name="total_capacity" type="number" min="0" value="0">
@ -129,6 +138,7 @@
<th>Code</th>
<th>Name</th>
<th>Department</th>
<th>Nursing Station</th>
<th>Type</th>
<th>Capacity</th>
<th>Status</th>
@ -143,6 +153,13 @@
<td><?= esc($row->ward_code ?? '') ?></td>
<td><?= esc($row->ward_name ?? '') ?></td>
<td><?= esc($row->department_name ?? 'All Department') ?></td>
<td>
<?php if (!empty($row->station_name)): ?>
<span class="badge bg-primary-subtle text-primary border border-primary-subtle"><i class="bi bi-hospital me-1"></i><?= esc($row->station_name) ?></span>
<?php else: ?>
<span class="text-muted small">-</span>
<?php endif; ?>
</td>
<td><?= esc($row->ward_type ?? '') ?></td>
<td><?= esc($row->total_capacity ?? 0) ?></td>
<td><?= esc($row->status ?? '') ?></td>
@ -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');

View File

@ -0,0 +1,309 @@
<div class="container-fluid p-0">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="mb-0 text-primary"><i class="bi bi-person-heart me-2"></i>Nursing Staff Master</h5>
<small class="text-muted">Manage nursing staff records and credentials for hospital workspace</small>
</div>
<button type="button" class="btn btn-primary btn-sm" id="btn_add_nurse" data-bs-toggle="modal" data-bs-target="#nurseMasterModal">
<i class="bi bi-plus-lg me-1"></i> Add New Nurse
</button>
</div>
<div class="card shadow-sm border-0 mb-3">
<div class="card-body p-3">
<div class="row g-2 align-items-center">
<div class="col-md-6 col-lg-4">
<div class="input-group input-group-sm">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-search"></i></span>
<input type="text" class="form-control form-control-sm border-start-0" id="nurse_search_input" placeholder="Search by name, ID code, qualification..." value="<?= esc($searchQuery ?? '') ?>">
</div>
</div>
<div class="col-md-3">
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn_refresh_nurse_list">
<i class="bi bi-arrow-clockwise me-1"></i> Refresh
</button>
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0" id="nurse_table">
<thead class="table-light">
<tr>
<th style="width: 50px;" class="ps-3">#</th>
<th style="width: 120px;">Staff ID</th>
<th>Full Name</th>
<th style="width: 150px;">ABDM HPR ID</th>
<th style="width: 140px;">Council Reg No.</th>
<th style="width: 120px;">Designation</th>
<th>Qualification</th>
<th>Contact / Mobile</th>
<th style="width: 90px;">Status</th>
<th style="width: 110px;" class="text-end pe-3">Actions</th>
</tr>
</thead>
<tbody id="nurse_table_body">
<?php if (empty($nurses)): ?>
<tr>
<td colspan="10" class="text-center py-4 text-muted">
<i class="bi bi-person-x fs-3 d-block mb-1"></i>
No nursing staff records found. Click <strong>"Add New Nurse"</strong> to create one.
</td>
</tr>
<?php else: ?>
<?php foreach ($nurses as $idx => $nurse): ?>
<tr id="nurse_row_<?= (int)$nurse['id'] ?>" data-nurse='<?= json_encode($nurse, JSON_HEX_APOS | JSON_HEX_QUOT) ?>'>
<td class="ps-3 fw-bold text-secondary"><?= $idx + 1 ?></td>
<td><span class="badge bg-light text-dark border"><?= esc($nurse['nurse_code']) ?></span></td>
<td class="fw-semibold text-primary">
<?= esc($nurse['name']) ?>
<?php if (!empty($nurse['gender'])): ?>
<small class="text-muted"> (<?= esc($nurse['gender']) ?>)</small>
<?php endif; ?>
</td>
<td>
<?php if (!empty($nurse['hpr_id'])): ?>
<span class="badge bg-primary-subtle text-primary border border-primary-subtle"><i class="bi bi-shield-check me-1"></i><?= esc($nurse['hpr_id']) ?></span>
<?php else: ?>
<span class="text-muted small">-</span>
<?php endif; ?>
</td>
<td><code class="text-dark"><?= esc($nurse['registration_no'] ?: '-') ?></code></td>
<td><span class="badge bg-secondary-subtle text-dark border"><?= esc($nurse['designation'] ?: 'Staff Nurse') ?></span></td>
<td><?= esc($nurse['qualification'] ?: '-') ?></td>
<td><?= esc($nurse['contact_no'] ?: '-') ?></td>
<td>
<?php if ((int)$nurse['is_active'] === 1): ?>
<span class="badge bg-success">Active</span>
<?php else: ?>
<span class="badge bg-secondary">Inactive</span>
<?php endif; ?>
</td>
<td class="text-end pe-3">
<button type="button" class="btn btn-outline-primary btn-sm py-0 px-2 btn-edit-nurse" data-id="<?= (int)$nurse['id'] ?>">
<i class="bi bi-pencil-square"></i> Edit
</button>
<button type="button" class="btn btn-outline-danger btn-sm py-0 px-2 btn-delete-nurse" data-id="<?= (int)$nurse['id'] ?>" data-name="<?= esc($nurse['name']) ?>">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modal for Add/Edit Nurse -->
<div class="modal fade" id="nurseMasterModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-light py-2">
<h5 class="modal-title text-primary fs-6" id="nurseModalTitle"><i class="bi bi-person-plus me-1"></i> Add Nursing Staff</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="nurseMasterForm">
<input type="hidden" id="nurse_id" name="id" value="0">
<div class="modal-body">
<div id="nurse_form_status" class="alert alert-danger d-none py-2 px-3 mb-3"></div>
<div class="row g-2">
<div class="col-md-4">
<label class="form-label fw-bold small mb-1">Staff Code / ID <span class="text-danger">*</span></label>
<input type="text" class="form-control form-control-sm" id="nurse_code" name="nurse_code" placeholder="e.g. NUR-001" required>
</div>
<div class="col-md-4">
<label class="form-label fw-bold small mb-1">Designation</label>
<select class="form-select form-select-sm" id="nurse_designation" name="designation">
<option value="Staff Nurse">Staff Nurse</option>
<option value="Nursing Supervisor">Nursing Supervisor</option>
<option value="Head Nurse">Head Nurse</option>
<option value="Sister Incharge">Sister Incharge</option>
<option value="ANM">ANM (Auxiliary Nurse Midwife)</option>
<option value="GNM">GNM (General Nurse & Midwife)</option>
<option value="Nurse Practitioner">Nurse Practitioner</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label fw-bold small mb-1">Status</label>
<select class="form-select form-select-sm" id="nurse_is_active" name="is_active">
<option value="1">Active</option>
<option value="0">Inactive</option>
</select>
</div>
<div class="col-md-8">
<label class="form-label fw-bold small mb-1">Full Name <span class="text-danger">*</span></label>
<input type="text" class="form-control form-control-sm" id="nurse_name" name="name" placeholder="Full name of nurse" required>
</div>
<div class="col-md-4">
<label class="form-label fw-bold small mb-1">Gender</label>
<select class="form-select form-select-sm" id="nurse_gender" name="gender">
<option value="Female">Female</option>
<option value="Male">Male</option>
<option value="Other">Other</option>
</select>
</div>
<!-- ABDM / HPR & Registration Section -->
<div class="col-12 mt-2">
<div class="p-2 bg-light rounded border">
<div class="fw-bold small text-primary mb-1"><i class="bi bi-shield-check me-1"></i> ABDM &amp; Council Credentials</div>
<div class="row g-2">
<div class="col-md-6">
<label class="form-label small mb-1">ABDM HPR ID / Address</label>
<input type="text" class="form-control form-control-sm" id="nurse_hpr_id" name="hpr_id" placeholder="e.g. 12-3456-7890-1234 or nurse@hprid">
</div>
<div class="col-md-6">
<label class="form-label small mb-1">Nursing Council Registration No.</label>
<input type="text" class="form-control form-control-sm" id="nurse_registration_no" name="registration_no" placeholder="e.g. NC-98765/2023">
</div>
</div>
</div>
</div>
<div class="col-md-6 mt-2">
<label class="form-label fw-bold small mb-1">Qualification</label>
<input type="text" class="form-control form-control-sm" id="nurse_qualification" name="qualification" placeholder="e.g. B.Sc Nursing, M.Sc Nursing, GNM">
</div>
<div class="col-md-6 mt-2">
<label class="form-label fw-bold small mb-1">Department</label>
<input type="text" class="form-control form-control-sm" id="nurse_department" name="department" value="Nursing" placeholder="Department">
</div>
<div class="col-md-6">
<label class="form-label fw-bold small mb-1">Contact No</label>
<input type="text" class="form-control form-control-sm" id="nurse_contact_no" name="contact_no" placeholder="Phone number">
</div>
<div class="col-md-6">
<label class="form-label fw-bold small mb-1">Email Address</label>
<input type="email" class="form-control form-control-sm" id="nurse_email" name="email" placeholder="Email address">
</div>
</div>
</div>
<div class="modal-footer bg-light py-2">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary btn-sm" id="btn_save_nurse">
<i class="bi bi-check-lg me-1"></i> Save Nurse
</button>
</div>
</form>
</div>
</div>
</div>
<script>
(function() {
var saveUrl = '<?= base_url('setting/admin/nurse/save') ?>';
var deleteUrlBase = '<?= base_url('setting/admin/nurse/delete') ?>';
var csrfTokenName = '<?= csrf_token() ?>';
function resetNurseForm() {
$('#nurse_id').val('0');
$('#nurse_code').val('NUR-' + Math.floor(100 + Math.random() * 900));
$('#nurse_name').val('');
$('#nurse_hpr_id').val('');
$('#nurse_registration_no').val('');
$('#nurse_gender').val('Female');
$('#nurse_designation').val('Staff Nurse');
$('#nurse_qualification').val('');
$('#nurse_contact_no').val('');
$('#nurse_email').val('');
$('#nurse_department').val('Nursing');
$('#nurse_is_active').val('1');
$('#nurse_form_status').addClass('d-none').text('');
$('#nurseModalTitle').html('<i class="bi bi-person-plus me-1"></i> Add Nursing Staff');
}
$('#btn_add_nurse').on('click', function() {
resetNurseForm();
});
$(document).on('click', '.btn-edit-nurse', function() {
var id = $(this).data('id');
var row = $('#nurse_row_' + id);
if (!row.length) return;
var data = row.data('nurse');
if (!data) return;
$('#nurse_id').val(data.id || '0');
$('#nurse_code').val(data.nurse_code || '');
$('#nurse_name').val(data.name || '');
$('#nurse_hpr_id').val(data.hpr_id || '');
$('#nurse_registration_no').val(data.registration_no || '');
$('#nurse_gender').val(data.gender || 'Female');
$('#nurse_designation').val(data.designation || 'Staff Nurse');
$('#nurse_qualification').val(data.qualification || '');
$('#nurse_contact_no').val(data.contact_no || '');
$('#nurse_email').val(data.email || '');
$('#nurse_department').val(data.department || 'Nursing');
$('#nurse_is_active').val(data.is_active !== undefined ? data.is_active : '1');
$('#nurse_form_status').addClass('d-none').text('');
$('#nurseModalTitle').html('<i class="bi bi-pencil-square me-1"></i> Edit Nursing Staff');
var modalEl = document.getElementById('nurseMasterModal');
if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modalEl).show();
} else {
$(modalEl).modal('show');
}
});
$('#nurseMasterForm').on('submit', function(e) {
e.preventDefault();
var $status = $('#nurse_form_status');
$status.addClass('d-none').text('');
var formData = $(this).serialize();
$.post(saveUrl, formData, function(resp) {
if (resp && resp.ok) {
var modalEl = document.getElementById('nurseMasterModal');
if (window.bootstrap && window.bootstrap.Modal) {
var modal = window.bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
} else {
$(modalEl).modal('hide');
}
load_form_div('<?= base_url('setting/admin/nurse') ?>', 'maindiv', 'Nursing Staff Master');
} else {
$status.removeClass('d-none').text((resp && resp.error) ? resp.error : 'Failed to save nurse.');
}
}, 'json').fail(function() {
$status.removeClass('d-none').text('Server error while saving nurse record.');
});
});
$(document).on('click', '.btn-delete-nurse', function() {
var id = $(this).data('id');
var name = $(this).data('name');
if (!confirm('Are you sure you want to delete nurse: ' + name + '?')) return;
$.post(deleteUrlBase + '/' + id, {}, function(resp) {
if (resp && resp.ok) {
load_form_div('<?= base_url('setting/admin/nurse') ?>', 'maindiv', 'Nursing Staff Master');
} else {
alert((resp && resp.error) ? resp.error : 'Failed to delete nurse.');
}
}, 'json').fail(function() {
alert('Failed to delete nurse record.');
});
});
$('#btn_refresh_nurse_list').on('click', function() {
load_form_div('<?= base_url('setting/admin/nurse') ?>', 'maindiv', 'Nursing Staff Master');
});
$('#nurse_search_input').on('keyup', function(e) {
if (e.key === 'Enter') {
var q = encodeURIComponent($(this).val());
load_form_div('<?= base_url('setting/admin/nurse') ?>?q=' + q, 'maindiv', 'Nursing Staff Master');
}
});
})();
</script>

View File

@ -0,0 +1,279 @@
<div class="container-fluid p-0">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="text-primary mb-0"><i class="bi bi-hospital me-1"></i> Nursing Station Master</h5>
<small class="text-muted">Manage hospital nursing stations and assign in-charge staff & floors</small>
</div>
<button type="button" class="btn btn-primary btn-sm" id="btn_add_station">
<i class="bi bi-plus-lg me-1"></i> Add Nursing Station
</button>
</div>
<!-- Search & Filter Card -->
<div class="card shadow-sm border-0 mb-3">
<div class="card-body py-2 px-3">
<div class="row g-2 align-items-center">
<div class="col-md-9">
<div class="input-group input-group-sm">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-search text-muted"></i></span>
<input type="text" class="form-control border-start-0" id="station_search_input" placeholder="Search by station name, code, floor, or nurse name..." value="<?= esc($searchQuery ?? '') ?>">
<button class="btn btn-primary" type="button" id="btn_station_search">Search</button>
</div>
</div>
<div class="col-md-3 text-end">
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn_refresh_station_list">
<i class="bi bi-arrow-clockwise me-1"></i> Refresh
</button>
</div>
</div>
</div>
</div>
<!-- Stations Table -->
<div class="card shadow-sm border-0">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0" id="station_table">
<thead class="table-light">
<tr>
<th style="width: 50px;" class="ps-3">#</th>
<th style="width: 130px;">Station Code</th>
<th>Station Name</th>
<th style="width: 140px;">Floor / Location</th>
<th>In-charge Nurse</th>
<th>Contact No</th>
<th style="width: 90px;">Status</th>
<th style="width: 110px;" class="text-end pe-3">Actions</th>
</tr>
</thead>
<tbody id="station_table_body">
<?php if (empty($stations)): ?>
<tr>
<td colspan="8" class="text-center py-4 text-muted">
<i class="bi bi-hospital fs-3 d-block mb-1"></i>
No nursing stations found. Click <strong>"Add Nursing Station"</strong> to create one.
</td>
</tr>
<?php else: ?>
<?php foreach ($stations as $idx => $st): ?>
<tr id="station_row_<?= (int)$st['id'] ?>" data-station='<?= json_encode($st, JSON_HEX_APOS | JSON_HEX_QUOT) ?>'>
<td class="ps-3 fw-bold text-secondary"><?= $idx + 1 ?></td>
<td><span class="badge bg-light text-dark border"><?= esc($st['station_code']) ?></span></td>
<td class="fw-semibold text-primary"><?= esc($st['station_name']) ?></td>
<td><span class="badge bg-info-subtle text-info border border-info-subtle"><?= esc($st['floor_number'] ?: 'Ground Floor') ?></span></td>
<td>
<?php if (!empty($st['incharge_nurse_name'])): ?>
<span class="text-dark"><i class="bi bi-person-heart text-danger me-1"></i><?= esc($st['incharge_nurse_name']) ?></span>
<?php else: ?>
<span class="text-muted small">- Not Assigned -</span>
<?php endif; ?>
</td>
<td><?= esc($st['contact_no'] ?: '-') ?></td>
<td>
<?php if ($st['status'] === 'active'): ?>
<span class="badge bg-success">Active</span>
<?php else: ?>
<span class="badge bg-secondary">Inactive</span>
<?php endif; ?>
</td>
<td class="text-end pe-3">
<button type="button" class="btn btn-outline-primary btn-sm py-0 px-2 btn-edit-station" data-id="<?= (int)$st['id'] ?>">
<i class="bi bi-pencil-square"></i> Edit
</button>
<button type="button" class="btn btn-outline-danger btn-sm py-0 px-2 btn-delete-station" data-id="<?= (int)$st['id'] ?>" data-name="<?= esc($st['station_name']) ?>">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modal for Add/Edit Nursing Station -->
<div class="modal fade" id="nursingStationModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-light py-2">
<h5 class="modal-title text-primary fs-6" id="stationModalTitle"><i class="bi bi-plus-lg me-1"></i> Add Nursing Station</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="nursingStationForm">
<input type="hidden" id="station_id" name="id" value="0">
<div class="modal-body">
<div id="station_form_status" class="alert alert-danger d-none py-2 px-3 mb-3"></div>
<div class="row g-2">
<div class="col-md-6">
<label class="form-label fw-bold small mb-1">Station Code <span class="text-danger">*</span></label>
<input type="text" class="form-control form-control-sm" id="station_code" name="station_code" placeholder="e.g. NS-101" required>
</div>
<div class="col-md-6">
<label class="form-label fw-bold small mb-1">Status</label>
<select class="form-select form-select-sm" id="station_status" name="status">
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div class="col-12">
<label class="form-label fw-bold small mb-1">Station Name <span class="text-danger">*</span></label>
<input type="text" class="form-control form-control-sm" id="station_name" name="station_name" placeholder="e.g. 2nd Floor Wards Nursing Station" required>
</div>
<div class="col-md-6">
<label class="form-label fw-bold small mb-1">Floor / Location</label>
<select class="form-select form-select-sm" id="station_floor_number" name="floor_number">
<option value="Ground Floor">Ground Floor</option>
<option value="1st Floor">1st Floor</option>
<option value="2nd Floor">2nd Floor</option>
<option value="3rd Floor">3rd Floor</option>
<option value="4th Floor">4th Floor</option>
<option value="ICU Block">ICU Block</option>
<option value="OT Block">OT Block</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-bold small mb-1">Contact No</label>
<input type="text" class="form-control form-control-sm" id="station_contact_no" name="contact_no" placeholder="Extension / Phone">
</div>
<div class="col-12">
<label class="form-label fw-bold small mb-1">In-charge Nursing Staff</label>
<select class="form-select form-select-sm" id="station_incharge_nurse_id" name="incharge_nurse_id">
<option value="0">-- Select In-charge Nurse --</option>
<?php if (!empty($nurses)): ?>
<?php foreach ($nurses as $nurse): ?>
<option value="<?= (int)$nurse['id'] ?>"><?= esc(($nurse['nurse_code'] ? '[' . $nurse['nurse_code'] . '] ' : '') . $nurse['name']) ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<div class="col-12">
<label class="form-label fw-bold small mb-1">Remarks / Location Note</label>
<textarea class="form-control form-control-sm" id="station_remarks" name="remarks" rows="2" placeholder="Optional notes"></textarea>
</div>
</div>
</div>
<div class="modal-footer bg-light py-2">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary btn-sm" id="btn_save_station">
<i class="bi bi-check-lg me-1"></i> Save Station
</button>
</div>
</form>
</div>
</div>
</div>
<script>
(function() {
var saveUrl = '<?= base_url('setting/admin/nursing-station/save') ?>';
var deleteUrlBase = '<?= base_url('setting/admin/nursing-station/delete') ?>';
function resetStationForm() {
$('#station_id').val('0');
$('#station_code').val('NS-' + Math.floor(100 + Math.random() * 900));
$('#station_name').val('');
$('#station_floor_number').val('2nd Floor');
$('#station_incharge_nurse_id').val('0');
$('#station_contact_no').val('');
$('#station_status').val('active');
$('#station_remarks').val('');
$('#station_form_status').addClass('d-none').text('');
$('#stationModalTitle').html('<i class="bi bi-plus-lg me-1"></i> Add Nursing Station');
}
$('#btn_add_station').on('click', function() {
resetStationForm();
var modalEl = document.getElementById('nursingStationModal');
if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modalEl).show();
} else {
$(modalEl).modal('show');
}
});
$(document).on('click', '.btn-edit-station', function() {
var id = $(this).data('id');
var row = $('#station_row_' + id);
if (!row.length) return;
var data = row.data('station');
if (!data) return;
$('#station_id').val(data.id || '0');
$('#station_code').val(data.station_code || '');
$('#station_name').val(data.station_name || '');
$('#station_floor_number').val(data.floor_number || 'Ground Floor');
$('#station_incharge_nurse_id').val(data.incharge_nurse_id || '0');
$('#station_contact_no').val(data.contact_no || '');
$('#station_status').val(data.status || 'active');
$('#station_remarks').val(data.remarks || '');
$('#station_form_status').addClass('d-none').text('');
$('#stationModalTitle').html('<i class="bi bi-pencil-square me-1"></i> Edit Nursing Station');
var modalEl = document.getElementById('nursingStationModal');
if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modalEl).show();
} else {
$(modalEl).modal('show');
}
});
$('#nursingStationForm').on('submit', function(e) {
e.preventDefault();
var $status = $('#station_form_status');
$status.addClass('d-none').text('');
var formData = $(this).serialize();
$.post(saveUrl, formData, function(resp) {
if (resp && resp.ok) {
var modalEl = document.getElementById('nursingStationModal');
if (window.bootstrap && window.bootstrap.Modal) {
var modal = window.bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
} else {
$(modalEl).modal('hide');
}
load_form_div('<?= base_url('setting/admin/nursing-station') ?>', 'maindiv', 'Nursing Station Master');
} else {
$status.removeClass('d-none').text((resp && resp.error) ? resp.error : 'Failed to save station.');
}
}, 'json').fail(function() {
$status.removeClass('d-none').text('Server error while saving station record.');
});
});
$(document).on('click', '.btn-delete-station', function() {
var id = $(this).data('id');
var name = $(this).data('name');
if (!confirm('Are you sure you want to delete nursing station "' + name + '"?')) return;
$.post(deleteUrlBase, { id: id, '<?= csrf_token() ?>': '<?= csrf_hash() ?>' }, function(resp) {
if (resp && resp.ok) {
load_form_div('<?= base_url('setting/admin/nursing-station') ?>', 'maindiv', 'Nursing Station Master');
} else {
alert((resp && resp.error) ? resp.error : 'Failed to delete station.');
}
}, 'json').fail(function() {
alert('Server error while deleting station.');
});
});
$('#btn_station_search').on('click', function() {
var q = $('#station_search_input').val();
load_form_div('<?= base_url('setting/admin/nursing-station') ?>?q=' + encodeURIComponent(q), 'maindiv', 'Nursing Station Master');
});
$('#station_search_input').on('keyup', function(e) {
if (e.key === 'Enter') {
$('#btn_station_search').click();
}
});
$('#btn_refresh_station_list').on('click', function() {
load_form_div('<?= base_url('setting/admin/nursing-station') ?>', 'maindiv', 'Nursing Station Master');
});
})();
</script>

File diff suppressed because it is too large Load Diff

View File

@ -1047,9 +1047,12 @@
<div class="mb-3" id="rx_sec_diagnosis">
<div class="d-flex align-items-center justify-content-between mb-2">
<h6 class="mb-0">Diagnosis</h6>
<div class="d-flex gap-1">
<button type="button" class="btn btn-sm btn-outline-secondary" id="btn_opd_manage_diagnosis_master">Master CRUD</button>
<button type="button" class="btn btn-sm btn-outline-secondary btn-save-autotype-keyword"
data-section="diagnosis" data-target="diagnosis"
title="Save as custom keyword">+keyword</button>
</div>
</div>
<!-- Healthplix-style inline diagnosis table -->
@ -9707,10 +9710,6 @@
return (row && row.finding_examinations) ? row.finding_examinations : '';
});
if (localStorage.getItem(draftKey)) {
setStatus('dirty', 'Draft available');
}
loadAdviceList();
loadRecentEntryChips();
loadInvestigationList();
@ -9718,5 +9717,263 @@
loadOldPrescribedPanel();
loadRxGroupCatalog();
updateScanBanner();
/* ══ OPD Final Diagnosis Master CRUD Modal Handler ══════════════════ */
var opdDiagMasterRows = [];
var _opdDiagMasterNameTimer = null;
var _opdDiagMasterNameXhr = null;
var _opdDiagMasterNameCache = {};
var _opdDiagMasterNameDdIdx = -1;
function closeOpdDiagMasterNameDd() {
if (_opdDiagMasterNameTimer) { clearTimeout(_opdDiagMasterNameTimer); _opdDiagMasterNameTimer = null; }
if (_opdDiagMasterNameXhr) { try { _opdDiagMasterNameXhr.abort(); } catch(e){} _opdDiagMasterNameXhr = null; }
$('#opd_diagnosis_master_name_dropdown').hide().empty();
_opdDiagMasterNameDdIdx = -1;
}
function openOpdDiagMasterNameDd(rows) {
var $dd = $('#opd_diagnosis_master_name_dropdown');
if (!rows || !rows.length) { $dd.hide().empty(); return; }
var html = '';
rows.forEach(function(item, idx) {
var name = item.name || item.term || '';
var snomedId = item.snomed_concept_id || item.concept_id || '';
var source = item.source || 'snomed';
var badgeClass = (source === 'disease_master' || source === 'local') ? 'bg-primary' : 'bg-info text-dark';
var badgeText = snomedId ? ('SNOMED: ' + snomedId) : (source === 'disease_master' ? 'Master' : 'Local');
html += '<div class="dropdown-item py-1 px-2 opd-diag-master-dd-item" data-idx="' + idx + '" style="cursor:pointer; font-size:13px;">'
+ '<div class="d-flex justify-content-between align-items-center">'
+ '<span>' + $('<div>').text(name).html() + '</span>'
+ '<span class="badge ' + badgeClass + ' ms-2" style="font-size:10px;">' + $('<div>').text(badgeText).html() + '</span>'
+ '</div>'
+ '</div>';
});
$dd.html(html).show();
_opdDiagMasterNameDdIdx = -1;
$dd.find('.opd-diag-master-dd-item').on('click', function() {
var idx = parseInt($(this).data('idx'), 10);
if (rows[idx]) {
var sel = rows[idx];
var selName = sel.name || sel.term || '';
var selSnomedId = sel.snomed_concept_id || sel.concept_id || '';
var selSnomedTerm = sel.snomed_term || sel.fsn || selName;
$('#opd_diagnosis_master_name').val(selName);
$('#opd_diagnosis_master_snomed_id').val(selSnomedId);
$('#opd_diagnosis_master_snomed_term').val(selSnomedTerm);
$('#opd_diagnosis_master_status').text(selSnomedId ? ('SNOMED concept ' + selSnomedId + ' selected.') : 'Diagnosis selected.').css('color', '#0f5132');
}
closeOpdDiagMasterNameDd();
});
}
function clearOpdDiagMasterForm() {
closeOpdDiagMasterNameDd();
$('#opd_diagnosis_master_code').val('0');
$('#opd_diagnosis_master_name').val('');
$('#opd_diagnosis_master_snomed_id').val('');
$('#opd_diagnosis_master_snomed_term').val('');
$('#opd_diagnosis_master_active').val('1');
}
function renderOpdDiagMasterRows() {
var $tbody = $('#opd_diagnosis_master_rows');
if (!opdDiagMasterRows.length) {
$tbody.html('<tr><td colspan="4" class="text-center text-muted">No records.</td></tr>');
return;
}
var html = '';
opdDiagMasterRows.forEach(function(row) {
var code = parseInt(row.Code || '0', 10);
var active = typeof row.is_active === 'undefined' || parseInt(row.is_active || '0', 10) === 1;
html += '<tr>'
+ '<td>' + $('<div>').text(row.Name || '').html() + '</td>'
+ '<td>' + $('<div>').text(row.snomed_concept_id || '').html() + '</td>'
+ '<td>' + (active ? 'Active' : 'Inactive') + '</td>'
+ '<td><button type="button" class="btn btn-outline-primary btn-sm py-0 px-1 btn-opd-diag-master-edit" data-code="' + code + '">Edit</button> '
+ (active ? '<button type="button" class="btn btn-outline-danger btn-sm py-0 px-1 btn-opd-diag-master-remove" data-code="' + code + '">Deactivate</button>' : '')
+ '</td></tr>';
});
$tbody.html(html);
}
function fetchOpdDiagMasterRows() {
var filterVal = ($('#opd_diagnosis_master_search').val() || '').toString().trim();
$.get('<?= base_url('Opd_prescription/disease_master_data') ?>', { start: 0, length: 100, filter: filterVal }, function(data) {
opdDiagMasterRows = (data && data.data) ? data.data : [];
renderOpdDiagMasterRows();
}, 'json');
}
$(document).on('click', '#btn_opd_manage_diagnosis_master', function() {
clearOpdDiagMasterForm();
$('#opd_diagnosis_master_search').val('');
$('#opd_diagnosis_master_status').text('');
fetchOpdDiagMasterRows();
var modalEl = document.getElementById('opdDiagnosisMasterModal');
if (modalEl) {
var modalObj = bootstrap.Modal.getInstance(modalEl) || new bootstrap.Modal(modalEl);
modalObj.show();
}
});
$(document).on('click', '.btn-opd-diag-master-edit', function() {
var code = parseInt($(this).data('code') || '0', 10);
var row = opdDiagMasterRows.find(function(item) { return parseInt(item.Code || '0', 10) === code; });
if (!row) return;
$('#opd_diagnosis_master_code').val(String(code));
$('#opd_diagnosis_master_name').val(row.Name || '');
$('#opd_diagnosis_master_snomed_id').val(row.snomed_concept_id || '');
$('#opd_diagnosis_master_snomed_term').val(row.snomed_term || '');
$('#opd_diagnosis_master_active').val(typeof row.is_active === 'undefined' || parseInt(row.is_active || '0', 10) === 1 ? '1' : '0');
});
$(document).on('click', '.btn-opd-diag-master-remove', function() {
var code = parseInt($(this).data('code') || '0', 10);
if (code <= 0 || !window.confirm('Deactivate this diagnosis?')) return;
var csrfName = '<?= csrf_token() ?>';
var csrfHash = '<?= csrf_hash() ?>';
var payload = {};
payload[csrfName] = csrfHash;
$.post('<?= base_url('Opd_prescription/disease_master_remove') ?>/' + code, payload, function(data) {
$('#opd_diagnosis_master_status').text((data && data.error_text) ? data.error_text : 'Diagnosis deactivated.').css('color', '#0f5132');
fetchOpdDiagMasterRows();
}, 'json');
});
$('#btn_opd_diagnosis_master_refresh').on('click', fetchOpdDiagMasterRows);
$('#opd_diagnosis_master_search').on('input', fetchOpdDiagMasterRows);
$('#btn_opd_diagnosis_master_clear').on('click', clearOpdDiagMasterForm);
$('#btn_opd_diagnosis_master_save').on('click', function() {
var name = ($('#opd_diagnosis_master_name').val() || '').toString().trim();
if (name === '') {
$('#opd_diagnosis_master_status').text('Name is required.').css('color', '#842029');
return;
}
var csrfName = '<?= csrf_token() ?>';
var csrfHash = '<?= csrf_hash() ?>';
var payload = {
Code: parseInt($('#opd_diagnosis_master_code').val() || '0', 10),
Name: name,
snomed_concept_id: ($('#opd_diagnosis_master_snomed_id').val() || '').toString().trim(),
snomed_term: ($('#opd_diagnosis_master_snomed_term').val() || '').toString().trim(),
is_active: parseInt($('#opd_diagnosis_master_active').val() || '1', 10)
};
payload[csrfName] = csrfHash;
$.post('<?= base_url('Opd_prescription/disease_master_save') ?>', payload, function(data) {
if (!data || parseInt(data.update || '0', 10) !== 1) {
$('#opd_diagnosis_master_status').text((data && data.error_text) ? data.error_text : 'Unable to save diagnosis.').css('color', '#842029');
return;
}
$('#opd_diagnosis_master_status').text(data.error_text || 'Diagnosis saved.').css('color', '#0f5132');
clearOpdDiagMasterForm();
fetchOpdDiagMasterRows();
}, 'json').fail(function() {
$('#opd_diagnosis_master_status').text('Unable to save diagnosis.').css('color', '#842029');
});
});
$('#opd_diagnosis_master_name').on('input.opdDiagSrch', function() {
var q = (this.value || '').trim();
if (_opdDiagMasterNameTimer) clearTimeout(_opdDiagMasterNameTimer);
if (_opdDiagMasterNameXhr) { try { _opdDiagMasterNameXhr.abort(); } catch(e){} _opdDiagMasterNameXhr = null; }
if (q.length < 1) { closeOpdDiagMasterNameDd(); return; }
var cacheKey = q.toUpperCase();
if (_opdDiagMasterNameCache[cacheKey]) { openOpdDiagMasterNameDd(_opdDiagMasterNameCache[cacheKey]); return; }
_opdDiagMasterNameTimer = setTimeout(function() {
_opdDiagMasterNameXhr = $.ajax({
url: '<?= base_url('Opd_prescription/provisional_diagnosis_search') ?>',
data: { q: q },
dataType: 'json',
success: function(data) {
_opdDiagMasterNameXhr = null;
var rows = (data && data.rows) ? data.rows : [];
_opdDiagMasterNameCache[cacheKey] = rows;
openOpdDiagMasterNameDd(rows);
},
error: function() { _opdDiagMasterNameXhr = null; }
});
}, 250);
}).on('keydown.opdDiagSrch', function(e) {
var $dd = $('#opd_diagnosis_master_name_dropdown');
var $items = $dd.find('.opd-diag-master-dd-item');
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
if (!$dd.is(':visible') || !$items.length) return;
e.preventDefault();
_opdDiagMasterNameDdIdx = e.key === 'ArrowDown'
? Math.min(_opdDiagMasterNameDdIdx + 1, $items.length - 1)
: Math.max(_opdDiagMasterNameDdIdx - 1, 0);
$items.css('background', '').eq(_opdDiagMasterNameDdIdx).css('background', '#f0f4ff');
} else if (e.key === 'Enter') {
if ($dd.is(':visible') && _opdDiagMasterNameDdIdx >= 0 && _opdDiagMasterNameDdIdx < $items.length) {
e.preventDefault();
$items.eq(_opdDiagMasterNameDdIdx).trigger('click');
}
} else if (e.key === 'Escape') {
closeOpdDiagMasterNameDd();
}
}).on('blur.opdDiagSrch', function() {
setTimeout(function() { closeOpdDiagMasterNameDd(); }, 200);
});
})();
</script>
<!-- Final Diagnosis Master Modal for OPD Consult -->
<div class="modal fade" id="opdDiagnosisMasterModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header py-2">
<h5 class="modal-title fs-6 fw-bold">Final Diagnosis Master</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row g-2 mb-2">
<div class="col-md-9">
<input type="text" class="form-control form-control-sm" id="opd_diagnosis_master_search" placeholder="Search diagnosis or SNOMED term">
</div>
<div class="col-md-3 d-grid">
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn_opd_diagnosis_master_refresh">Refresh List</button>
</div>
</div>
<div class="table-responsive" style="max-height:240px;overflow-y:auto;">
<table class="table table-sm table-bordered align-middle mb-0">
<thead class="table-light"><tr><th>Name</th><th>SNOMED ID</th><th>Status</th><th style="width:140px;">Action</th></tr></thead>
<tbody id="opd_diagnosis_master_rows"><tr><td colspan="4" class="text-center text-muted">No records.</td></tr></tbody>
</table>
</div>
<hr class="my-2">
<input type="hidden" id="opd_diagnosis_master_code" value="0">
<div class="row g-2">
<div class="col-md-6 position-relative">
<label class="form-label mb-1 small fw-semibold">Name <small class="text-muted">(Type to search SNOMED)</small></label>
<input type="text" class="form-control form-control-sm" id="opd_diagnosis_master_name" autocomplete="off" placeholder="Search diagnosis or SNOMED term">
<div id="opd_diagnosis_master_name_dropdown" class="dropdown-menu shadow-sm w-100" style="display:none; position:absolute; top:100%; left:0; z-index:1060; max-height:200px; overflow-y:auto;"></div>
</div>
<div class="col-md-3">
<label class="form-label mb-1 small fw-semibold">SNOMED ID</label>
<input type="text" class="form-control form-control-sm" id="opd_diagnosis_master_snomed_id">
</div>
<div class="col-md-3">
<label class="form-label mb-1 small fw-semibold">Status</label>
<select class="form-select form-select-sm" id="opd_diagnosis_master_active"><option value="1">Active</option><option value="0">Inactive</option></select>
</div>
<div class="col-12">
<label class="form-label mb-1 small fw-semibold">SNOMED Term</label>
<input type="text" class="form-control form-control-sm" id="opd_diagnosis_master_snomed_term">
</div>
</div>
<div class="mt-2 d-flex gap-2">
<button type="button" class="btn btn-primary btn-sm" id="btn_opd_diagnosis_master_save">Save</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn_opd_diagnosis_master_clear">New</button>
</div>
<div id="opd_diagnosis_master_status" class="small text-muted mt-2"></div>
</div>
</div>
</div>
</div>

View File

@ -1,47 +1,360 @@
<section class="content">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Admitted IPD Patient List (Nursing)</h5>
<div class="pagetitle mb-3 d-flex justify-content-between align-items-center">
<div>
<h1 class="mb-0 text-primary fs-4"><i class="bi bi-hospital me-2"></i>Admitted IPD Patient List &amp; Nursing Map</h1>
<small class="text-muted">Real-time bed management, nursing station assignments, and patient workspaces</small>
</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>
<div class="btn-group btn-group-sm" role="group" aria-label="View Switch">
<button type="button" class="btn btn-primary active" id="btn_view_grid" onclick="switchNursingView('grid')">
<i class="bi bi-grid-3x3-gap-fill me-1"></i> Visual Bed Grid
</button>
<button type="button" class="btn btn-outline-primary" id="btn_view_table" onclick="switchNursingView('table')">
<i class="bi bi-table me-1"></i> Table View
</button>
</div>
</div>
<!-- Top KPI Stats Row (Matching Image 2 Style) -->
<div class="row g-2 mb-3">
<div class="col-6 col-sm-4 col-md-2">
<div class="card border-0 shadow-sm text-center py-2 bg-dark text-white rounded-3">
<div class="fs-4 fw-bold mb-0"><?= (int)($stats['total_beds'] ?? 0) ?></div>
<div class="small opacity-75">Total Beds</div>
</div>
</div>
<div class="col-6 col-sm-4 col-md-2">
<div class="card border-0 shadow-sm text-center py-2 bg-success-subtle text-success rounded-3 border border-success-subtle">
<div class="fs-4 fw-bold mb-0"><?= (int)($stats['available'] ?? 0) ?></div>
<div class="small text-success-emphasis">Available</div>
</div>
</div>
<div class="col-6 col-sm-4 col-md-2">
<div class="card border-0 shadow-sm text-center py-2 bg-danger-subtle text-danger rounded-3 border border-danger-subtle">
<div class="fs-4 fw-bold mb-0"><?= (int)($stats['occupied'] ?? 0) ?></div>
<div class="small text-danger-emphasis">Occupied</div>
</div>
</div>
<div class="col-6 col-sm-4 col-md-2">
<div class="card border-0 shadow-sm text-center py-2 bg-warning-subtle text-warning-emphasis rounded-3 border border-warning-subtle">
<div class="fs-4 fw-bold mb-0"><?= (int)($stats['reserved'] ?? 0) ?></div>
<div class="small">Reserved</div>
</div>
</div>
<div class="col-6 col-sm-4 col-md-2">
<div class="card border-0 shadow-sm text-center py-2 bg-primary-subtle text-primary rounded-3 border border-primary-subtle">
<div class="fs-4 fw-bold mb-0"><?= (int)($stats['nursing_stations'] ?? 0) ?></div>
<div class="small">Nursing Stations</div>
</div>
</div>
<div class="col-6 col-sm-4 col-md-2">
<div class="card border-0 shadow-sm text-center py-2 bg-info-subtle text-info-emphasis rounded-3 border border-info-subtle">
<div class="fs-4 fw-bold mb-0"><?= (int)($stats['total_admitted'] ?? 0) ?></div>
<div class="small">Admitted Patients</div>
</div>
</div>
</div>
<!-- Filter Toolbar -->
<div class="card shadow-sm border-0 mb-3">
<div class="card-body py-2 px-3">
<div class="row g-2 align-items-center">
<div class="col-md-4">
<div class="input-group input-group-sm">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-search text-muted"></i></span>
<input type="text" class="form-control border-start-0" id="nursing_search_input" placeholder="Search patient name, IPD code, bed..." onkeyup="filterNursingBeds()">
<button class="btn btn-outline-secondary" type="button" onclick="$('#nursing_search_input').val(''); filterNursingBeds();">&times;</button>
</div>
</div>
<div class="col-md-3">
<select class="form-select form-select-sm" id="filter_nursing_station" onchange="filterNursingBeds()">
<option value="">All Nursing Stations</option>
<?php foreach (($nursingStations ?? []) as $st): ?>
<option value="<?= esc($st['station_name']) ?>"><?= esc($st['station_name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<select class="form-select form-select-sm" id="filter_nursing_ward" onchange="filterNursingBeds()">
<option value="">All Wards</option>
<?php foreach (($wards ?? []) as $wId => $wName): ?>
<option value="<?= esc($wName) ?>"><?= esc($wName) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Status Filters">
<button type="button" class="btn btn-outline-secondary active btn-status-filter" data-status="all" onclick="setStatusFilter('all', this)">All</button>
<button type="button" class="btn btn-outline-success btn-status-filter" data-status="available" onclick="setStatusFilter('available', this)">Free</button>
<button type="button" class="btn btn-outline-danger btn-status-filter" data-status="occupied" onclick="setStatusFilter('occupied', this)">Busy</button>
</div>
</div>
</div>
</div>
</div>
<!-- Visual Bed Grid View Container (Image 2 Style) -->
<div id="view_grid_container">
<?php if (empty($floors)): ?>
<div class="card shadow-sm border-0">
<div class="card-body text-center py-5 text-muted">
<i class="bi bi-hospital fs-1 d-block mb-2 text-secondary opacity-50"></i>
<h5>No active beds or wards configured</h5>
<p class="mb-0 small">Please setup Wards and Beds in Admin Panel &gt; Bed Management.</p>
</div>
</div>
<?php else: ?>
<?php foreach ($floors as $floorName => $wardGroup): ?>
<?php
$floorBeds = 0; $floorFree = 0; $floorBusy = 0;
foreach ($wardGroup as $w) {
$floorBeds += $w['count']['total'];
$floorFree += $w['count']['available'];
$floorBusy += $w['count']['occupied'];
}
?>
<!-- Floor Header Banner -->
<div class="bg-dark text-white px-3 py-2 rounded-3 mb-3 d-flex align-items-center justify-content-between shadow-sm">
<div class="fw-bold fs-6">
<i class="bi bi-layers me-2 text-info"></i><?= esc($floorName) ?>
</div>
<div>
<span class="badge bg-light text-dark me-1"><?= $floorBeds ?> beds</span>
<span class="badge bg-success me-1"><?= $floorFree ?> free</span>
<span class="badge bg-danger"><?= $floorBusy ?> busy</span>
</div>
</div>
<!-- Wards Grid Under Floor -->
<?php foreach ($wardGroup as $wId => $wardInfo): ?>
<div class="card shadow-sm border-0 mb-4 ward-block-card" data-ward="<?= esc($wardInfo['ward_name']) ?>" data-station="<?= esc($wardInfo['station_name']) ?>">
<div class="card-header bg-light py-2 d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-diagram-3 text-primary fs-5"></i>
<strong class="text-primary fs-6 mb-0"><?= esc($wardInfo['ward_name']) ?></strong>
<?php if (!empty($wardInfo['ward_type'])): ?>
<span class="badge bg-secondary-subtle text-dark border me-2"><?= esc($wardInfo['ward_type']) ?></span>
<?php endif; ?>
<?php if (!empty($wardInfo['station_name']) && $wardInfo['station_name'] !== 'Unassigned Station'): ?>
<span class="badge bg-primary-subtle text-primary border border-primary-subtle">
<i class="bi bi-hospital me-1"></i>Station: <?= esc($wardInfo['station_name']) ?>
</span>
<?php endif; ?>
</div>
<div>
<span class="badge bg-success-subtle text-success border border-success-subtle me-1"><?= (int)$wardInfo['count']['available'] ?> Free</span>
<span class="badge bg-danger-subtle text-danger border border-danger-subtle me-1"><?= (int)$wardInfo['count']['occupied'] ?> Occupied</span>
<span class="badge bg-secondary"><?= (int)$wardInfo['count']['total'] ?> Total</span>
</div>
</div>
<div class="card-body p-3">
<div class="row g-2">
<?php foreach ($wardInfo['beds'] as $bed): ?>
<?php
$isOccupied = ($bed['bed_status'] === 'occupied');
$cardClass = 'border-success-subtle bg-success-subtle bg-opacity-25';
$headerColor = 'text-success';
$statusBadgeClass = 'bg-success';
$statusLabel = 'Available';
if ($isOccupied) {
$cardClass = 'border-danger-subtle bg-danger-subtle bg-opacity-25';
$headerColor = 'text-danger';
$statusBadgeClass = 'bg-danger';
$statusLabel = 'Occupied';
} elseif ($bed['bed_status'] === 'reserved') {
$cardClass = 'border-warning-subtle bg-warning-subtle bg-opacity-25';
$headerColor = 'text-warning-emphasis';
$statusBadgeClass = 'bg-warning text-dark';
$statusLabel = 'Reserved';
} elseif (strpos($bed['bed_status'], 'maint') !== false || $bed['bed_status'] === 'cleaning') {
$cardClass = 'border-secondary-subtle bg-secondary-subtle bg-opacity-25';
$headerColor = 'text-secondary';
$statusBadgeClass = 'bg-secondary';
$statusLabel = ucfirst($bed['bed_status']);
}
$searchText = strtolower($bed['bed_number'] . ' ' . $bed['bed_code'] . ' ' . $bed['patient_name'] . ' ' . $bed['doctor_name'] . ' ' . $bed['ipd_code'] . ' ' . $wardInfo['ward_name'] . ' ' . $bed['station_name']);
?>
<div class="col-6 col-sm-4 col-md-3 col-lg-2 bed-card-col"
data-status="<?= esc($bed['bed_status']) ?>"
data-ward="<?= esc($wardInfo['ward_name']) ?>"
data-station="<?= esc($bed['station_name']) ?>"
data-search="<?= esc($searchText) ?>">
<div class="card h-100 border rounded-3 p-2 text-center transition-all <?= $cardClass ?>">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-bold small <?= $headerColor ?>"><?= esc($bed['bed_number']) ?></span>
<span class="badge <?= $statusBadgeClass ?> font-monospace" style="font-size: 10px;"><?= $statusLabel ?></span>
</div>
<?php if ($isOccupied): ?>
<div class="my-1">
<div class="fw-bold text-dark text-truncate small" title="<?= esc($bed['patient_name']) ?>">
<i class="bi bi-person-fill text-danger me-1"></i><?= esc($bed['patient_name']) ?>
</div>
<?php if (!empty($bed['doctor_name'])): ?>
<div class="text-muted text-truncate" style="font-size: 11px;" title="<?= esc($bed['doctor_name']) ?>">
<?= esc($bed['doctor_name']) ?>
</div>
<?php endif; ?>
<?php if ($bed['days_admitted'] !== null): ?>
<span class="badge bg-light text-dark border border-secondary-subtle mt-1" style="font-size: 10px;">
<i class="bi bi-clock-history me-1"></i><?= $bed['days_admitted'] ?> days
</span>
<?php endif; ?>
</div>
<?php else: ?>
<div class="my-2 text-success small fw-semibold">
<i class="bi bi-check-circle me-1"></i>Ready
</div>
<?php endif; ?>
<!-- Features Badges -->
<div class="d-flex justify-content-center gap-1 my-1">
<?php if ($bed['has_oxygen']): ?><span class="badge bg-white text-dark border" style="font-size:9px;" title="Oxygen Supply">O<sub>2</sub></span><?php endif; ?>
<?php if ($bed['has_monitor']): ?><span class="badge bg-white text-dark border" style="font-size:9px;" title="Cardiac Monitor">MON</span><?php endif; ?>
<?php if ($bed['has_ventilator']): ?><span class="badge bg-white text-dark border" style="font-size:9px;" title="Ventilator">VENT</span><?php endif; ?>
</div>
<!-- Action Button -->
<div class="mt-auto pt-1">
<?php if ($isOccupied && $bed['ipd_id'] > 0): ?>
<button type="button" class="btn btn-primary btn-sm w-100 py-0 text-nowrap" style="font-size:11px;" onclick="load_form('<?= base_url('ipd/patient/workspace/' . (int)$bed['ipd_id']) ?>','Nursing Workspace');">
Open Workspace
</button>
<?php else: ?>
<button type="button" class="btn btn-outline-secondary btn-sm w-100 py-0 text-nowrap disabled" style="font-size:11px;">
Vacant Bed
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- Table View Container (Image 1 Style) -->
<div id="view_table_container" class="d-none">
<div class="card shadow-sm border-0">
<div class="card-header bg-light py-2">
<h6 class="mb-0 text-primary fw-bold"><i class="bi bi-list-task me-1"></i> Admitted IPD Patient List</h6>
</div>
<div class="card-body p-0 table-responsive">
<table class="table table-striped table-hover align-middle mb-0" id="ipdNursingListTable">
<thead class="table-light">
<tr>
<th style="width: 50px;">#</th>
<th>IPD Code</th>
<th>UHID</th>
<th>Patient Name</th>
<th>Bed &amp; Ward Location</th>
<th>Doctor</th>
<th>Admit Date</th>
<th>Days</th>
<th class="text-end pe-3">Action</th>
</tr>
</thead>
<tbody>
<?php if (empty($records)): ?>
<tr>
<td colspan="9" class="text-center py-4 text-muted">
<i class="bi bi-person-x fs-3 d-block mb-1"></i> No current IPD admissions found.
</td>
</tr>
<?php else: ?>
<?php foreach ($records as $idx => $row) : ?>
<tr>
<td class="fw-bold text-secondary ps-3"><?= $idx + 1 ?></td>
<td><span class="badge bg-light text-dark border"><?= esc($row->ipd_code ?? '') ?></span></td>
<td><code><?= esc($row->p_code ?? '') ?></code></td>
<td class="fw-semibold text-primary"><?= esc(trim((string) (($row->p_fname ?? '') . ' ' . ($row->p_rname ?? '')))) ?></td>
<td>
<span class="badge bg-info-subtle text-info-emphasis border border-info-subtle">
<i class="bi bi-hospital me-1"></i><?= esc($row->Bed_Desc ?? '') ?>
</span>
</td>
<td><?= esc($row->doc_name ?? '') ?></td>
<td><?= esc($row->str_register_date ?? '') ?></td>
<td><span class="badge bg-secondary"><?= esc($row->no_days ?? '0') ?> days</span></td>
<td class="text-end pe-3">
<button type="button" class="btn btn-primary btn-sm py-1 px-3" onclick="load_form('<?= base_url('ipd/patient/workspace/' . (int) ($row->id ?? 0)) ?>','Nursing Workspace');">
<i class="bi bi-box-arrow-in-right me-1"></i> Open Workspace
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</section>
<script>
(function () {
if (window.jQuery && $.fn.DataTable) {
$('#ipdNursingListTable').DataTable();
var currentStatusFilter = 'all';
function switchNursingView(mode) {
if (mode === 'grid') {
$('#view_grid_container').removeClass('d-none');
$('#view_table_container').addClass('d-none');
$('#btn_view_grid').addClass('btn-primary active').removeClass('btn-outline-primary');
$('#btn_view_table').addClass('btn-outline-primary').removeClass('btn-primary active');
} else {
$('#view_grid_container').addClass('d-none');
$('#view_table_container').removeClass('d-none');
$('#btn_view_table').addClass('btn-primary active').removeClass('btn-outline-primary');
$('#btn_view_grid').addClass('btn-outline-primary').removeClass('btn-primary active');
if (window.jQuery && $.fn.DataTable && !$.fn.DataTable.isDataTable('#ipdNursingListTable')) {
$('#ipdNursingListTable').DataTable();
}
}
})();
}
function setStatusFilter(status, btn) {
currentStatusFilter = status;
$('.btn-status-filter').removeClass('active btn-secondary btn-success btn-danger').addClass('btn-outline-secondary');
$(btn).removeClass('btn-outline-secondary').addClass('active');
filterNursingBeds();
}
function filterNursingBeds() {
var search = ($('#nursing_search_input').val() || '').toLowerCase().trim();
var station = ($('#filter_nursing_station').val() || '').toLowerCase().trim();
var ward = ($('#filter_nursing_ward').val() || '').toLowerCase().trim();
$('.bed-card-col').each(function() {
var $col = $(this);
var bStatus = ($col.data('status') || '').toLowerCase();
var bWard = ($col.data('ward') || '').toLowerCase();
var bStation = ($col.data('station') || '').toLowerCase();
var bSearch = ($col.data('search') || '').toLowerCase();
var matchSearch = (search === '' || bSearch.indexOf(search) !== -1);
var matchStation = (station === '' || bStation.indexOf(station) !== -1);
var matchWard = (ward === '' || bWard.indexOf(ward) !== -1);
var matchStatus = (currentStatusFilter === 'all' || bStatus === currentStatusFilter);
if (matchSearch && matchStation && matchWard && matchStatus) {
$col.removeClass('d-none');
} else {
$col.addClass('d-none');
}
});
$('.ward-block-card').each(function() {
var $card = $(this);
var visibleBeds = $card.find('.bed-card-col:not(.d-none)').length;
if (visibleBeds === 0) {
$card.addClass('d-none');
} else {
$card.removeClass('d-none');
}
});
}
</script>

View File

@ -86,39 +86,60 @@ ksort($nurseNames);
.nursing-workspace-page .card {
border: 1px solid #e6edf5;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
box-shadow: 0 1px 3px rgba(16, 24, 40, 0.04);
border-radius: 0.5rem;
}
.nursing-workspace-page #nursing_entry_tabs {
gap: 0.35rem;
border-bottom: 1px solid #e6edf5;
padding: 0.65rem 0.85rem 0 !important;
background: #f8f9fc;
display: flex;
flex-direction: column;
gap: 0.3rem;
padding: 0;
background: transparent;
border: none;
}
.nursing-workspace-page #nursing_entry_tabs .nav-link {
border: 1px solid transparent;
border-radius: 0.45rem 0.45rem 0 0;
color: #4154f1;
border-radius: 0.45rem;
color: #334155;
font-weight: 500;
padding: 0.5rem 0.85rem;
margin-bottom: -1px;
font-size: 0.88rem;
padding: 0.65rem 0.85rem;
text-align: left;
display: flex;
align-items: center;
width: 100%;
background: #f8fafc;
border: 1px solid #e2e8f0;
transition: all 0.15s ease-in-out;
}
.nursing-workspace-page #nursing_entry_tabs .nav-link i {
font-size: 1.05rem;
margin-right: 0.65rem;
flex-shrink: 0;
}
.nursing-workspace-page #nursing_entry_tabs .nav-link:hover {
background: #eef2ff;
border-color: #dbe2f3;
border-color: #cbd5e1;
color: #1e40af;
}
.nursing-workspace-page #nursing_entry_tabs .nav-link.active {
background: #fff;
border-color: #e6edf5 #e6edf5 #fff;
color: #012970;
background: #0d6efd;
border-color: #0d6efd;
color: #ffffff !important;
box-shadow: 0 2px 6px rgba(13, 110, 253, 0.3);
font-weight: 600;
}
.nursing-workspace-page #nursing_entry_tabs .nav-link.active i {
color: #ffffff !important;
}
.nursing-workspace-page .nursing-tab-pane {
margin-top: 0.85rem;
padding: 0.25rem 0.45rem 0.75rem;
padding: 0.1rem;
}
.nursing-workspace-page .nursing-tab-pane .card,
@ -133,23 +154,11 @@ ksort($nurseNames);
.nursing-workspace-page .table-responsive {
margin-top: 0.35rem;
}
@media (max-width: 991.98px) {
.nursing-workspace-page #nursing_entry_tabs {
overflow-x: auto;
flex-wrap: nowrap;
white-space: nowrap;
}
.nursing-workspace-page #nursing_entry_tabs .nav-item {
flex: 0 0 auto;
}
}
</style>
<section class="content nursing-workspace-page">
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="card-header d-flex justify-content-between align-items-center py-2">
<h5 class="mb-0">Nursing Workspace</h5>
<div class="d-flex gap-2">
<button
@ -169,8 +178,8 @@ ksort($nurseNames);
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="load_form('<?= base_url('ipd/patient') ?>','IPD Patient List');">Back to IPD Patient List</button>
</div>
</div>
<div class="card-body">
<p class="mb-1">
<div class="card-body py-2">
<p class="mb-0">
<strong>IPD:</strong> <?= esc((string) ($ipd->ipd_code ?? '')) ?> |
<strong>Patient:</strong> <?= esc((string) ($person->p_fname ?? '')) ?> |
<strong>UHID:</strong> <?= esc((string) ($person->p_code ?? '')) ?>
@ -179,41 +188,53 @@ ksort($nurseNames);
</div>
<div id="nursing_panel_content">
<div class="card mb-3">
<div class="card-header p-0">
<ul class="nav nav-tabs card-header-tabs px-2 pt-2" id="nursing_entry_tabs" role="tablist">
<li class="nav-item" role="presentation">
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_admission">Admission Snapshot</button>
</li>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_history">History &amp; Physical Assessment</button>
</li>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link active" data-nursing-tab="nursing_tab_vitals">Vitals</button>
</li>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_fluid">Fluid Intake / Output</button>
</li>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_treatment">Treatment / Procedure Note</button>
</li>
<?php if ($canViewIpdOt) : ?>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_ot" data-url="<?= esc($otPanelUrl, 'attr') ?>">Surgery / OT</button>
</li>
<?php endif ?>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_scan">Scan Paper -> Auto Fill</button>
</li>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_charge">Charge Entry</button>
</li>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_bed">Bed Management</button>
</li>
</ul>
<div class="row g-3">
<!-- Left Navigation Menu -->
<div class="col-md-4 col-lg-3">
<div class="card shadow-sm border-0 sticky-top" style="top: 15px; z-index: 10;">
<div class="card-header bg-light py-2 border-bottom">
<strong class="text-dark" style="font-size: 0.88rem;">
<i class="bi bi-layout-sidebar-inset me-1 text-primary"></i> Workspace Menu
</strong>
</div>
<div class="p-2" id="nursing_entry_tabs" role="tablist">
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_admission">
<i class="bi bi-person-vcard text-primary"></i> Admission Snapshot
</button>
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_history">
<i class="bi bi-file-earmark-medical text-info"></i> History &amp; Physical
</button>
<button type="button" class="nav-link active" data-nursing-tab="nursing_tab_vitals">
<i class="bi bi-heart-pulse text-danger"></i> Vitals
</button>
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_fluid">
<i class="bi bi-droplet text-primary"></i> Fluid Intake / Output
</button>
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_treatment">
<i class="bi bi-prescription2 text-warning"></i> Treatment / Procedure Note
</button>
<?php if ($canViewIpdOt) : ?>
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_ot" data-url="<?= esc($otPanelUrl, 'attr') ?>">
<i class="bi bi-hospital text-secondary"></i> Surgery / OT
</button>
<?php endif ?>
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_scan">
<i class="bi bi-qr-code-scan text-success"></i> Scan Paper -&gt; Auto Fill
</button>
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_charge">
<i class="bi bi-currency-dollar text-success"></i> Charge Entry
</button>
<button type="button" class="nav-link" data-nursing-tab="nursing_tab_bed">
<i class="bi bi-door-open text-primary"></i> Bed Management
</button>
</div>
</div>
</div>
<div class="card-body">
<!-- Right Content Area -->
<div class="col-md-8 col-lg-9">
<div class="card shadow-sm border-0 mb-3">
<div class="card-body p-3">
<!-- Bed Management Tab -->
<div class="nursing-tab-pane d-none" id="nursing_tab_bed">
@ -382,7 +403,17 @@ ksort($nurseNames);
<?= csrf_field() ?>
<input type="hidden" name="entry_type" value="admission">
<div class="col-md-3"><label class="form-label">Recorded At</label><input type="datetime-local" class="form-control" name="recorded_at" value="<?= date('Y-m-d\\TH:i') ?>"></div>
<div class="col-md-2"><label class="form-label">Shift</label><input type="text" class="form-control nursing-shift-display" value="Morning" readonly></div>
<div class="col-md-4">
<label class="form-label">Nursing Staff (ID / Name)</label>
<select name="recorded_by_nurse_id" class="form-select">
<option value="0">-- Select Nursing Staff --</option>
<?php foreach (($nurse_list ?? []) as $nRow): ?>
<option value="<?= (int) $nRow['id'] ?>">
<?= esc(($nRow['nurse_code'] ? '[' . $nRow['nurse_code'] . '] ' : '') . $nRow['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-12"><label class="form-label">Admission Complaints / Reason</label><textarea class="form-control" name="treatment_text" rows="3" placeholder="Chief complaints and reason for admission"></textarea></div>
<div class="col-md-1"><label class="form-label">Temp °F</label><input type="number" step="0.1" class="form-control" name="temperature_f"></div>
<div class="col-md-1"><label class="form-label">Pulse</label><input type="number" class="form-control" name="pulse_rate"></div>
@ -489,7 +520,17 @@ ksort($nurseNames);
<?= csrf_field() ?>
<input type="hidden" name="entry_type" value="vitals">
<div class="col-md-3"><label class="form-label">Recorded At</label><input type="datetime-local" class="form-control" name="recorded_at" value="<?= date('Y-m-d\\TH:i') ?>"></div>
<div class="col-md-2"><label class="form-label">Shift</label><input type="text" class="form-control nursing-shift-display" value="Morning" readonly></div>
<div class="col-md-4">
<label class="form-label">Nursing Staff (ID / Name)</label>
<select name="recorded_by_nurse_id" class="form-select">
<option value="0">-- Select Nursing Staff --</option>
<?php foreach (($nurse_list ?? []) as $nRow): ?>
<option value="<?= (int) $nRow['id'] ?>">
<?= esc(($nRow['nurse_code'] ? '[' . $nRow['nurse_code'] . '] ' : '') . $nRow['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-1"><label class="form-label">Temp °F</label><input type="number" step="0.1" class="form-control" name="temperature_f"></div>
<div class="col-md-1"><label class="form-label">Pulse</label><input type="number" class="form-control" name="pulse_rate"></div>
<div class="col-md-1"><label class="form-label">Resp</label><input type="number" class="form-control" name="resp_rate"></div>
@ -507,7 +548,17 @@ ksort($nurseNames);
<?= csrf_field() ?>
<input type="hidden" name="entry_type" value="fluid">
<div class="col-md-3"><label class="form-label">Recorded At</label><input type="datetime-local" class="form-control" name="recorded_at" value="<?= date('Y-m-d\\TH:i') ?>"></div>
<div class="col-md-2"><label class="form-label">Shift</label><input type="text" class="form-control nursing-shift-display" value="Morning" readonly></div>
<div class="col-md-4">
<label class="form-label">Nursing Staff (ID / Name)</label>
<select name="recorded_by_nurse_id" class="form-select">
<option value="0">-- Select Nursing Staff --</option>
<?php foreach (($nurse_list ?? []) as $nRow): ?>
<option value="<?= (int) $nRow['id'] ?>">
<?= esc(($nRow['nurse_code'] ? '[' . $nRow['nurse_code'] . '] ' : '') . $nRow['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2"><label class="form-label">Direction</label><select class="form-select" name="fluid_direction"><option value="intake">Intake</option><option value="output">Output</option></select></div>
<div class="col-md-2"><label class="form-label">Route</label><input type="text" class="form-control" name="fluid_route"></div>
<div class="col-md-2"><label class="form-label">Amount (ml)</label><input type="number" class="form-control" name="fluid_amount_ml" required></div>
@ -521,7 +572,17 @@ ksort($nurseNames);
<?= csrf_field() ?>
<input type="hidden" name="entry_type" value="treatment">
<div class="col-md-3"><label class="form-label">Recorded At</label><input type="datetime-local" class="form-control" name="recorded_at" value="<?= date('Y-m-d\\TH:i') ?>"></div>
<div class="col-md-2"><label class="form-label">Shift</label><input type="text" class="form-control nursing-shift-display" value="Morning" readonly></div>
<div class="col-md-4">
<label class="form-label">Nursing Staff (ID / Name)</label>
<select name="recorded_by_nurse_id" class="form-select">
<option value="0">-- Select Nursing Staff --</option>
<?php foreach (($nurse_list ?? []) as $nRow): ?>
<option value="<?= (int) $nRow['id'] ?>">
<?= esc(($nRow['nurse_code'] ? '[' . $nRow['nurse_code'] . '] ' : '') . $nRow['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-12"><label class="form-label">Treatment Given</label><textarea class="form-control" name="treatment_text" rows="3" required></textarea></div>
<div class="col-md-12"><label class="form-label">Additional Note</label><textarea class="form-control" name="general_note" rows="2"></textarea></div>
<div class="col-md-12"><button type="submit" class="btn btn-primary btn-sm">Save Treatment Note</button></div>
@ -804,16 +865,16 @@ ksort($nurseNames);
</div>
<table class="table table-sm table-bordered align-middle">
<thead><tr><th style="width: 150px;">Time</th><th style="width: 90px;">Type</th><th style="width: 90px;">Shift</th><th>Details</th><th style="width: 130px;">By</th></tr></thead>
<thead><tr><th style="width: 150px;">Time</th><th style="width: 100px;">Type</th><th style="width: 180px;">Nursing Staff</th><th>Details</th></tr></thead>
<tbody>
<?php if (empty($entries)) : ?>
<tr><td colspan="5" class="text-center text-muted" id="nursing_no_data_row">No nursing entries yet.</td></tr>
<tr><td colspan="4" class="text-center text-muted" id="nursing_no_data_row">No nursing entries yet.</td></tr>
<?php else : ?>
<?php foreach ($entries as $entry) : ?>
<tr class="nursing-entry-row" data-nurse="<?= esc((string) ($entry['recorded_by'] ?? '')) ?>" data-type="<?= esc(strtolower((string) ($entry['entry_type'] ?? ''))) ?>">
<td><?= esc((string) ($entry['recorded_at'] ?? '')) ?></td>
<td><?= esc((string) ($entry['entry_type'] ?? '')) ?></td>
<td><?= esc((string) ($entry['shift_name'] ?? '')) ?></td>
<td><span class="badge bg-light text-dark border"><?= esc((string) ($entry['entry_type'] ?? '')) ?></span></td>
<td><strong class="text-primary"><?= esc((string) ($entry['recorded_by'] ?? 'Staff')) ?></strong></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'] ?? '')) ?>
@ -824,7 +885,6 @@ ksort($nurseNames);
<?php endif; ?>
<?php if (! empty($entry['general_note'])) : ?><br><small><strong>Note:</strong> <?= esc((string) $entry['general_note']) ?></small><?php endif; ?>
</td>
<td><?= esc((string) ($entry['recorded_by'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
@ -871,6 +931,8 @@ ksort($nurseNames);
</div>
</div>
</div>
</div>
</div>
</section>
<script>