devsofttech_hms_ci4/public/App/Nursing/index.html

1315 lines
80 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 EMR & Bedside PWA App</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="apple-touch-icon" href="https://cdn-icons-png.flaticon.com/512/3304/3304567.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>
<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;
}
/* 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;
}
.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;
function NursingApp() {
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('');
// 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 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);
fetchBeds();
fetchOpdAppointments(opdDateMode, selectedDoctorId, vitalsFilter);
} else {
localStorage.removeItem(DEVICE_AUTH_KEY);
}
} catch (e) {}
}
fetchBeds();
fetchNurses();
fetchDoctors();
}, []);
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);
} 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!');
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');
}
};
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>
);
};
// Filtered lists
const filteredBeds = beds.filter(bed => {
const matchesWard = selectedWard === 'all' || bed.ward_name === selectedWard;
const q = searchQuery.toLowerCase();
const matchesSearch = searchQuery === '' ||
(bed.bed_number || '').toLowerCase().includes(q) ||
(bed.patient_name || '').toLowerCase().includes(q) ||
(bed.uhid || '').toLowerCase().includes(q);
return matchesWard && matchesSearch;
});
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">
<div className="lock-card">
<div className="p-4 text-center text-white" style={{background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)'}}>
<i className="bi bi-shield-lock-fill display-4 d-block mb-2 text-success"></i>
<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>
{/* 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={() => { fetchBeds(); fetchOpdAppointments(opdDateMode); }} 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">
{!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')}>
<i className="bi bi-hospital-fill me-2"></i> Admitted IPDs ({beds.filter(b => b.status === 'occupied').length})
</button>
<button className={`segmented-tab ${mainTab === 'opd' ? 'active-blue' : ''}`} onClick={() => setMainTab('opd')}>
<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.map(bed => (
<div key={bed.id} className="col-6 col-md-4 col-lg-3">
<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>
)}
{bed.status === 'occupied' && (
<div className="mt-3 pt-2 border-top d-flex gap-1">
<button className="btn btn-sm btn-outline-primary w-100 py-1 fw-bold" style={{fontSize: '11px'}}>
<i className="bi bi-heart-pulse me-1"></i> Care 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">
<li className="nav-item flex-fill text-center">
<button className={`nav-link w-100 ${activeTab === 'vitals' ? 'active' : ''}`} onClick={() => setActiveTab('vitals')}>
<i className="bi bi-heart-pulse me-1"></i> Vitals
</button>
</li>
<li className="nav-item flex-fill text-center">
<button className={`nav-link w-100 ${activeTab === 'fluid' ? 'active' : ''}`} onClick={() => setActiveTab('fluid')}>
<i className="bi bi-droplet me-1"></i> Fluid I/O
</button>
</li>
<li className="nav-item flex-fill text-center">
<button className={`nav-link w-100 ${activeTab === 'treatment' ? 'active' : ''}`} onClick={() => setActiveTab('treatment')}>
<i className="bi bi-clipboard2-pulse me-1"></i> Treatment
</button>
</li>
</ul>
{activeTab === 'vitals' && (
<div className="card p-3 shadow-sm border-0 mb-3 rounded-4">
<h6 className="fw-bold mb-3"><i className="bi bi-heart-pulse-fill text-danger me-1"></i> Bedside Vitals Entry</h6>
<div className="row g-2 mb-3">
<div className="col-6"><label className="form-label small mb-1">BP Sys/Dia</label><div className="input-group input-group-sm"><input type="number" className="form-control" 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" placeholder="80" value={vitalsForm.bp_diastolic} onChange={e => setVitalsForm({...vitalsForm, bp_diastolic: e.target.value})} /></div></div>
<div className="col-6"><label className="form-label small mb-1">Pulse Rate</label><input type="number" className="form-control form-control-sm" placeholder="72 bpm" value={vitalsForm.pulse_rate} onChange={e => setVitalsForm({...vitalsForm, pulse_rate: e.target.value})} /></div>
<div className="col-6"><label className="form-label small mb-1">SpO2 %</label><input type="number" className="form-control form-control-sm" placeholder="98%" value={vitalsForm.spo2} onChange={e => setVitalsForm({...vitalsForm, spo2: e.target.value})} /></div>
<div className="col-6"><label className="form-label small mb-1">Temp °F</label><input type="number" step="0.1" className="form-control form-control-sm" placeholder="98.6" value={vitalsForm.temperature_f} onChange={e => setVitalsForm({...vitalsForm, temperature_f: e.target.value})} /></div>
<div className="col-6"><label className="form-label small mb-1">Resp Rate</label><input type="number" className="form-control form-control-sm" placeholder="18/min" value={vitalsForm.resp_rate} onChange={e => setVitalsForm({...vitalsForm, resp_rate: e.target.value})} /></div>
<div className="col-6"><label className="form-label small mb-1">Weight (kg)</label><input type="number" className="form-control form-control-sm" placeholder="70" value={vitalsForm.weight_kg} onChange={e => setVitalsForm({...vitalsForm, weight_kg: 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>
)}
{activeTab === 'treatment' && (
<div className="card p-3 shadow-sm border-0 mb-3 rounded-4">
<h6 className="fw-bold mb-2"><i className="bi bi-journal-check text-primary me-1"></i> Nursing Procedure &amp; Notes</h6>
<textarea className="form-control mb-3 rounded-3" rows="3" placeholder="Enter nursing procedure given or medication administered..." value={treatmentForm.treatment_text} onChange={e => setTreatmentForm({...treatmentForm, treatment_text: e.target.value})}></textarea>
<button className="btn btn-primary fw-bold py-2 w-100 rounded-3 shadow-sm" onClick={() => handleSaveIpdEntry('treatment', treatmentForm)}>
<i className="bi bi-send-fill me-1"></i> Save Procedure Note
</button>
</div>
)}
{/* Bedside Log Entries */}
<div className="card p-3 shadow-sm border-0 rounded-4">
<h6 className="fw-bold mb-3"><i className="bi bi-clock-history me-1"></i> Patient Bedside History Log</h6>
{workspaceData.map(e => (
<div key={e.id} className="p-3 border rounded-3 bg-light mb-2">
<div className="d-flex justify-content-between align-items-center mb-1">
<span className="badge bg-secondary" style={{fontSize: '10px'}}>{e.entry_type}</span>
<small className="text-muted" style={{fontSize: '10px'}}>{e.recorded_at} - by {e.recorded_by}</small>
</div>
{renderNoteContent(e)}
</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>
)}
</main>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<NursingApp />);
</script>
</body>
</html>