devsofttech_hms_ci4/public/App/Doctor/index.html

1884 lines
122 KiB
HTML

<!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>Doctor EMR & Bedside Care - HMS Mobile</title>
<link rel="manifest" href="/App/Doctor/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>
<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;
}
/* Sleek Doctor Topbar */
.doctor-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, #3b82f6 0%, #1d4ed8 100%);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 14px;
box-shadow: 0 2px 8px rgba(37, 99, 235, 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;
}
/* Modern Segmented Navigation 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, #2563eb 0%, #1d4ed8 100%);
color: #ffffff;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.25);
}
/* Modern Patient Card Styling */
.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);
position: relative;
overflow: hidden;
}
.mobile-patient-card:hover {
box-shadow: 0 8px 24px -4px rgba(15, 23, 42, 0.1);
border-color: #cbd5e1;
}
.mobile-patient-card.visited {
border-left-color: #0891b2;
}
.mobile-patient-card.booked {
border-left-color: #64748b;
}
.mobile-patient-card.waiting {
border-left-color: #2563eb;
}
/* 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 */
.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);
}
/* Action Button */
.btn-action-primary {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: #ffffff;
border: none;
border-radius: 12px;
font-weight: 700;
padding: 10px 16px;
box-shadow: 0 4px 14px rgba(37, 99, 235, 0.3);
transition: all 0.2s ease;
}
.btn-action-primary:hover, .btn-action-primary:active {
background: linear-gradient(135deg, #1d4ed8 0%, #1e40af 100%);
transform: translateY(-1px);
box-shadow: 0 6px 18px rgba(37, 99, 235, 0.4);
color: #ffffff;
}
.btn-action-success {
background: linear-gradient(135deg, #16a34a 0%, #15803d 100%);
color: #ffffff;
border: none;
border-radius: 12px;
font-weight: 700;
padding: 10px 16px;
box-shadow: 0 4px 14px rgba(22, 163, 74, 0.3);
transition: all 0.2s ease;
}
/* Touch Chip Buttons */
.chip-btn {
font-size: 11px;
font-weight: 600;
padding: 5px 12px;
border-radius: 20px;
border: 1px solid #cbd5e1;
background: #ffffff;
color: #334155;
cursor: pointer;
transition: all 0.15s ease;
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
}
.chip-btn:hover, .chip-btn:active {
background: #2563eb;
color: #ffffff;
border-color: #2563eb;
transform: translateY(-1px);
}
/* Lock Screen Styling */
.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;
}
/* Custom Scrollbar for chips */
.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 DOC_DEVICE_AUTH_KEY = 'doctor_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 DoctorApp() {
const [showAppsModal, setShowAppsModal] = useState(false);
const [doctors, setDoctors] = useState([]);
const [myIpdPatients, setMyIpdPatients] = useState([]);
const [allIpdPatients, setAllIpdPatients] = useState([]);
const [ipdViewMode, setIpdViewMode] = useState('mine'); // 'mine' or 'all'
const [opdAppointments, setOpdAppointments] = useState([]);
const [opdCounts, setOpdCounts] = useState({ all: 0, waiting: 0, visited: 0, booked: 0, cancelled: 0 });
const [opdFilter, setOpdFilter] = useState('all');
const [opdDateMode, setOpdDateMode] = useState('today'); // 'today' or 'all'
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [mainTab, setMainTab] = useState('opd');
// Active Workspace state
const [activePatient, setActivePatient] = useState(null);
const [activeOpd, setActiveOpd] = useState(null);
const [workspaceEntries, setWorkspaceEntries] = useState([]);
const [newTreatmentNote, setNewTreatmentNote] = useState('');
const [noteImageBase64, setNoteImageBase64] = useState('');
const [noteImagePreview, setNoteImagePreview] = useState('');
const [installPrompt, setInstallPrompt] = useState(null);
// Auth & Device Security State
const PAIRED_DOC_KEY = 'hms_doctor_paired_profile';
const [authenticatedDoctor, setAuthenticatedDoctor] = useState(null);
const [pairedDoctor, setPairedDoctor] = useState(() => {
try {
const item = localStorage.getItem(PAIRED_DOC_KEY);
return item ? JSON.parse(item) : null;
} catch (e) {
return null;
}
});
const [docIdInput, setDocIdInput] = useState('');
const [deviceCodeInput, setDeviceCodeInput] = useState('');
const [pinInput, setPinInput] = useState('');
const [pinError, setPinError] = useState('');
const [showPairingForm, setShowPairingForm] = useState(false);
const [isVerifying, setIsVerifying] = useState(false);
const [isSettingNewPin, setIsSettingNewPin] = useState(false);
const [newPin, setNewPin] = useState('');
const [confirmPin, setConfirmPin] = useState('');
// OPD Prescription Consultation Controls State
const [prescForm, setPrescForm] = useState({
chief_complaints: '',
finding_examinations: '',
provisional_diagnosis: '',
investigation: '',
advice: '',
next_visit: '',
temp: '',
pulse: '',
bp_systolic: '',
bp_diastolic: '',
spo2: '',
weight: '',
height: '',
rr_min: ''
});
// Medicines Repeater Array
const [medicines, setMedicines] = useState([
{ drug_name: '', dosage: '1 Tab', frequency: '1-0-1', duration: '5 Days', instructions: 'After Food' }
]);
// Quick Chips presets for Mobile Typing
const COMPLAINT_CHIPS = ['Fever', 'Cough', 'Swelling', 'Joint Pain', 'Headache', 'Back Ache', 'Nausea / Vomiting', 'Weakness', 'Chest Pain', 'Shortness of Breath'];
const EXAMINATION_CHIPS = ['Pallor (+)', 'Icterus (-)', 'Cyanosis (-)', 'Clubbing (-)', 'Edema (-)', 'B/L Chest Clear', 'S1 S2 Heard'];
const DIAGNOSIS_CHIPS = ['Acute Rheumatic Fever', 'Viral Infection', 'Essential Hypertension', 'Type 2 Diabetes Mellitus', 'GERD', 'Acute Bronchitis', 'Osteoarthritis'];
const DRUG_CHIPS = ['Paracetamol 500mg', 'Amoxicillin 500mg', 'Pantoprazole 40mg', 'Cetirizine 10mg', 'Ibuprofen 400mg', 'Azithromycin 500mg', 'Multivitamin'];
const LAB_CHIPS = ['CBC', 'Serum Creatinine', 'LFT', 'KFT', 'Chest X-Ray PA', 'ECG', 'Urine RE', 'HbA1c', 'USG Abdomen'];
const fetchIpdPatients = async (docId) => {
try {
setLoading(true);
const res = await fetch(`/api/v1/doctor/ipd/list/${docId || 0}`);
const json = await res.json();
if (json.status === 1) {
setAllIpdPatients(json.all_patients || json.patients || []);
setMyIpdPatients(json.patients || []);
}
} catch (err) {
console.error('Failed to load IPD patients', err);
} finally {
setLoading(false);
}
};
const fetchOpdAppointments = async (docId, dateModeOverride) => {
try {
const mode = dateModeOverride || opdDateMode;
const res = await fetch(`/api/v1/doctor/opd/list/${docId || 0}?date_mode=${mode}`);
const json = await res.json();
if (json.status === 1) {
setOpdAppointments(json.appointments || []);
if (json.counts) {
setOpdCounts(json.counts);
}
}
} catch (err) {
console.error('Failed to load OPD appointments', err);
}
};
useEffect(() => {
const savedAuth = localStorage.getItem(DOC_DEVICE_AUTH_KEY);
if (savedAuth) {
try {
const parsed = JSON.parse(savedAuth);
if (parsed && parsed.doctor && parsed.expires_at > Date.now()) {
setAuthenticatedDoctor(parsed.doctor);
fetchIpdPatients(parsed.doctor.id);
fetchOpdAppointments(parsed.doctor.id, opdDateMode);
} else {
localStorage.removeItem(DOC_DEVICE_AUTH_KEY);
}
} catch (e) {}
}
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
setInstallPrompt(e);
});
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/App/Doctor/sw.js')
.then(() => console.log('Doctor PWA Service Worker Registered'))
.catch(err => console.error('SW failed', err));
}
}, []);
const handleVerifyPin = async () => {
const isPairing = !pairedDoctor || showPairingForm;
const targetDocId = isPairing ? docIdInput.trim() : (pairedDoctor.doc_id || '');
if (!targetDocId) {
setPinError('Please enter your Doctor ID or Username.');
return;
}
if (isPairing && (!deviceCodeInput || deviceCodeInput.trim().length < 4)) {
setPinError('Please enter the 6-digit Device Security Code from Doctor Master.');
return;
}
if (!pinInput || pinInput.length < 4) {
setPinError('Enter your 4 to 6 digit Security PIN or Password.');
return;
}
setPinError('');
setIsVerifying(true);
try {
const body = new URLSearchParams();
body.append('doctor_id', targetDocId);
body.append('pin', pinInput);
if (isPairing) {
body.append('device_code', deviceCodeInput.trim());
} else if (pairedDoctor && pairedDoctor.device_token) {
body.append('device_token', pairedDoctor.device_token);
}
const res = await fetch('/api/v1/doctor/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.doctor) {
setAuthenticatedDoctor(json.doctor);
const deviceAuthObj = {
doctor: json.doctor,
expires_at: Date.now() + THIRTY_DAYS_MS
};
localStorage.setItem(DOC_DEVICE_AUTH_KEY, JSON.stringify(deviceAuthObj));
const pairedObj = {
id: json.doctor.id,
doc_id: json.doctor.id ? ('DOC-' + json.doctor.id) : targetDocId,
name: json.doctor.name,
device_token: json.device_token || ''
};
localStorage.setItem(PAIRED_DOC_KEY, JSON.stringify(pairedObj));
setPairedDoctor(pairedObj);
setShowPairingForm(false);
setPinInput('');
setDeviceCodeInput('');
fetchIpdPatients(json.doctor.id);
fetchOpdAppointments(json.doctor.id, opdDateMode);
} else if (json.status === 2) {
setIsSettingNewPin(true);
} else {
if (json.need_device_code) {
setShowPairingForm(true);
}
setPinError(json.message || 'Invalid Security PIN or Device Code');
}
} catch (err) {
setPinError('Connection error verifying credentials.');
} finally {
setIsVerifying(false);
}
};
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 targetDocId = pairedDoctor ? pairedDoctor.id : (authenticatedDoctor ? authenticatedDoctor.id : 0);
const body = new URLSearchParams();
body.append('doctor_id', targetDocId);
body.append('new_pin', newPin);
const res = await fetch('/api/v1/doctor/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 = () => {
setAuthenticatedDoctor(null);
localStorage.removeItem(DOC_DEVICE_AUTH_KEY);
setPinInput('');
setActivePatient(null);
setActiveOpd(null);
};
const handleUnpairDevice = () => {
if (confirm('Unpair this device? You will need the Doctor ID and Device Security Code to pair it again.')) {
setAuthenticatedDoctor(null);
setPairedDoctor(null);
localStorage.removeItem(DOC_DEVICE_AUTH_KEY);
localStorage.removeItem(PAIRED_DOC_KEY);
setShowPairingForm(true);
setPinInput('');
setDocIdInput('');
setDeviceCodeInput('');
setPinError('');
}
};
const openIpdWorkspace = async (patient) => {
setActivePatient(patient);
clearCapturedImage();
try {
const res = await fetch(`/api/v1/doctor/ipd/workspace/${patient.ipd_id}`);
const json = await res.json();
if (json.status === 1) {
setWorkspaceEntries(json.entries || []);
}
} catch (err) {
console.error('Failed to load IPD workspace', err);
}
};
const [cropSrc, setCropSrc] = useState('');
const [cropper, setCropper] = useState(null);
const imageRef = React.useRef(null);
const handleImageCapture = (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);
setNoteImageBase64(croppedBase64);
setNoteImagePreview(croppedBase64);
}
cropper.destroy();
setCropper(null);
setCropSrc('');
}
};
const cancelCrop = () => {
if (cropper) {
cropper.destroy();
setCropper(null);
}
setCropSrc('');
};
const clearCapturedImage = () => {
setNoteImageBase64('');
setNoteImagePreview('');
};
const handleSaveTreatmentNote = async () => {
if (!activePatient || (!newTreatmentNote.trim() && !noteImageBase64)) {
alert('Please enter clinical observations or take a picture of your handwritten note.');
return;
}
try {
const payload = {
doctor_id: authenticatedDoctor ? authenticatedDoctor.id : 0,
doctor_name: authenticatedDoctor ? authenticatedDoctor.name : 'Doctor',
treatment_text: newTreatmentNote,
image_base64: noteImageBase64 || ''
};
const res = await fetch(`/api/v1/doctor/ipd/treatment/save/${activePatient.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 || 'Clinical note / handwritten note saved successfully!');
setNewTreatmentNote('');
clearCapturedImage();
openIpdWorkspace(activePatient);
} else {
alert(json.message || 'Failed to save treatment note');
}
} catch (err) {
alert('Error saving treatment note');
}
};
const openOpdConsultation = async (app) => {
setActiveOpd(app);
clearCapturedImage();
try {
const res = await fetch(`/api/v1/doctor/opd/prescription/${app.opd_id}`);
const json = await res.json();
if (json.status === 1 && json.prescription) {
const pr = json.prescription;
setPrescForm({
chief_complaints: pr.chief_complaints || pr.complaints || '',
finding_examinations: pr.finding_examinations || pr.Finding_Examinations || '',
provisional_diagnosis: pr.provisional_diagnosis || pr.diagnosis || '',
investigation: pr.investigation || '',
advice: pr.advice || '',
next_visit: pr.next_visit || '',
temp: pr.temp || '',
pulse: pr.pulse || '',
bp_systolic: pr.bp || '',
bp_diastolic: pr.diastolic || '',
spo2: pr.spo2 || '',
weight: pr.weight || '',
height: pr.height || '',
rr_min: pr.rr_min || ''
});
if (json.medicines && json.medicines.length > 0) {
setMedicines(json.medicines);
} else if (pr.json_medicines) {
try {
const parsedMeds = JSON.parse(pr.json_medicines);
if (Array.isArray(parsedMeds) && parsedMeds.length > 0) {
setMedicines(parsedMeds);
}
} catch (e) {}
}
} else {
setPrescForm({
chief_complaints: '', finding_examinations: '', provisional_diagnosis: '', investigation: '', advice: '', next_visit: '',
temp: '', pulse: '', bp_systolic: '', bp_diastolic: '', spo2: '', weight: '', height: '', rr_min: ''
});
setMedicines([{ drug_name: '', dosage: '1 Tab', frequency: '1-0-1', duration: '5 Days', instructions: 'After Food' }]);
}
} catch (err) {
console.error('Failed to load OPD prescription', err);
}
};
const handleSaveOpdPrescription = async () => {
if (!activeOpd) return;
try {
const payload = {
doctor_id: authenticatedDoctor ? authenticatedDoctor.id : 0,
...prescForm,
medicines: medicines,
image_base64: noteImageBase64 || ''
};
const res = await fetch(`/api/v1/doctor/opd/prescription/save/${activeOpd.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 || 'Prescription saved & OPD Visit completed!');
setActiveOpd(null);
clearCapturedImage();
fetchOpdAppointments(authenticatedDoctor ? authenticatedDoctor.id : 0, opdDateMode);
} else {
alert(json.message || 'Failed to save prescription');
}
} catch (err) {
alert('Error saving OPD consultation');
}
};
const addMedicineRow = (drugName = '') => {
setMedicines([...medicines, { drug_name: drugName, dosage: '1 Tab', frequency: '1-0-1', duration: '5 Days', instructions: 'After Food' }]);
};
const removeMedicineRow = (idx) => {
if (medicines.length === 1) return;
setMedicines(medicines.filter((_, i) => i !== idx));
};
const updateMedicineRow = (idx, field, val) => {
const updated = [...medicines];
updated[idx][field] = val;
setMedicines(updated);
};
const appendChipText = (field, text) => {
const current = prescForm[field];
if (!current) {
setPrescForm({ ...prescForm, [field]: text });
} else if (!current.includes(text)) {
setPrescForm({ ...prescForm, [field]: `${current}, ${text}` });
}
};
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 matchesSearch = searchQuery === '' || name.includes(q) || uhid.includes(q) || opdCode.includes(q);
return matchesFilter && matchesSearch;
});
const activeIpdSource = (ipdViewMode === 'mine') ? myIpdPatients : allIpdPatients;
const filteredIpdPatients = activeIpdSource.filter(pat => {
const q = searchQuery.toLowerCase();
const name = (pat.patient_display_name || pat.fname || '').toLowerCase();
const uhid = (pat.uhid || '').toLowerCase();
const ipdCode = (pat.ipd_code || '').toLowerCase();
const docName = (pat.assigned_doctors || pat.r_doc_name || '').toLowerCase();
return searchQuery === '' || name.includes(q) || uhid.includes(q) || ipdCode.includes(q) || docName.includes(q);
});
const getInitials = (nameStr) => {
if (!nameStr) return 'P';
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> Doctor Handwritten Note Photo
</span>
<a href={match[1]} target="_blank" rel="noreferrer">
<img src={match[1]} alt="Doctor Handwritten Note" className="img-fluid rounded-3 border shadow-sm d-block mt-1" style={{maxHeight: '240px', objectFit: 'cover'}} />
</a>
</div>
)}
</div>
);
};
// -------------------------------------------------------------
// RENDER: LOCK SCREEN (DEVICE REGISTRATION & PIN AUTHENTICATION)
// -------------------------------------------------------------
if (!authenticatedDoctor) {
const isQuickUnlock = Boolean(pairedDoctor && !showPairingForm);
return (
<div className="lock-screen-overlay">
<MobileAppsModal show={showAppsModal} onClose={() => setShowAppsModal(false)} currentApp="doctor" />
<div className="lock-card">
<div className="p-4 text-center bg-primary 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">Doctor EMR Mobile Portal</h5>
<div className="d-flex align-items-center justify-content-center gap-1 opacity-75 small">
<i className="bi bi-shield-check text-success"></i>
<span>Hospital Intranet &amp; Device Protected</span>
</div>
</div>
<div className="p-4">
{pinError && <div className="alert alert-danger py-2 px-3 small mb-3">{pinError}</div>}
{!isSettingNewPin ? (
isQuickUnlock ? (
/* -------------------------------------------------------------
PAIRED DEVICE QUICK UNLOCK
------------------------------------------------------------- */
<div>
<div className="text-center mb-4">
<div className="avatar-circle mx-auto mb-2" style={{width: '54px', height: '54px', fontSize: '18px'}}>
{getInitials(pairedDoctor.name || pairedDoctor.doc_id)}
</div>
<h6 className="fw-bold text-dark mb-0">{pairedDoctor.name || 'Doctor'}</h6>
<span className="badge bg-secondary-subtle text-dark border font-monospace mt-1">
{pairedDoctor.doc_id}
</span>
<div className="text-muted small mt-1" style={{fontSize: '11px'}}>
<i className="bi bi-phone-fill me-1 text-primary"></i> Trusted Hospital Device
</div>
</div>
<div className="mb-4">
<label className="form-label fw-bold small text-muted">Enter PIN or Password to Unlock</label>
<input
type="password"
maxLength="20"
className="form-control text-center fs-3 letter-spacing-2"
placeholder="••••"
value={pinInput}
autoFocus
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'}}>
30-day verified session active
</small>
</div>
<button
className="btn btn-primary btn-lg w-100 py-3 fw-bold shadow-sm mb-2"
onClick={handleVerifyPin}
disabled={isVerifying}
>
{isVerifying ? (
<span><span className="spinner-border spinner-border-sm me-2"></span>Verifying...</span>
) : (
<span><i className="bi bi-unlock-fill me-2"></i> Unlock Doctor Portal</span>
)}
</button>
<div className="text-center mt-3 pt-2 border-top">
<button
type="button"
className="btn btn-link btn-sm text-secondary text-decoration-none p-0"
onClick={() => { setShowPairingForm(true); setPinError(''); }}
>
<i className="bi bi-arrow-repeat me-1"></i> Switch Doctor / Re-Pair Device
</button>
</div>
</div>
) : (
/* -------------------------------------------------------------
FIRST-TIME DEVICE REGISTRATION / PAIRING FORM
------------------------------------------------------------- */
<div>
<div className="alert alert-primary py-2 px-3 small d-flex align-items-center gap-2 mb-3">
<i className="bi bi-shield-lock-fill fs-5 text-primary"></i>
<div>
<div className="fw-bold">First-Time Device Pairing</div>
<div style={{fontSize: '11px'}}>Enter Doctor ID and Device Security Code from Doctor Master.</div>
</div>
</div>
<div className="mb-3">
<label className="form-label fw-bold small text-dark mb-1">
Doctor ID or Username <span className="text-danger">*</span>
</label>
<div className="input-group">
<span className="input-group-text bg-light"><i className="bi bi-person-badge"></i></span>
<input
type="text"
className="form-control"
placeholder="e.g. DOC-123 or doctor username"
value={docIdInput}
onChange={(e) => setDocIdInput(e.target.value)}
/>
</div>
</div>
<div className="mb-3">
<label className="form-label fw-bold small text-dark mb-1">
Device Security Code <span className="text-danger">*</span>
</label>
<div className="input-group">
<span className="input-group-text bg-light"><i className="bi bi-key-fill text-warning"></i></span>
<input
type="text"
maxLength="10"
className="form-control font-monospace fw-bold"
placeholder="6-digit code from Admin"
value={deviceCodeInput}
onChange={(e) => setDeviceCodeInput(e.target.value)}
/>
</div>
<small className="text-muted" style={{fontSize: '11px'}}>
Displayed on HMS Admin &gt; Doctors
</small>
</div>
<div className="mb-4">
<label className="form-label fw-bold small text-dark mb-1">
App PIN or Password <span className="text-danger">*</span>
</label>
<div className="input-group">
<span className="input-group-text bg-light"><i className="bi bi-lock-fill"></i></span>
<input
type="password"
maxLength="20"
className="form-control"
placeholder="••••"
value={pinInput}
onChange={(e) => setPinInput(e.target.value)}
onKeyUp={(e) => e.key === 'Enter' && handleVerifyPin()}
/>
</div>
</div>
<button
className="btn btn-primary btn-lg w-100 py-3 fw-bold shadow-sm"
onClick={handleVerifyPin}
disabled={isVerifying}
>
{isVerifying ? (
<span><span className="spinner-border spinner-border-sm me-2"></span>Pairing Device...</span>
) : (
<span><i className="bi bi-shield-check me-2"></i> Register Device &amp; Unlock</span>
)}
</button>
{pairedDoctor && (
<div className="text-center mt-3 pt-2 border-top">
<button
type="button"
className="btn btn-link btn-sm text-secondary text-decoration-none p-0"
onClick={() => { setShowPairingForm(false); setPinError(''); }}
>
<i className="bi bi-arrow-left me-1"></i> Back to Quick Unlock
</button>
</div>
)}
</div>
)
) : (
/* -------------------------------------------------------------
INITIAL PIN CREATION (IF DOCTOR DOES NOT HAVE A PIN YET)
------------------------------------------------------------- */
<div>
<div className="alert alert-info py-2 small mb-3">
Initial PIN Setup for Doctor 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-success 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>
);
}
// -------------------------------------------------------------
// RENDER: UNLOCKED DOCTOR PWA APP
// -------------------------------------------------------------
return (
<div>
<MobileAppsModal show={showAppsModal} onClose={() => setShowAppsModal(false)} currentApp="doctor" />
{/* Top Doctor Header */}
<header className="doctor-header d-flex justify-content-between align-items-center">
<div className="d-flex align-items-center gap-2">
<div className="avatar-circle">
{getInitials(authenticatedDoctor.name)}
</div>
<div>
<h6 className="mb-0 fw-bold">{authenticatedDoctor.name}</h6>
<small className="text-light opacity-75" style={{fontSize: '11px'}}>
Doctor EMR <span className="badge bg-success ms-1" style={{fontSize: '9px'}}>● Active Session</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={() => { fetchOpdAppointments(authenticatedDoctor.id, opdDateMode); fetchIpdPatients(authenticatedDoctor.id); }} title="Refresh Queue">
<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 Device Access">
<i className="bi bi-lock-fill"></i>
</button>
</div>
</header>
{/* Main Container */}
<main className="container-fluid py-3 px-2 px-md-3">
{!activeOpd && !activePatient ? (
<div>
{/* Segmented Tab Controller */}
<div className="segmented-tabs mb-3">
<button className={`segmented-tab ${mainTab === 'opd' ? 'active' : ''}`} onClick={() => setMainTab('opd')}>
<i className="bi bi-calendar-check-fill me-2"></i> OPD Appointments ({opdAppointments.length})
</button>
<button className={`segmented-tab ${mainTab === 'ipd' ? 'active' : ''}`} onClick={() => setMainTab('ipd')}>
<i className="bi bi-hospital-fill me-2"></i> Admitted IPDs ({activeIpdSource.length})
</button>
</div>
{mainTab === 'opd' ? (
<div>
{/* Date Filter Segmented Bar (Current Date vs All Dates) */}
<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(authenticatedDoctor.id, 'today'); }}
>
<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(authenticatedDoctor.id, 'all'); }}
>
<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 Only' : '● All Appointments'}
</span>
</div>
{/* Status Filter Pills Bar */}
<div className="d-flex gap-2 mb-3 overflow-auto no-scrollbar pb-1">
<button
className={`btn btn-sm rounded-pill text-nowrap px-3 ${opdFilter === 'all' ? 'btn-primary fw-bold shadow-sm' : 'btn-white bg-white text-secondary border'}`}
onClick={() => setOpdFilter('all')}
>
All <span className={`badge ms-1 ${opdFilter === 'all' ? 'bg-white text-primary' : 'bg-light text-dark'}`}>{opdCounts.all}</span>
</button>
<button
className={`btn btn-sm rounded-pill text-nowrap px-3 ${opdFilter === 'waiting' ? 'btn-primary fw-bold shadow-sm' : 'btn-white bg-white text-secondary border'}`}
onClick={() => setOpdFilter('waiting')}
>
Waiting <span className={`badge ms-1 ${opdFilter === 'waiting' ? 'bg-white text-primary' : 'bg-light text-dark'}`}>{opdCounts.waiting}</span>
</button>
<button
className={`btn btn-sm rounded-pill text-nowrap px-3 ${opdFilter === 'visited' ? 'btn-info text-dark fw-bold shadow-sm' : 'btn-white bg-white text-secondary border'}`}
onClick={() => setOpdFilter('visited')}
>
Visited <span className={`badge ms-1 ${opdFilter === 'visited' ? 'bg-white text-dark' : 'bg-light text-dark'}`}>{opdCounts.visited}</span>
</button>
<button
className={`btn btn-sm rounded-pill text-nowrap px-3 ${opdFilter === 'booked' ? 'btn-secondary fw-bold shadow-sm' : 'btn-white bg-white text-secondary border'}`}
onClick={() => setOpdFilter('booked')}
>
Booked <span className={`badge ms-1 ${opdFilter === 'booked' ? 'bg-white text-secondary' : 'bg-light text-dark'}`}>{opdCounts.booked}</span>
</button>
</div>
{/* Search Input Bar */}
<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, UHID, OPD Code..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{searchQuery && (
<button className="btn bg-white border-0 pe-3 text-secondary" onClick={() => setSearchQuery('')}>
<i className="bi bi-x-circle-fill"></i>
</button>
)}
</div>
{/* ULTRA-PREMIUM MOBILE PATIENT CARDS LIST */}
<div className="d-block d-md-none">
{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>
{opdDateMode === 'today'
? "No OPD appointments registered for today's date."
: "No OPD appointments found for selected filter."}
</div>
) : (
filteredOpdAppointments.map(app => {
const statusClass = app.status_key === 'waiting' ? 'waiting'
: app.status_key === 'visited' ? 'visited'
: 'booked';
return (
<div key={app.opd_id} className={`mobile-patient-card ${statusClass}`}>
{/* Card Header Row */}
<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 ${statusClass}`}>
● {app.status_label}
</span>
</div>
<code className="fw-bold text-slate-800 fs-6">{app.opd_code}</code>
</div>
{/* Patient Info Row with Avatar */}
<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>
<div className="d-flex flex-wrap gap-1 align-items-center mt-1">
<span className="badge bg-slate-100 text-dark border px-2 py-1" style={{fontSize: '11px'}}>
<i className="bi bi-person-fill text-primary me-1"></i>{app.age_display || '47 Year'} • {app.gender_label}
</span>
<span className="badge bg-rose-50 text-danger border border-danger-subtle px-2 py-1" style={{fontSize: '11px'}}>
<i className="bi bi-card-text me-1"></i>{app.uhid || '-'}
</span>
<span className="badge bg-emerald-50 text-success border border-success-subtle px-2 py-1" style={{fontSize: '11px'}}>
<i className="bi bi-wallet2 me-1"></i>{app.opd_type || 'Cash'}
</span>
</div>
</div>
</div>
{/* Action Button Row */}
<div>
<button
className="btn btn-action-primary w-100 py-2.5"
onClick={() => openOpdConsultation(app)}
>
<i className="bi bi-file-earmark-medical-fill me-2"></i> Consult Patient
</button>
</div>
</div>
);
})
)}
</div>
{/* DESKTOP TABLE VIEW (Monitors >= 768px) */}
<div className="card shadow-sm border-0 d-none d-md-block rounded-4 overflow-hidden">
<div className="card-body p-0 table-responsive">
<table className="table table-hover align-middle mb-0">
<thead className="table-light">
<tr>
<th>Status</th>
<th>OPD No.</th>
<th>Current Patient</th>
<th>Q No.</th>
<th>UHID</th>
<th>OPD Type</th>
<th className="text-center">Action</th>
</tr>
</thead>
<tbody>
{filteredOpdAppointments.length === 0 ? (
<tr>
<td colSpan="7" className="text-center text-muted py-4">
No OPD appointments found for selected filter.
</td>
</tr>
) : (
filteredOpdAppointments.map(app => {
const statusBadgeClass = app.status_key === 'waiting' ? 'bg-primary'
: app.status_key === 'visited' ? 'bg-info text-dark'
: app.status_key === 'booked' ? 'bg-secondary' : 'bg-danger';
return (
<tr key={app.opd_id}>
<td>
<span className={`badge ${statusBadgeClass} text-uppercase px-2 py-1`}>
{app.status_label}
</span>
</td>
<td><strong className="text-dark">{app.opd_code}</strong></td>
<td>
<div className="fw-bold text-primary">{app.patient_display_name}</div>
<small className="text-muted">({app.age_display || '47 Year'} / {app.gender_label})</small>
</td>
<td><span className="badge bg-dark text-white fs-6">{app.queue_no || app.opd_no || '1'}</span></td>
<td><code>{app.uhid || '-'}</code></td>
<td><small className="text-muted">{app.opd_type || 'Cash'}</small></td>
<td className="text-center">
<button
className="btn btn-sm btn-action-primary px-3"
onClick={() => openOpdConsultation(app)}
>
<i className="bi bi-file-earmark-medical me-1"></i> Consult
</button>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
</div>
) : (
/* -------------------------------------------------------------
ADMITTED IPD PATIENTS LIST (ASSIGNED TO DOCTOR / ALL ADMISSIONS)
------------------------------------------------------------- */
<div>
{/* IPD Admission View Mode Segmented Bar */}
<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 ${ipdViewMode === 'mine' ? 'btn-success text-white shadow-sm' : 'btn-light text-secondary'}`}
onClick={() => setIpdViewMode('mine')}
>
<i className="bi bi-person-check-fill me-1"></i> My Assigned Patients ({myIpdPatients.length})
</button>
<button
className={`btn btn-sm rounded-pill px-3 py-1 fw-bold ${ipdViewMode === 'all' ? 'btn-success text-white shadow-sm' : 'btn-light text-secondary'}`}
onClick={() => setIpdViewMode('all')}
>
<i className="bi bi-hospital-fill me-1"></i> All Hospital Admissions ({allIpdPatients.length})
</button>
</div>
<span className="badge bg-success-subtle text-success border border-success-subtle px-2 py-1 rounded-pill small" style={{fontSize: '11px'}}>
{ipdViewMode === 'mine' ? '● Doctor Assignments' : '● Hospital Wide IPDs'}
</span>
</div>
{/* Search Input Bar */}
<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 Admitted Patient Name, UHID, IPD Code..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{/* MOBILE CARDS LIST VIEW FOR IPD (Phones < 768px) */}
<div className="d-block d-md-none">
{filteredIpdPatients.length === 0 ? (
<div className="text-center text-muted py-5 bg-white rounded-4 border shadow-sm">
<i className="bi bi-hospital fs-1 d-block text-secondary opacity-50 mb-2"></i>
No Admitted IPD Patients found.
</div>
) : (
filteredIpdPatients.map(pat => (
<div key={pat.ipd_id} className="mobile-patient-card" style={{borderLeft: '6px solid #16a34a'}}>
<div className="d-flex justify-content-between align-items-center mb-2 pb-2 border-bottom">
<span className="badge bg-success-subtle text-success border border-success-subtle px-2 py-1 rounded-pill fw-bold" style={{fontSize: '11px'}}>
● Current Admission
</span>
<code className="fw-bold text-dark fs-6">{pat.ipd_code}</code>
</div>
<div className="d-flex align-items-start gap-3 mb-3">
<div className="patient-avatar-circle" style={{background: '#f0fdf4', color: '#16a34a', borderColor: '#bbf7d0'}}>
{getInitials(pat.patient_display_name)}
</div>
<div className="flex-grow-1">
<h5 className="fw-bold text-slate-900 mb-1" style={{fontSize: '16px'}}>
{pat.patient_display_name}
</h5>
<small className="text-muted d-block mb-2" style={{fontSize: '12px'}}>
{pat.rname ? `S/D/W of ${pat.rname}` : `(${pat.age_display} • ${pat.gender_label})`}
</small>
<div className="d-flex flex-wrap gap-1">
<span className="badge bg-primary-subtle text-primary border border-primary-subtle px-2 py-1" style={{fontSize: '11px'}}>
<i className="bi bi-door-closed me-1"></i>{pat.bed_label}
</span>
<span className="badge bg-rose-50 text-danger border border-danger-subtle px-2 py-1" style={{fontSize: '11px'}}>
<i className="bi bi-card-text me-1"></i>{pat.uhid}
</span>
<span className="badge bg-light text-dark border px-2 py-1" style={{fontSize: '11px'}}>
<i className="bi bi-person-badge me-1"></i>{pat.assigned_doctors || pat.r_doc_name || `Dr. ${authenticatedDoctor.name}`}
</span>
</div>
</div>
</div>
<div>
<button
className="btn btn-action-success w-100 py-2.5"
onClick={() => openIpdWorkspace(pat)}
>
<i className="bi bi-heart-pulse-fill me-2"></i> Open Bedside Workspace
</button>
</div>
</div>
))
)}
</div>
{/* DESKTOP TABLE VIEW FOR IPD (Monitors >= 768px) */}
<div className="card shadow-sm border-0 d-none d-md-block rounded-4 overflow-hidden">
<div className="card-body p-0 table-responsive">
<table className="table table-hover align-middle mb-0">
<thead className="table-light">
<tr>
<th>IPD Code</th>
<th>Name / Patient Code</th>
<th>Bed No. [Type]</th>
<th>Register Date</th>
<th>Dr. Name</th>
<th className="text-center">Action</th>
</tr>
</thead>
<tbody>
{filteredIpdPatients.length === 0 ? (
<tr>
<td colSpan="6" className="text-center text-muted py-4">
<i className="bi bi-hospital fs-1 d-block text-secondary mb-2"></i>
No Admitted IPD Patients found.
</td>
</tr>
) : (
filteredIpdPatients.map(pat => (
<tr key={pat.ipd_id}>
<td>
<strong className="text-dark d-block">{pat.ipd_code}</strong>
<span className="badge bg-success" style={{fontSize: '9px'}}>CURRENT ADMISSION</span>
</td>
<td>
<small className="text-danger fw-bold d-block">[{pat.uhid}]</small>
<div className="fw-bold text-primary">{pat.patient_display_name}</div>
<small className="text-muted">{pat.rname ? `S/D/W of ${pat.rname}` : `(${pat.age_display} / ${pat.gender_label})`}</small>
</td>
<td>
<span className="badge bg-primary-subtle text-primary border">{pat.bed_label}</span>
</td>
<td>
<small className="text-dark fw-semibold">{pat.register_date || 'Today'}</small>
</td>
<td>
<small className="text-dark fw-semibold">{pat.assigned_doctors || pat.r_doc_name || `Dr. ${authenticatedDoctor.name}`}</small>
</td>
<td className="text-center">
<button
className="btn btn-sm btn-action-success px-3"
onClick={() => openIpdWorkspace(pat)}
>
<i className="bi bi-heart-pulse-fill me-1"></i> Bedside Workspace
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
)}
</div>
) : activePatient ? (
/* -------------------------------------------------------------
IPD BEDSIDE WORKSPACE & CLINICAL ROUNDS
------------------------------------------------------------- */
<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={() => setActivePatient(null)}>
<i className="bi bi-arrow-left me-1"></i> Back to Admitted IPDs
</button>
<span className="badge bg-success-subtle text-success border border-success-subtle px-3 py-1 rounded-pill fw-bold">
● CURRENT ADMISSION
</span>
</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">{activePatient.patient_display_name}</h5>
<div className="d-flex flex-wrap gap-2 opacity-90 small mt-2">
<span className="badge bg-white text-dark">{activePatient.age_display} • {activePatient.gender_label}</span>
<span className="badge bg-white text-danger">UHID: {activePatient.uhid}</span>
<span className="badge bg-white text-primary">IPD: {activePatient.ipd_code}</span>
<span className="badge bg-white text-success">Bed: {activePatient.bed_label}</span>
</div>
</div>
</div>
{/* Write Doctor Round Note / Take Picture of Handwritten Note */}
<div className="card p-3 shadow-sm border-0 mb-3 rounded-4">
<div className="d-flex justify-content-between align-items-center mb-2">
<label className="form-label fw-bold small text-dark mb-0"><i className="bi bi-pencil-square text-primary me-1"></i> Write Clinical Round Note or Attach Photo</label>
<label className="btn btn-sm btn-outline-primary rounded-pill px-3 py-1 fw-bold" style={{fontSize: '11px'}}>
<i className="bi bi-camera-fill me-1"></i> Take Photo / Attach
<input type="file" accept="image/*" capture="environment" className="d-none" onChange={handleImageCapture} />
</label>
</div>
{/* Image Preview Box if picture taken */}
{noteImagePreview && (
<div className="position-relative mb-2 p-2 border rounded-3 bg-light text-center">
<span className="badge bg-primary position-absolute top-0 start-0 m-2"><i className="bi bi-camera me-1"></i> Handwritten Note Attached</span>
<img src={noteImagePreview} alt="Handwritten Note Preview" className="img-fluid rounded-3 border shadow-sm" style={{maxHeight: '180px', objectFit: 'contain'}} />
<button className="btn btn-sm btn-danger position-absolute top-0 end-0 m-2 rounded-circle p-1" style={{width: '28px', height: '28px'}} onClick={clearCapturedImage} title="Remove Photo">
<i className="bi bi-x-lg"></i>
</button>
</div>
)}
<textarea className="form-control mb-2 rounded-3" rows="3" placeholder="Enter clinical observations, daily round progress note, or bedside treatment instructions..." value={newTreatmentNote} onChange={e => setNewTreatmentNote(e.target.value)}></textarea>
<button className="btn btn-action-primary btn-sm fw-bold align-self-end px-4" onClick={handleSaveTreatmentNote}>
<i className="bi bi-send-fill me-1"></i> Save Round Note &amp; Share with Nursing
</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-journal-medical me-1"></i> Bedside Clinical Log History</h6>
{workspaceEntries.length === 0 ? (
<p className="text-muted small">No bedside log entries recorded yet.</p>
) : (
workspaceEntries.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>
) : activeOpd ? (
/* -------------------------------------------------------------
OPD CONSULTATION & PRESCRIPTION FORM
------------------------------------------------------------- */
<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={() => setActiveOpd(null)}>
<i className="bi bi-arrow-left me-1"></i> Back to Appointments
</button>
<div className="d-flex gap-2">
<button className="btn btn-sm btn-outline-secondary fw-bold rounded-pill px-3" onClick={() => window.open(`/Opd/opd_lettre_pdf/${activeOpd.opd_id}?layout=content_only`, '_blank')}>
<i className="bi bi-printer me-1"></i> Print PDF
</button>
<button className="btn btn-sm btn-action-success fw-bold rounded-pill px-3" onClick={handleSaveOpdPrescription}>
<i className="bi bi-check-circle-fill me-1"></i> Save &amp; Mark Visited
</button>
</div>
</div>
{/* Patient Header Card */}
<div className="card shadow-sm mb-3 border-0 rounded-4 text-white" style={{background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)'}}>
<div className="card-body p-3">
<div className="d-flex justify-content-between align-items-start">
<div>
<h5 className="fw-bold mb-1">{activeOpd.patient_display_name}</h5>
<div className="d-flex flex-wrap gap-1 opacity-90 small mt-2">
<span className="badge bg-white text-dark">{activeOpd.age_display || '47 Year'} • {activeOpd.gender_label}</span>
<span className="badge bg-white text-danger">UHID: {activeOpd.uhid}</span>
<span className="badge bg-white text-primary">OPD: {activeOpd.opd_code}</span>
</div>
</div>
<span className="queue-badge fs-6">Q #{activeOpd.queue_no || '1'}</span>
</div>
</div>
</div>
{/* Camera / Photo Attach Handwritten Prescription Option */}
<div className="card p-3 shadow-sm border-0 mb-3 rounded-4 bg-primary-subtle border-primary-subtle">
<div className="d-flex justify-content-between align-items-center">
<div>
<h6 className="fw-bold mb-0 text-primary"><i className="bi bi-camera-fill me-1"></i> Attach Photo of Handwritten Rx Note</h6>
<small className="text-muted d-block" style={{fontSize: '11px'}}>Snap a picture of your paper prescription to send to Pharmacy / Patient</small>
</div>
<label className="btn btn-sm btn-primary rounded-pill px-3 py-1.5 fw-bold">
<i className="bi bi-camera me-1"></i> Snap Photo
<input type="file" accept="image/*" capture="environment" className="d-none" onChange={handleImageCapture} />
</label>
</div>
{/* Preview if image captured */}
{noteImagePreview && (
<div className="position-relative mt-2 p-2 border rounded-3 bg-white text-center">
<span className="badge bg-success position-absolute top-0 start-0 m-2"><i className="bi bi-check-circle me-1"></i> Rx Photo Attached</span>
<img src={noteImagePreview} alt="Handwritten Rx Preview" className="img-fluid rounded-3 border shadow-sm" style={{maxHeight: '200px', objectFit: 'contain'}} />
<button className="btn btn-sm btn-danger position-absolute top-0 end-0 m-2 rounded-circle p-1" style={{width: '28px', height: '28px'}} onClick={clearCapturedImage} title="Remove Photo">
<i className="bi bi-x-lg"></i>
</button>
</div>
)}
</div>
{/* Vitals Entry Bar */}
<div className="card p-3 shadow-sm border-0 mb-3 rounded-4">
<h6 className="fw-bold mb-2 text-danger small"><i className="bi bi-heart-pulse-fill me-1"></i> Patient Vitals (BP, Pulse, Temp, SpO2, Weight)</h6>
<div className="row g-2">
<div className="col-4 col-md-2">
<label className="form-label small mb-1" style={{fontSize: '11px'}}>BP Sys/Dia</label>
<div className="input-group input-group-sm">
<input type="number" className="form-control px-1 text-center" placeholder="120" value={prescForm.bp_systolic} onChange={e => setPrescForm({...prescForm, bp_systolic: e.target.value})} />
<span className="input-group-text px-1">/</span>
<input type="number" className="form-control px-1 text-center" placeholder="80" value={prescForm.bp_diastolic} onChange={e => setPrescForm({...prescForm, bp_diastolic: e.target.value})} />
</div>
</div>
<div className="col-4 col-md-2">
<label className="form-label small mb-1" style={{fontSize: '11px'}}>Pulse</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="72" value={prescForm.pulse} onChange={e => setPrescForm({...prescForm, pulse: e.target.value})} />
</div>
<div className="col-4 col-md-2">
<label className="form-label small mb-1" style={{fontSize: '11px'}}>SpO2 %</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="98" value={prescForm.spo2} onChange={e => setPrescForm({...prescForm, spo2: e.target.value})} />
</div>
<div className="col-4 col-md-2">
<label className="form-label small mb-1" style={{fontSize: '11px'}}>Temp °F</label>
<input type="number" step="0.1" className="form-control form-control-sm text-center" placeholder="98.6" value={prescForm.temp} onChange={e => setPrescForm({...prescForm, temp: e.target.value})} />
</div>
<div className="col-4 col-md-2">
<label className="form-label small mb-1" style={{fontSize: '11px'}}>Weight (kg)</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="70" value={prescForm.weight} onChange={e => setPrescForm({...prescForm, weight: e.target.value})} />
</div>
<div className="col-4 col-md-2">
<label className="form-label small mb-1" style={{fontSize: '11px'}}>Height (cm)</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="165" value={prescForm.height} onChange={e => setPrescForm({...prescForm, height: e.target.value})} />
</div>
</div>
</div>
{/* Chief Complaints */}
<div className="card p-3 shadow-sm border-0 mb-3 rounded-4">
<label className="form-label fw-bold small text-dark mb-1"><i className="bi bi-chat-square-text text-primary me-1"></i> Chief Complaints</label>
<div className="d-flex gap-1 flex-wrap mb-2">
{COMPLAINT_CHIPS.map(c => (
<button key={c} type="button" className="chip-btn" onClick={() => appendChipText('chief_complaints', c)}>+ {c}</button>
))}
</div>
<textarea className="form-control form-control-sm rounded-3" rows="2" placeholder="Enter chief complaints..." value={prescForm.chief_complaints} onChange={e => setPrescForm({...prescForm, chief_complaints: e.target.value})}></textarea>
</div>
{/* Clinical Examination & Findings */}
<div className="card p-3 shadow-sm border-0 mb-3 rounded-4">
<label className="form-label fw-bold small text-dark mb-1"><i className="bi bi-clipboard2-pulse text-info me-1"></i> Clinical Examination &amp; Findings</label>
<div className="d-flex gap-1 flex-wrap mb-2">
{EXAMINATION_CHIPS.map(e => (
<button key={e} type="button" className="chip-btn" onClick={() => appendChipText('finding_examinations', e)}>+ {e}</button>
))}
</div>
<textarea className="form-control form-control-sm rounded-3" rows="2" placeholder="Enter clinical examination & findings..." value={prescForm.finding_examinations} onChange={e => setPrescForm({...prescForm, finding_examinations: e.target.value})}></textarea>
</div>
{/* Provisional Diagnosis */}
<div className="card p-3 shadow-sm border-0 mb-3 rounded-4">
<label className="form-label fw-bold small text-dark mb-1"><i className="bi bi-stethoscope text-primary me-1"></i> Provisional Diagnosis</label>
<div className="d-flex gap-1 flex-wrap mb-2">
{DIAGNOSIS_CHIPS.map(d => (
<button key={d} type="button" className="chip-btn" onClick={() => appendChipText('provisional_diagnosis', d)}>+ {d}</button>
))}
</div>
<textarea className="form-control form-control-sm rounded-3" rows="2" placeholder="Enter diagnosis..." value={prescForm.provisional_diagnosis} onChange={e => setPrescForm({...prescForm, provisional_diagnosis: e.target.value})}></textarea>
</div>
{/* Medicines Rx Repeater */}
<div className="card p-3 shadow-sm border-0 mb-3 rounded-4">
<div className="d-flex justify-content-between align-items-center mb-2">
<label className="form-label fw-bold small text-dark mb-0"><i className="bi bi-capsule text-success me-1"></i> Prescribed Medicines (Rx)</label>
<button className="btn btn-sm btn-outline-success py-0 px-2 fw-bold rounded-pill" style={{fontSize: '11px'}} onClick={() => addMedicineRow()}>+ Add Drug</button>
</div>
<div className="d-flex gap-1 flex-wrap mb-2">
{DRUG_CHIPS.map(drug => (
<button key={drug} type="button" className="chip-btn" onClick={() => addMedicineRow(drug)}>+ {drug}</button>
))}
</div>
{medicines.map((med, idx) => (
<div key={idx} className="p-2 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'}}>Item #{idx + 1}</span>
{medicines.length > 1 && (
<button className="btn btn-sm btn-link text-danger p-0" onClick={() => removeMedicineRow(idx)}><i className="bi bi-trash"></i> Remove</button>
)}
</div>
<div className="row g-2">
<div className="col-12 col-md-4">
<input type="text" className="form-control form-control-sm" placeholder="Medicine / Drug Name" value={med.drug_name} onChange={e => updateMedicineRow(idx, 'drug_name', e.target.value)} />
</div>
<div className="col-4 col-md-2">
<input type="text" className="form-control form-control-sm" placeholder="Dosage (1 Tab)" value={med.dosage} onChange={e => updateMedicineRow(idx, 'dosage', e.target.value)} />
</div>
<div className="col-4 col-md-3">
<select className="form-select form-select-sm" value={med.frequency} onChange={e => updateMedicineRow(idx, 'frequency', e.target.value)}>
<option value="1-0-1">1-0-1 (BD)</option>
<option value="1-1-1">1-1-1 (TID)</option>
<option value="1-0-0">1-0-0 (OD Morning)</option>
<option value="0-0-1">0-0-1 (OD Night)</option>
<option value="SOS">SOS (When needed)</option>
<option value="STAT">STAT (Immediately)</option>
</select>
</div>
<div className="col-4 col-md-3">
<select className="form-select form-select-sm" value={med.duration} onChange={e => updateMedicineRow(idx, 'duration', e.target.value)}>
<option value="3 Days">3 Days</option>
<option value="5 Days">5 Days</option>
<option value="7 Days">7 Days</option>
<option value="15 Days">15 Days</option>
<option value="1 Month">1 Month</option>
</select>
</div>
</div>
</div>
))}
</div>
{/* Lab Investigations & Advice */}
<div className="card p-3 shadow-sm border-0 mb-3 rounded-4">
<label className="form-label fw-bold small text-dark mb-1"><i className="bi bi-file-earmark-medical text-warning me-1"></i> Lab &amp; Pathology Orders</label>
<div className="d-flex gap-1 flex-wrap mb-2">
{LAB_CHIPS.map(lab => (
<button key={lab} type="button" className="chip-btn" onClick={() => appendChipText('investigation', lab)}>+ {lab}</button>
))}
</div>
<textarea className="form-control form-control-sm mb-3 rounded-3" rows="2" placeholder="Lab / Pathology / Radiology tests..." value={prescForm.investigation} onChange={e => setPrescForm({...prescForm, investigation: e.target.value})}></textarea>
<label className="form-label fw-bold small text-dark mb-1"><i className="bi bi-card-text me-1"></i> General Advice &amp; Instructions</label>
<textarea className="form-control form-control-sm mb-3 rounded-3" rows="2" placeholder="Doctor's advice to patient..." value={prescForm.advice} onChange={e => setPrescForm({...prescForm, advice: e.target.value})}></textarea>
<label className="form-label fw-bold small text-dark mb-1"><i className="bi bi-calendar-event me-1"></i> Follow-up Next Visit</label>
<div className="d-flex gap-1 mb-2">
{['After 3 Days', 'After 7 Days', 'After 15 Days', 'After 1 Month'].map(d => (
<button key={d} type="button" className="chip-btn" onClick={() => setPrescForm({...prescForm, next_visit: d})}>{d}</button>
))}
</div>
<input type="text" className="form-control form-control-sm rounded-3" placeholder="Follow-up advice" value={prescForm.next_visit} onChange={e => setPrescForm({...prescForm, next_visit: e.target.value})} />
</div>
<div className="d-flex gap-2 mb-4">
<button className="btn btn-outline-secondary btn-lg w-50 fw-bold py-2.5 rounded-3 shadow-sm" onClick={() => window.open(`/Opd/opd_lettre_pdf/${activeOpd.opd_id}?layout=content_only`, '_blank')}>
<i className="bi bi-printer me-1"></i> Print PDF
</button>
<button className="btn btn-action-success btn-lg w-50 fw-bold py-2.5 rounded-3 shadow-sm" onClick={handleSaveOpdPrescription}>
<i className="bi bi-check-circle-fill me-1"></i> Save &amp; Complete Visit
</button>
</div>
</div>
) : null}
{/* 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-primary"></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(<DoctorApp />);
</script>
</body>
</html>