From 7bdfbab2bd538236b0143e1f10b3e8fcd07ba436 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Sat, 29 Aug 2026 04:16:08 +0530 Subject: [PATCH] feat(nursing): add OPD Patients list, Fill OPD Vitals, and Document Scan/Camera Upload features to Nursing PWA App --- app/Config/Routes.php | 4 + app/Controllers/Api/v1/NursingApi.php | 258 ++++++ public/App/Nursing/index.html | 1033 +++++++++++++++++-------- 3 files changed, 983 insertions(+), 312 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 5def21d..d1887cd 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1577,6 +1577,10 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static funct $routes->post('nursing/auth/set-pin', 'NursingApi::setPin'); $routes->get('nursing/workspace/(:num)', 'NursingApi::workspace/$1'); $routes->post('nursing/entry/save/(:num)', 'NursingApi::saveEntry/$1'); + $routes->get('nursing/opd/list', 'NursingApi::opdList'); + $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'); // 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 085d393..e421ba2 100644 --- a/app/Controllers/Api/v1/NursingApi.php +++ b/app/Controllers/Api/v1/NursingApi.php @@ -236,4 +236,262 @@ class NursingApi extends BaseController 'message' => $msg, ]); } + + /** + * GET api/v1/nursing/opd/list + */ + public function opdList() + { + $db = db_connect(); + helper('common'); + + $dateMode = $this->request->getGet('date_mode') ?? 'today'; + $docId = (int) ($this->request->getGet('doctor_id') ?? 0); + + $whereClause = "WHERE 1=1"; + if ($dateMode === 'today') { + $whereClause .= " AND DATE(o.date_of_assign) = 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.date_of_assign, o.opd_status, o.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 + FROM opd_master o + JOIN patient_master p ON o.p_id = p.id + LEFT JOIN doctor_master d ON o.doc_id = d.id + LEFT JOIN opd_prescription pr ON o.opd_id = pr.opd_id + {$whereClause} + ORDER BY o.opd_id DESC"; + + $query = $db->query($sql); + $records = $query->getResultArray(); + + $todayTotalCount = (int) $db->query("SELECT count(*) as c FROM opd_master WHERE DATE(date_of_assign) = CURDATE()")->getRow()->c; + $allTotalCount = (int) $db->query("SELECT count(*) as c FROM opd_master")->getRow()->c; + + $counts = ['all' => 0, 'waiting' => 0, 'visited' => 0, 'booked' => 0, 'cancelled' => 0, 'today_total' => $todayTotalCount, 'all_total' => $allTotalCount]; + $appointments = []; + + foreach ($records as $r) { + $statusKey = 'waiting'; + $statusLabel = 'Waiting'; + $st = (int) ($r['opd_status'] ?? 0); + if ($st === 2) { + $statusKey = 'visited'; + $statusLabel = 'Visited'; + } elseif ($st === 3) { + $statusKey = 'cancelled'; + $statusLabel = 'Cancelled'; + } elseif ($st === 1) { + $statusKey = 'booked'; + $statusLabel = 'Booked'; + } + + $counts['all']++; + if (isset($counts[$statusKey])) { + $counts[$statusKey]++; + } + + $ageDisplay = 'N/A'; + if (function_exists('get_age_1')) { + $ageDisplay = get_age_1($r['dob'] ?? null, $r['age'] ?? '', $r['age_in_month'] ?? '', $r['estimate_dob'] ?? ''); + } elseif (! empty($r['age'])) { + $ageDisplay = $r['age'] . ' Year'; + } + + $hasVitals = !empty($r['temp']) || !empty($r['pulse']) || !empty($r['bp']) || !empty($r['spo2']) || !empty($r['weight']); + + $r['status_key'] = $statusKey; + $r['status_label'] = $statusLabel; + $r['age_display'] = $ageDisplay; + $r['gender_label'] = ((int)($r['gender'] ?? 1) === 1) ? 'Male' : 'Female'; + $r['patient_display_name'] = $r['fname'] ?? 'Patient'; + $r['has_vitals'] = $hasVitals; + + $appointments[] = $r; + } + + return $this->response->setJSON([ + 'status' => 1, + 'counts' => $counts, + 'date_mode' => $dateMode, + 'appointments' => $appointments, + ]); + } + + /** + * POST api/v1/nursing/opd/vitals/save/(:num) + */ + public function saveOpdVitals(int $opdId) + { + $db = db_connect(); + $json = $this->request->getJSON(true); + $post = $this->request->getPost() ?: []; + $dataInput = ! empty($json) ? $json : $post; + + $opdRow = $db->table('opd_master')->where('opd_id', $opdId)->get()->getRowArray(); + if (! $opdRow) { + return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'OPD record not found']); + } + + $vitalsData = [ + 'opd_id' => $opdId, + 'doc_id' => (int) ($opdRow['doc_id'] ?? 0), + 'p_id' => (int) ($opdRow['p_id'] ?? 0), + 'date_opd_visit' => date('Y-m-d'), + 'temp' => trim((string) ($dataInput['temp'] ?? '')), + 'pulse' => trim((string) ($dataInput['pulse'] ?? '')), + 'bp' => trim((string) ($dataInput['bp_systolic'] ?? '')), + 'diastolic' => trim((string) ($dataInput['bp_diastolic'] ?? '')), + 'spo2' => trim((string) ($dataInput['spo2'] ?? '')), + 'weight' => trim((string) ($dataInput['weight'] ?? '')), + 'height' => trim((string) ($dataInput['height'] ?? '')), + 'rr_min' => trim((string) ($dataInput['rr_min'] ?? '')), + ]; + + $existing = $db->table('opd_prescription')->where('opd_id', $opdId)->get()->getRowArray(); + if ($existing) { + $db->table('opd_prescription')->where('opd_id', $opdId)->update($vitalsData); + } else { + $db->table('opd_prescription')->insert($vitalsData); + } + + return $this->response->setJSON([ + 'status' => 1, + 'message' => 'Patient Vitals saved successfully by Nursing Staff', + ]); + } + + /** + * POST api/v1/nursing/opd/scan/save/(:num) + */ + public function saveOpdScan(int $opdId) + { + $db = db_connect(); + $json = $this->request->getJSON(true); + $post = $this->request->getPost() ?: []; + $dataInput = ! empty($json) ? $json : $post; + + $imageBase64 = $dataInput['image_base64'] ?? null; + if (empty($imageBase64)) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Scan photo required']); + } + + $docType = trim((string) ($dataInput['document_type'] ?? 'Paper Document')); + $nurseName = trim((string) ($dataInput['nurse_name'] ?? 'Nursing Staff')); + + $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/nursing_scans/'; + if (! is_dir($uploadDir)) { + @mkdir($uploadDir, 0777, true); + } + $filename = 'nursing_opd_doc_' . $opdId . '_' . time() . '_' . rand(100, 999) . '.' . $ext; + file_put_contents($uploadDir . $filename, $binary); + $imageUrl = '/uploads/nursing_scans/' . $filename; + + // Append attachment to OPD prescription advice/investigation + $existing = $db->table('opd_prescription')->where('opd_id', $opdId)->get()->getRowArray(); + $attachmentText = '[Nursing Scan: ' . $docType . ' by ' . $nurseName . '] [IMAGE_ATTACHMENT:' . $imageUrl . ']'; + + if ($existing) { + $newAdvice = trim(($existing['advice'] ?? '') . "\n" . $attachmentText); + $db->table('opd_prescription')->where('opd_id', $opdId)->update(['advice' => $newAdvice]); + } else { + $opdRow = $db->table('opd_master')->where('opd_id', $opdId)->get()->getRowArray(); + $db->table('opd_prescription')->insert([ + 'opd_id' => $opdId, + 'doc_id' => (int) ($opdRow['doc_id'] ?? 0), + 'p_id' => (int) ($opdRow['p_id'] ?? 0), + 'date_opd_visit' => date('Y-m-d'), + 'advice' => $attachmentText, + ]); + } + + return $this->response->setJSON([ + 'status' => 1, + 'message' => 'OPD Scanned Document saved successfully', + 'image_url' => $imageUrl, + ]); + } + + /** + * POST api/v1/nursing/ipd/scan/save/(:num) + */ + public function saveIpdScan(int $ipdId) + { + $json = $this->request->getJSON(true); + $post = $this->request->getPost() ?: []; + $dataInput = ! empty($json) ? $json : $post; + + $imageBase64 = $dataInput['image_base64'] ?? null; + if (empty($imageBase64)) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Scan photo required']); + } + + $docType = trim((string) ($dataInput['document_type'] ?? 'Paper Document')); + $nurseId = (int) ($dataInput['nurse_id'] ?? 0); + $nurseName = trim((string) ($dataInput['nurse_name'] ?? 'Nursing Staff')); + + $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/nursing_scans/'; + if (! is_dir($uploadDir)) { + @mkdir($uploadDir, 0777, true); + } + $filename = 'nursing_ipd_doc_' . $ipdId . '_' . time() . '_' . rand(100, 999) . '.' . $ext; + file_put_contents($uploadDir . $filename, $binary); + $imageUrl = '/uploads/nursing_scans/' . $filename; + + $fullNoteText = '[Nursing Scanned Document: ' . $docType . '] [IMAGE_ATTACHMENT:' . $imageUrl . ']'; + + $data = [ + 'ipd_id' => $ipdId, + 'entry_type' => 'treatment', + 'recorded_at' => date('Y-m-d H:i:s'), + 'treatment_text' => $fullNoteText, + 'general_note' => 'Scanned Document: ' . $docType . ' (' . $imageUrl . ')', + 'recorded_by' => '[Nurse] ' . $nurseName, + 'recorded_by_id' => $nurseId, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]; + + $this->ipdNursingEntryModel->insert($data); + + return $this->response->setJSON([ + 'status' => 1, + 'message' => 'IPD Scanned Document uploaded successfully to Patient Chart', + 'image_url' => $imageUrl, + ]); + } } + diff --git a/public/App/Nursing/index.html b/public/App/Nursing/index.html index ed51499..5561acf 100644 --- a/public/App/Nursing/index.html +++ b/public/App/Nursing/index.html @@ -3,14 +3,17 @@ - Nursing Care PWA App + Nursing Care EMR & Bedside PWA App - + - + + + + @@ -22,84 +25,167 @@ @@ -131,6 +220,7 @@ const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; function NursingApp() { + const [mainTab, setMainTab] = useState('ipd'); // 'ipd' or 'opd' const [beds, setBeds] = useState([]); const [loading, setLoading] = useState(true); const [selectedWard, setSelectedWard] = useState('all'); @@ -138,10 +228,27 @@ const [activeBed, setActiveBed] = useState(null); const [activeTab, setActiveTab] = useState('vitals'); const [workspaceData, setWorkspaceData] = useState([]); - const [installPrompt, setInstallPrompt] = useState(null); const [nurses, setNurses] = useState([]); - // Authentication & Security State + // OPD Patients State + const [opdAppointments, setOpdAppointments] = useState([]); + const [opdCounts, setOpdCounts] = useState({ all: 0, waiting: 0, visited: 0, booked: 0, cancelled: 0 }); + const [opdFilter, setOpdFilter] = useState('all'); + const [opdDateMode, setOpdDateMode] = useState('today'); + + // Modals & Action State + const [activeOpdForVitals, setActiveOpdForVitals] = useState(null); + const [opdVitalsForm, setOpdVitalsForm] = useState({ + temp: '', pulse: '', bp_systolic: '', bp_diastolic: '', spo2: '', weight: '', height: '', rr_min: '' + }); + + const [scanTarget, setScanTarget] = useState(null); // OPD or IPD object + const [scanType, setScanType] = useState('opd'); // 'opd' or 'ipd' + const [scanDocCategory, setScanDocCategory] = useState('Paper Document'); + const [scanImageBase64, setScanImageBase64] = useState(''); + const [scanImagePreview, setScanImagePreview] = useState(''); + + // Auth State const [authenticatedNurse, setAuthenticatedNurse] = useState(null); const [authNurseId, setAuthNurseId] = useState(0); const [pinInput, setPinInput] = useState(''); @@ -150,7 +257,7 @@ const [newPin, setNewPin] = useState(''); const [confirmPin, setConfirmPin] = useState(''); - // Form state for workspace + // Workspace Forms (IPD) const [vitalsForm, setVitalsForm] = useState({ temperature_f: '', pulse_rate: '', resp_rate: '', bp_systolic: '', bp_diastolic: '', spo2: '', weight_kg: '', general_note: '' @@ -165,7 +272,6 @@ treatment_text: '', general_note: '' }); - // Fetch live bed list from CI4 API const fetchBeds = async () => { try { setLoading(true); @@ -181,7 +287,22 @@ } }; - // Fetch active nurses + const fetchOpdAppointments = async (dateModeOverride) => { + try { + const mode = dateModeOverride || opdDateMode; + const res = await fetch(`/api/v1/nursing/opd/list?date_mode=${mode}`); + const json = await res.json(); + if (json.status === 1) { + setOpdAppointments(json.appointments || []); + if (json.counts) { + setOpdCounts(json.counts); + } + } + } catch (err) { + console.error('Failed to load OPD appointments', err); + } + }; + const fetchNurses = async () => { try { const res = await fetch('/api/v1/nursing/nurses'); @@ -189,7 +310,6 @@ if (json.status === 1 && json.data) { setNurses(json.data); - // Check URL query parameters for nurse_code (QR Code scan or Direct URL) const params = new URLSearchParams(window.location.search); const codeParam = params.get('nurse_code'); if (codeParam) { @@ -204,7 +324,6 @@ } }; - // Restore 30-day device authentication from localStorage useEffect(() => { const savedAuth = localStorage.getItem(DEVICE_AUTH_KEY); if (savedAuth) { @@ -212,6 +331,8 @@ const parsed = JSON.parse(savedAuth); if (parsed && parsed.nurse && parsed.expires_at > Date.now()) { setAuthenticatedNurse(parsed.nurse); + fetchBeds(); + fetchOpdAppointments(opdDateMode); } else { localStorage.removeItem(DEVICE_AUTH_KEY); } @@ -219,25 +340,11 @@ } fetchBeds(); fetchNurses(); - - // PWA Install Prompt Listener - window.addEventListener('beforeinstallprompt', (e) => { - e.preventDefault(); - setInstallPrompt(e); - }); - - // Service worker registration - if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('/App/Nursing/sw.js') - .then(() => console.log('Nursing PWA Service Worker Registered')) - .catch(err => console.error('SW failed', err)); - } }, []); - // Authenticate Nurse with Security PIN & Save 30-Day Device Auth const handleVerifyPin = async () => { if (!authNurseId || authNurseId === '0') { - setPinError('Please select your Nursing Staff ID / Profile.'); + setPinError('Please select your Nursing Staff Profile.'); return; } if (!pinInput || pinInput.length < 4) { @@ -260,25 +367,24 @@ if (json.status === 1 && json.nurse) { setAuthenticatedNurse(json.nurse); - - // Save 30-day persistent device authentication in localStorage const deviceAuthObj = { nurse: json.nurse, expires_at: Date.now() + THIRTY_DAYS_MS }; localStorage.setItem(DEVICE_AUTH_KEY, JSON.stringify(deviceAuthObj)); setPinInput(''); + fetchBeds(); + fetchOpdAppointments(opdDateMode); } else if (json.status === 2) { setIsSettingNewPin(true); } else { - setPinError(json.message || 'Invalid Security PIN'); + setPinError(json.message || 'Incorrect Security PIN'); } } catch (err) { setPinError('Connection error verifying PIN.'); } }; - // Set Initial PIN or Change PIN const handleSaveNewPin = async () => { if (!newPin || newPin.length < 4) { setPinError('PIN must be 4 to 6 numeric digits.'); @@ -322,79 +428,209 @@ setActiveBed(null); }; - // Fetch patient workspace entries - const fetchWorkspace = async (ipdId) => { + const openWorkspace = async (bed) => { + if (!bed.ipd_id || bed.status !== 'occupied') return; + setActiveBed(bed); try { - const res = await fetch(`/api/v1/nursing/workspace/${ipdId}`); + const res = await fetch(`/api/v1/nursing/workspace/${bed.ipd_id}`); const json = await res.json(); if (json.status === 1) { setWorkspaceData(json.entries || []); } } catch (err) { - console.error('Failed to load workspace data', err); + console.error('Failed to load workspace', err); } }; - const openBedWorkspace = (bed) => { - setActiveBed(bed); - if (bed.current_ipd_id) { - fetchWorkspace(bed.current_ipd_id); - } else { - setWorkspaceData([]); - } - }; - - const handleSaveEntry = async (entryType, formData) => { - if (!activeBed || !activeBed.current_ipd_id) { - alert('Please select an occupied bed to save entries.'); - return; - } - - const body = new URLSearchParams(); - body.append('entry_type', entryType); - body.append('recorded_by_id', authenticatedNurse ? authenticatedNurse.id : 0); - body.append('recorded_by', authenticatedNurse ? `[${authenticatedNurse.nurse_code}] ${authenticatedNurse.name}` : 'Staff'); - Object.keys(formData).forEach(key => body.append(key, formData[key])); + const handleSaveIpdEntry = async (entryType, formData) => { + if (!activeBed || !activeBed.ipd_id) return; try { - const res = await fetch(`/api/v1/nursing/entry/save/${activeBed.current_ipd_id}`, { + const payload = { + entry_type: entryType, + recorded_by: authenticatedNurse ? authenticatedNurse.name : 'Staff Nurse', + recorded_by_id: authenticatedNurse ? authenticatedNurse.id : 0, + ...formData + }; + + const res = await fetch(`/api/v1/nursing/entry/save/${activeBed.ipd_id}`, { method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: body.toString() + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) }); const json = await res.json(); if (json.status === 1) { - alert(json.message || 'Saved successfully'); - fetchWorkspace(activeBed.current_ipd_id); + alert(json.message || 'Saved successfully!'); + openWorkspace(activeBed); } else { - alert(json.message || 'Failed to save'); + alert(json.message || 'Failed to save entry'); } } catch (err) { - alert('Error submitting entry'); + alert('Error saving nursing entry'); } }; - const uniqueWards = ['all', ...new Set(beds.map(b => b.ward_name).filter(Boolean))]; + // Open OPD Vitals Modal + const openOpdVitalsModal = (app) => { + setActiveOpdForVitals(app); + setOpdVitalsForm({ + temp: app.temp || '', + pulse: app.pulse || '', + bp_systolic: app.bp || '', + bp_diastolic: app.diastolic || '', + spo2: app.spo2 || '', + weight: app.weight || '', + height: app.height || '', + rr_min: app.rr_min || '' + }); + }; + const handleSaveOpdVitals = async () => { + if (!activeOpdForVitals) return; + + try { + const payload = { + nurse_id: authenticatedNurse ? authenticatedNurse.id : 0, + nurse_name: authenticatedNurse ? authenticatedNurse.name : 'Nurse', + ...opdVitalsForm + }; + + const res = await fetch(`/api/v1/nursing/opd/vitals/save/${activeOpdForVitals.opd_id}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + const json = await res.json(); + if (json.status === 1) { + alert(json.message || 'OPD Patient Vitals saved successfully!'); + setActiveOpdForVitals(null); + fetchOpdAppointments(opdDateMode); + } else { + alert(json.message || 'Failed to save vitals'); + } + } catch (err) { + alert('Error saving OPD Vitals'); + } + }; + + // Open Document Scan Modal (OPD / IPD) + const openScanModal = (targetObj, targetType) => { + setScanTarget(targetObj); + setScanType(targetType); + setScanDocCategory('Paper Document'); + setScanImageBase64(''); + setScanImagePreview(''); + }; + + const handleScanImageCapture = (e) => { + const file = e.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (event) => { + setScanImageBase64(event.target.result); + setScanImagePreview(event.target.result); + }; + reader.readAsDataURL(file); + }; + + const handleSaveScanDocument = async () => { + if (!scanTarget || !scanImageBase64) { + alert('Please capture or select a document photo.'); + return; + } + + try { + const payload = { + nurse_id: authenticatedNurse ? authenticatedNurse.id : 0, + nurse_name: authenticatedNurse ? authenticatedNurse.name : 'Nurse', + document_type: scanDocCategory, + image_base64: scanImageBase64 + }; + + const url = scanType === 'opd' + ? `/api/v1/nursing/opd/scan/save/${scanTarget.opd_id}` + : `/api/v1/nursing/ipd/scan/save/${scanTarget.ipd_id || scanTarget.id}`; + + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + const json = await res.json(); + if (json.status === 1) { + alert(json.message || 'Scanned Document uploaded successfully!'); + setScanTarget(null); + if (scanType === 'opd') fetchOpdAppointments(opdDateMode); + if (scanType === 'ipd' && activeBed) openWorkspace(activeBed); + } else { + alert(json.message || 'Failed to save document scan'); + } + } catch (err) { + alert('Error saving document scan'); + } + }; + + const getInitials = (nameStr) => { + if (!nameStr) return 'N'; + const parts = nameStr.trim().split(' '); + if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase(); + return nameStr.substring(0, 2).toUpperCase(); + }; + + const renderNoteContent = (entry) => { + const text = entry.treatment_text || entry.general_note || ''; + const match = text.match(/\[IMAGE_ATTACHMENT:(.*?)\]/); + const cleanText = text.replace(/\[IMAGE_ATTACHMENT:.*?\]/, '').replace('[Doctor Note] ', ''); + + return ( +
+ {cleanText &&
{cleanText}
} + {match && match[1] && ( +
+ + Attached Document / Photo + + + Attached Document + +
+ )} +
+ ); + }; + + // Filtered lists const filteredBeds = beds.filter(bed => { const matchesWard = selectedWard === 'all' || bed.ward_name === selectedWard; + const q = searchQuery.toLowerCase(); const matchesSearch = searchQuery === '' || - (bed.bed_code && bed.bed_code.toLowerCase().includes(searchQuery.toLowerCase())) || - (bed.ward_name && bed.ward_name.toLowerCase().includes(searchQuery.toLowerCase())); + (bed.bed_number || '').toLowerCase().includes(q) || + (bed.patient_name || '').toLowerCase().includes(q) || + (bed.uhid || '').toLowerCase().includes(q); return matchesWard && matchesSearch; }); + const filteredOpdAppointments = opdAppointments.filter(app => { + const matchesFilter = opdFilter === 'all' || app.status_key === opdFilter; + const q = searchQuery.toLowerCase(); + const name = (app.patient_display_name || '').toLowerCase(); + const uhid = (app.uhid || '').toLowerCase(); + const opdCode = (app.opd_code || '').toLowerCase(); + const docName = (app.doctor_name || '').toLowerCase(); + return matchesFilter && (searchQuery === '' || name.includes(q) || uhid.includes(q) || opdCode.includes(q) || docName.includes(q)); + }); + // ------------------------------------------------------------- - // RENDER: LOCK SCREEN (PIN AUTHENTICATION) + // LOCK SCREEN // ------------------------------------------------------------- if (!authenticatedNurse) { return (
-
- -
Nursing Care PWA App
- 30-Day Device Authentication Security +
+ +
Nursing Care Mobile App
+ 30-Day Device Security Authentication
@@ -405,11 +641,11 @@
setPinInput(e.target.value)} @@ -432,21 +668,21 @@
-
) : (
- Initial PIN Setup for selected nurse. Set a 4-6 digit Security PIN. + Initial PIN Setup for Nurse Profile. Set a 4-6 digit Security PIN.
setNewPin(e.target.value)} @@ -457,14 +693,14 @@ setConfirmPin(e.target.value)} />
-
)} @@ -475,251 +711,424 @@ } // ------------------------------------------------------------- - // RENDER: UNLOCKED PWA APP DASHBOARD + // UNLOCKED NURSING PWA APP // ------------------------------------------------------------- return (
- {/* PWA Install Banner */} - {installPrompt && ( -
- Install Nursing App on your device home screen - -
- )} - - {/* Top Nursing Header */} + {/* Header Topbar */}
-
- +
+
+ {getInitials(authenticatedNurse.name)} +
-
NursingCare PWA
- Staff: [{authenticatedNurse.nurse_code}] {authenticatedNurse.name} 30-Day Device Auth +
{authenticatedNurse.name}
+ + {authenticatedNurse.designation || 'Staff Nurse'} ● Active +
- -
- {/* Main Container */} -
+
{!activeBed ? (
- {/* Filter Bar */} -
-
-
- + {/* Segmented Tab Controller: IPD Admitted vs OPD Patients */} +
+ + +
+ + {mainTab === 'ipd' ? ( + /* ------------------------------------------------------------- + ADMITTED IPD PATIENTS & BEDS + ------------------------------------------------------------- */ +
+
+
+
+ + setSearchQuery(e.target.value)} + /> +
+
+
+ +
+
+ +
+ {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} +
+ ) : ( +
Available for Admission
+ )} + + {bed.status === 'occupied' && ( +
+ +
+ )} +
+
+ ))} +
+
+ ) : ( + /* ------------------------------------------------------------- + OPD PATIENTS TAB (VITALS & DOCUMENT SCANNING) + ------------------------------------------------------------- */ +
+ {/* Date Mode Segmented Controller */} +
+
+ + +
+ + {opdDateMode === 'today' ? '● Current Date' : '● All Appointments'} + +
+ + {/* Status Filters */} +
+ + + +
+ + {/* Search Input */} +
+ setSearchQuery(e.target.value)} />
-
-
- {uniqueWards.map(ward => ( - - ))} -
-
- {/* Bed Matrix */} - {loading ? ( -
-
-

Loading Hospital Bed Matrix...

-
- ) : ( -
- {filteredBeds.map(bed => { - const statusClass = `bed-status-${bed.bed_status || 'available'}`; - const badgeClass = bed.bed_status === 'occupied' ? 'badge-soft-blue' - : bed.bed_status === 'available' ? 'badge-soft-green' - : bed.bed_status === 'cleaning' ? 'badge-soft-amber' : 'badge-soft-red'; - - return ( -
-
openBedWorkspace(bed)}> -
-
-
{bed.bed_code || bed.bed_number}
- {bed.ward_name || 'General Ward'} + {/* OPD Patients Cards List */} +
+ {filteredOpdAppointments.length === 0 ? ( +
+ + No OPD appointments found for selected filter. +
+ ) : ( + filteredOpdAppointments.map(app => ( +
+
+
+ Q #{app.queue_no || app.opd_no || '1'} + ● {app.status_label}
- - {bed.bed_status || 'Available'} - + {app.opd_code}
- - {bed.current_ipd_id ? ( -
- IPD #{bed.current_ipd_id} - Patient Admitted + +
+
+ {getInitials(app.patient_display_name)} +
+
+
+ {app.patient_display_name} +
+ + {app.age_display} • {app.gender_label} | UHID: {app.uhid} + + + {app.doctor_name || 'Doctor'} + +
+
+ + {/* Vitals Summary Pill if recorded */} + {app.has_vitals && ( +
+ Vitals: BP {app.bp || '-'}/{app.diastolic || '-'} | Pulse {app.pulse || '-'} | Temp {app.temp || '-'}°F + Vitals ✓
- ) : ( - Ready for Admission )} -
- {bed.has_oxygen == 1 && O2} - {bed.has_suction == 1 && Suction} - {bed.has_monitor == 1 && Monitor} + {/* Nursing Action Buttons: [ Fill Vitals ] & [ Scan Document ] */} +
+ + +
-
- ); - })} + )) + )} +
)}
) : ( - /* Bedside Patient Workspace */ + /* ------------------------------------------------------------- + IPD BEDSIDE WORKSPACE FOR NURSING STAFF + ------------------------------------------------------------- */
- +
+ + +
-
-
-
-
Bed: {activeBed.bed_code}
- {activeBed.ward_name} - {activeBed.current_ipd_id ? ( - IPD ID: #{activeBed.current_ipd_id} - ) : ( - Unoccupied Bed - )} +
+
+
{activeBed.patient_name}
+
+ Bed {activeBed.bed_number} ({activeBed.ward_name}) + UHID: {activeBed.uhid} + IPD: {activeBed.ipd_code}
- {/* Tabs */} -
    -
  • -
  • -
  • -
  • -
  • -
- {/* Tab Content */} -
- {activeTab === 'vitals' && ( -
-
Log Patient Vitals
+ {activeTab === 'vitals' && ( +
+
Bedside Vitals Entry
+
+
setVitalsForm({...vitalsForm, bp_systolic: e.target.value})} />/ setVitalsForm({...vitalsForm, bp_diastolic: e.target.value})} />
+
setVitalsForm({...vitalsForm, pulse_rate: e.target.value})} />
+
setVitalsForm({...vitalsForm, spo2: e.target.value})} />
+
setVitalsForm({...vitalsForm, temperature_f: e.target.value})} />
+
setVitalsForm({...vitalsForm, resp_rate: e.target.value})} />
+
setVitalsForm({...vitalsForm, weight_kg: e.target.value})} />
+
+ +
+ )} + + {activeTab === 'treatment' && ( +
+
Nursing Procedure & Notes
+ + +
+ )} + + {/* Bedside Log Entries */} +
+
Patient Bedside History Log
+ {workspaceData.map(e => ( +
+
+ {e.entry_type} + {e.recorded_at} - by {e.recorded_by} +
+ {renderNoteContent(e)} +
+ ))} +
+
+ )} + + {/* ------------------------------------------------------------- + MODAL: FILL OPD VITALS + ------------------------------------------------------------- */} + {activeOpdForVitals && ( +
+
+
+
+
+ Fill OPD Patient Vitals +
+ +
+
+
+ {activeOpdForVitals.patient_display_name} ({activeOpdForVitals.age_display} • {activeOpdForVitals.gender_label}) +
UHID: {activeOpdForVitals.uhid} | OPD Code: {activeOpdForVitals.opd_code} +
+
-
setVitalsForm({...vitalsForm, temperature_f: e.target.value})} />
-
setVitalsForm({...vitalsForm, pulse_rate: e.target.value})} />
-
setVitalsForm({...vitalsForm, resp_rate: e.target.value})} />
-
setVitalsForm({...vitalsForm, bp_systolic: e.target.value})} />
-
setVitalsForm({...vitalsForm, bp_diastolic: e.target.value})} />
-
setVitalsForm({...vitalsForm, spo2: e.target.value})} />
-
-
- +
+ +
+ setOpdVitalsForm({...opdVitalsForm, bp_systolic: e.target.value})} /> + / + setOpdVitalsForm({...opdVitalsForm, bp_diastolic: e.target.value})} /> +
+
+
+ + setOpdVitalsForm({...opdVitalsForm, pulse: e.target.value})} /> +
+
+ + setOpdVitalsForm({...opdVitalsForm, spo2: e.target.value})} /> +
+
+ + setOpdVitalsForm({...opdVitalsForm, temp: e.target.value})} /> +
+
+ + setOpdVitalsForm({...opdVitalsForm, weight: e.target.value})} /> +
+
+ + setOpdVitalsForm({...opdVitalsForm, height: e.target.value})} />
- )} - - {activeTab === 'fluid' && ( -
-
Log Fluid Intake / Output
-
-
- - -
-
- - setFluidForm({...fluidForm, fluid_route: e.target.value})} /> -
-
- - setFluidForm({...fluidForm, fluid_amount_ml: e.target.value})} /> -
-
-
- -
-
+
+ +
- )} +
+
+
+ )} - {activeTab === 'treatment' && ( -
-
Log Treatment / Procedure Note
-
-
-
-
- -
-
+ {/* ------------------------------------------------------------- + MODAL: SCAN / PHOTO UPLOAD FOR PAPER DOCUMENTS + ------------------------------------------------------------- */} + {scanTarget && ( +
+
+
+
+
+ Scan / Photo Paper Document +
+
- )} +
+
+ + +
- {/* Workspace Entries Table */} -
-
Live Entries History ({workspaceData.length})
-
- - - - - - - - - - - {workspaceData.length === 0 ? ( - - ) : ( - workspaceData.map(entry => ( - - - - - - - )) - )} - -
Recorded AtTypeStaffSummary
No nursing entries found for this admission.
{entry.recorded_at}{entry.entry_type}{entry.recorded_by || 'Staff'} - - {entry.entry_type === 'vitals' && `T: ${entry.temperature_c ? ((entry.temperature_c * 9/5) + 32).toFixed(1) + '°F' : '-'}, P: ${entry.pulse_rate || '-'}, BP: ${entry.bp_systolic || '-'}/${entry.bp_diastolic || '-'}, SpO2: ${entry.spo2 || '-'}`} - {entry.entry_type === 'fluid' && `${entry.fluid_direction?.toUpperCase()} - ${entry.fluid_route}: ${entry.fluid_amount_ml}ml`} - {entry.entry_type === 'treatment' && entry.treatment_text} - {entry.general_note && ` (${entry.general_note})`} - -
+
+ +
+ + {scanImagePreview && ( +
+ Document Photo Captured + Document Scan Preview +
+ )} +
+
+ +