diff --git a/android_opd_queue_tv/app/src/main/res/drawable/ic_launcher_background.xml b/android_opd_queue_tv/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d66a50e --- /dev/null +++ b/android_opd_queue_tv/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/android_opd_queue_tv/app/src/main/res/drawable/ic_launcher_foreground.xml b/android_opd_queue_tv/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..fe6816e --- /dev/null +++ b/android_opd_queue_tv/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,10 @@ + + + + diff --git a/android_opd_queue_tv/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android_opd_queue_tv/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6b78462 --- /dev/null +++ b/android_opd_queue_tv/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android_opd_queue_tv/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android_opd_queue_tv/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6b78462 --- /dev/null +++ b/android_opd_queue_tv/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android_opd_queue_tv/app/src/main/res/values/colors.xml b/android_opd_queue_tv/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..ca1931b --- /dev/null +++ b/android_opd_queue_tv/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + diff --git a/android_opd_queue_tv/build.gradle b/android_opd_queue_tv/build.gradle index df4514e..e596520 100644 --- a/android_opd_queue_tv/build.gradle +++ b/android_opd_queue_tv/build.gradle @@ -4,17 +4,10 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.2.2' + classpath 'com.android.tools.build:gradle:8.13.2' } } -allprojects { - repositories { - google() - mavenCentral() - } -} - -task clean(type: Delete) { - delete rootProject.buildDir +tasks.register('clean', Delete) { + delete rootProject.layout.buildDirectory } diff --git a/android_opd_queue_tv/settings.gradle b/android_opd_queue_tv/settings.gradle index e391733..419f328 100644 --- a/android_opd_queue_tv/settings.gradle +++ b/android_opd_queue_tv/settings.gradle @@ -5,6 +5,9 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { diff --git a/app/Config/Routes.php b/app/Config/Routes.php index f16ef5a..e0e9884 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1592,6 +1592,10 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static funct $routes->post('nursing/opd/vitals/save/(:num)', 'NursingApi::saveOpdVitals/$1'); $routes->post('nursing/opd/scan/save/(:num)', 'NursingApi::saveOpdScan/$1'); $routes->post('nursing/ipd/scan/save/(:num)', 'NursingApi::saveIpdScan/$1'); + $routes->get('nursing/patient/search', 'NursingApi::searchPatient'); + $routes->post('nursing/patient/wellness/save', 'NursingApi::savePatientWellness'); + $routes->get('nursing/patient/wellness/history/(:num)', 'NursingApi::getPatientWellnessHistory/$1'); + $routes->post('nursing/patient/document/save', 'NursingApi::savePatientDocument'); // Doctor PWA APIs $routes->get('doctor/list', 'DoctorApi::doctors'); diff --git a/app/Controllers/Api/v1/NursingApi.php b/app/Controllers/Api/v1/NursingApi.php index 30aa45f..cb39241 100644 --- a/app/Controllers/Api/v1/NursingApi.php +++ b/app/Controllers/Api/v1/NursingApi.php @@ -78,7 +78,7 @@ class NursingApi extends BaseController FROM bed_master b LEFT JOIN ward_master w ON b.ward_id = w.id LEFT JOIN bed_category_master c ON b.bed_category_id = c.id - LEFT JOIN ipd_master i ON (b.current_ipd_id = i.id AND i.ipd_status = 0) + LEFT JOIN ipd_master i ON ((b.current_ipd_id = i.id OR (b.bed_number = i.bed_no AND i.bed_no != '')) AND i.ipd_status = 0) LEFT JOIN patient_master p ON i.p_id = p.id LEFT JOIN ( select i.ipd_id, group_concat(distinct concat_ws(' ', 'Dr.', d.p_fname, d.p_mname, d.p_lname)) as doc_name @@ -224,7 +224,15 @@ class NursingApi extends BaseController */ public function saveEntry(int $ipdId) { - $post = $this->request->getPost() ?: ($this->request->getJSON(true) ?? []); + $post = $this->request->getPost(); + if (empty($post)) { + try { + $post = $this->request->getJSON(true) ?? []; + } catch (\Throwable $e) { + $raw = $this->request->getBody(); + $post = is_string($raw) ? (json_decode($raw, true) ?? []) : []; + } + } $entryType = (string) ($post['entry_type'] ?? ''); if (! in_array($entryType, ['vitals', 'fluid', 'treatment', 'admission'], true)) { return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Invalid nursing entry type']); @@ -288,13 +296,13 @@ class NursingApi extends BaseController $whereClause = "WHERE 1=1"; if ($dateMode === 'today') { - $whereClause .= " AND DATE(o.apointment_date) = CURDATE()"; + $whereClause .= " AND (DATE(o.apointment_date) = CURDATE() OR (o.apointment_date IS NULL AND DATE(o.opd_book_date) = CURDATE()))"; } if ($docId > 0) { $whereClause .= " AND o.doc_id = " . $docId; } - $sql = "SELECT o.opd_id, o.opd_code, o.opd_no, o.p_id, o.doc_id, o.apointment_date, o.opd_status, coalesce(o.opd_fee_type, 'Cash') as opd_type, + $sql = "SELECT o.opd_id, o.opd_code, o.opd_no, o.p_id, o.doc_id, o.apointment_date, o.opd_book_date, o.opd_status, coalesce(o.opd_fee_type, 'Cash') as opd_type, p.p_fname as fname, p.p_rname as rname, p.p_code as uhid, p.mphone1, p.gender, p.age, p.age_in_month, p.estimate_dob, p.dob, concat('Dr. ', d.p_fname, ' ', coalesce(d.p_lname, '')) as doctor_name, pr.temp, pr.pulse, pr.bp, pr.diastolic, pr.spo2, pr.weight, pr.height, pr.rr_min @@ -308,8 +316,9 @@ class NursingApi extends BaseController $query = $db->query($sql); $records = $query->getResultArray(); - $todayTotalCount = (int) $db->query("SELECT count(*) as c FROM opd_master WHERE DATE(apointment_date) = CURDATE()")->getRow()->c; - $allTotalCount = (int) $db->query("SELECT count(*) as c FROM opd_master")->getRow()->c; + $docWhere = $docId > 0 ? " AND doc_id = " . $docId : ""; + $todayTotalCount = (int) $db->query("SELECT count(*) as c FROM opd_master WHERE (DATE(apointment_date) = CURDATE() OR (apointment_date IS NULL AND DATE(opd_book_date) = CURDATE()))" . $docWhere)->getRow()->c; + $allTotalCount = (int) $db->query("SELECT count(*) as c FROM opd_master WHERE 1=1" . $docWhere)->getRow()->c; $counts = [ 'all' => 0, 'waiting' => 0, 'visited' => 0, 'booked' => 0, 'cancelled' => 0, @@ -358,6 +367,10 @@ class NursingApi extends BaseController $ageDisplay = get_age_1($r['dob'] ?? null, $r['age'] ?? '', $r['age_in_month'] ?? '', $r['estimate_dob'] ?? ''); } elseif (! empty($r['age'])) { $ageDisplay = $r['age'] . ' Year'; + } elseif (! empty($r['dob']) && $r['dob'] !== '0000-00-00') { + try { + $ageDisplay = date_diff(date_create($r['dob']), date_create('today'))->y . ' Year'; + } catch (\Throwable $e) {} } $r['status_key'] = $statusKey; @@ -621,5 +634,361 @@ class NursingApi extends BaseController 'ai_alert_flag' => 0, ]); } + + /** + * Search patient by Barcode, QR Code payload, UHID, ABHA ID/Address, Mobile, or Name + * GET api/v1/nursing/patient/search?q=... + */ + public function searchPatient() + { + $db = db_connect(); + $q = trim((string) $this->request->getGet('q')); + if ($q === '') { + return $this->response->setJSON(['status' => 1, 'data' => []]); + } + + // Check if $q is a JSON payload from ABHA QR or Hospital QR + $abhaHidn = null; + $abhaAddress = null; + $abhaMobile = null; + $abhaName = null; + + if (str_starts_with($q, '{') && str_ends_with($q, '}')) { + $jsonParsed = json_decode($q, true); + if (is_array($jsonParsed)) { + $abhaHidn = $jsonParsed['hidn'] ?? $jsonParsed['abha_number'] ?? $jsonParsed['hid_number'] ?? null; + $abhaAddress = $jsonParsed['hid'] ?? $jsonParsed['abha_address'] ?? null; + $abhaMobile = $jsonParsed['mobile'] ?? $jsonParsed['phone'] ?? null; + $abhaName = $jsonParsed['name'] ?? null; + $uhidFromJson = $jsonParsed['uhid'] ?? $jsonParsed['p_code'] ?? null; + if ($uhidFromJson) { + $q = $uhidFromJson; + } + } + } + + // Clean query (e.g. remove hyphens if looks like ABHA number) + $cleanAbha = str_replace('-', '', $q); + + $builder = $db->table('patient_master p'); + $builder->select('p.id, p.p_code, p.p_fname, p.p_lname, p.title, p.gender, p.dob, p.age, p.mphone1, p.mphone2, p.email1, p.city, p.district, p.state, p.abha_id, p.abha_address, p.abha_verified_status, p.abha_kyc_verified, p.abha_mobile_verified, p.abha_profile_photo_base64'); + + $builder->groupStart(); + $builder->where('p.p_code', $q); + $builder->orWhere('p.old_uhid', $q); + $builder->orWhere('p.mphone1', $q); + $builder->orWhere('p.mphone2', $q); + $builder->orWhere('p.abha_id', $q); + $builder->orWhere('p.abha_id', $cleanAbha); + $builder->orWhere('p.abha_address', $q); + + if ($abhaHidn) { + $builder->orWhere('p.abha_id', $abhaHidn); + $builder->orWhere('p.abha_id', str_replace('-', '', $abhaHidn)); + } + if ($abhaAddress) { + $builder->orWhere('p.abha_address', $abhaAddress); + } + if ($abhaMobile) { + $builder->orWhere('p.mphone1', $abhaMobile); + $builder->orWhere('p.mphone2', $abhaMobile); + } + + if (strlen($q) >= 2) { + $builder->orLike('p.p_fname', $q); + $builder->orLike('p.p_lname', $q); + $builder->orLike("CONCAT(p.p_fname, ' ', p.p_lname)", $q); + $builder->orLike('p.mphone1', $q); + $builder->orLike('p.p_code', $q); + } + $builder->groupEnd(); + + $patients = $builder->limit(15)->get()->getResultArray(); + + // Enrich with current IPD bed admission and today's OPD appointment status + $today = date('Y-m-d'); + foreach ($patients as &$pat) { + $pId = (int) $pat['id']; + $pat['fullName'] = trim(($pat['title'] ? $pat['title'] . ' ' : '') . $pat['p_fname'] . ($pat['p_lname'] && $pat['p_lname'] !== '0' ? ' ' . $pat['p_lname'] : '')); + + // Check IPD admission + $ipdRow = $db->table('ipd_master i') + ->select('i.id as ipd_id, i.ipd_code, i.bed_no, i.register_date, i.r_doc_name, b.bed_number, w.ward_name') + ->join('bed_master b', 'b.id = i.bed_no OR b.bed_number = i.bed_no', 'left') + ->join('ward_master w', 'w.id = b.ward_id', 'left') + ->where('i.p_id', $pId) + ->groupStart() + ->where('i.discharge_date IS NULL') + ->orWhere('i.discharge_date', '0000-00-00') + ->groupEnd() + ->orderBy('i.id', 'DESC') + ->limit(1) + ->get() + ->getRowArray(); + + $pat['ipd_admission'] = $ipdRow ?: null; + + // Check Today OPD appointment + $opdRow = $db->table('opd_master o') + ->select('o.opd_id, o.opd_code, o.opd_no as token_no, o.doc_id, o.doc_name as doctor_name, o.apointment_date, o.opd_book_date, pr.temp, pr.pulse, pr.bp, pr.diastolic, pr.spo2, pr.weight, pr.height, pr.rr_min') + ->join('opd_prescription pr', 'pr.opd_id = o.opd_id', 'left') + ->where('o.p_id', $pId) + ->groupStart() + ->where('DATE(o.apointment_date)', $today) + ->orGroupStart() + ->where('o.apointment_date IS NULL') + ->where('DATE(o.opd_book_date)', $today) + ->groupEnd() + ->groupEnd() + ->orderBy('o.opd_id', 'DESC') + ->limit(1) + ->get() + ->getRowArray(); + + $pat['opd_today'] = $opdRow ?: null; + } + + return $this->response->setJSON([ + 'status' => 1, + 'data' => $patients, + 'count' => count($patients), + ]); + } + + /** + * Save ABDM M2 Wellness Vitals for any registered patient + * POST api/v1/nursing/patient/wellness/save + */ + public function savePatientWellness() + { + $db = db_connect(); + $post = $this->request->getPost(); + if (empty($post)) { + try { + $post = $this->request->getJSON(true) ?? []; + } catch (\Throwable $e) { + $raw = $this->request->getBody(); + $post = is_string($raw) ? (json_decode($raw, true) ?? []) : []; + } + } + + $patientId = (int) ($post['patient_id'] ?? 0); + if ($patientId <= 0) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Patient ID is required']); + } + + $patient = $db->table('patient_master')->where('id', $patientId)->get()->getRowArray(); + if (! $patient) { + return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Patient record not found']); + } + + $recordedAtInput = (string) ($post['recorded_at'] ?? ''); + $recordedAt = $recordedAtInput !== '' + ? str_replace('T', ' ', $recordedAtInput) . (strlen($recordedAtInput) === 16 ? ':00' : '') + : date('Y-m-d H:i:s'); + + $tempF = isset($post['temperature_f']) && $post['temperature_f'] !== '' ? (float) $post['temperature_f'] : null; + $tempC = $tempF !== null ? (($tempF - 32) * 5 / 9) : null; + $heightCm = isset($post['height_cm']) && $post['height_cm'] !== '' ? (float) $post['height_cm'] : null; + $weightKg = isset($post['weight_kg']) && $post['weight_kg'] !== '' ? (float) $post['weight_kg'] : null; + $bmi = null; + if ($heightCm && $heightCm > 0 && $weightKg && $weightKg > 0) { + $heightM = $heightCm / 100; + $bmi = round($weightKg / ($heightM * $heightM), 2); + } + + $wellnessData = [ + 'patient_id' => $patientId, + 'uhid' => $patient['p_code'] ?? '', + 'abha_id' => $patient['abha_id'] ?? null, + 'abha_address' => $patient['abha_address'] ?? null, + 'recorded_at' => $recordedAt, + 'temperature_f' => $tempF, + 'temperature_c' => $tempC !== null ? round($tempC, 2) : null, + 'pulse_rate' => isset($post['pulse_rate']) && $post['pulse_rate'] !== '' ? (int) $post['pulse_rate'] : null, + 'resp_rate' => isset($post['resp_rate']) && $post['resp_rate'] !== '' ? (int) $post['resp_rate'] : null, + 'bp_systolic' => isset($post['bp_systolic']) && $post['bp_systolic'] !== '' ? (int) $post['bp_systolic'] : null, + 'bp_diastolic' => isset($post['bp_diastolic']) && $post['bp_diastolic'] !== '' ? (int) $post['bp_diastolic'] : null, + 'spo2' => isset($post['spo2']) && $post['spo2'] !== '' ? (int) $post['spo2'] : null, + 'weight_kg' => $weightKg, + 'height_cm' => $heightCm, + 'bmi' => $bmi, + 'general_advice' => (string) ($post['general_advice'] ?? ''), + 'diet_lifestyle_note' => (string) ($post['diet_lifestyle_note'] ?? ''), + 'recorded_by' => (string) ($post['recorded_by'] ?? 'Staff Nurse'), + 'recorded_by_id' => isset($post['recorded_by_id']) ? (int) $post['recorded_by_id'] : null, + 'abdm_status' => 'ready', + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]; + + $db->table('patient_wellness_records')->insert($wellnessData); + $wellnessId = $db->insertID(); + + // Also register in health_records for ABDM M2 / M3 discovery & push + if ($db->tableExists('health_records')) { + $careContextRef = 'WELLNESS-' . $wellnessId . '-' . date('Ymd'); + $db->table('health_records')->insert([ + 'patient_id' => $patientId, + 'abha_id' => $patient['abha_id'] ?? null, + 'hi_type' => 'WellnessRecord', + 'entity_type' => 'wellness', + 'entity_id' => (string) $wellnessId, + 'push_status' => 'pending', + 'care_context_reference' => $careContextRef, + 'record_data' => json_encode($wellnessData), + 'created_by_name' => (string) ($post['recorded_by'] ?? 'Staff Nurse'), + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + } + + return $this->response->setJSON([ + 'status' => 1, + 'message' => 'ABDM M2 Wellness & Vitals recorded successfully', + 'wellness_id' => $wellnessId, + 'bmi' => $bmi, + ]); + } + + /** + * Get patient wellness & vitals history + * GET api/v1/nursing/patient/wellness/history/(:num) + */ + public function getPatientWellnessHistory(int $patientId) + { + $db = db_connect(); + $records = $db->table('patient_wellness_records') + ->where('patient_id', $patientId) + ->orderBy('recorded_at', 'DESC') + ->limit(30) + ->get() + ->getResultArray(); + + return $this->response->setJSON([ + 'status' => 1, + 'patient_id' => $patientId, + 'records' => $records, + ]); + } + + /** + * Save ABDM M2 Health Document from scan/camera for any registered patient + * POST api/v1/nursing/patient/document/save + */ + public function savePatientDocument() + { + $db = db_connect(); + $post = $this->request->getPost(); + if (empty($post)) { + try { + $post = $this->request->getJSON(true) ?? []; + } catch (\Throwable $e) { + $raw = $this->request->getBody(); + $post = is_string($raw) ? (json_decode($raw, true) ?? []) : []; + } + } + + $patientId = (int) ($post['patient_id'] ?? 0); + if ($patientId <= 0) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Patient ID required']); + } + + $patient = $db->table('patient_master')->where('id', $patientId)->get()->getRowArray(); + if (! $patient) { + return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Patient record not found']); + } + + $imageBase64 = $post['image_base64'] ?? null; + if (empty($imageBase64)) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Scanned document image required']); + } + + $docCategory = trim((string) ($post['document_type'] ?? 'Health Document')); + $nurseName = trim((string) ($post['nurse_name'] ?? 'Staff Nurse')); + $nurseId = (int) ($post['nurse_id'] ?? 0); + $docDate = trim((string) ($post['document_date'] ?? date('Y-m-d'))); + $remarks = trim((string) ($post['remarks'] ?? '')); + + $imgData = $imageBase64; + $ext = 'jpg'; + if (preg_match('/^data:image\/(\w+);base64,/', $imageBase64, $type)) { + $imgData = substr($imageBase64, strpos($imageBase64, ',') + 1); + $ext = strtolower($type[1]); + if ($ext === 'jpeg') $ext = 'jpg'; + } + $imgData = str_replace(' ', '+', $imgData); + $binary = base64_decode($imgData); + + if ($binary === false || strlen($binary) === 0) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Failed to process document image']); + } + + $uploadDir = FCPATH . 'uploads/health_documents/'; + if (! is_dir($uploadDir)) { + @mkdir($uploadDir, 0777, true); + } + $filename = 'abdm_m2_doc_p' . $patientId . '_' . time() . '_' . rand(100, 999) . '.' . $ext; + file_put_contents($uploadDir . $filename, $binary); + $imageUrl = '/uploads/health_documents/' . $filename; + + // Map to ABDM HI Type + $hiType = 'HealthDocumentRecord'; + $catLower = strtolower($docCategory); + if (str_contains($catLower, 'presc')) { + $hiType = 'PrescriptionRecord'; + } elseif (str_contains($catLower, 'diag') || str_contains($catLower, 'lab') || str_contains($catLower, 'report')) { + $hiType = 'DiagnosticReport'; + } elseif (str_contains($catLower, 'discharg')) { + $hiType = 'DischargeSummary'; + } elseif (str_contains($catLower, 'immun')) { + $hiType = 'ImmunizationRecord'; + } + + $healthRecordId = null; + if ($db->tableExists('health_records')) { + $careContextRef = 'DOC-' . $patientId . '-' . time(); + $db->table('health_records')->insert([ + 'patient_id' => $patientId, + 'abha_id' => $patient['abha_id'] ?? null, + 'hi_type' => $hiType, + 'entity_type' => 'document', + 'entity_id' => (string) time(), + 'attachment_path' => $imageUrl, + 'push_status' => 'pending', + 'care_context_reference' => $careContextRef, + 'record_data' => json_encode([ + 'category' => $docCategory, + 'document_date' => $docDate, + 'remarks' => $remarks, + 'uploaded_by' => $nurseName, + 'image_url' => $imageUrl, + ]), + 'created_by_name' => $nurseName, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + $healthRecordId = $db->insertID(); + } + + // Register in file_upload_data for HMS Scan Doc List popup + $this->registerFileUploadData([ + 'filename' => $filename, + 'public_path' => $imageUrl, + 'p_id' => $patientId, + 'upload_by' => $nurseName, + 'doc_type' => $docCategory, + 'file_size_kb' => round(strlen($binary) / 1024, 2), + ]); + + return $this->response->setJSON([ + 'status' => 1, + 'message' => 'ABDM M2 Health Document uploaded successfully', + 'image_url' => $imageUrl, + 'health_record_id' => $healthRecordId, + 'hi_type' => $hiType, + ]); + } } + diff --git a/public/App/Nursing/index.html b/public/App/Nursing/index.html index a6cebf5..ad94edc 100644 --- a/public/App/Nursing/index.html +++ b/public/App/Nursing/index.html @@ -27,6 +27,9 @@ + + + @@ -369,6 +475,36 @@ const [scanImageBase64, setScanImageBase64] = useState(''); const [scanImagePreview, setScanImagePreview] = useState(''); + // ABDM M2 & Patient Search / Barcode Scanner State + const [showPatientSearchModal, setShowPatientSearchModal] = useState(false); + const [searchModalTab, setSearchModalTab] = useState('scan'); // 'scan' or 'type' + const [patientSearchQuery, setPatientSearchQuery] = useState(''); + const [patientSearchResults, setPatientSearchResults] = useState([]); + const [searchingPatient, setSearchingPatient] = useState(false); + const [scannerActive, setScannerActive] = useState(false); + const [scannerError, setScannerError] = useState(''); + + // ABDM M2 Wellness Vitals Modal State + const [activeWellnessPatient, setActiveWellnessPatient] = useState(null); + const [wellnessForm, setWellnessForm] = useState({ + temperature_f: '', pulse_rate: '', resp_rate: '', + bp_systolic: '', bp_diastolic: '', spo2: '', weight_kg: '', height_cm: '', + general_advice: '', diet_lifestyle_note: '' + }); + + // ABDM M2 Health Document Modal State + const [activeDocPatient, setActiveDocPatient] = useState(null); + const [patientDocForm, setPatientDocForm] = useState({ + document_type: 'Health Document', + document_date: new Date().toISOString().slice(0, 10), + remarks: '' + }); + + // Vitals History Modal State + const [wellnessHistoryPatient, setWellnessHistoryPatient] = useState(null); + const [wellnessHistoryList, setWellnessHistoryList] = useState([]); + const [loadingHistory, setLoadingHistory] = useState(false); + // Auth State const [authenticatedNurse, setAuthenticatedNurse] = useState(null); const [authNurseId, setAuthNurseId] = useState(0); @@ -393,6 +529,8 @@ treatment_text: '', general_note: '' }); + const [showAllDoctorOrders, setShowAllDoctorOrders] = useState(false); + const fetchBeds = async () => { try { setLoading(true); @@ -467,8 +605,6 @@ const parsed = JSON.parse(savedAuth); if (parsed && parsed.nurse && parsed.expires_at > Date.now()) { setAuthenticatedNurse(parsed.nurse); - fetchBeds(); - fetchOpdAppointments(opdDateMode, selectedDoctorId, vitalsFilter); } else { localStorage.removeItem(DEVICE_AUTH_KEY); } @@ -477,6 +613,7 @@ fetchBeds(); fetchNurses(); fetchDoctors(); + fetchOpdAppointments('today', '0', 'all'); }, []); const handleVerifyPin = async () => { @@ -511,7 +648,7 @@ localStorage.setItem(DEVICE_AUTH_KEY, JSON.stringify(deviceAuthObj)); setPinInput(''); fetchBeds(); - fetchOpdAppointments(opdDateMode); + fetchOpdAppointments(opdDateMode, selectedDoctorId, vitalsFilter); } else if (json.status === 2) { setIsSettingNewPin(true); } else { @@ -598,6 +735,21 @@ const json = await res.json(); if (json.status === 1) { alert(json.message || 'Saved successfully!'); + if (entryType === 'vitals') { + setVitalsForm({ + temperature_f: '', pulse_rate: '', resp_rate: '', + bp_systolic: '', bp_diastolic: '', spo2: '', weight_kg: '', general_note: '' + }); + } else if (entryType === 'fluid') { + setFluidForm({ + fluid_direction: 'intake', fluid_route: 'IV (Intravenous)', + fluid_amount_ml: '', general_note: '' + }); + } else if (entryType === 'treatment') { + setTreatmentForm({ + treatment_text: '', general_note: '' + }); + } openWorkspace(activeBed); } else { alert(json.message || 'Failed to save entry'); @@ -759,6 +911,255 @@ } }; + // ----------------------------------------------------------------- + // ABDM M2 PATIENT SEARCH & BARCODE / QR SCANNER HANDLERS + // ----------------------------------------------------------------- + let scannerInstance = null; + + const startBarcodeQrScanner = () => { + if (typeof Html5Qrcode === 'undefined') { + setScannerError('Scanner library is loading. Please try in a moment.'); + return; + } + setScannerError(''); + setScannerActive(true); + + setTimeout(() => { + const readerElem = document.getElementById('patient-qr-reader'); + if (!readerElem) return; + + try { + if (scannerInstance) { + scannerInstance.stop().catch(() => {}).finally(() => { + initHtml5QrCode(readerElem); + }); + } else { + initHtml5QrCode(readerElem); + } + } catch (e) { + setScannerError('Could not access camera: ' + (e.message || e)); + setScannerActive(false); + } + }, 300); + }; + + const initHtml5QrCode = (readerElem) => { + try { + const html5QrCode = new Html5Qrcode("patient-qr-reader"); + scannerInstance = html5QrCode; + const config = { + fps: 12, + qrbox: { width: 280, height: 180 }, + aspectRatio: 1.333334, + formatsToSupport: [ + Html5QrcodeSupportedFormats.QR_CODE, + Html5QrcodeSupportedFormats.CODE_128, + Html5QrcodeSupportedFormats.CODE_39, + Html5QrcodeSupportedFormats.EAN_13, + Html5QrcodeSupportedFormats.EAN_8, + Html5QrcodeSupportedFormats.DATA_MATRIX + ] + }; + + html5QrCode.start( + { facingMode: "environment" }, + config, + (decodedText) => { + // On successful scan + stopBarcodeQrScanner(); + setSearchModalTab('type'); + setPatientSearchQuery(decodedText); + executePatientSearch(decodedText); + }, + (errorMessage) => { + // ignore frame errors + } + ).catch(err => { + setScannerError('Camera permission required or camera busy: ' + (err.message || err)); + setScannerActive(false); + }); + } catch (err) { + setScannerError('Failed to initialize camera scanner: ' + (err.message || err)); + setScannerActive(false); + } + }; + + const stopBarcodeQrScanner = () => { + if (scannerInstance) { + try { + scannerInstance.stop().then(() => { + scannerInstance.clear(); + scannerInstance = null; + setScannerActive(false); + }).catch(() => { + scannerInstance = null; + setScannerActive(false); + }); + } catch (e) { + scannerInstance = null; + setScannerActive(false); + } + } else { + setScannerActive(false); + } + }; + + const openPatientSearch = (defaultTab = 'scan') => { + setShowPatientSearchModal(true); + setSearchModalTab(defaultTab); + setPatientSearchResults([]); + setPatientSearchQuery(''); + if (defaultTab === 'scan') { + startBarcodeQrScanner(); + } else { + stopBarcodeQrScanner(); + } + }; + + const closePatientSearch = () => { + stopBarcodeQrScanner(); + setShowPatientSearchModal(false); + setPatientSearchResults([]); + setPatientSearchQuery(''); + }; + + const executePatientSearch = async (queryText) => { + const q = queryText !== undefined ? queryText : patientSearchQuery; + if (!q || q.trim() === '') { + setPatientSearchResults([]); + return; + } + setSearchingPatient(true); + try { + const res = await fetch(`/api/v1/nursing/patient/search?q=${encodeURIComponent(q.trim())}`); + const json = await res.json(); + if (json.status === 1) { + setPatientSearchResults(json.data || []); + } else { + setPatientSearchResults([]); + } + } catch (err) { + console.error('Patient search error', err); + } finally { + setSearchingPatient(false); + } + }; + + // ABDM M2 Wellness Actions + const openWellnessModal = (patient) => { + setActiveWellnessPatient(patient); + const opdVitals = patient.opd_today || {}; + setWellnessForm({ + temperature_f: opdVitals.temp || '', + pulse_rate: opdVitals.pulse || '', + resp_rate: opdVitals.rr_min || '', + bp_systolic: opdVitals.bp || '', + bp_diastolic: opdVitals.diastolic || '', + spo2: opdVitals.spo2 || '', + weight_kg: opdVitals.weight || '', + height_cm: opdVitals.height || '', + general_advice: '', + diet_lifestyle_note: '' + }); + setShowPatientSearchModal(false); + stopBarcodeQrScanner(); + }; + + const handleSaveWellness = async () => { + if (!activeWellnessPatient) return; + try { + const payload = { + patient_id: activeWellnessPatient.id, + recorded_by: authenticatedNurse ? authenticatedNurse.name : 'Staff Nurse', + recorded_by_id: authenticatedNurse ? authenticatedNurse.id : 0, + ...wellnessForm + }; + const res = await fetch('/api/v1/nursing/patient/wellness/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + const json = await res.json(); + if (json.status === 1) { + alert('✅ ' + (json.message || 'ABDM M2 Wellness Vitals saved successfully!') + (json.bmi ? ` (BMI: ${json.bmi})` : '')); + setActiveWellnessPatient(null); + setWellnessForm({ + temperature_f: '', pulse_rate: '', resp_rate: '', + bp_systolic: '', bp_diastolic: '', spo2: '', weight_kg: '', height_cm: '', + general_advice: '', diet_lifestyle_note: '' + }); + } else { + alert(json.message || 'Failed to save wellness vitals'); + } + } catch (err) { + alert('Error saving wellness vitals'); + } + }; + + // ABDM M2 Health Document Actions + const openDocModal = (patient) => { + setActiveDocPatient(patient); + setScanImageBase64(''); + setScanImagePreview(''); + setPatientDocForm({ + document_type: 'Health Document', + document_date: new Date().toISOString().slice(0, 10), + remarks: '' + }); + setShowPatientSearchModal(false); + stopBarcodeQrScanner(); + }; + + const handleSavePatientDocument = async () => { + if (!activeDocPatient) return; + if (!scanImageBase64) { + alert('Please capture or upload a document photo first.'); + return; + } + try { + const payload = { + patient_id: activeDocPatient.id, + nurse_name: authenticatedNurse ? authenticatedNurse.name : 'Staff Nurse', + nurse_id: authenticatedNurse ? authenticatedNurse.id : 0, + image_base64: scanImageBase64, + ...patientDocForm + }; + const res = await fetch('/api/v1/nursing/patient/document/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + const json = await res.json(); + if (json.status === 1) { + alert('✅ ' + (json.message || 'ABDM M2 Health Document uploaded successfully!')); + setActiveDocPatient(null); + setScanImageBase64(''); + setScanImagePreview(''); + } else { + alert(json.message || 'Failed to upload health document'); + } + } catch (err) { + alert('Error uploading health document'); + } + }; + + // Vitals History Actions + const openWellnessHistory = async (patient) => { + setWellnessHistoryPatient(patient); + setLoadingHistory(true); + try { + const res = await fetch(`/api/v1/nursing/patient/wellness/history/${patient.id}`); + const json = await res.json(); + if (json.status === 1) { + setWellnessHistoryList(json.records || []); + } + } catch (err) { + console.error('Failed to load history', err); + } finally { + setLoadingHistory(false); + } + }; + const getInitials = (nameStr) => { if (!nameStr) return 'N'; const parts = nameStr.trim().split(' '); @@ -788,15 +1189,35 @@ ); }; - // Filtered lists + const displayTempF = (tempC) => { + if (tempC === null || tempC === undefined || tempC === '') return '-'; + const val = (parseFloat(tempC) * 9 / 5) + 32; + return val.toFixed(1) + '°F'; + }; + + const formatDateTime = (dtStr) => { + if (!dtStr) return '-'; + try { + const d = new Date(dtStr.replace(/-/g, '/')); + if (isNaN(d.getTime())) return dtStr; + return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) + ' ' + d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); + } catch (e) { + return dtStr; + } + }; + + // Filtered lists: Only Admitted IPD Patients with Bed (Occupied Beds) const filteredBeds = beds.filter(bed => { + const isOccupied = bed.status === 'occupied' || (bed.patient_name && bed.patient_name.trim() !== '') || Boolean(bed.ipd_id); const matchesWard = selectedWard === 'all' || bed.ward_name === selectedWard; const q = searchQuery.toLowerCase(); const matchesSearch = searchQuery === '' || (bed.bed_number || '').toLowerCase().includes(q) || (bed.patient_name || '').toLowerCase().includes(q) || - (bed.uhid || '').toLowerCase().includes(q); - return matchesWard && matchesSearch; + (bed.uhid || '').toLowerCase().includes(q) || + (bed.ipd_code || '').toLowerCase().includes(q) || + (bed.doctor_name || '').toLowerCase().includes(q); + return isOccupied && matchesWard && matchesSearch; }); const filteredOpdAppointments = opdAppointments.filter(app => { @@ -932,7 +1353,7 @@ setShowAppsModal(true)} title="HMS Mobile & PWA Apps Launcher"> - { fetchBeds(); fetchOpdAppointments(opdDateMode); }} title="Refresh"> + { fetchBeds(); fetchOpdAppointments(opdDateMode, selectedDoctorId, vitalsFilter); }} title="Refresh"> @@ -941,15 +1362,15 @@ - + {!activeBed ? ( {/* Segmented Tab Controller: IPD Admitted vs OPD Patients */} - setMainTab('ipd')}> - Admitted IPDs ({beds.filter(b => b.status === 'occupied').length}) + { setMainTab('ipd'); fetchBeds(); }}> + Admitted IPDs ({beds.filter(b => b.status === 'occupied' || b.patient_name).length}) - setMainTab('opd')}> + { setMainTab('opd'); fetchOpdAppointments(opdDateMode, selectedDoctorId, vitalsFilter); }}> OPD Patients ({opdAppointments.length}) @@ -987,42 +1408,48 @@ - {filteredBeds.map(bed => ( - - openWorkspace(bed)} - > - - Bed {bed.bed_number} - - {bed.status} - - - {bed.ward_name || 'General Ward'} - - {bed.patient_name ? ( - - {bed.patient_name} - UHID: {bed.uhid || 'N/A'} • IPD: {bed.ipd_code || 'N/A'} - - {bed.doctor_name || 'Dr. Assigned'} + {filteredBeds.length === 0 ? ( + + + No Admitted Patients Found + There are currently no admitted IPD patients in the selected ward. + + ) : ( + filteredBeds.map(bed => ( + + openWorkspace(bed)} + > + + Bed {bed.bed_number} + + {bed.status} - ) : ( - Available for Admission - )} + {bed.ward_name || 'General Ward'} + + {bed.patient_name ? ( + + {bed.patient_name} + UHID: {bed.uhid || 'N/A'} • IPD: {bed.ipd_code || 'N/A'} + + {bed.doctor_name || 'Dr. Assigned'} + + + ) : ( + Available for Admission + )} - {bed.status === 'occupied' && ( - - Care Workspace + + Open Bedside Workspace - )} + - - ))} + )) + )} ) : ( @@ -1219,64 +1646,448 @@ {/* Form Tabs for Bedside Care */} - + - setActiveTab('vitals')}> - Vitals + setActiveTab('vitals')}> + Vitals ({workspaceData.filter(e => e.entry_type === 'vitals').length}) - setActiveTab('fluid')}> - Fluid I/O + setActiveTab('fluid')}> + Fluid I/O ({workspaceData.filter(e => e.entry_type === 'fluid').length}) - setActiveTab('treatment')}> - Treatment + setActiveTab('treatment')}> + Treatment & Orders ({workspaceData.filter(e => e.entry_type === 'treatment').length}) + {/* TAB 1: VITALS (FORM & HISTORY TABLE) */} {activeTab === 'vitals' && ( - - Bedside Vitals Entry - - BP Sys/Dia setVitalsForm({...vitalsForm, bp_systolic: e.target.value})} />/ setVitalsForm({...vitalsForm, bp_diastolic: e.target.value})} /> - Pulse Rate setVitalsForm({...vitalsForm, pulse_rate: e.target.value})} /> - SpO2 % setVitalsForm({...vitalsForm, spo2: e.target.value})} /> - Temp °F setVitalsForm({...vitalsForm, temperature_f: e.target.value})} /> - Resp Rate setVitalsForm({...vitalsForm, resp_rate: e.target.value})} /> - Weight (kg) setVitalsForm({...vitalsForm, weight_kg: e.target.value})} /> - - handleSaveIpdEntry('vitals', vitalsForm)}> - Save Bedside Vitals - - - )} - - {activeTab === 'treatment' && ( - - Nursing Procedure & Notes - setTreatmentForm({...treatmentForm, treatment_text: e.target.value})}> - handleSaveIpdEntry('treatment', treatmentForm)}> - Save Procedure Note - - - )} - - {/* Bedside Log Entries */} - - Patient Bedside History Log - {workspaceData.map(e => ( - - - {e.entry_type} - {e.recorded_at} - by {e.recorded_by} + + + Record Bedside Vitals + + + BP Sys/Dia (mmHg) + + setVitalsForm({...vitalsForm, bp_systolic: e.target.value})} /> + / + setVitalsForm({...vitalsForm, bp_diastolic: e.target.value})} /> + + + + Pulse Rate (bpm) + setVitalsForm({...vitalsForm, pulse_rate: e.target.value})} /> + + + SpO2 % + setVitalsForm({...vitalsForm, spo2: e.target.value})} /> + + + Temp °F + setVitalsForm({...vitalsForm, temperature_f: e.target.value})} /> + + + Resp Rate (/min) + setVitalsForm({...vitalsForm, resp_rate: e.target.value})} /> + + + Weight (kg) + setVitalsForm({...vitalsForm, weight_kg: e.target.value})} /> + + + Nursing Remark / Observation + setVitalsForm({...vitalsForm, general_note: e.target.value})} /> + - {renderNoteContent(e)} + handleSaveIpdEntry('vitals', vitalsForm)}> + Save Bedside Vitals + - ))} - + + {/* Vitals History Table */} + + + Vitals History Records + {workspaceData.filter(e => e.entry_type === 'vitals').length} Records + + + + + + Date & Time + BP (mmHg) + Pulse + Temp + Resp + SpO2 + Weight + Staff + Remarks + + + + {workspaceData.filter(e => e.entry_type === 'vitals').length === 0 ? ( + + No vitals records entered yet. + + ) : ( + workspaceData.filter(e => e.entry_type === 'vitals').map(v => ( + + {formatDateTime(v.recorded_at)} + {v.bp_systolic ? `${v.bp_systolic}/${v.bp_diastolic}` : '-'} + {v.pulse_rate ? `${v.pulse_rate} bpm` : '-'} + {displayTempF(v.temperature_c)} + {v.resp_rate ? `${v.resp_rate}/min` : '-'} + {v.spo2 ? `${v.spo2}%` : '-'} + {v.weight_kg ? `${v.weight_kg} kg` : '-'} + {v.recorded_by || 'Staff'} + {v.general_note || '-'} + + )) + )} + + + + + + )} + + {/* TAB 2: FLUID INTAKE / OUTPUT (CALCULATED DIFFERENCE & HISTORY) */} + {activeTab === 'fluid' && (() => { + const fluidList = workspaceData.filter(e => e.entry_type === 'fluid'); + const totalIn = fluidList.filter(e => (e.fluid_direction || '').toLowerCase() === 'intake').reduce((sum, e) => sum + (parseFloat(e.fluid_amount_ml) || 0), 0); + const totalOut = fluidList.filter(e => (e.fluid_direction || '').toLowerCase() === 'output').reduce((sum, e) => sum + (parseFloat(e.fluid_amount_ml) || 0), 0); + const netDiff = totalIn - totalOut; + + const intakeRoutes = ['IV (Intravenous)', 'Oral / Water / Juice', 'Ryles Tube / NG Feed', 'Normal Saline (NS)', 'Dextrose (DNS / RL)', 'Blood / Plasma', 'TPN', 'Other Intake']; + const outputRoutes = ['Urine / Foley Catheter', 'Drain / Ryle Tube', 'Vomitus', 'Stool / Loose Motion', 'Chest Tube Drain', 'Wound Drain', 'Other Output']; + + return ( + + {/* Summary Balance Metric Cards */} + + + + Total Intake + {totalIn.toLocaleString()} ml + + + + + Total Output + {totalOut.toLocaleString()} ml + + + + + Difference (In-Out) + = 0 ? 'text-primary' : 'text-danger'}`}> + {netDiff > 0 ? `+${netDiff.toLocaleString()}` : netDiff.toLocaleString()} ml + + + + + + {/* Add Fluid Entry Form */} + + Add Fluid Intake / Output Entry + + + Direction + + setFluidForm({...fluidForm, fluid_direction: 'intake', fluid_route: 'IV (Intravenous)'})} + > + Fluid Intake (In) + + setFluidForm({...fluidForm, fluid_direction: 'output', fluid_route: 'Urine / Foley Catheter'})} + > + Fluid Output (Out) + + + + + + + Route / Site + setFluidForm({...fluidForm, fluid_route: e.target.value})} + > + {(fluidForm.fluid_direction === 'intake' ? intakeRoutes : outputRoutes).map(r => ( + {r} + ))} + + + + Amount (ml) * + setFluidForm({...fluidForm, fluid_amount_ml: e.target.value})} + /> + + + Observation / Note + setFluidForm({...fluidForm, general_note: e.target.value})} + /> + + + + { + if (!fluidForm.fluid_amount_ml) { + alert('Please enter fluid amount in ml.'); + return; + } + handleSaveIpdEntry('fluid', fluidForm); + }} + > + Save Fluid Entry + + + + {/* Fluid History Table */} + + + Fluid Intake & Output Log + {fluidList.length} Records + + + + + + Date & Time + Direction + Route + Amount (ml) + Observation / Note + Staff + + + + {fluidList.length === 0 ? ( + + No fluid entries recorded yet. + + ) : ( + fluidList.map(fl => ( + + {formatDateTime(fl.recorded_at)} + + + {fl.fluid_direction === 'intake' ? '↓ Intake' : '↑ Output'} + + + {fl.fluid_route || '-'} + {parseFloat(fl.fluid_amount_ml || 0).toLocaleString()} ml + {fl.general_note || '-'} + {fl.recorded_by || 'Staff'} + + )) + )} + + + + + + ); + })()} + + {/* TAB 3: TREATMENT & DOCTOR ORDERS REFERENCE */} + {activeTab === 'treatment' && (() => { + const allTreatments = workspaceData.filter(e => e.entry_type === 'treatment'); + const doctorOrders = allTreatments.filter(e => + (e.recorded_by && e.recorded_by.toLowerCase().startsWith('dr.')) || + (e.treatment_text && e.treatment_text.includes('[Doctor Note]')) + ); + const latestDocOrder = doctorOrders.length > 0 ? doctorOrders[0] : null; + + const nurseTreatments = allTreatments.filter(e => + !(e.recorded_by && e.recorded_by.toLowerCase().startsWith('dr.')) && + !(e.treatment_text && e.treatment_text.includes('[Doctor Note]')) + ); + + return ( + + {/* DOCTOR TREATMENT REFERENCE CARD */} + + + + + Doctor's Prescribed Treatment & Orders + + {doctorOrders.length} Doctor Orders + + + + {latestDocOrder ? ( + + + + {latestDocOrder.recorded_by} + + {formatDateTime(latestDocOrder.recorded_at)} + + + + {renderNoteContent(latestDocOrder)} + + + { + const cleanText = (latestDocOrder.treatment_text || '') + .replace(/\[IMAGE_ATTACHMENT:.*?\]/, '') + .replace('[Doctor Note] ', '') + .trim(); + setTreatmentForm({ + ...treatmentForm, + treatment_text: `Administered as prescribed: ${cleanText}` + }); + }} + > + Follow & Administer This Order + + + {doctorOrders.length > 1 && ( + + setShowAllDoctorOrders(!showAllDoctorOrders)} + > + + {showAllDoctorOrders ? 'Hide Previous Doctor Notes' : `View All ${doctorOrders.length} Doctor Notes`} + + + {showAllDoctorOrders && ( + + {doctorOrders.slice(1).map(docOrd => ( + + + {docOrd.recorded_by} + {formatDateTime(docOrd.recorded_at)} + + {renderNoteContent(docOrd)} + + ))} + + )} + + )} + + ) : ( + + + No doctor orders or clinical prescriptions recorded yet. + + )} + + + + {/* NURSING TREATMENT GIVEN FORM */} + + Log Nursing Treatment Given / Medication Administered + + + Treatment / Medication Administered * + setTreatmentForm({...treatmentForm, treatment_text: e.target.value})} + > + + + + Observation / Patient Response + setTreatmentForm({...treatmentForm, general_note: e.target.value})} + /> + + + { + if (!treatmentForm.treatment_text.trim()) { + alert('Please enter treatment or medication administered.'); + return; + } + handleSaveIpdEntry('treatment', treatmentForm); + }} + > + Log Treatment Administration + + + + {/* NURSING TREATMENT HISTORY LOG WITH TIME */} + + + Nursing Treatment Given History + {nurseTreatments.length} Records + + + + + + Time Administered + Treatment / Medication Given + Observation & Remarks + Staff Nurse + + + + {nurseTreatments.length === 0 ? ( + + No nursing treatments logged yet. + + ) : ( + nurseTreatments.map(tr => ( + + {formatDateTime(tr.recorded_at)} + + {renderNoteContent(tr)} + + {tr.general_note || '-'} + + + {tr.recorded_by || 'Staff Nurse'} + + + + )) + )} + + + + + + ); + })()} )} @@ -1430,6 +2241,544 @@ )} + + {/* ------------------------------------------------------------- + FLOATING BOTTOM QUICK ACTION BAR (DOCK) + ------------------------------------------------------------- */} + + openPatientSearch('scan')} + title="Scan Barcode / QR or Search Patient" + > + + Scan / Search + + + { + if (activeBed) { + openWellnessModal({ id: activeBed.p_id || activeBed.patient_id, p_code: activeBed.uhid, fullName: activeBed.patient_name, abha_id: activeBed.abha_id, gender: activeBed.gender }); + } else { + openPatientSearch('type'); + } + }} + title="Add ABDM M2 Wellness Vitals" + > + + ABDM Vitals + + + { + if (activeBed) { + openDocModal({ id: activeBed.p_id || activeBed.patient_id, p_code: activeBed.uhid, fullName: activeBed.patient_name, abha_id: activeBed.abha_id }); + } else { + openPatientSearch('type'); + } + }} + title="Add ABDM M2 Health Document (Scan/Photo)" + > + + ABDM Health Doc + + + + {/* ------------------------------------------------------------- + MODAL 1: SEARCH PATIENT BY BARCODE, QR CODE & TEXT (ABDM M2) + ------------------------------------------------------------- */} + {showPatientSearchModal && ( + + + + + + + + Search Patient by Barcode, QR & Name + Scan Hospital Card Barcode, ABHA QR, or search registered patients + + + + + + + {/* Segmented Mode Selector */} + + { + setSearchModalTab('scan'); + startBarcodeQrScanner(); + }} + > + Camera Scanner (Barcode & QR) + + { + setSearchModalTab('type'); + stopBarcodeQrScanner(); + }} + > + Manual Search (UHID / Mobile / ABHA) + + + + {/* CAMERA SCANNER TAB */} + {searchModalTab === 'scan' && ( + + + + + + {scannerError ? ( + + {scannerError} + + ) : ( + + Point camera at Patient Card Barcode or ABHA QR Code to scan instantly. + + )} + + )} + + {/* SEARCH INPUT */} + + + + + { + setPatientSearchQuery(e.target.value); + executePatientSearch(e.target.value); + }} + onKeyDown={e => { + if (e.key === 'Enter') executePatientSearch(patientSearchQuery); + }} + /> + {patientSearchQuery && ( + { setPatientSearchQuery(''); setPatientSearchResults([]); }}> + + + )} + executePatientSearch(patientSearchQuery)}> + {searchingPatient ? : 'Search'} + + + + {/* SEARCH RESULTS LIST */} + + + + Matched Patients ({patientSearchResults.length}) + + {searchingPatient && Searching...} + + + {patientSearchResults.length === 0 ? ( + + + + {patientSearchQuery ? 'No patients found matching this query.' : 'Type patient name, UHID, ABHA number or scan barcode above.'} + + + ) : ( + + {patientSearchResults.map(pat => ( + + + + + {pat.fullName} + + {pat.gender === '1' || pat.gender === 1 ? 'Male' : (pat.gender === '2' || pat.gender === 2 ? 'Female' : 'Other')} + {pat.dob ? ` • DOB: ${pat.dob}` : ''} + + + + UHID: {pat.p_code} + {pat.mphone1 && {pat.mphone1}} + {pat.abha_id && ( + + ABHA: {pat.abha_id} + + )} + {pat.abha_address && ( + + {pat.abha_address} + + )} + + {pat.ipd_admission && ( + + + Admitted IPD: {pat.ipd_admission.ipd_code} (Bed {pat.ipd_admission.bed_number || pat.ipd_admission.bed_no}) + + + )} + {pat.opd_today && ( + + + OPD Today: Token #{pat.opd_today.token_no} ({pat.opd_today.doctor_name || 'Doctor'}) + + + )} + + + {/* 1-Click Action Buttons */} + + openWellnessModal(pat)} + title="Add ABDM M2 Wellness Vitals" + > + Add Vitals + + + openDocModal(pat)} + title="Add ABDM M2 Health Document" + > + Add Doc + + + openWellnessHistory(pat)} + title="View Vitals History" + > + + + + {pat.ipd_admission && ( + { + closePatientSearch(); + openWorkspace({ ipd_id: pat.ipd_admission.ipd_id, patient_name: pat.fullName, uhid: pat.p_code, ipd_code: pat.ipd_admission.ipd_code, bed_number: pat.ipd_admission.bed_number, ward_name: pat.ipd_admission.ward_name, status: 'occupied' }); + }} + title="Go to IPD Bedside Care" + > + IPD Bed + + )} + + + + ))} + + )} + + + + + ABDM M2 Standard Compliance + Close + + + + + )} + + {/* ------------------------------------------------------------- + MODAL 2: ABDM M2 WELLNESS & VITALS ENTRY + ------------------------------------------------------------- */} + {activeWellnessPatient && ( + + + + + + + + Record ABDM M2 Wellness & Vitals + Stores standardized vital signs observation bundle + + + setActiveWellnessPatient(null)}> + + + + {/* Patient Context Banner */} + + + + {activeWellnessPatient.fullName || activeWellnessPatient.p_fname} + + UHID: {activeWellnessPatient.p_code || activeWellnessPatient.uhid} + {activeWellnessPatient.mphone1 && ` • Phone: ${activeWellnessPatient.mphone1}`} + + + {activeWellnessPatient.abha_id && ( + + ABHA: {activeWellnessPatient.abha_id} + + )} + + + + {/* Vitals Form Grid */} + + + BP Sys/Dia (mmHg) + + setWellnessForm({...wellnessForm, bp_systolic: e.target.value})} /> + / + setWellnessForm({...wellnessForm, bp_diastolic: e.target.value})} /> + + + + Pulse Rate (bpm) + setWellnessForm({...wellnessForm, pulse_rate: e.target.value})} /> + + + SpO2 % + setWellnessForm({...wellnessForm, spo2: e.target.value})} /> + + + Temp °F + setWellnessForm({...wellnessForm, temperature_f: e.target.value})} /> + + + Resp Rate (/min) + setWellnessForm({...wellnessForm, resp_rate: e.target.value})} /> + + + Height (cm) + setWellnessForm({...wellnessForm, height_cm: e.target.value})} /> + + + Weight (kg) + setWellnessForm({...wellnessForm, weight_kg: e.target.value})} /> + + + Calculated BMI + 0 + ? (parseFloat(wellnessForm.weight_kg) / Math.pow(parseFloat(wellnessForm.height_cm) / 100, 2)).toFixed(2) + : '-' + } + /> + + + + Diet & Lifestyle Guidance (ABDM) + setWellnessForm({...wellnessForm, diet_lifestyle_note: e.target.value})} /> + + + + General Nursing Observations + setWellnessForm({...wellnessForm, general_advice: e.target.value})} /> + + + + + + openWellnessHistory(activeWellnessPatient)}> + Past History + + + setActiveWellnessPatient(null)}>Cancel + + Save ABDM Wellness Vitals + + + + + + + )} + + {/* ------------------------------------------------------------- + MODAL 3: ABDM M2 HEALTH DOCUMENT SCAN & UPLOAD + ------------------------------------------------------------- */} + {activeDocPatient && ( + + + + + + + + Add ABDM M2 Health Document + Scan paper prescriptions, lab reports or clinical documents + + + setActiveDocPatient(null)}> + + + + {/* Patient Context Banner */} + + + + {activeDocPatient.fullName || activeDocPatient.p_fname} + + UHID: {activeDocPatient.p_code || activeDocPatient.uhid} + + + {activeDocPatient.abha_id && ( + + ABHA: {activeDocPatient.abha_id} + + )} + + + + + + Document Category (ABDM HI Type) + setPatientDocForm({...patientDocForm, document_type: e.target.value})} + > + Prescription Record (PrescriptionRecord) + Diagnostic / Lab Report (DiagnosticReport) + Discharge Summary (DischargeSummary) + Radiology / Scan (HealthDocumentRecord) + Paper Medical Record (HealthDocumentRecord) + Immunization Record (ImmunizationRecord) + Other Health Document (HealthDocumentRecord) + + + + + Document Date + setPatientDocForm({...patientDocForm, document_date: e.target.value})} + /> + + + + Remarks / Document Title + setPatientDocForm({...patientDocForm, remarks: e.target.value})} + /> + + + + {/* Camera Capture / Photo Upload */} + + + Take Document Photo / Choose Image + + + + + {scanImagePreview && ( + + + Document Photo Captured + + + + )} + + + + setActiveDocPatient(null)}>Cancel + + Upload ABDM Health Document + + + + + + )} + + {/* ------------------------------------------------------------- + MODAL 4: PATIENT VITALS & WELLNESS HISTORY + ------------------------------------------------------------- */} + {wellnessHistoryPatient && ( + + + + + + + + Vitals & Wellness History + + {wellnessHistoryPatient.fullName || wellnessHistoryPatient.p_fname} (UHID: {wellnessHistoryPatient.p_code || wellnessHistoryPatient.uhid}) + + + + setWellnessHistoryPatient(null)}> + + + + {loadingHistory ? ( + + + Loading historical vitals... + + ) : wellnessHistoryList.length === 0 ? ( + + + No past vitals or wellness entries recorded yet for this patient. + + ) : ( + + + + + Date & Time + BP + Pulse + Temp + SpO2 + BMI + Staff + Advice / Remarks + + + + {wellnessHistoryList.map(rec => ( + + {formatDateTime(rec.recorded_at)} + {rec.bp_systolic ? `${rec.bp_systolic}/${rec.bp_diastolic}` : '-'} + {rec.pulse_rate ? `${rec.pulse_rate} bpm` : '-'} + {rec.temperature_f ? `${rec.temperature_f} °F` : '-'} + {rec.spo2 ? `${rec.spo2}%` : '-'} + {rec.bmi || '-'} + {rec.recorded_by || 'Staff'} + {rec.general_advice || rec.diet_lifestyle_note || '-'} + + ))} + + + + )} + + + + setWellnessHistoryPatient(null)}>Close + + + + + )} );
+ {patientSearchQuery ? 'No patients found matching this query.' : 'Type patient name, UHID, ABHA number or scan barcode above.'} +
Loading historical vitals...
No past vitals or wellness entries recorded yet for this patient.