devsofttech_hms_ci4/public/App/Nursing/index.html

3156 lines
226 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Nursing Care - HMS Mobile</title>
<link rel="manifest" href="/App/Nursing/manifest.json">
<meta name="theme-color" content="#0f172a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="icon" href="/assets/img/logo.png" type="image/png">
<link rel="apple-touch-icon" href="/assets/img/logo.png">
<!-- Google Fonts & Bootstrap 5 CSS & Icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<!-- React 18 & ReactDOM UMD -->
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js" crossorigin></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone/babel.min.js"></script>
<!-- Cropper.js for Document Photo Cropping -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.1/cropper.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.1/cropper.min.js"></script>
<!-- HTML5 QR & Barcode Scanner for Patient Barcodes and ABHA QR -->
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
<style>
:root {
--primary-bg: #0f172a;
--card-bg: #ffffff;
--accent-blue: #2563eb;
--accent-cyan: #0891b2;
--accent-green: #16a34a;
--accent-amber: #d97706;
--accent-red: #dc2626;
--slate-800: #1e293b;
--slate-900: #0f172a;
}
body {
background-color: #f1f5f9;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
padding: 0;
user-select: none;
-webkit-user-select: none;
overflow-x: hidden;
color: #334155;
}
/* Android TV D-Pad Remote Focus Glow Outline */
:focus-visible, button:focus, select:focus, input:focus, .card:focus, a:focus {
outline: 3px solid #2563eb !important;
outline-offset: 2px !important;
box-shadow: 0 0 16px rgba(37, 99, 235, 0.6) !important;
}
/* Topbar Header */
.nursing-header {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
color: #ffffff;
padding: 12px 16px;
box-shadow: 0 4px 20px rgba(15, 23, 42, 0.25);
position: sticky;
top: 0;
z-index: 1000;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.avatar-circle {
width: 38px;
height: 38px;
border-radius: 50%;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 14px;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
border: 2px solid rgba(255,255,255,0.2);
}
.patient-avatar-circle {
width: 44px;
height: 44px;
border-radius: 50%;
background: #eff6ff;
color: #2563eb;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 15px;
border: 1.5px solid #dbeafe;
flex-shrink: 0;
}
/* Segmented Tabs */
.segmented-tabs {
background: #e2e8f0;
border-radius: 12px;
padding: 4px;
display: flex;
gap: 4px;
}
.segmented-tab {
flex: 1;
border: none;
background: transparent;
padding: 10px 14px;
border-radius: 9px;
font-weight: 600;
font-size: 13px;
color: #64748b;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.segmented-tab.active {
background: linear-gradient(135deg, #059669 0%, #047857 100%);
color: #ffffff;
box-shadow: 0 4px 12px rgba(5, 150, 105, 0.25);
}
.segmented-tab.active-blue {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: #ffffff;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.25);
}
/* Bed Cards */
.bed-card {
border: 1px solid #e2e8f0;
border-radius: 16px;
background: #ffffff;
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
cursor: pointer;
box-shadow: 0 4px 16px -2px rgba(15, 23, 42, 0.05);
position: relative;
overflow: hidden;
}
.bed-card:hover, .bed-card:active {
transform: translateY(-2px);
box-shadow: 0 8px 24px -4px rgba(15, 23, 42, 0.12);
border-color: #cbd5e1;
}
.bed-status-available { border-top: 5px solid #16a34a; }
.bed-status-occupied { border-top: 5px solid #2563eb; }
.bed-status-cleaning { border-top: 5px solid #d97706; }
.bed-status-maintenance { border-top: 5px solid #dc2626; }
.mobile-patient-card {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 16px;
padding: 16px;
margin-bottom: 14px;
box-shadow: 0 4px 16px -2px rgba(15, 23, 42, 0.05);
border-left: 6px solid var(--accent-blue);
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
/* Status Pills */
.status-pill {
font-size: 11px;
font-weight: 700;
padding: 4px 10px;
border-radius: 20px;
text-transform: uppercase;
letter-spacing: 0.3px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.status-pill.waiting { background-color: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; }
.status-pill.visited { background-color: #ecfeff; color: #0e7490; border: 1px solid #a5f3fc; }
.status-pill.booked { background-color: #f8fafc; color: #475569; border: 1px solid #cbd5e1; }
.queue-badge {
background: #0f172a;
color: #ffffff;
font-weight: 800;
font-size: 12px;
padding: 4px 10px;
border-radius: 20px;
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.2);
}
/* Lock Screen Overlay */
.lock-screen-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.lock-card {
background: #ffffff;
border-radius: 20px;
width: 100%;
max-width: 400px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
overflow: hidden;
}
/* Fluid I/O Stats Cards */
.stat-card-intake {
background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
border: 1.5px solid #a7f3d0;
border-radius: 14px;
padding: 10px 8px;
}
.stat-card-output {
background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
border: 1.5px solid #fde68a;
border-radius: 14px;
padding: 10px 8px;
}
.stat-card-balance {
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
border: 1.5px solid #bfdbfe;
border-radius: 14px;
padding: 10px 8px;
}
/* Floating Bottom Dock Action Bar */
.bottom-dock-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1040;
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border-top: 1px solid #e2e8f0;
box-shadow: 0 -6px 25px rgba(15, 23, 42, 0.1);
padding: 8px 12px;
padding-bottom: calc(10px + env(safe-area-inset-bottom));
display: flex;
gap: 10px;
max-width: 600px;
margin: 0 auto;
}
.bottom-dock-btn {
border: none;
border-radius: 14px;
padding: 8px 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
font-size: 11px;
font-weight: 700;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
color: #ffffff;
text-decoration: none;
flex: 1;
cursor: pointer;
}
.bottom-dock-btn:active {
transform: scale(0.95);
}
.bottom-dock-btn.btn-scan {
background: linear-gradient(135deg, #0f172a 0%, #334155 100%);
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.25);
}
.bottom-dock-btn.btn-vitals {
background: linear-gradient(135deg, #059669 0%, #047857 100%);
box-shadow: 0 4px 14px rgba(5, 150, 105, 0.25);
}
.bottom-dock-btn.btn-doc {
background: linear-gradient(135deg, #4f46e5 0%, #3730a3 100%);
box-shadow: 0 4px 14px rgba(79, 70, 229, 0.25);
}
.content-with-bottom-dock {
padding-bottom: 96px !important;
}
.qr-scanner-wrapper {
position: relative;
background: #000000;
border-radius: 16px;
overflow: hidden;
border: 2px solid #3b82f6;
min-height: 240px;
}
.patient-search-result-card {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 12px;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.patient-search-result-card:hover {
border-color: #93c5fd;
box-shadow: 0 4px 14px rgba(37,99,235,0.08);
}
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
const DEVICE_AUTH_KEY = 'nursing_pwa_device_auth_v1';
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
const MobileAppsModal = ({ show, onClose, currentApp }) => {
if (!show) return null;
const navigateToApp = (appKey) => {
if (appKey === currentApp) {
onClose();
return;
}
if (appKey === 'doctor') {
window.location.href = '/app/doctor';
} else if (appKey === 'nursing') {
window.location.href = '/app/nursing';
}
};
return (
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(15, 23, 42, 0.7)', zIndex: 1055, backdropFilter: 'blur(4px)' }} tabIndex="-1">
<div className="modal-dialog modal-dialog-centered px-2">
<div className="modal-content border-0 shadow-lg rounded-4 overflow-hidden">
<div className="modal-header text-white p-3" style={{ background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)' }}>
<div className="d-flex align-items-center gap-2">
<img src="/assets/img/logo.png" alt="Hospital Logo" style={{ height: '28px', width: 'auto', filter: 'brightness(0) invert(1)' }} onError={(e) => { e.target.style.display = 'none'; }} />
<h6 className="modal-title fw-bold mb-0 text-white" style={{ fontSize: '15px' }}>
<i className="bi bi-grid-fill me-1 text-primary"></i> HMS Mobile &amp; PWA Apps Launcher
</h6>
</div>
<button type="button" className="btn-close btn-close-white" onClick={onClose}></button>
</div>
<div className="modal-body p-3 bg-light">
<label className="form-label fw-bold small text-secondary mb-2" style={{ letterSpacing: '0.5px' }}>
1. SELECT APPLICATION
</label>
<div className="row g-2 mb-3">
<div className="col-4">
<div
className={`card p-2 text-center h-100 ${currentApp === 'nursing' ? 'border-2 border-primary bg-primary-subtle shadow-sm' : 'border bg-white text-dark'}`}
style={{ cursor: 'pointer', borderRadius: '12px' }}
onClick={() => navigateToApp('nursing')}
>
<div className="my-1">
<i className="bi bi-heart-pulse-fill fs-3 text-primary"></i>
</div>
<div className="fw-bold small text-truncate" style={{ fontSize: '12px' }}>Nursing Care</div>
{currentApp === 'nursing' ? (
<span className="badge bg-primary mt-1" style={{ fontSize: '9px' }}>● Active App</span>
) : (
<span className="badge bg-info-subtle text-info-emphasis border border-info-subtle mt-1" style={{ fontSize: '9px' }}>Active PWA</span>
)}
</div>
</div>
<div className="col-4">
<div
className={`card p-2 text-center h-100 ${currentApp === 'doctor' ? 'border-2 border-primary bg-primary-subtle shadow-sm' : 'border bg-white text-dark'}`}
style={{ cursor: 'pointer', borderRadius: '12px' }}
onClick={() => navigateToApp('doctor')}
>
<div className="my-1">
<i className="bi bi-person-badge-fill fs-3 text-primary"></i>
</div>
<div className="fw-bold small text-truncate" style={{ fontSize: '12px' }}>Doctor App</div>
{currentApp === 'doctor' ? (
<span className="badge bg-primary mt-1" style={{ fontSize: '9px' }}>● Active App</span>
) : (
<span className="badge bg-success mt-1" style={{ fontSize: '9px' }}>Active PWA</span>
)}
</div>
</div>
<div className="col-4">
<div
className="card p-2 text-center border bg-white text-muted opacity-75 h-100"
style={{ borderRadius: '12px', cursor: 'not-allowed' }}
>
<div className="my-1">
<i className="bi bi-capsule fs-3 text-secondary"></i>
</div>
<div className="fw-bold small text-truncate" style={{ fontSize: '12px' }}>Pharmacy App</div>
<span className="badge bg-secondary mt-1" style={{ fontSize: '9px' }}>Coming Soon</span>
</div>
</div>
</div>
<div className="p-3 bg-white rounded-3 border text-center shadow-sm">
<div className="d-flex align-items-center justify-content-center gap-2 mb-2">
<img src="/assets/img/logo.png" alt="Logo" style={{ height: '28px' }} onError={(e) => e.target.style.display = 'none'} />
<div className="text-start">
<div className="fw-bold small text-dark">HMS Unified Mobile Portal</div>
<div className="small text-muted" style={{ fontSize: '10px' }}>Domain: {window.location.host}</div>
</div>
</div>
<p className="small text-muted mb-2" style={{ fontSize: '11px' }}>
Tap any active app card above to switch between modules in your mobile app.
</p>
<a href="/app" className="btn btn-outline-primary btn-sm w-100 fw-bold rounded-pill" style={{ fontSize: '12px' }}>
<i className="bi bi-house-door me-1"></i> Open Mobile App Hub
</a>
</div>
</div>
</div>
</div>
</div>
);
};
function NursingApp() {
const [showAppsModal, setShowAppsModal] = useState(false);
const [mainTab, setMainTab] = useState('ipd'); // 'ipd' or 'opd'
const [beds, setBeds] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedWard, setSelectedWard] = useState('all');
const [searchQuery, setSearchQuery] = useState('');
const [activeBed, setActiveBed] = useState(null);
const [activeTab, setActiveTab] = useState('vitals');
const [workspaceData, setWorkspaceData] = useState([]);
const [nurses, setNurses] = useState([]);
// OPD Patients State
const [opdAppointments, setOpdAppointments] = useState([]);
const [opdCounts, setOpdCounts] = useState({ all: 0, waiting: 0, visited: 0, booked: 0, cancelled: 0, pending_vitals: 0, done_vitals: 0 });
const [opdFilter, setOpdFilter] = useState('all');
const [opdDateMode, setOpdDateMode] = useState('today');
const [doctors, setDoctors] = useState([]);
const [selectedDoctorId, setSelectedDoctorId] = useState('0');
const [vitalsFilter, setVitalsFilter] = useState('all'); // 'all', 'pending', 'done'
// 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('');
// 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 [wellnessModalTab, setWellnessModalTab] = useState('vitals'); // 'vitals', 'sugar', 'activity', 'lifestyle', 'women'
const [wellnessForm, setWellnessForm] = useState({
temperature_f: '', pulse_rate: '', resp_rate: '',
bp_systolic: '', bp_diastolic: '', spo2: '', weight_kg: '', height_cm: '',
pain_score: '',
waist_circumference_cm: '', hip_circumference_cm: '', waist_hip_ratio: '',
sugar_random: '', sugar_fasting: '', sugar_pp: '', hba1c: '', hemoglobin: '',
daily_steps: '', sleep_hours: '', exercise_min_per_day: '',
diet_type: '', tobacco_status: '', alcohol_status: '',
women_lmp: '', women_pregnancy_status: '',
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);
const [pinInput, setPinInput] = useState('');
const [pinError, setPinError] = useState('');
const [isSettingNewPin, setIsSettingNewPin] = useState(false);
const [newPin, setNewPin] = useState('');
const [confirmPin, setConfirmPin] = useState('');
// Workspace Forms (IPD)
const [vitalsForm, setVitalsForm] = useState({
temperature_f: '', pulse_rate: '', resp_rate: '',
bp_systolic: '', bp_diastolic: '', spo2: '', weight_kg: '', general_note: ''
});
const [fluidForm, setFluidForm] = useState({
fluid_direction: 'intake', fluid_route: 'IV (Intravenous)',
fluid_amount_ml: '', general_note: ''
});
const [treatmentForm, setTreatmentForm] = useState({
treatment_text: '', general_note: ''
});
const [showAllDoctorOrders, setShowAllDoctorOrders] = useState(false);
const fetchBeds = async () => {
try {
setLoading(true);
const res = await fetch('/api/v1/nursing/beds');
const json = await res.json();
if (json.status === 1 && json.data) {
setBeds(json.data.beds || []);
}
} catch (err) {
console.error('Failed to load beds', err);
} finally {
setLoading(false);
}
};
const fetchDoctors = async () => {
try {
const res = await fetch('/api/v1/doctor/list');
const json = await res.json();
if (json.status === 1 && json.data) {
setDoctors(json.data);
}
} catch (err) {
console.error('Failed to load doctors', err);
}
};
const fetchOpdAppointments = async (dateModeOverride, docIdOverride, vitalsOverride) => {
try {
const mode = dateModeOverride !== undefined ? dateModeOverride : opdDateMode;
const docId = docIdOverride !== undefined ? docIdOverride : selectedDoctorId;
const vFilter = vitalsOverride !== undefined ? vitalsOverride : vitalsFilter;
const res = await fetch(`/api/v1/nursing/opd/list?date_mode=${mode}&doctor_id=${docId}&vitals_status=${vFilter}`);
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');
const json = await res.json();
if (json.status === 1 && json.data) {
setNurses(json.data);
const params = new URLSearchParams(window.location.search);
const codeParam = params.get('nurse_code');
if (codeParam) {
const matched = json.data.find(n => n.nurse_code === codeParam);
if (matched) {
setAuthNurseId(matched.id);
}
}
}
} catch (err) {
console.error('Failed to load nurses', err);
}
};
useEffect(() => {
const savedAuth = localStorage.getItem(DEVICE_AUTH_KEY);
if (savedAuth) {
try {
const parsed = JSON.parse(savedAuth);
if (parsed && parsed.nurse && parsed.expires_at > Date.now()) {
setAuthenticatedNurse(parsed.nurse);
} else {
localStorage.removeItem(DEVICE_AUTH_KEY);
}
} catch (e) {}
}
fetchBeds();
fetchNurses();
fetchDoctors();
fetchOpdAppointments('today', '0', 'all');
}, []);
const handleVerifyPin = async () => {
if (!authNurseId || authNurseId === '0') {
setPinError('Please select your Nursing Staff Profile.');
return;
}
if (!pinInput || pinInput.length < 4) {
setPinError('Enter your 4 to 6 digit Security PIN.');
return;
}
setPinError('');
try {
const body = new URLSearchParams();
body.append('nurse_id', authNurseId);
body.append('pin', pinInput);
const res = await fetch('/api/v1/nursing/auth/verify-pin', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString()
});
const json = await res.json();
if (json.status === 1 && json.nurse) {
setAuthenticatedNurse(json.nurse);
const deviceAuthObj = {
nurse: json.nurse,
expires_at: Date.now() + THIRTY_DAYS_MS
};
localStorage.setItem(DEVICE_AUTH_KEY, JSON.stringify(deviceAuthObj));
setPinInput('');
fetchBeds();
fetchOpdAppointments(opdDateMode, selectedDoctorId, vitalsFilter);
} else if (json.status === 2) {
setIsSettingNewPin(true);
} else {
setPinError(json.message || 'Incorrect Security PIN');
}
} catch (err) {
setPinError('Connection error verifying PIN.');
}
};
const handleSaveNewPin = async () => {
if (!newPin || newPin.length < 4) {
setPinError('PIN must be 4 to 6 numeric digits.');
return;
}
if (newPin !== confirmPin) {
setPinError('New PIN and Confirm PIN do not match.');
return;
}
try {
const body = new URLSearchParams();
body.append('nurse_id', authNurseId || (authenticatedNurse ? authenticatedNurse.id : 0));
body.append('new_pin', newPin);
const res = await fetch('/api/v1/nursing/auth/set-pin', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString()
});
const json = await res.json();
if (json.status === 1) {
alert('Security PIN set successfully! Enter your PIN to unlock.');
setIsSettingNewPin(false);
setNewPin('');
setConfirmPin('');
setPinInput('');
} else {
setPinError(json.message || 'Failed to update PIN');
}
} catch (err) {
setPinError('Error setting PIN.');
}
};
const handleLogout = () => {
setAuthenticatedNurse(null);
localStorage.removeItem(DEVICE_AUTH_KEY);
setPinInput('');
setActiveBed(null);
};
const openWorkspace = async (bed) => {
if (!bed.ipd_id || bed.status !== 'occupied') return;
setActiveBed(bed);
try {
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', err);
}
};
const handleSaveIpdEntry = async (entryType, formData) => {
if (!activeBed || !activeBed.ipd_id) return;
try {
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/json' },
body: JSON.stringify(payload)
});
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');
}
} catch (err) {
alert('Error saving nursing entry');
}
};
// 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 [cropSrc, setCropSrc] = useState('');
const [cropper, setCropper] = useState(null);
const imageRef = React.useRef(null);
const handleScanImageCapture = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
setCropSrc(event.target.result);
};
reader.readAsDataURL(file);
e.target.value = '';
};
useEffect(() => {
if (cropSrc && imageRef.current) {
if (cropper) {
cropper.destroy();
}
const newCropper = new Cropper(imageRef.current, {
viewMode: 1,
autoCropArea: 0.9,
responsive: true,
restore: false,
guides: true,
center: true,
highlight: true,
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: false,
});
setCropper(newCropper);
}
}, [cropSrc]);
const applyCrop = () => {
if (cropper) {
const croppedCanvas = cropper.getCroppedCanvas({
maxWidth: 1600,
maxHeight: 1600,
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high',
});
if (croppedCanvas) {
const croppedBase64 = croppedCanvas.toDataURL('image/jpeg', 0.85);
setScanImageBase64(croppedBase64);
setScanImagePreview(croppedBase64);
}
cropper.destroy();
setCropper(null);
setCropSrc('');
}
};
const cancelCrop = () => {
if (cropper) {
cropper.destroy();
setCropper(null);
}
setCropSrc('');
};
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');
}
};
// -----------------------------------------------------------------
// 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);
setWellnessModalTab('vitals');
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 || '',
pain_score: '',
waist_circumference_cm: opdVitals.waist || '',
hip_circumference_cm: '',
waist_hip_ratio: '',
sugar_random: opdVitals.glucose || '',
sugar_fasting: '',
sugar_pp: '',
hba1c: '',
hemoglobin: '',
daily_steps: '',
sleep_hours: '',
exercise_min_per_day: '',
diet_type: '',
tobacco_status: '',
alcohol_status: '',
women_lmp: '',
women_pregnancy_status: '',
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: '',
pain_score: '',
waist_circumference_cm: '', hip_circumference_cm: '', waist_hip_ratio: '',
sugar_random: '', sugar_fasting: '', sugar_pp: '', hba1c: '', hemoglobin: '',
daily_steps: '', sleep_hours: '', exercise_min_per_day: '',
diet_type: '', tobacco_status: '', alcohol_status: '',
women_lmp: '', women_pregnancy_status: '',
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(' ');
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 (
<div>
{cleanText && <div className="small fw-semibold">{cleanText}</div>}
{match && match[1] && (
<div className="mt-2">
<span className="badge bg-primary mb-1 d-inline-flex align-items-center gap-1" style={{fontSize: '10px'}}>
<i className="bi bi-paperclip"></i> Attached Document / Photo
</span>
<a href={match[1]} target="_blank" rel="noreferrer">
<img src={match[1]} alt="Attached Document" className="img-fluid rounded-3 border shadow-sm d-block mt-1" style={{maxHeight: '220px', objectFit: 'cover'}} />
</a>
</div>
)}
</div>
);
};
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) ||
(bed.ipd_code || '').toLowerCase().includes(q) ||
(bed.doctor_name || '').toLowerCase().includes(q);
return isOccupied && 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));
});
// -------------------------------------------------------------
// LOCK SCREEN
// -------------------------------------------------------------
if (!authenticatedNurse) {
return (
<div className="lock-screen-overlay">
<MobileAppsModal show={showAppsModal} onClose={() => setShowAppsModal(false)} currentApp="nursing" />
<div className="lock-card">
<div className="p-4 text-center text-white position-relative" style={{background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)'}}>
<button
type="button"
className="btn btn-sm btn-outline-light position-absolute top-0 end-0 m-2 rounded-circle"
style={{width: '32px', height: '32px', padding: 0}}
onClick={() => setShowAppsModal(true)}
title="HMS Mobile & PWA Apps Launcher"
>
<i className="bi bi-grid-3x3-gap-fill" style={{fontSize: '12px'}}></i>
</button>
<img src="/assets/img/logo.png" alt="Hospital Logo" className="d-block mx-auto mb-2" style={{maxHeight: '40px', filter: 'brightness(0) invert(1)'}} onError={(e) => { e.target.style.display = 'none'; }} />
<h5 className="fw-bold mb-1">Nursing Care Mobile App</h5>
<small className="opacity-75">30-Day Device Security Authentication</small>
</div>
<div className="p-4">
{pinError && <div className="alert alert-danger py-2 px-3 small mb-3">{pinError}</div>}
{!isSettingNewPin ? (
<div>
<div className="mb-3">
<label className="form-label fw-bold small">Select Nursing Staff Profile</label>
<select
className="form-select form-select-lg fs-6"
value={authNurseId}
onChange={(e) => setAuthNurseId(e.target.value)}
>
<option value="0">-- Choose Nurse Profile --</option>
{nurses.map(n => (
<option key={n.id} value={n.id}>[{n.nurse_code}] {n.name}</option>
))}
</select>
</div>
<div className="mb-4">
<label className="form-label fw-bold small">Enter App Security PIN</label>
<input
type="password"
maxLength="6"
className="form-control text-center fs-3"
placeholder="••••"
value={pinInput}
onChange={(e) => setPinInput(e.target.value)}
onKeyUp={(e) => e.key === 'Enter' && handleVerifyPin()}
/>
<small className="text-muted text-center d-block mt-1" style={{fontSize: '11px'}}>
Unlocks device access for 30 days
</small>
</div>
<button className="btn btn-success btn-lg w-100 py-3 fw-bold shadow-sm" onClick={handleVerifyPin}>
<i className="bi bi-unlock-fill me-2"></i> Unlock Nursing App
</button>
</div>
) : (
<div>
<div className="alert alert-info py-2 small mb-3">
Initial PIN Setup for Nurse Profile. Set a 4-6 digit Security PIN.
</div>
<div className="mb-3">
<label className="form-label fw-bold small">New Security PIN</label>
<input
type="password"
maxLength="6"
className="form-control text-center fs-3"
placeholder="••••"
value={newPin}
onChange={(e) => setNewPin(e.target.value)}
/>
</div>
<div className="mb-3">
<label className="form-label fw-bold small">Confirm New PIN</label>
<input
type="password"
maxLength="6"
className="form-control text-center fs-3"
placeholder="••••"
value={confirmPin}
onChange={(e) => setConfirmPin(e.target.value)}
/>
</div>
<button className="btn btn-primary btn-lg w-100 py-3 fw-bold shadow-sm" onClick={handleSaveNewPin}>
<i className="bi bi-check-circle-fill me-2"></i> Save PIN &amp; Unlock
</button>
</div>
)}
</div>
</div>
</div>
);
}
// -------------------------------------------------------------
// UNLOCKED NURSING PWA APP
// -------------------------------------------------------------
return (
<div>
<MobileAppsModal show={showAppsModal} onClose={() => setShowAppsModal(false)} currentApp="nursing" />
{/* Header Topbar */}
<header className="nursing-header d-flex justify-content-between align-items-center">
<div className="d-flex align-items-center gap-2">
<div className="avatar-circle">
{getInitials(authenticatedNurse.name)}
</div>
<div>
<h6 className="mb-0 fw-bold">{authenticatedNurse.name}</h6>
<small className="text-light opacity-75" style={{fontSize: '11px'}}>
{authenticatedNurse.designation || 'Staff Nurse'} <span className="badge bg-success ms-1" style={{fontSize: '9px'}}>● Active</span>
</small>
</div>
</div>
<div className="d-flex align-items-center gap-2">
<button className="btn btn-sm btn-outline-light rounded-circle p-2" style={{width: '36px', height: '36px'}} onClick={() => setShowAppsModal(true)} title="HMS Mobile & PWA Apps Launcher">
<i className="bi bi-grid-3x3-gap-fill"></i>
</button>
<button className="btn btn-sm btn-outline-light rounded-circle p-2" style={{width: '36px', height: '36px'}} onClick={() => { fetchBeds(); fetchOpdAppointments(opdDateMode, selectedDoctorId, vitalsFilter); }} title="Refresh">
<i className="bi bi-arrow-clockwise"></i>
</button>
<button className="btn btn-sm btn-outline-danger rounded-circle p-2" style={{width: '36px', height: '36px'}} onClick={handleLogout} title="Lock Access">
<i className="bi bi-lock-fill"></i>
</button>
</div>
</header>
<main className="container-fluid py-3 px-2 px-md-3 content-with-bottom-dock">
{!activeBed ? (
<div>
{/* Segmented Tab Controller: IPD Admitted vs OPD Patients */}
<div className="segmented-tabs mb-3">
<button className={`segmented-tab ${mainTab === 'ipd' ? 'active' : ''}`} onClick={() => { setMainTab('ipd'); fetchBeds(); }}>
<i className="bi bi-hospital-fill me-2"></i> Admitted IPDs ({beds.filter(b => b.status === 'occupied' || b.patient_name).length})
</button>
<button className={`segmented-tab ${mainTab === 'opd' ? 'active-blue' : ''}`} onClick={() => { setMainTab('opd'); fetchOpdAppointments(opdDateMode, selectedDoctorId, vitalsFilter); }}>
<i className="bi bi-calendar-check-fill me-2"></i> OPD Patients ({opdAppointments.length})
</button>
</div>
{mainTab === 'ipd' ? (
/* -------------------------------------------------------------
ADMITTED IPD PATIENTS & BEDS
------------------------------------------------------------- */
<div>
<div className="row g-2 mb-3 align-items-center">
<div className="col-12 col-md-6">
<div className="input-group shadow-sm rounded-pill overflow-hidden bg-white border">
<span className="input-group-text bg-white border-0 ps-3 text-secondary"><i className="bi bi-search"></i></span>
<input
type="text"
className="form-control border-0 py-2 shadow-none"
placeholder="Search Bed No, Patient Name, UHID..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
<div className="col-12 col-md-6">
<select
className="form-select rounded-pill shadow-sm py-2"
value={selectedWard}
onChange={(e) => setSelectedWard(e.target.value)}
>
<option value="all">All Wards / Units</option>
{Array.from(new Set(beds.map(b => b.ward_name))).filter(Boolean).map(w => (
<option key={w} value={w}>{w}</option>
))}
</select>
</div>
</div>
<div className="row g-3">
{filteredBeds.length === 0 ? (
<div className="col-12 text-center text-muted py-5 bg-white rounded-4 border shadow-sm">
<i className="bi bi-hospital fs-1 text-secondary opacity-50 d-block mb-2"></i>
<h6 className="fw-bold text-slate-800 mb-1">No Admitted Patients Found</h6>
<small className="text-muted">There are currently no admitted IPD patients in the selected ward.</small>
</div>
) : (
filteredBeds.map(bed => (
<div key={bed.id} className="col-12 col-md-6 col-lg-4">
<div
className={`bed-card p-3 bed-status-${bed.status}`}
onClick={() => openWorkspace(bed)}
>
<div className="d-flex justify-content-between align-items-start mb-2">
<span className="fw-bold text-dark fs-5">Bed {bed.bed_number}</span>
<span className={`badge ${bed.status === 'occupied' ? 'bg-primary' : 'bg-success'} text-uppercase`} style={{fontSize: '9px'}}>
{bed.status}
</span>
</div>
<small className="text-muted d-block mb-2" style={{fontSize: '11px'}}>{bed.ward_name || 'General Ward'}</small>
{bed.patient_name ? (
<div>
<div className="fw-bold text-primary text-truncate" style={{fontSize: '15px'}}>{bed.patient_name}</div>
<small className="text-muted d-block" style={{fontSize: '11px'}}>UHID: {bed.uhid || 'N/A'} • IPD: {bed.ipd_code || 'N/A'}</small>
<span className="badge bg-primary-subtle text-primary border border-primary-subtle px-2 py-1 mt-2 d-inline-block" style={{fontSize: '11px'}}>
<i className="bi bi-person-badge me-1"></i> {bed.doctor_name || 'Dr. Assigned'}
</span>
</div>
) : (
<div className="text-success small fw-semibold py-2">Available for Admission</div>
)}
<div className="mt-3 pt-2 border-top d-flex gap-1">
<button className="btn btn-sm btn-primary w-100 py-2 fw-bold rounded-3 shadow-sm" style={{fontSize: '12px'}}>
<i className="bi bi-heart-pulse me-1"></i> Open Bedside Workspace
</button>
</div>
</div>
</div>
))
)}
</div>
</div>
) : (
/* -------------------------------------------------------------
OPD PATIENTS TAB (VITALS & DOCUMENT SCANNING)
------------------------------------------------------------- */
<div>
{/* Date Mode Segmented Controller & Filters */}
<div className="d-flex align-items-center justify-content-between mb-3 bg-white p-2 rounded-4 border shadow-sm flex-wrap gap-2">
<div className="d-flex gap-1 bg-light p-1 rounded-pill border">
<button
className={`btn btn-sm rounded-pill px-3 py-1 fw-bold ${opdDateMode === 'today' ? 'btn-primary shadow-sm' : 'btn-light text-secondary'}`}
onClick={() => { setOpdDateMode('today'); fetchOpdAppointments('today', selectedDoctorId, vitalsFilter); }}
>
<i className="bi bi-calendar-event me-1"></i> Today's OPDs ({opdCounts.today_total || 0})
</button>
<button
className={`btn btn-sm rounded-pill px-3 py-1 fw-bold ${opdDateMode === 'all' ? 'btn-primary shadow-sm' : 'btn-light text-secondary'}`}
onClick={() => { setOpdDateMode('all'); fetchOpdAppointments('all', selectedDoctorId, vitalsFilter); }}
>
<i className="bi bi-calendar3 me-1"></i> All Dates ({opdCounts.all_total || 0})
</button>
</div>
<span className="badge bg-primary-subtle text-primary border border-primary-subtle px-2 py-1 rounded-pill small" style={{fontSize: '11px'}}>
{opdDateMode === 'today' ? '● Current Date' : '● All Appointments'}
</span>
</div>
{/* Doctor Filter & Vitals Filter Controls Box */}
<div className="bg-white p-3 mb-3 rounded-4 border shadow-sm">
<div className="row g-2 align-items-center">
<div className="col-12 col-md-6">
<label className="form-label small fw-bold text-dark mb-1">
<i className="bi bi-person-badge text-primary me-1"></i> Filter by Doctor
</label>
<select
className="form-select form-select-sm rounded-pill fw-semibold"
value={selectedDoctorId}
onChange={(e) => {
const val = e.target.value;
setSelectedDoctorId(val);
fetchOpdAppointments(opdDateMode, val, vitalsFilter);
}}
>
<option value="0">-- All Hospital Doctors --</option>
{doctors.map(d => (
<option key={d.id} value={d.id}>{d.name}</option>
))}
</select>
</div>
<div className="col-12 col-md-6">
<label className="form-label small fw-bold text-dark mb-1">
<i className="bi bi-heart-pulse-fill text-danger me-1"></i> Vitals Status Filter
</label>
<div className="d-flex gap-1">
<button
className={`btn btn-sm flex-fill rounded-pill py-1 fw-bold ${vitalsFilter === 'all' ? 'btn-primary shadow-sm' : 'btn-light border text-secondary'}`}
onClick={() => { setVitalsFilter('all'); fetchOpdAppointments(opdDateMode, selectedDoctorId, 'all'); }}
>
All ({opdCounts.all})
</button>
<button
className={`btn btn-sm flex-fill rounded-pill py-1 fw-bold ${vitalsFilter === 'pending' ? 'btn-warning text-dark shadow-sm' : 'btn-light border text-secondary'}`}
onClick={() => { setVitalsFilter('pending'); fetchOpdAppointments(opdDateMode, selectedDoctorId, 'pending'); }}
>
⚠️ Pending ({opdCounts.pending_vitals || 0})
</button>
<button
className={`btn btn-sm flex-fill rounded-pill py-1 fw-bold ${vitalsFilter === 'done' ? 'btn-success text-white shadow-sm' : 'btn-light border text-secondary'}`}
onClick={() => { setVitalsFilter('done'); fetchOpdAppointments(opdDateMode, selectedDoctorId, 'done'); }}
>
✅ Filled ({opdCounts.done_vitals || 0})
</button>
</div>
</div>
</div>
</div>
{/* Visit Status Filter Pills */}
<div className="d-flex gap-2 mb-3 overflow-auto no-scrollbar pb-1">
<button className={`btn btn-sm rounded-pill px-3 ${opdFilter === 'all' ? 'btn-primary fw-bold' : 'btn-white bg-white text-secondary border'}`} onClick={() => setOpdFilter('all')}>
All Visits
</button>
<button className={`btn btn-sm rounded-pill px-3 ${opdFilter === 'waiting' ? 'btn-primary fw-bold' : 'btn-white bg-white text-secondary border'}`} onClick={() => setOpdFilter('waiting')}>
Waiting
</button>
<button className={`btn btn-sm rounded-pill px-3 ${opdFilter === 'visited' ? 'btn-info text-dark fw-bold' : 'btn-white bg-white text-secondary border'}`} onClick={() => setOpdFilter('visited')}>
Visited
</button>
</div>
{/* Search Input */}
<div className="input-group mb-3 shadow-sm rounded-pill overflow-hidden bg-white border">
<span className="input-group-text bg-white border-0 ps-3 text-secondary"><i className="bi bi-search"></i></span>
<input
type="text"
className="form-control border-0 py-2 shadow-none"
placeholder="Search Patient Name, Doctor Name, UHID, OPD Code..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{/* OPD Patients Cards List */}
<div>
{filteredOpdAppointments.length === 0 ? (
<div className="text-center text-muted py-5 bg-white rounded-4 border shadow-sm">
<i className="bi bi-person-x fs-1 text-secondary opacity-50 d-block mb-2"></i>
No OPD appointments found for selected filter.
</div>
) : (
filteredOpdAppointments.map(app => (
<div key={app.opd_id} className="mobile-patient-card">
<div className="d-flex justify-content-between align-items-center mb-2 pb-2 border-bottom">
<div className="d-flex align-items-center gap-2">
<span className="queue-badge">Q #{app.queue_no || app.opd_no || '1'}</span>
<span className={`status-pill ${app.status_key}`}>● {app.status_label}</span>
</div>
<code className="fw-bold text-dark fs-6">{app.opd_code}</code>
</div>
<div className="d-flex align-items-start gap-3 mb-3">
<div className="patient-avatar-circle">
{getInitials(app.patient_display_name)}
</div>
<div className="flex-grow-1">
<h5 className="fw-bold text-slate-900 mb-1" style={{fontSize: '16px'}}>
{app.patient_display_name}
</h5>
<small className="text-muted d-block mb-2" style={{fontSize: '12px'}}>
{app.age_display} • {app.gender_label} | UHID: {app.uhid}
</small>
<span className="badge bg-primary-subtle text-primary border border-primary-subtle px-2 py-1" style={{fontSize: '11px'}}>
<i className="bi bi-person-badge me-1"></i>{app.doctor_name || 'Doctor'}
</span>
</div>
</div>
{/* Vitals Summary Pill if recorded */}
{app.has_vitals && (
<div className="alert alert-success py-1.5 px-3 mb-3 small rounded-3 d-flex align-items-center justify-content-between" style={{fontSize: '12px'}}>
<span><i className="bi bi-heart-pulse-fill text-danger me-1"></i> <strong>Vitals:</strong> BP {app.bp || '-'}/{app.diastolic || '-'} | Pulse {app.pulse || '-'} | Temp {app.temp || '-'}°F</span>
<span className="badge bg-success">Vitals ✓</span>
</div>
)}
{/* Nursing Action Buttons: [ Fill Vitals ] & [ Scan Document ] */}
<div className="d-flex gap-2">
<button
className={`btn ${app.has_vitals ? 'btn-success' : 'btn-outline-success'} w-50 py-2 fw-bold rounded-3 shadow-sm`}
onClick={() => openOpdVitalsModal(app)}
>
<i className="bi bi-heart-pulse me-1"></i> {app.has_vitals ? 'Vitals ✓' : 'Fill Vitals'}
</button>
<button
className="btn btn-outline-primary w-50 py-2 fw-bold rounded-3 shadow-sm"
onClick={() => openScanModal(app, 'opd')}
>
<i className="bi bi-camera me-1"></i> Scan / Photo
</button>
</div>
</div>
))
)}
</div>
</div>
)}
</div>
) : (
/* -------------------------------------------------------------
IPD BEDSIDE WORKSPACE FOR NURSING STAFF
------------------------------------------------------------- */
<div>
<div className="d-flex justify-content-between align-items-center mb-3">
<button className="btn btn-sm btn-outline-dark rounded-pill px-3" onClick={() => setActiveBed(null)}>
<i className="bi bi-arrow-left me-1"></i> Back to Bed List
</button>
<button className="btn btn-sm btn-outline-primary fw-bold rounded-pill px-3" onClick={() => openScanModal(activeBed, 'ipd')}>
<i className="bi bi-camera me-1"></i> Scan Paper Doc
</button>
</div>
<div className="card shadow-sm mb-3 border-0 bg-primary text-white rounded-4" style={{background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)'}}>
<div className="card-body p-3">
<h5 className="fw-bold mb-1">{activeBed.patient_name}</h5>
<div className="d-flex flex-wrap gap-2 opacity-90 small mt-2">
<span className="badge bg-white text-dark">Bed {activeBed.bed_number} ({activeBed.ward_name})</span>
<span className="badge bg-white text-danger">UHID: {activeBed.uhid}</span>
<span className="badge bg-white text-primary">IPD: {activeBed.ipd_code}</span>
</div>
</div>
</div>
{/* Form Tabs for Bedside Care */}
<ul className="nav nav-pills mb-3 bg-white p-1 rounded-3 border shadow-sm">
<li className="nav-item flex-fill text-center">
<button className={`nav-link w-100 fw-bold ${activeTab === 'vitals' ? 'active' : ''}`} onClick={() => setActiveTab('vitals')}>
<i className="bi bi-heart-pulse me-1"></i> Vitals ({workspaceData.filter(e => e.entry_type === 'vitals').length})
</button>
</li>
<li className="nav-item flex-fill text-center">
<button className={`nav-link w-100 fw-bold ${activeTab === 'fluid' ? 'active' : ''}`} onClick={() => setActiveTab('fluid')}>
<i className="bi bi-droplet me-1"></i> Fluid I/O ({workspaceData.filter(e => e.entry_type === 'fluid').length})
</button>
</li>
<li className="nav-item flex-fill text-center">
<button className={`nav-link w-100 fw-bold ${activeTab === 'treatment' ? 'active' : ''}`} onClick={() => setActiveTab('treatment')}>
<i className="bi bi-clipboard2-pulse me-1"></i> Treatment &amp; Orders ({workspaceData.filter(e => e.entry_type === 'treatment').length})
</button>
</li>
</ul>
{/* TAB 1: VITALS (FORM & HISTORY TABLE) */}
{activeTab === 'vitals' && (
<div>
<div className="card p-3 shadow-sm border-0 mb-3 rounded-4">
<h6 className="fw-bold mb-3 text-dark"><i className="bi bi-heart-pulse-fill text-danger me-1"></i> Record Bedside Vitals</h6>
<div className="row g-2 mb-3">
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">BP Sys/Dia (mmHg)</label>
<div className="input-group input-group-sm">
<input type="number" className="form-control text-center" placeholder="120" value={vitalsForm.bp_systolic} onChange={e => setVitalsForm({...vitalsForm, bp_systolic: e.target.value})} />
<span className="input-group-text">/</span>
<input type="number" className="form-control text-center" placeholder="80" value={vitalsForm.bp_diastolic} onChange={e => setVitalsForm({...vitalsForm, bp_diastolic: e.target.value})} />
</div>
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Pulse Rate (bpm)</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="72" value={vitalsForm.pulse_rate} onChange={e => setVitalsForm({...vitalsForm, pulse_rate: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">SpO2 %</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="98" value={vitalsForm.spo2} onChange={e => setVitalsForm({...vitalsForm, spo2: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Temp °F</label>
<input type="number" step="0.1" className="form-control form-control-sm text-center" placeholder="98.6" value={vitalsForm.temperature_f} onChange={e => setVitalsForm({...vitalsForm, temperature_f: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Resp Rate (/min)</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="18" value={vitalsForm.resp_rate} onChange={e => setVitalsForm({...vitalsForm, resp_rate: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Weight (kg)</label>
<input type="number" step="0.1" className="form-control form-control-sm text-center" placeholder="70" value={vitalsForm.weight_kg} onChange={e => setVitalsForm({...vitalsForm, weight_kg: e.target.value})} />
</div>
<div className="col-12 col-md-6">
<label className="form-label small mb-1 fw-bold">Nursing Remark / Observation</label>
<input type="text" className="form-control form-control-sm" placeholder="e.g. Patient resting, vitals stable" value={vitalsForm.general_note} onChange={e => setVitalsForm({...vitalsForm, general_note: e.target.value})} />
</div>
</div>
<button className="btn btn-success fw-bold py-2 w-100 rounded-3 shadow-sm" onClick={() => handleSaveIpdEntry('vitals', vitalsForm)}>
<i className="bi bi-check-circle-fill me-1"></i> Save Bedside Vitals
</button>
</div>
{/* Vitals History Table */}
<div className="card shadow-sm border-0 rounded-4 overflow-hidden mb-3">
<div className="card-header py-2 bg-white d-flex justify-content-between align-items-center border-bottom">
<strong className="text-dark"><i className="bi bi-clock-history me-1 text-danger"></i> Vitals History Records</strong>
<span className="badge bg-danger-subtle text-danger border border-danger-subtle">{workspaceData.filter(e => e.entry_type === 'vitals').length} Records</span>
</div>
<div className="table-responsive">
<table className="table table-sm table-striped table-hover align-middle mb-0" style={{fontSize: '12px'}}>
<thead className="table-light">
<tr>
<th>Date &amp; Time</th>
<th>BP (mmHg)</th>
<th>Pulse</th>
<th>Temp</th>
<th>Resp</th>
<th>SpO2</th>
<th>Weight</th>
<th>Staff</th>
<th>Remarks</th>
</tr>
</thead>
<tbody>
{workspaceData.filter(e => e.entry_type === 'vitals').length === 0 ? (
<tr>
<td colSpan="9" className="text-center text-muted py-4">No vitals records entered yet.</td>
</tr>
) : (
workspaceData.filter(e => e.entry_type === 'vitals').map(v => (
<tr key={v.id}>
<td className="fw-semibold text-nowrap">{formatDateTime(v.recorded_at)}</td>
<td><span className="badge bg-light text-dark border">{v.bp_systolic ? `${v.bp_systolic}/${v.bp_diastolic}` : '-'}</span></td>
<td>{v.pulse_rate ? `${v.pulse_rate} bpm` : '-'}</td>
<td><span className="badge bg-danger-subtle text-danger">{displayTempF(v.temperature_c)}</span></td>
<td>{v.resp_rate ? `${v.resp_rate}/min` : '-'}</td>
<td>{v.spo2 ? `${v.spo2}%` : '-'}</td>
<td>{v.weight_kg ? `${v.weight_kg} kg` : '-'}</td>
<td><small className="text-muted">{v.recorded_by || 'Staff'}</small></td>
<td><small>{v.general_note || '-'}</small></td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* 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 (
<div>
{/* Summary Balance Metric Cards */}
<div className="row g-2 mb-3">
<div className="col-4">
<div className="stat-card-intake text-center shadow-sm">
<small className="text-success fw-bold d-block mb-1" style={{fontSize: '11px'}}><i className="bi bi-arrow-down-left-circle-fill me-1"></i> Total Intake</small>
<h5 className="fw-bold text-success mb-0">{totalIn.toLocaleString()} <span style={{fontSize: '11px'}}>ml</span></h5>
</div>
</div>
<div className="col-4">
<div className="stat-card-output text-center shadow-sm">
<small className="text-warning fw-bold d-block mb-1" style={{fontSize: '11px'}}><i className="bi bi-arrow-up-right-circle-fill me-1"></i> Total Output</small>
<h5 className="fw-bold text-warning mb-0">{totalOut.toLocaleString()} <span style={{fontSize: '11px'}}>ml</span></h5>
</div>
</div>
<div className="col-4">
<div className="stat-card-balance text-center shadow-sm">
<small className="text-primary fw-bold d-block mb-1" style={{fontSize: '11px'}}><i className="bi bi-calculator me-1"></i> Difference (In-Out)</small>
<h5 className={`fw-bold mb-0 ${netDiff >= 0 ? 'text-primary' : 'text-danger'}`}>
{netDiff > 0 ? `+${netDiff.toLocaleString()}` : netDiff.toLocaleString()} <span style={{fontSize: '11px'}}>ml</span>
</h5>
</div>
</div>
</div>
{/* Add Fluid Entry Form */}
<div className="card p-3 shadow-sm border-0 mb-3 rounded-4">
<h6 className="fw-bold mb-3 text-dark"><i className="bi bi-droplet-fill text-primary me-1"></i> Add Fluid Intake / Output Entry</h6>
<div className="mb-3">
<label className="form-label small fw-bold mb-1">Direction</label>
<div className="d-flex gap-2">
<button
type="button"
className={`btn btn-sm flex-fill py-2 fw-bold rounded-3 ${fluidForm.fluid_direction === 'intake' ? 'btn-success text-white shadow-sm' : 'btn-light border text-secondary'}`}
onClick={() => setFluidForm({...fluidForm, fluid_direction: 'intake', fluid_route: 'IV (Intravenous)'})}
>
<i className="bi bi-arrow-down-left-circle me-1"></i> Fluid Intake (In)
</button>
<button
type="button"
className={`btn btn-sm flex-fill py-2 fw-bold rounded-3 ${fluidForm.fluid_direction === 'output' ? 'btn-warning text-dark shadow-sm' : 'btn-light border text-secondary'}`}
onClick={() => setFluidForm({...fluidForm, fluid_direction: 'output', fluid_route: 'Urine / Foley Catheter'})}
>
<i className="bi bi-arrow-up-right-circle me-1"></i> Fluid Output (Out)
</button>
</div>
</div>
<div className="row g-2 mb-3">
<div className="col-12 col-md-6">
<label className="form-label small fw-bold mb-1">Route / Site</label>
<select
className="form-select form-select-sm"
value={fluidForm.fluid_route}
onChange={e => setFluidForm({...fluidForm, fluid_route: e.target.value})}
>
{(fluidForm.fluid_direction === 'intake' ? intakeRoutes : outputRoutes).map(r => (
<option key={r} value={r}>{r}</option>
))}
</select>
</div>
<div className="col-12 col-md-6">
<label className="form-label small fw-bold mb-1">Amount (ml) <span className="text-danger">*</span></label>
<input
type="number"
className="form-control form-control-sm text-center fw-bold fs-6"
placeholder="e.g. 500"
value={fluidForm.fluid_amount_ml}
onChange={e => setFluidForm({...fluidForm, fluid_amount_ml: e.target.value})}
/>
</div>
<div className="col-12">
<label className="form-label small fw-bold mb-1">Observation / Note</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. Clear urine, Normal fluid color, IV site intact"
value={fluidForm.general_note}
onChange={e => setFluidForm({...fluidForm, general_note: e.target.value})}
/>
</div>
</div>
<button
className="btn btn-primary fw-bold py-2 w-100 rounded-3 shadow-sm"
onClick={() => {
if (!fluidForm.fluid_amount_ml) {
alert('Please enter fluid amount in ml.');
return;
}
handleSaveIpdEntry('fluid', fluidForm);
}}
>
<i className="bi bi-plus-circle-fill me-1"></i> Save Fluid Entry
</button>
</div>
{/* Fluid History Table */}
<div className="card shadow-sm border-0 rounded-4 overflow-hidden mb-3">
<div className="card-header py-2 bg-white d-flex justify-content-between align-items-center border-bottom">
<strong className="text-dark"><i className="bi bi-clock-history me-1 text-primary"></i> Fluid Intake &amp; Output Log</strong>
<span className="badge bg-primary-subtle text-primary border border-primary-subtle">{fluidList.length} Records</span>
</div>
<div className="table-responsive">
<table className="table table-sm table-striped table-hover align-middle mb-0" style={{fontSize: '12px'}}>
<thead className="table-light">
<tr>
<th>Date &amp; Time</th>
<th>Direction</th>
<th>Route</th>
<th>Amount (ml)</th>
<th>Observation / Note</th>
<th>Staff</th>
</tr>
</thead>
<tbody>
{fluidList.length === 0 ? (
<tr>
<td colSpan="6" className="text-center text-muted py-4">No fluid entries recorded yet.</td>
</tr>
) : (
fluidList.map(fl => (
<tr key={fl.id}>
<td className="fw-semibold text-nowrap">{formatDateTime(fl.recorded_at)}</td>
<td>
<span className={`badge ${fl.fluid_direction === 'intake' ? 'bg-success' : 'bg-warning text-dark'} text-uppercase`}>
{fl.fluid_direction === 'intake' ? '↓ Intake' : '↑ Output'}
</span>
</td>
<td className="fw-semibold">{fl.fluid_route || '-'}</td>
<td className="fw-bold">{parseFloat(fl.fluid_amount_ml || 0).toLocaleString()} ml</td>
<td><small>{fl.general_note || '-'}</small></td>
<td><small className="text-muted">{fl.recorded_by || 'Staff'}</small></td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
})()}
{/* 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 (
<div>
{/* DOCTOR TREATMENT REFERENCE CARD */}
<div className="card shadow-sm border-0 mb-3 rounded-4 overflow-hidden" style={{border: '1.5px solid #818cf8'}}>
<div className="card-header py-2 text-white d-flex justify-content-between align-items-center" style={{background: 'linear-gradient(135deg, #3730a3 0%, #4f46e5 100%)'}}>
<div className="d-flex align-items-center gap-2">
<i className="bi bi-file-earmark-medical-fill"></i>
<strong className="small">Doctor's Prescribed Treatment &amp; Orders</strong>
</div>
<span className="badge bg-white text-dark small">{doctorOrders.length} Doctor Orders</span>
</div>
<div className="card-body p-3 bg-light">
{latestDocOrder ? (
<div>
<div className="d-flex justify-content-between align-items-start mb-2">
<span className="badge bg-primary px-2 py-1">
<i className="bi bi-person-badge me-1"></i> {latestDocOrder.recorded_by}
</span>
<small className="text-muted fw-semibold">{formatDateTime(latestDocOrder.recorded_at)}</small>
</div>
<div className="p-3 bg-white rounded-3 border mb-2 shadow-sm">
{renderNoteContent(latestDocOrder)}
</div>
<button
className="btn btn-sm btn-outline-primary fw-bold w-100 rounded-pill py-2"
onClick={() => {
const cleanText = (latestDocOrder.treatment_text || '')
.replace(/\[IMAGE_ATTACHMENT:.*?\]/, '')
.replace('[Doctor Note] ', '')
.trim();
setTreatmentForm({
...treatmentForm,
treatment_text: `Administered as prescribed: ${cleanText}`
});
}}
>
<i className="bi bi-clipboard-check-fill me-1"></i> Follow &amp; Administer This Order
</button>
{doctorOrders.length > 1 && (
<div className="mt-3">
<button
type="button"
className="btn btn-sm btn-light border w-100 text-secondary"
onClick={() => setShowAllDoctorOrders(!showAllDoctorOrders)}
>
<i className={`bi bi-chevron-${showAllDoctorOrders ? 'up' : 'down'} me-1`}></i>
{showAllDoctorOrders ? 'Hide Previous Doctor Notes' : `View All ${doctorOrders.length} Doctor Notes`}
</button>
{showAllDoctorOrders && (
<div className="mt-2 d-flex flex-column gap-2">
{doctorOrders.slice(1).map(docOrd => (
<div key={docOrd.id} className="p-2.5 bg-white rounded-3 border">
<div className="d-flex justify-content-between align-items-center mb-1">
<strong className="text-primary small">{docOrd.recorded_by}</strong>
<small className="text-muted" style={{fontSize: '11px'}}>{formatDateTime(docOrd.recorded_at)}</small>
</div>
{renderNoteContent(docOrd)}
</div>
))}
</div>
)}
</div>
)}
</div>
) : (
<div className="text-center text-muted py-3">
<i className="bi bi-clipboard-x fs-2 text-secondary opacity-50 d-block mb-1"></i>
<small>No doctor orders or clinical prescriptions recorded yet.</small>
</div>
)}
</div>
</div>
{/* NURSING TREATMENT GIVEN FORM */}
<div className="card p-3 shadow-sm border-0 mb-3 rounded-4">
<h6 className="fw-bold mb-3 text-dark"><i className="bi bi-capsule text-success me-1"></i> Log Nursing Treatment Given / Medication Administered</h6>
<div className="mb-3">
<label className="form-label small fw-bold mb-1">Treatment / Medication Administered <span className="text-danger">*</span></label>
<textarea
className="form-control rounded-3"
rows="3"
placeholder="e.g. Inj. Ceftriaxone 1g IV given slowly, Tab Pantop 40mg given orally..."
value={treatmentForm.treatment_text}
onChange={e => setTreatmentForm({...treatmentForm, treatment_text: e.target.value})}
></textarea>
</div>
<div className="mb-3">
<label className="form-label small fw-bold mb-1">Observation / Patient Response</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. Tolerated well, No adverse reaction, IV site patent"
value={treatmentForm.general_note}
onChange={e => setTreatmentForm({...treatmentForm, general_note: e.target.value})}
/>
</div>
<button
className="btn btn-success fw-bold py-2 w-100 rounded-3 shadow-sm"
onClick={() => {
if (!treatmentForm.treatment_text.trim()) {
alert('Please enter treatment or medication administered.');
return;
}
handleSaveIpdEntry('treatment', treatmentForm);
}}
>
<i className="bi bi-send-check-fill me-1"></i> Log Treatment Administration
</button>
</div>
{/* NURSING TREATMENT HISTORY LOG WITH TIME */}
<div className="card shadow-sm border-0 rounded-4 overflow-hidden mb-3">
<div className="card-header py-2 bg-white d-flex justify-content-between align-items-center border-bottom">
<strong className="text-dark"><i className="bi bi-clock-history me-1 text-success"></i> Nursing Treatment Given History</strong>
<span className="badge bg-success-subtle text-success border border-success-subtle">{nurseTreatments.length} Records</span>
</div>
<div className="table-responsive">
<table className="table table-sm table-striped table-hover align-middle mb-0" style={{fontSize: '12px'}}>
<thead className="table-light">
<tr>
<th style={{width: '140px'}}>Time Administered</th>
<th>Treatment / Medication Given</th>
<th>Observation &amp; Remarks</th>
<th style={{width: '120px'}}>Staff Nurse</th>
</tr>
</thead>
<tbody>
{nurseTreatments.length === 0 ? (
<tr>
<td colSpan="4" className="text-center text-muted py-4">No nursing treatments logged yet.</td>
</tr>
) : (
nurseTreatments.map(tr => (
<tr key={tr.id}>
<td className="fw-semibold text-nowrap">{formatDateTime(tr.recorded_at)}</td>
<td>
<div className="fw-bold text-dark">{renderNoteContent(tr)}</div>
</td>
<td><small className="text-muted">{tr.general_note || '-'}</small></td>
<td>
<span className="badge bg-secondary-subtle text-secondary border">
{tr.recorded_by || 'Staff Nurse'}
</span>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
})()}
</div>
)}
{/* -------------------------------------------------------------
MODAL: FILL OPD VITALS
------------------------------------------------------------- */}
{activeOpdForVitals && (
<div className="modal show d-block" style={{backgroundColor: 'rgba(0,0,0,0.5)'}}>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content rounded-4 border-0 shadow-lg">
<div className="modal-header bg-success text-white">
<h6 className="modal-title fw-bold">
<i className="bi bi-heart-pulse-fill me-2"></i> Fill OPD Patient Vitals
</h6>
<button className="btn-close btn-close-white" onClick={() => setActiveOpdForVitals(null)}></button>
</div>
<div className="modal-body p-3">
<div className="alert alert-light border py-2 mb-3 small">
<strong>{activeOpdForVitals.patient_display_name}</strong> ({activeOpdForVitals.age_display} • {activeOpdForVitals.gender_label})
<br /><small className="text-muted">UHID: {activeOpdForVitals.uhid} | OPD Code: {activeOpdForVitals.opd_code}</small>
</div>
<div className="row g-2">
<div className="col-6">
<label className="form-label small fw-bold mb-1">BP Sys/Dia</label>
<div className="input-group input-group-sm">
<input type="number" className="form-control text-center" placeholder="120" value={opdVitalsForm.bp_systolic} onChange={e => setOpdVitalsForm({...opdVitalsForm, bp_systolic: e.target.value})} />
<span className="input-group-text">/</span>
<input type="number" className="form-control text-center" placeholder="80" value={opdVitalsForm.bp_diastolic} onChange={e => setOpdVitalsForm({...opdVitalsForm, bp_diastolic: e.target.value})} />
</div>
</div>
<div className="col-6">
<label className="form-label small fw-bold mb-1">Pulse Rate (bpm)</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="72" value={opdVitalsForm.pulse} onChange={e => setOpdVitalsForm({...opdVitalsForm, pulse: e.target.value})} />
</div>
<div className="col-6">
<label className="form-label small fw-bold mb-1">SpO2 %</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="98" value={opdVitalsForm.spo2} onChange={e => setOpdVitalsForm({...opdVitalsForm, spo2: e.target.value})} />
</div>
<div className="col-6">
<label className="form-label small fw-bold mb-1">Temp °F</label>
<input type="number" step="0.1" className="form-control form-control-sm text-center" placeholder="98.6" value={opdVitalsForm.temp} onChange={e => setOpdVitalsForm({...opdVitalsForm, temp: e.target.value})} />
</div>
<div className="col-6">
<label className="form-label small fw-bold mb-1">Weight (kg)</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="70" value={opdVitalsForm.weight} onChange={e => setOpdVitalsForm({...opdVitalsForm, weight: e.target.value})} />
</div>
<div className="col-6">
<label className="form-label small fw-bold mb-1">Height (cm)</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="165" value={opdVitalsForm.height} onChange={e => setOpdVitalsForm({...opdVitalsForm, height: e.target.value})} />
</div>
</div>
</div>
<div className="modal-footer p-2 bg-light">
<button className="btn btn-secondary btn-sm" onClick={() => setActiveOpdForVitals(null)}>Cancel</button>
<button className="btn btn-success btn-sm fw-bold px-3" onClick={handleSaveOpdVitals}>
<i className="bi bi-check-circle-fill me-1"></i> Save Vitals for Doctor
</button>
</div>
</div>
</div>
</div>
)}
{/* -------------------------------------------------------------
MODAL: SCAN / PHOTO UPLOAD FOR PAPER DOCUMENTS
------------------------------------------------------------- */}
{scanTarget && (
<div className="modal show d-block" style={{backgroundColor: 'rgba(0,0,0,0.5)'}}>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content rounded-4 border-0 shadow-lg">
<div className="modal-header bg-primary text-white">
<h6 className="modal-title fw-bold">
<i className="bi bi-camera-fill me-2"></i> Scan / Photo Paper Document
</h6>
<button className="btn-close btn-close-white" onClick={() => setScanTarget(null)}></button>
</div>
<div className="modal-body p-3">
<div className="mb-3">
<label className="form-label small fw-bold">Document Type / Category</label>
<select className="form-select form-select-sm" value={scanDocCategory} onChange={e => setScanDocCategory(e.target.value)}>
<option value="Paper Document">Paper Document (General)</option>
<option value="Vitals Sheet">Vitals Sheet / Chart</option>
<option value="Lab Test Report">Lab / Pathology Report</option>
<option value="Handwritten Doctor Note">Handwritten Doctor Note</option>
<option value="Consent Form">Signed Consent Form</option>
</select>
</div>
<div className="mb-3 text-center">
<label className="btn btn-primary btn-lg w-100 py-3 fw-bold rounded-3 shadow-sm">
<i className="bi bi-camera me-2"></i> Take Photo / Choose File
<input type="file" accept="image/*" capture="environment" className="d-none" onChange={handleScanImageCapture} />
</label>
</div>
{scanImagePreview && (
<div className="position-relative p-2 border rounded-3 bg-light text-center">
<span className="badge bg-success position-absolute top-0 start-0 m-2"><i className="bi bi-check-circle me-1"></i> Document Photo Captured</span>
<img src={scanImagePreview} alt="Document Scan Preview" className="img-fluid rounded-3 border shadow-sm" style={{maxHeight: '220px', objectFit: 'contain'}} />
</div>
)}
</div>
<div className="modal-footer p-2 bg-light">
<button className="btn btn-secondary btn-sm" onClick={() => setScanTarget(null)}>Cancel</button>
<button className="btn btn-success btn-sm fw-bold px-3" onClick={handleSaveScanDocument} disabled={!scanImageBase64}>
<i className="bi bi-cloud-upload-fill me-1"></i> Save Document to Chart
</button>
</div>
</div>
</div>
</div>
)}
{/* DOCUMENT PHOTO CROPPER MODAL */}
{cropSrc && (
<div className="modal show d-block" style={{backgroundColor: 'rgba(0,0,0,0.85)', zIndex: 10000}}>
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content rounded-4 border-0 shadow-lg overflow-hidden">
<div className="modal-header bg-dark text-white py-2">
<h6 className="modal-title fw-bold mb-0">
<i className="bi bi-crop me-2 text-success"></i> Crop Document Photo
</h6>
<button className="btn-close btn-close-white" onClick={cancelCrop}></button>
</div>
<div className="modal-body p-2 text-center bg-black" style={{maxHeight: '65vh', overflow: 'hidden'}}>
<div style={{maxHeight: '60vh', width: '100%'}}>
<img ref={imageRef} src={cropSrc} alt="Crop Target" style={{maxWidth: '100%', display: 'block'}} />
</div>
</div>
<div className="modal-footer p-2 bg-dark d-flex justify-content-between align-items-center flex-wrap gap-2">
<div className="d-flex gap-1">
<button className="btn btn-sm btn-outline-light px-2" onClick={() => cropper && cropper.rotate(-90)} title="Rotate Left 90°">
<i className="bi bi-arrow-counterclockwise"></i> 90°
</button>
<button className="btn btn-sm btn-outline-light px-2" onClick={() => cropper && cropper.rotate(90)} title="Rotate Right 90°">
<i className="bi bi-arrow-clockwise"></i> 90°
</button>
<button className="btn btn-sm btn-outline-light px-2" onClick={() => cropper && cropper.reset()} title="Reset Crop Box">
<i className="bi bi-aspect-ratio"></i> Reset
</button>
</div>
<div className="d-flex gap-2">
<button className="btn btn-sm btn-secondary" onClick={cancelCrop}>Cancel</button>
<button className="btn btn-sm btn-success fw-bold px-3" onClick={applyCrop}>
<i className="bi bi-check-lg me-1"></i> Apply Crop &amp; Save Photo
</button>
</div>
</div>
</div>
</div>
</div>
)}
{/* -------------------------------------------------------------
FLOATING BOTTOM QUICK ACTION BAR (DOCK)
------------------------------------------------------------- */}
<div className="bottom-dock-container">
<button
className="bottom-dock-btn btn-scan"
onClick={() => openPatientSearch('scan')}
title="Scan Barcode / QR or Search Patient"
>
<i className="bi bi-qr-code-scan fs-5"></i>
<span>Scan / Search</span>
</button>
<button
className="bottom-dock-btn btn-vitals"
onClick={() => {
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"
>
<i className="bi bi-heart-pulse-fill fs-5"></i>
<span>ABDM Vitals</span>
</button>
<button
className="bottom-dock-btn btn-doc"
onClick={() => {
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)"
>
<i className="bi bi-file-earmark-medical-fill fs-5"></i>
<span>ABDM Health Doc</span>
</button>
</div>
{/* -------------------------------------------------------------
MODAL 1: SEARCH PATIENT BY BARCODE, QR CODE & TEXT (ABDM M2)
------------------------------------------------------------- */}
{showPatientSearchModal && (
<div className="modal show d-block" style={{backgroundColor: 'rgba(0,0,0,0.65)', zIndex: 1050}}>
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content rounded-4 border-0 shadow-lg overflow-hidden">
<div className="modal-header bg-dark text-white py-2.5">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-upc-scan fs-5 text-primary"></i>
<div>
<h6 className="modal-title fw-bold mb-0">Search Patient by Barcode, QR &amp; Name</h6>
<small className="text-white-50" style={{fontSize: '11px'}}>Scan Hospital Card Barcode, ABHA QR, or search registered patients</small>
</div>
</div>
<button className="btn-close btn-close-white" onClick={closePatientSearch}></button>
</div>
<div className="modal-body p-3">
{/* Segmented Mode Selector */}
<div className="segmented-tabs mb-3">
<button
className={`segmented-tab ${searchModalTab === 'scan' ? 'active-blue' : ''}`}
onClick={() => {
setSearchModalTab('scan');
startBarcodeQrScanner();
}}
>
<i className="bi bi-camera-video-fill me-1"></i> Camera Scanner (Barcode &amp; QR)
</button>
<button
className={`segmented-tab ${searchModalTab === 'type' ? 'active-blue' : ''}`}
onClick={() => {
setSearchModalTab('type');
stopBarcodeQrScanner();
}}
>
<i className="bi bi-search me-1"></i> Manual Search (UHID / Mobile / ABHA)
</button>
</div>
{/* CAMERA SCANNER TAB */}
{searchModalTab === 'scan' && (
<div className="text-center mb-3">
<div className="qr-scanner-wrapper mx-auto mb-2" style={{maxWidth: '420px'}}>
<div id="patient-qr-reader" style={{width: '100%'}}></div>
</div>
{scannerError ? (
<div className="alert alert-warning py-2 px-3 small d-inline-block">
<i className="bi bi-exclamation-triangle-fill me-1"></i> {scannerError}
</div>
) : (
<small className="text-muted d-block">
<i className="bi bi-info-circle me-1"></i> Point camera at <strong>Patient Card Barcode</strong> or <strong>ABHA QR Code</strong> to scan instantly.
</small>
)}
</div>
)}
{/* SEARCH INPUT */}
<div className="input-group mb-3">
<span className="input-group-text bg-white border-end-0">
<i className="bi bi-search text-muted"></i>
</span>
<input
type="text"
className="form-control border-start-0 ps-0"
placeholder="Search by UHID (P...), ABHA No, Mobile (98...), Name..."
value={patientSearchQuery}
onChange={e => {
setPatientSearchQuery(e.target.value);
executePatientSearch(e.target.value);
}}
onKeyDown={e => {
if (e.key === 'Enter') executePatientSearch(patientSearchQuery);
}}
/>
{patientSearchQuery && (
<button className="btn btn-outline-secondary" onClick={() => { setPatientSearchQuery(''); setPatientSearchResults([]); }}>
<i className="bi bi-x"></i>
</button>
)}
<button className="btn btn-primary fw-bold px-3" onClick={() => executePatientSearch(patientSearchQuery)}>
{searchingPatient ? <span className="spinner-border spinner-border-sm"></span> : 'Search'}
</button>
</div>
{/* SEARCH RESULTS LIST */}
<div>
<div className="d-flex justify-content-between align-items-center mb-2">
<strong className="small text-secondary text-uppercase">
Matched Patients ({patientSearchResults.length})
</strong>
{searchingPatient && <span className="badge bg-primary-subtle text-primary">Searching...</span>}
</div>
{patientSearchResults.length === 0 ? (
<div className="text-center py-4 bg-light rounded-3 border">
<i className="bi bi-person-badge fs-2 text-muted opacity-50 d-block mb-1"></i>
<p className="small text-muted mb-0">
{patientSearchQuery ? 'No patients found matching this query.' : 'Type patient name, UHID, ABHA number or scan barcode above.'}
</p>
</div>
) : (
<div className="d-flex flex-column gap-2" style={{maxHeight: '40vh', overflowY: 'auto'}}>
{patientSearchResults.map(pat => (
<div key={pat.id} className="patient-search-result-card">
<div className="d-flex justify-content-between align-items-start flex-wrap gap-2">
<div>
<div className="d-flex align-items-center gap-2">
<h6 className="fw-bold mb-0 text-dark">{pat.fullName}</h6>
<span className="badge bg-light text-dark border" style={{fontSize: '11px'}}>
{pat.gender === '1' || pat.gender === 1 ? 'Male' : (pat.gender === '2' || pat.gender === 2 ? 'Female' : 'Other')}
{pat.dob ? ` • DOB: ${pat.dob}` : ''}
</span>
</div>
<div className="d-flex flex-wrap gap-2 mt-1 small">
<span className="text-danger fw-semibold">UHID: {pat.p_code}</span>
{pat.mphone1 && <span className="text-muted"><i className="bi bi-telephone me-1"></i>{pat.mphone1}</span>}
{pat.abha_id && (
<span className="badge bg-success-subtle text-success border border-success-subtle">
<i className="bi bi-shield-check me-1"></i>ABHA: {pat.abha_id}
</span>
)}
{pat.abha_address && (
<span className="badge bg-primary-subtle text-primary border border-primary-subtle">
{pat.abha_address}
</span>
)}
</div>
{pat.ipd_admission && (
<div className="mt-1">
<span className="badge bg-primary">
<i className="bi bi-hospital me-1"></i> Admitted IPD: {pat.ipd_admission.ipd_code} (Bed {pat.ipd_admission.bed_number || pat.ipd_admission.bed_no})
</span>
</div>
)}
{pat.opd_today && (
<div className="mt-1">
<span className="badge bg-success">
<i className="bi bi-calendar-check me-1"></i> OPD Today: Token #{pat.opd_today.token_no} ({pat.opd_today.doctor_name || 'Doctor'})
</span>
</div>
)}
</div>
{/* 1-Click Action Buttons */}
<div className="d-flex flex-wrap gap-1.5 align-items-center">
<button
className="btn btn-sm btn-success fw-bold rounded-3 px-2 py-1.5"
onClick={() => openWellnessModal(pat)}
title="Add ABDM M2 Wellness Vitals"
>
<i className="bi bi-heart-pulse-fill me-1"></i> Add Vitals
</button>
<button
className="btn btn-sm btn-primary fw-bold rounded-3 px-2 py-1.5"
onClick={() => openDocModal(pat)}
title="Add ABDM M2 Health Document"
>
<i className="bi bi-file-earmark-medical-fill me-1"></i> Add Doc
</button>
<button
className="btn btn-sm btn-outline-secondary rounded-3 px-2 py-1.5"
onClick={() => openWellnessHistory(pat)}
title="View Vitals History"
>
<i className="bi bi-clock-history"></i>
</button>
{pat.ipd_admission && (
<button
className="btn btn-sm btn-dark rounded-3 px-2 py-1.5"
onClick={() => {
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"
>
<i className="bi bi-arrow-right-circle"></i> IPD Bed
</button>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
<div className="modal-footer p-2 bg-light d-flex justify-content-between">
<small className="text-muted"><i className="bi bi-shield-lock me-1"></i> ABDM M2 Standard Compliance</small>
<button className="btn btn-secondary btn-sm px-3" onClick={closePatientSearch}>Close</button>
</div>
</div>
</div>
</div>
)}
{/* -------------------------------------------------------------
MODAL 2: ABDM M2 WELLNESS & VITALS ENTRY (COMPREHENSIVE SUITE)
------------------------------------------------------------- */}
{activeWellnessPatient && (
<div className="modal show d-block" style={{backgroundColor: 'rgba(0,0,0,0.65)', zIndex: 1060}}>
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content rounded-4 border-0 shadow-lg overflow-hidden">
<div className="modal-header bg-success text-white py-2.5">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-heart-pulse-fill fs-5"></i>
<div>
<h6 className="modal-title fw-bold mb-0">Record ABDM M2 Wellness &amp; Vitals</h6>
<small className="text-white-50" style={{fontSize: '11px'}}>NRCES FHIR R4 Compliant Comprehensive Health &amp; Wellness Suite</small>
</div>
</div>
<button className="btn-close btn-close-white" onClick={() => setActiveWellnessPatient(null)}></button>
</div>
<div className="modal-body p-3">
{/* Patient Context Banner */}
<div className="p-2.5 rounded-3 bg-light border mb-3">
<div className="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<strong className="fs-6 text-dark me-2">{activeWellnessPatient.fullName || activeWellnessPatient.p_fname}</strong>
<span className="badge bg-secondary me-1">{activeWellnessPatient.gender ? activeWellnessPatient.gender.toUpperCase() : 'PATIENT'}</span>
{activeWellnessPatient.age && <span className="badge bg-light text-dark border me-1">{activeWellnessPatient.age} Yrs</span>}
<div className="small text-muted mt-0.5">
UHID: <span className="text-danger fw-bold">{activeWellnessPatient.p_code || activeWellnessPatient.uhid}</span>
{activeWellnessPatient.mphone1 && ` • Phone: ${activeWellnessPatient.mphone1}`}
</div>
</div>
<div>
{activeWellnessPatient.abha_id ? (
<span className="badge bg-success">
<i className="bi bi-shield-check me-1"></i> ABHA: {activeWellnessPatient.abha_id}
</span>
) : (
<span className="badge bg-secondary-subtle text-secondary border">
<i className="bi bi-shield-exclamation me-1"></i> ABHA Not Linked
</span>
)}
</div>
</div>
</div>
{/* Category Tab Switcher */}
<ul className="nav nav-pills nav-fill bg-light p-1 rounded-3 mb-3 border small">
<li className="nav-item">
<button
type="button"
className={`nav-link py-1.5 px-2 fw-semibold d-flex align-items-center justify-content-center gap-1 ${wellnessModalTab === 'vitals' ? 'active shadow-sm' : 'text-secondary'}`}
onClick={() => setWellnessModalTab('vitals')}
>
<i className="bi bi-heart-pulse-fill text-danger"></i>
<span>Vitals &amp; Triage</span>
{(wellnessForm.bp_systolic || wellnessForm.pulse_rate || wellnessForm.temperature_f) && (
<span className="badge rounded-pill bg-success p-1 ms-1" style={{fontSize: '7px'}}>✓</span>
)}
</button>
</li>
<li className="nav-item">
<button
type="button"
className={`nav-link py-1.5 px-2 fw-semibold d-flex align-items-center justify-content-center gap-1 ${wellnessModalTab === 'sugar' ? 'active shadow-sm' : 'text-secondary'}`}
onClick={() => setWellnessModalTab('sugar')}
>
<i className="bi bi-droplet-half text-danger"></i>
<span>Sugar &amp; Labs</span>
{(wellnessForm.sugar_random || wellnessForm.sugar_fasting || wellnessForm.sugar_pp || wellnessForm.hba1c || wellnessForm.hemoglobin) && (
<span className="badge rounded-pill bg-success p-1 ms-1" style={{fontSize: '7px'}}>✓</span>
)}
</button>
</li>
<li className="nav-item">
<button
type="button"
className={`nav-link py-1.5 px-2 fw-semibold d-flex align-items-center justify-content-center gap-1 ${wellnessModalTab === 'activity' ? 'active shadow-sm' : 'text-secondary'}`}
onClick={() => setWellnessModalTab('activity')}
>
<i className="bi bi-person-bounding-box text-primary"></i>
<span>Waist &amp; Activity</span>
{(wellnessForm.waist_circumference_cm || wellnessForm.hip_circumference_cm || wellnessForm.daily_steps || wellnessForm.sleep_hours) && (
<span className="badge rounded-pill bg-success p-1 ms-1" style={{fontSize: '7px'}}>✓</span>
)}
</button>
</li>
<li className="nav-item">
<button
type="button"
className={`nav-link py-1.5 px-2 fw-semibold d-flex align-items-center justify-content-center gap-1 ${wellnessModalTab === 'lifestyle' ? 'active shadow-sm' : 'text-secondary'}`}
onClick={() => setWellnessModalTab('lifestyle')}
>
<i className="bi bi-apple text-success"></i>
<span>Lifestyle &amp; Notes</span>
{(wellnessForm.diet_type || wellnessForm.tobacco_status || wellnessForm.diet_lifestyle_note || wellnessForm.general_advice) && (
<span className="badge rounded-pill bg-success p-1 ms-1" style={{fontSize: '7px'}}>✓</span>
)}
</button>
</li>
{String(activeWellnessPatient.gender || '').toLowerCase() !== 'male' && (
<li className="nav-item">
<button
type="button"
className={`nav-link py-1.5 px-2 fw-semibold d-flex align-items-center justify-content-center gap-1 ${wellnessModalTab === 'women' ? 'active shadow-sm' : 'text-secondary'}`}
onClick={() => setWellnessModalTab('women')}
>
<i className="bi bi-gender-female text-warning"></i>
<span>Women's Health</span>
{(wellnessForm.women_lmp || wellnessForm.women_pregnancy_status) && (
<span className="badge rounded-pill bg-success p-1 ms-1" style={{fontSize: '7px'}}>✓</span>
)}
</button>
</li>
)}
</ul>
{/* TAB 1: VITALS & TRIAGE */}
{wellnessModalTab === 'vitals' && (
<div className="row g-2.5 mb-2">
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">BP Sys/Dia (mmHg)</label>
<div className="input-group input-group-sm">
<input type="number" className="form-control text-center" placeholder="120" value={wellnessForm.bp_systolic} onChange={e => setWellnessForm({...wellnessForm, bp_systolic: e.target.value})} />
<span className="input-group-text px-1">/</span>
<input type="number" className="form-control text-center" placeholder="80" value={wellnessForm.bp_diastolic} onChange={e => setWellnessForm({...wellnessForm, bp_diastolic: e.target.value})} />
</div>
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Pulse Rate (bpm)</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="72" value={wellnessForm.pulse_rate} onChange={e => setWellnessForm({...wellnessForm, pulse_rate: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">SpO2 %</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="98" value={wellnessForm.spo2} onChange={e => setWellnessForm({...wellnessForm, spo2: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Temp °F</label>
<input type="number" step="0.1" className="form-control form-control-sm text-center" placeholder="98.6" value={wellnessForm.temperature_f} onChange={e => setWellnessForm({...wellnessForm, temperature_f: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Resp Rate (/min)</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="18" value={wellnessForm.resp_rate} onChange={e => setWellnessForm({...wellnessForm, resp_rate: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Height (cm)</label>
<input type="number" step="0.1" className="form-control form-control-sm text-center" placeholder="165" value={wellnessForm.height_cm} onChange={e => setWellnessForm({...wellnessForm, height_cm: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Weight (kg)</label>
<input type="number" step="0.1" className="form-control form-control-sm text-center" placeholder="70" value={wellnessForm.weight_kg} onChange={e => setWellnessForm({...wellnessForm, weight_kg: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Calculated BMI</label>
<div className="input-group input-group-sm">
<input
type="text"
className="form-control form-control-sm text-center bg-light fw-bold"
readOnly
value={
wellnessForm.height_cm && wellnessForm.weight_kg && parseFloat(wellnessForm.height_cm) > 0
? (parseFloat(wellnessForm.weight_kg) / Math.pow(parseFloat(wellnessForm.height_cm) / 100, 2)).toFixed(2)
: '-'
}
/>
</div>
</div>
<div className="col-12 mt-2">
<label className="form-label small mb-1 fw-bold d-flex justify-content-between">
<span><i className="bi bi-emoji-frown me-1 text-warning"></i> Pain Severity Score (0 - 10 VRS)</span>
<span className="text-muted fw-normal">
{wellnessForm.pain_score === '' ? 'Not assessed' : (
wellnessForm.pain_score == 0 ? '0 - No Pain' :
wellnessForm.pain_score <= 3 ? `${wellnessForm.pain_score} - Mild Pain` :
wellnessForm.pain_score <= 6 ? `${wellnessForm.pain_score} - Moderate Pain` :
`${wellnessForm.pain_score} - Severe Pain`
)}
</span>
</label>
<div className="d-flex gap-1 flex-wrap">
{[0,1,2,3,4,5,6,7,8,9,10].map(val => (
<button
key={val}
type="button"
className={`btn btn-sm flex-fill py-1 px-1.5 ${wellnessForm.pain_score === String(val) ? (val === 0 ? 'btn-success text-white' : val <= 3 ? 'btn-info text-white' : val <= 6 ? 'btn-warning text-dark' : 'btn-danger text-white') : 'btn-outline-secondary'}`}
onClick={() => setWellnessForm({...wellnessForm, pain_score: wellnessForm.pain_score === String(val) ? '' : String(val)})}
>
{val}
</button>
))}
</div>
</div>
</div>
)}
{/* TAB 2: SUGAR & POC LABS */}
{wellnessModalTab === 'sugar' && (
<div className="row g-2.5 mb-2">
<div className="col-12">
<div className="alert alert-info py-1.5 px-3 small d-flex align-items-center gap-2 mb-2">
<i className="bi bi-info-circle-fill fs-6 text-primary"></i>
<span>Point-of-care capillary blood sugar &amp; POC lab values (NRCES <strong>GeneralAssessment</strong> slice)</span>
</div>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Random Blood Sugar (RBS)</label>
<div className="input-group input-group-sm">
<input type="number" step="1" className="form-control text-center" placeholder="110" value={wellnessForm.sugar_random} onChange={e => setWellnessForm({...wellnessForm, sugar_random: e.target.value})} />
<span className="input-group-text">mg/dL</span>
</div>
<small className="text-muted" style={{fontSize: '10px'}}>Normal: &lt; 140 mg/dL</small>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Fasting Blood Sugar (FBS)</label>
<div className="input-group input-group-sm">
<input type="number" step="1" className="form-control text-center" placeholder="95" value={wellnessForm.sugar_fasting} onChange={e => setWellnessForm({...wellnessForm, sugar_fasting: e.target.value})} />
<span className="input-group-text">mg/dL</span>
</div>
<small className="text-muted" style={{fontSize: '10px'}}>Normal: 70 - 100 mg/dL</small>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Post-Prandial (PPBS)</label>
<div className="input-group input-group-sm">
<input type="number" step="1" className="form-control text-center" placeholder="130" value={wellnessForm.sugar_pp} onChange={e => setWellnessForm({...wellnessForm, sugar_pp: e.target.value})} />
<span className="input-group-text">mg/dL</span>
</div>
<small className="text-muted" style={{fontSize: '10px'}}>Normal: &lt; 140 mg/dL (2h post meal)</small>
</div>
<div className="col-6 col-md-6">
<label className="form-label small mb-1 fw-bold">HbA1c (Glycated Hemoglobin)</label>
<div className="input-group input-group-sm">
<input type="number" step="0.1" className="form-control text-center" placeholder="5.6" value={wellnessForm.hba1c} onChange={e => setWellnessForm({...wellnessForm, hba1c: e.target.value})} />
<span className="input-group-text">%</span>
</div>
<small className="text-muted" style={{fontSize: '10px'}}>Normal: &lt; 5.7% | Prediabetes: 5.7-6.4% | Diabetes: ≥ 6.5%</small>
</div>
<div className="col-6 col-md-6">
<label className="form-label small mb-1 fw-bold">POC Hemoglobin (Hb)</label>
<div className="input-group input-group-sm">
<input type="number" step="0.1" className="form-control text-center" placeholder="13.5" value={wellnessForm.hemoglobin} onChange={e => setWellnessForm({...wellnessForm, hemoglobin: e.target.value})} />
<span className="input-group-text">g/dL</span>
</div>
<small className="text-muted" style={{fontSize: '10px'}}>Normal: 12.0 - 17.5 g/dL</small>
</div>
</div>
)}
{/* TAB 3: WAIST, HIP & ACTIVITY */}
{wellnessModalTab === 'activity' && (
<div className="row g-2.5 mb-2">
<div className="col-12">
<h6 className="fw-bold small text-primary mb-1 border-bottom pb-1"><i className="bi bi-rulers me-1"></i> Body Circumferences (NRCES BodyMeasurement)</h6>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Waist Circumference</label>
<div className="input-group input-group-sm">
<input type="number" step="0.1" className="form-control text-center" placeholder="85" value={wellnessForm.waist_circumference_cm} onChange={e => setWellnessForm({...wellnessForm, waist_circumference_cm: e.target.value})} />
<span className="input-group-text">cm</span>
</div>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Hip Circumference</label>
<div className="input-group input-group-sm">
<input type="number" step="0.1" className="form-control text-center" placeholder="95" value={wellnessForm.hip_circumference_cm} onChange={e => setWellnessForm({...wellnessForm, hip_circumference_cm: e.target.value})} />
<span className="input-group-text">cm</span>
</div>
</div>
<div className="col-12 col-md-4">
<label className="form-label small mb-1 fw-bold">Waist-to-Hip Ratio (WHR)</label>
<input
type="text"
className="form-control form-control-sm text-center bg-light fw-bold"
readOnly
value={
wellnessForm.waist_circumference_cm && wellnessForm.hip_circumference_cm && parseFloat(wellnessForm.hip_circumference_cm) > 0
? (parseFloat(wellnessForm.waist_circumference_cm) / parseFloat(wellnessForm.hip_circumference_cm)).toFixed(2)
: '-'
}
/>
<small className="text-muted" style={{fontSize: '10px'}}>Normal: Men &lt; 0.90 | Women &lt; 0.85</small>
</div>
<div className="col-12 mt-2">
<h6 className="fw-bold small text-success mb-1 border-bottom pb-1"><i className="bi bi-activity me-1"></i> Physical Activity &amp; Sleep (NRCES PhysicalActivity)</h6>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Daily Steps (24h)</label>
<div className="input-group input-group-sm">
<input type="number" step="100" className="form-control text-center" placeholder="8000" value={wellnessForm.daily_steps} onChange={e => setWellnessForm({...wellnessForm, daily_steps: e.target.value})} />
<span className="input-group-text">steps</span>
</div>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Sleep Duration</label>
<div className="input-group input-group-sm">
<input type="number" step="0.5" className="form-control text-center" placeholder="7.5" value={wellnessForm.sleep_hours} onChange={e => setWellnessForm({...wellnessForm, sleep_hours: e.target.value})} />
<span className="input-group-text">hours</span>
</div>
</div>
<div className="col-12 col-md-4">
<label className="form-label small mb-1 fw-bold">Exercise Duration</label>
<div className="input-group input-group-sm">
<input type="number" step="5" className="form-control text-center" placeholder="30" value={wellnessForm.exercise_min_per_day} onChange={e => setWellnessForm({...wellnessForm, exercise_min_per_day: e.target.value})} />
<span className="input-group-text">min/day</span>
</div>
</div>
</div>
)}
{/* TAB 4: LIFESTYLE & GUIDANCE */}
{wellnessModalTab === 'lifestyle' && (
<div className="row g-2.5 mb-2">
<div className="col-12 col-md-4">
<label className="form-label small mb-1 fw-bold">Diet Pattern (ABDM)</label>
<select className="form-select form-select-sm" value={wellnessForm.diet_type} onChange={e => setWellnessForm({...wellnessForm, diet_type: e.target.value})}>
<option value="">Select diet...</option>
<option value="Balanced Diet">Balanced Diet</option>
<option value="Vegetarian">Vegetarian</option>
<option value="Lacto-Vegetarian">Lacto-Vegetarian</option>
<option value="Non-Vegetarian">Non-Vegetarian</option>
<option value="Vegan">Vegan</option>
<option value="Diabetic Diet">Diabetic Diet (Low GI)</option>
<option value="Low Sodium / DASH Diet">Low Sodium / DASH Diet</option>
<option value="Renal Diet">Renal Diet</option>
</select>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Tobacco / Smoking</label>
<select className="form-select form-select-sm" value={wellnessForm.tobacco_status} onChange={e => setWellnessForm({...wellnessForm, tobacco_status: e.target.value})}>
<option value="">Select status...</option>
<option value="Never Smoked">Never Smoked</option>
<option value="Former / Ex-Smoker">Former / Ex-Smoker</option>
<option value="Current Smoker (Occasional)">Current Smoker (Occasional)</option>
<option value="Current Smoker (Daily)">Current Smoker (Daily)</option>
<option value="Tobacco Chewer">Tobacco Chewer</option>
</select>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Alcohol Consumption</label>
<select className="form-select form-select-sm" value={wellnessForm.alcohol_status} onChange={e => setWellnessForm({...wellnessForm, alcohol_status: e.target.value})}>
<option value="">Select status...</option>
<option value="Lifetime Non-Drinker">Lifetime Non-Drinker</option>
<option value="Former Drinker">Former Drinker</option>
<option value="Occasional / Social Drinker">Occasional / Social Drinker</option>
<option value="Regular Drinker">Regular Drinker</option>
</select>
</div>
<div className="col-12">
<label className="form-label small mb-1 fw-bold">Diet &amp; Lifestyle Guidance (ABDM)</label>
<input type="text" className="form-control form-control-sm" placeholder="e.g. Low sodium diet, 30 min daily brisk walking, adequate hydration" value={wellnessForm.diet_lifestyle_note} onChange={e => setWellnessForm({...wellnessForm, diet_lifestyle_note: e.target.value})} />
</div>
<div className="col-12">
<label className="form-label small mb-1 fw-bold">General Nursing Observations</label>
<input type="text" className="form-control form-control-sm" placeholder="e.g. Patient conscious, oriented to time/place, vitals within acceptable limits" value={wellnessForm.general_advice} onChange={e => setWellnessForm({...wellnessForm, general_advice: e.target.value})} />
</div>
</div>
)}
{/* TAB 5: WOMEN'S HEALTH */}
{wellnessModalTab === 'women' && (
<div className="row g-2.5 mb-2">
<div className="col-12 col-md-6">
<label className="form-label small mb-1 fw-bold">Last Menstrual Period (LMP Date)</label>
<input type="date" className="form-control form-control-sm" value={wellnessForm.women_lmp} onChange={e => setWellnessForm({...wellnessForm, women_lmp: e.target.value})} />
</div>
<div className="col-12 col-md-6">
<label className="form-label small mb-1 fw-bold">Pregnancy / Reproductive Status</label>
<select className="form-select form-select-sm" value={wellnessForm.women_pregnancy_status} onChange={e => setWellnessForm({...wellnessForm, women_pregnancy_status: e.target.value})}>
<option value="">Select reproductive status...</option>
<option value="Not Pregnant">Not Pregnant</option>
<option value="Pregnant - 1st Trimester (1 - 12 wks)">Pregnant - 1st Trimester (1 - 12 wks)</option>
<option value="Pregnant - 2nd Trimester (13 - 26 wks)">Pregnant - 2nd Trimester (13 - 26 wks)</option>
<option value="Pregnant - 3rd Trimester (27 - 40+ wks)">Pregnant - 3rd Trimester (27 - 40+ wks)</option>
<option value="Postpartum (< 6 weeks post delivery)">Postpartum (&lt; 6 weeks post delivery)</option>
<option value="Lactating / Breastfeeding">Lactating / Breastfeeding</option>
<option value="Perimenopausal">Perimenopausal</option>
<option value="Postmenopausal">Postmenopausal</option>
</select>
</div>
</div>
)}
</div>
<div className="modal-footer p-2 bg-light d-flex justify-content-between">
<button className="btn btn-outline-secondary btn-sm" onClick={() => openWellnessHistory(activeWellnessPatient)}>
<i className="bi bi-clock-history me-1"></i> Past History
</button>
<div className="d-flex gap-2">
<button className="btn btn-secondary btn-sm" onClick={() => setActiveWellnessPatient(null)}>Cancel</button>
<button className="btn btn-success btn-sm fw-bold px-3 shadow-sm" onClick={handleSaveWellness}>
<i className="bi bi-check-circle-fill me-1"></i> Save ABDM Wellness Vitals
</button>
</div>
</div>
</div>
</div>
</div>
)}
{/* -------------------------------------------------------------
MODAL 3: ABDM M2 HEALTH DOCUMENT SCAN & UPLOAD
------------------------------------------------------------- */}
{activeDocPatient && (
<div className="modal show d-block" style={{backgroundColor: 'rgba(0,0,0,0.65)', zIndex: 1060}}>
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content rounded-4 border-0 shadow-lg overflow-hidden">
<div className="modal-header bg-primary text-white py-2.5">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-file-earmark-medical-fill fs-5"></i>
<div>
<h6 className="modal-title fw-bold mb-0">Add ABDM M2 Health Document</h6>
<small className="text-white-50" style={{fontSize: '11px'}}>Scan paper prescriptions, lab reports or clinical documents</small>
</div>
</div>
<button className="btn-close btn-close-white" onClick={() => setActiveDocPatient(null)}></button>
</div>
<div className="modal-body p-3">
{/* Patient Context Banner */}
<div className="p-2.5 rounded-3 bg-light border mb-3">
<div className="d-flex justify-content-between align-items-start flex-wrap gap-1">
<div>
<strong className="fs-6 text-dark">{activeDocPatient.fullName || activeDocPatient.p_fname}</strong>
<div className="small text-muted">
UHID: <span className="text-danger fw-bold">{activeDocPatient.p_code || activeDocPatient.uhid}</span>
</div>
</div>
{activeDocPatient.abha_id && (
<span className="badge bg-success">
<i className="bi bi-shield-check me-1"></i> ABHA: {activeDocPatient.abha_id}
</span>
)}
</div>
</div>
<div className="row g-2 mb-3">
<div className="col-12 col-md-6">
<label className="form-label small fw-bold mb-1">Document Category (ABDM HI Type)</label>
<select
className="form-select form-select-sm"
value={patientDocForm.document_type}
onChange={e => setPatientDocForm({...patientDocForm, document_type: e.target.value})}
>
<option value="Prescription">Prescription Record (PrescriptionRecord)</option>
<option value="Diagnostic Report / Lab">Diagnostic / Lab Report (DiagnosticReport)</option>
<option value="Discharge Summary">Discharge Summary (DischargeSummary)</option>
<option value="Scan / Radiology (X-Ray/CT/MRI)">Radiology / Scan (HealthDocumentRecord)</option>
<option value="Paper Medical Record">Paper Medical Record (HealthDocumentRecord)</option>
<option value="Immunization Record">Immunization Record (ImmunizationRecord)</option>
<option value="Health Document">Other Health Document (HealthDocumentRecord)</option>
</select>
</div>
<div className="col-12 col-md-6">
<label className="form-label small fw-bold mb-1">Document Date</label>
<input
type="date"
className="form-control form-control-sm"
value={patientDocForm.document_date}
onChange={e => setPatientDocForm({...patientDocForm, document_date: e.target.value})}
/>
</div>
<div className="col-12">
<label className="form-label small fw-bold mb-1">Remarks / Document Title</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. Chest X-Ray Report, Blood CBC Test, Consultation Note"
value={patientDocForm.remarks}
onChange={e => setPatientDocForm({...patientDocForm, remarks: e.target.value})}
/>
</div>
</div>
{/* Camera Capture / Photo Upload */}
<div className="mb-3 text-center">
<label className="btn btn-primary btn-lg w-100 py-3 fw-bold rounded-3 shadow-sm">
<i className="bi bi-camera me-2"></i> Take Document Photo / Choose Image
<input type="file" accept="image/*" capture="environment" className="d-none" onChange={handleScanImageCapture} />
</label>
</div>
{scanImagePreview && (
<div className="position-relative p-2 border rounded-3 bg-light text-center">
<span className="badge bg-success position-absolute top-0 start-0 m-2">
<i className="bi bi-check-circle me-1"></i> Document Photo Captured
</span>
<img src={scanImagePreview} alt="Document Scan Preview" className="img-fluid rounded-3 border shadow-sm" style={{maxHeight: '200px', objectFit: 'contain'}} />
</div>
)}
</div>
<div className="modal-footer p-2 bg-light">
<button className="btn btn-secondary btn-sm" onClick={() => setActiveDocPatient(null)}>Cancel</button>
<button className="btn btn-primary btn-sm fw-bold px-3" onClick={handleSavePatientDocument} disabled={!scanImageBase64}>
<i className="bi bi-cloud-upload-fill me-1"></i> Upload ABDM Health Document
</button>
</div>
</div>
</div>
</div>
)}
{/* -------------------------------------------------------------
MODAL 4: PATIENT VITALS & WELLNESS HISTORY (COMPREHENSIVE)
------------------------------------------------------------- */}
{wellnessHistoryPatient && (
<div className="modal show d-block" style={{backgroundColor: 'rgba(0,0,0,0.65)', zIndex: 1070}}>
<div className="modal-dialog modal-dialog-centered modal-xl">
<div className="modal-content rounded-4 border-0 shadow-lg overflow-hidden">
<div className="modal-header bg-dark text-white py-2.5">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-clock-history fs-5 text-warning"></i>
<div>
<h6 className="modal-title fw-bold mb-0">Vitals &amp; Wellness History</h6>
<small className="text-white-50" style={{fontSize: '11px'}}>
{wellnessHistoryPatient.fullName || wellnessHistoryPatient.p_fname} (UHID: {wellnessHistoryPatient.p_code || wellnessHistoryPatient.uhid})
</small>
</div>
</div>
<button className="btn-close btn-close-white" onClick={() => setWellnessHistoryPatient(null)}></button>
</div>
<div className="modal-body p-3">
{loadingHistory ? (
<div className="text-center py-4">
<div className="spinner-border text-primary" role="status"></div>
<p className="small text-muted mt-2">Loading historical vitals...</p>
</div>
) : wellnessHistoryList.length === 0 ? (
<div className="text-center py-4 bg-light rounded-3">
<i className="bi bi-heart-pulse fs-2 text-muted opacity-50 d-block mb-1"></i>
<p className="small text-muted mb-0">No past vitals or wellness entries recorded yet for this patient.</p>
</div>
) : (
<div className="table-responsive">
<table className="table table-sm table-striped table-hover align-middle mb-0" style={{fontSize: '12px'}}>
<thead className="table-light">
<tr>
<th>Date &amp; Time</th>
<th>Vitals (BP / Pulse / SpO2 / Temp)</th>
<th>Body / BMI</th>
<th>Blood Glucose (mg/dL)</th>
<th>POC Labs</th>
<th>Activity / Lifestyle</th>
<th>Staff</th>
<th>Advice / Remarks</th>
</tr>
</thead>
<tbody>
{wellnessHistoryList.map(rec => (
<tr key={rec.id}>
<td className="fw-semibold text-nowrap">{formatDateTime(rec.recorded_at)}</td>
<td>
<div className="d-flex flex-wrap gap-1 align-items-center">
{rec.bp_systolic && <span className="badge bg-light text-dark border">BP {rec.bp_systolic}/{rec.bp_diastolic}</span>}
{rec.pulse_rate && <span className="badge bg-light text-dark border">{rec.pulse_rate} bpm</span>}
{rec.spo2 && <span className="badge bg-info-subtle text-info border border-info-subtle">{rec.spo2}% O2</span>}
{rec.temperature_f && <span className="badge bg-danger-subtle text-danger border border-danger-subtle">{rec.temperature_f}°F</span>}
{rec.pain_score !== null && rec.pain_score !== undefined && rec.pain_score !== '' && (
<span className={`badge ${rec.pain_score == 0 ? 'bg-success-subtle text-success' : rec.pain_score <= 3 ? 'bg-info-subtle text-info' : 'bg-warning-subtle text-warning-emphasis'}`}>
Pain: {rec.pain_score}/10
</span>
)}
</div>
</td>
<td>
<div>
{rec.bmi && <span className="badge bg-secondary-subtle text-secondary me-1">BMI {rec.bmi}</span>}
{rec.weight_kg && <small className="text-muted">{rec.weight_kg}kg </small>}
{rec.height_cm && <small className="text-muted">{rec.height_cm}cm</small>}
</div>
{(rec.waist_circumference_cm || rec.waist_hip_ratio) && (
<small className="text-muted d-block" style={{fontSize: '11px'}}>
{rec.waist_circumference_cm ? `Waist: ${rec.waist_circumference_cm}cm` : ''}
{rec.waist_hip_ratio ? ` • WHR: ${rec.waist_hip_ratio}` : ''}
</small>
)}
</td>
<td>
<div className="d-flex flex-wrap gap-1">
{rec.sugar_random && <span className="badge bg-warning-subtle text-warning-emphasis border">RBS: {rec.sugar_random}</span>}
{rec.sugar_fasting && <span className="badge bg-primary-subtle text-primary border">FBS: {rec.sugar_fasting}</span>}
{rec.sugar_pp && <span className="badge bg-secondary-subtle text-secondary border">PP: {rec.sugar_pp}</span>}
{!rec.sugar_random && !rec.sugar_fasting && !rec.sugar_pp && <span className="text-muted">-</span>}
</div>
</td>
<td>
<div className="d-flex flex-wrap gap-1">
{rec.hemoglobin && <span className="badge bg-danger-subtle text-danger border">Hb: {rec.hemoglobin} g/dL</span>}
{rec.hba1c && <span className="badge bg-success-subtle text-success border">A1c: {rec.hba1c}%</span>}
{!rec.hemoglobin && !rec.hba1c && <span className="text-muted">-</span>}
</div>
</td>
<td>
<div className="small">
{rec.diet_type && <span className="badge bg-light text-dark border me-1">{rec.diet_type}</span>}
{rec.daily_steps && <span className="text-muted me-1">{rec.daily_steps} steps</span>}
{rec.sleep_hours && <span className="text-muted">{rec.sleep_hours}h sleep</span>}
{!rec.diet_type && !rec.daily_steps && !rec.sleep_hours && <span className="text-muted">-</span>}
</div>
</td>
<td><small className="text-muted">{rec.recorded_by || 'Staff'}</small></td>
<td><small className="text-truncate d-inline-block" style={{maxWidth: '180px'}} title={rec.general_advice || rec.diet_lifestyle_note}>{rec.general_advice || rec.diet_lifestyle_note || '-'}</small></td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
<div className="modal-footer p-2 bg-light">
<button className="btn btn-secondary btn-sm px-3" onClick={() => setWellnessHistoryPatient(null)}>Close</button>
</div>
</div>
</div>
</div>
)}
</main>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<NursingApp />);
</script>
</body>
</html>