1171 lines
75 KiB
HTML
1171 lines
75 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 PWA App</title>
|
|
<link rel="manifest" href="/App/Doctor/manifest.json">
|
|
<meta name="theme-color" content="#0d6efd">
|
|
<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/3774/3774299.png">
|
|
|
|
<!-- Bootstrap 5 CSS & Icons -->
|
|
<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>
|
|
|
|
<style>
|
|
:root {
|
|
--primary-bg: #0f172a;
|
|
--card-bg: #1e293b;
|
|
--accent-blue: #0d6efd;
|
|
--accent-green: #198754;
|
|
--accent-amber: #ffc107;
|
|
--accent-red: #dc3545;
|
|
}
|
|
|
|
body {
|
|
background-color: #f8fafc;
|
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
margin: 0;
|
|
padding: 0;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.doctor-header {
|
|
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
|
color: #ffffff;
|
|
padding: 12px 16px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.mobile-patient-card {
|
|
background: #ffffff;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 14px;
|
|
padding: 14px;
|
|
margin-bottom: 12px;
|
|
box-shadow: 0 2px 6px rgba(0,0,0,0.04);
|
|
border-left: 5px solid var(--accent-blue);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.mobile-patient-card:active {
|
|
transform: scale(0.99);
|
|
}
|
|
|
|
.mobile-patient-card.visited {
|
|
border-left-color: #0dcaf0;
|
|
}
|
|
|
|
.mobile-patient-card.booked {
|
|
border-left-color: #6c757d;
|
|
}
|
|
|
|
.mobile-patient-card.waiting {
|
|
border-left-color: #0d6efd;
|
|
}
|
|
|
|
.nav-pills .nav-link {
|
|
border-radius: 8px;
|
|
font-weight: 500;
|
|
color: #64748b;
|
|
}
|
|
|
|
.nav-pills .nav-link.active {
|
|
background-color: var(--accent-blue);
|
|
color: #ffffff;
|
|
}
|
|
|
|
/* 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: 16px;
|
|
width: 100%;
|
|
max-width: 400px;
|
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.chip-btn {
|
|
font-size: 11px;
|
|
padding: 4px 10px;
|
|
border-radius: 14px;
|
|
border: 1px solid #cbd5e1;
|
|
background: #f8fafc;
|
|
color: #334155;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.chip-btn:hover, .chip-btn.active {
|
|
background: #0d6efd;
|
|
color: #ffffff;
|
|
border-color: #0d6efd;
|
|
}
|
|
</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;
|
|
|
|
function DoctorApp() {
|
|
const [doctors, setDoctors] = useState([]);
|
|
const [ipdPatients, setIpdPatients] = useState([]);
|
|
const [opdAppointments, setOpdAppointments] = useState([]);
|
|
const [opdCounts, setOpdCounts] = useState({ all: 0, waiting: 0, visited: 0, booked: 0, cancelled: 0 });
|
|
const [opdFilter, setOpdFilter] = useState('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 [emrTab, setEmrTab] = useState('vitals');
|
|
const [installPrompt, setInstallPrompt] = useState(null);
|
|
|
|
// Auth state
|
|
const [authenticatedDoctor, setAuthenticatedDoctor] = useState(null);
|
|
const [authDocId, setAuthDocId] = useState(0);
|
|
const [pinInput, setPinInput] = useState('');
|
|
const [pinError, setPinError] = useState('');
|
|
const [isSettingNewPin, setIsSettingNewPin] = useState(false);
|
|
const [newPin, setNewPin] = useState('');
|
|
const [confirmPin, setConfirmPin] = useState('');
|
|
|
|
// OPD Prescription Consultation Controls State (Full Copy of HMS Functionality)
|
|
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 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);
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
const docIdParam = params.get('doctor_id');
|
|
if (docIdParam) {
|
|
setAuthDocId(docIdParam);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load doctors', err);
|
|
}
|
|
};
|
|
|
|
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) {
|
|
setIpdPatients(json.patients || []);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load IPD patients', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchOpdAppointments = async (docId) => {
|
|
try {
|
|
const res = await fetch(`/api/v1/doctor/opd/list/${docId || 0}`);
|
|
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);
|
|
} else {
|
|
localStorage.removeItem(DOC_DEVICE_AUTH_KEY);
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
fetchDoctors();
|
|
|
|
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 () => {
|
|
if (!authDocId || authDocId === '0') {
|
|
setPinError('Please select your Doctor 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('doctor_id', authDocId);
|
|
body.append('pin', pinInput);
|
|
|
|
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));
|
|
setPinInput('');
|
|
fetchIpdPatients(json.doctor.id);
|
|
fetchOpdAppointments(json.doctor.id);
|
|
} else if (json.status === 2) {
|
|
setIsSettingNewPin(true);
|
|
} else {
|
|
setPinError(json.message || 'Invalid 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('doctor_id', authDocId || (authenticatedDoctor ? authenticatedDoctor.id : 0));
|
|
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 openIpdWorkspace = async (patient) => {
|
|
setActivePatient(patient);
|
|
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 handleSaveTreatmentNote = async () => {
|
|
if (!activePatient || !newTreatmentNote.trim()) return;
|
|
|
|
try {
|
|
const body = new URLSearchParams();
|
|
body.append('doctor_id', authenticatedDoctor ? authenticatedDoctor.id : 0);
|
|
body.append('doctor_name', authenticatedDoctor ? authenticatedDoctor.name : 'Doctor');
|
|
body.append('treatment_text', newTreatmentNote);
|
|
|
|
const res = await fetch(`/api/v1/doctor/ipd/treatment/save/${activePatient.ipd_id}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: body.toString()
|
|
});
|
|
const json = await res.json();
|
|
if (json.status === 1) {
|
|
alert(json.message || 'Clinical treatment note saved successfully!');
|
|
setNewTreatmentNote('');
|
|
openIpdWorkspace(activePatient);
|
|
} else {
|
|
alert(json.message || 'Failed to save treatment note');
|
|
}
|
|
} catch (err) {
|
|
alert('Error saving treatment note');
|
|
}
|
|
};
|
|
|
|
const openOpdConsultation = async (app) => {
|
|
setActiveOpd(app);
|
|
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 body = new URLSearchParams();
|
|
body.append('doctor_id', authenticatedDoctor ? authenticatedDoctor.id : 0);
|
|
Object.keys(prescForm).forEach(k => body.append(k, prescForm[k]));
|
|
body.append('medicines', JSON.stringify(medicines));
|
|
|
|
const res = await fetch(`/api/v1/doctor/opd/prescription/save/${activeOpd.opd_id}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: body.toString()
|
|
});
|
|
const json = await res.json();
|
|
if (json.status === 1) {
|
|
alert(json.message || 'Prescription saved & OPD Visit completed!');
|
|
setActiveOpd(null);
|
|
fetchOpdAppointments(authenticatedDoctor ? authenticatedDoctor.id : 0);
|
|
} 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 filteredIpdPatients = ipdPatients.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);
|
|
});
|
|
|
|
// -------------------------------------------------------------
|
|
// RENDER: LOCK SCREEN (PIN AUTHENTICATION)
|
|
// -------------------------------------------------------------
|
|
if (!authenticatedDoctor) {
|
|
return (
|
|
<div className="lock-screen-overlay">
|
|
<div className="lock-card">
|
|
<div className="p-4 text-center bg-primary text-white">
|
|
<i className="bi bi-person-badge-fill display-4 d-block mb-2"></i>
|
|
<h5 className="fw-bold mb-0">DoctorCare EMR App</h5>
|
|
<small className="opacity-75">30-Day Device Authentication Security</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 Doctor Profile</label>
|
|
<select
|
|
className="form-select"
|
|
value={authDocId}
|
|
onChange={(e) => setAuthDocId(e.target.value)}
|
|
>
|
|
<option value="0">-- Choose Doctor Profile --</option>
|
|
{doctors.map(d => (
|
|
<option key={d.id} value={d.id}>{d.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-4"
|
|
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-primary w-100 py-2 fw-bold" onClick={handleVerifyPin}>
|
|
<i className="bi bi-unlock-fill me-1"></i> Unlock Doctor App
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<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-4"
|
|
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-4"
|
|
placeholder="••••"
|
|
value={confirmPin}
|
|
onChange={(e) => setConfirmPin(e.target.value)}
|
|
/>
|
|
</div>
|
|
<button className="btn btn-success w-100 py-2 fw-bold" onClick={handleSaveNewPin}>
|
|
<i className="bi bi-check-circle-fill me-1"></i> Save PIN & Unlock
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// RENDER: UNLOCKED DOCTOR PWA APP
|
|
// -------------------------------------------------------------
|
|
return (
|
|
<div>
|
|
{/* Top Doctor Header */}
|
|
<header className="doctor-header d-flex justify-content-between align-items-center">
|
|
<div className="d-flex align-items-center">
|
|
<i className="bi bi-person-badge-fill text-primary fs-3 me-2"></i>
|
|
<div>
|
|
<h6 className="mb-0 fw-bold">{authenticatedDoctor.name}</h6>
|
|
<small className="text-light opacity-75">Doctor EMR <span className="badge bg-success ms-1" style={{fontSize: '9px'}}>30-Day Device Auth</span></small>
|
|
</div>
|
|
</div>
|
|
<div className="d-flex align-items-center gap-2">
|
|
<button className="btn btn-sm btn-outline-light" onClick={() => { fetchOpdAppointments(authenticatedDoctor.id); fetchIpdPatients(authenticatedDoctor.id); }} title="Refresh">
|
|
<i className="bi bi-arrow-clockwise"></i>
|
|
</button>
|
|
<button className="btn btn-sm btn-danger" onClick={handleLogout} title="Lock Device Access">
|
|
<i className="bi bi-lock-fill me-1"></i> Lock
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Container */}
|
|
<main className="container-fluid py-3 px-2 px-md-3">
|
|
{!activeOpd && !activePatient ? (
|
|
<div>
|
|
{/* Top Main Tabs */}
|
|
<ul className="nav nav-pills mb-3 gap-2">
|
|
<li className="nav-item">
|
|
<button className={`nav-link ${mainTab === 'opd' ? 'active' : ''}`} onClick={() => setMainTab('opd')}>
|
|
<i className="bi bi-calendar-check-fill me-1"></i> OPD Appointments ({opdAppointments.length})
|
|
</button>
|
|
</li>
|
|
<li className="nav-item">
|
|
<button className={`nav-link ${mainTab === 'ipd' ? 'active' : ''}`} onClick={() => setMainTab('ipd')}>
|
|
<i className="bi bi-hospital me-1"></i> Admitted IPDs ({ipdPatients.length})
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
{mainTab === 'opd' ? (
|
|
<div>
|
|
{/* Status Filter Badges (Matching HMS Screen) */}
|
|
<div className="d-flex gap-1 mb-3 overflow-auto pb-1">
|
|
<button
|
|
className={`btn btn-sm text-nowrap ${opdFilter === 'all' ? 'btn-primary' : 'btn-outline-primary'}`}
|
|
onClick={() => setOpdFilter('all')}
|
|
>
|
|
All <span className="badge bg-white text-primary ms-1">{opdCounts.all}</span>
|
|
</button>
|
|
<button
|
|
className={`btn btn-sm text-nowrap ${opdFilter === 'waiting' ? 'btn-primary' : 'btn-outline-primary'}`}
|
|
onClick={() => setOpdFilter('waiting')}
|
|
>
|
|
Waiting <span className="badge bg-white text-primary ms-1">{opdCounts.waiting}</span>
|
|
</button>
|
|
<button
|
|
className={`btn btn-sm text-nowrap ${opdFilter === 'visited' ? 'btn-info text-dark' : 'btn-outline-info'}`}
|
|
onClick={() => setOpdFilter('visited')}
|
|
>
|
|
Visited <span className="badge bg-white text-dark ms-1">{opdCounts.visited}</span>
|
|
</button>
|
|
<button
|
|
className={`btn btn-sm text-nowrap ${opdFilter === 'booked' ? 'btn-secondary' : 'btn-outline-secondary'}`}
|
|
onClick={() => setOpdFilter('booked')}
|
|
>
|
|
Booked <span className="badge bg-white text-secondary ms-1">{opdCounts.booked}</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Search Input */}
|
|
<div className="input-group mb-3">
|
|
<span className="input-group-text bg-white"><i className="bi bi-search"></i></span>
|
|
<input
|
|
type="text"
|
|
className="form-control"
|
|
placeholder="Search Patient Name, UHID, OPD No..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{/* MOBILE PROFESSIONAL CARDS LIST (Phones < 768px - NO Left-Right Scroll Needed!) */}
|
|
<div className="d-block d-md-none">
|
|
{filteredOpdAppointments.length === 0 ? (
|
|
<div className="text-center text-muted py-4 bg-white rounded border">
|
|
No OPD appointments found for selected filter.
|
|
</div>
|
|
) : (
|
|
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 (
|
|
<div key={app.opd_id} className={`mobile-patient-card ${app.status_key}`}>
|
|
<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="badge bg-dark fs-6 px-2 py-1">Q #{app.queue_no || app.opd_no || '1'}</span>
|
|
<span className={`badge ${statusBadgeClass} text-uppercase px-2 py-1`}>
|
|
{app.status_label}
|
|
</span>
|
|
</div>
|
|
<strong className="text-dark small">{app.opd_code}</strong>
|
|
</div>
|
|
|
|
<div className="mb-3">
|
|
<h6 className="fw-bold text-primary mb-1 fs-5">{app.patient_display_name}</h6>
|
|
<div className="d-flex flex-wrap gap-2 text-muted small">
|
|
<span className="badge bg-light text-dark border">
|
|
<i className="bi bi-person me-1"></i>{app.age_display || '47 Year'} / {app.gender_label}
|
|
</span>
|
|
<span className="badge bg-light text-danger border">
|
|
<i className="bi bi-card-text me-1"></i>{app.uhid || '-'}
|
|
</span>
|
|
<span className="badge bg-light text-secondary border">
|
|
<i className="bi bi-cash me-1"></i>{app.opd_type || 'Cash'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<button
|
|
className="btn btn-primary w-100 fw-bold py-2 shadow-sm"
|
|
onClick={() => openOpdConsultation(app)}
|
|
>
|
|
<i className="bi bi-file-earmark-medical me-1"></i> Consult Patient
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
{/* DESKTOP TABLE VIEW (Monitors >= 768px) */}
|
|
<div className="card shadow-sm border-0 d-none d-md-block">
|
|
<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`}>
|
|
{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-secondary-subtle text-dark 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-outline-primary fw-bold me-1"
|
|
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)
|
|
------------------------------------------------------------- */
|
|
<div>
|
|
{/* Search Input */}
|
|
<div className="input-group mb-3">
|
|
<span className="input-group-text bg-white"><i className="bi bi-search"></i></span>
|
|
<input
|
|
type="text"
|
|
className="form-control"
|
|
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-4 bg-white rounded border">
|
|
<i className="bi bi-hospital fs-1 d-block text-secondary mb-2"></i>
|
|
No Admitted IPD Patients found.
|
|
</div>
|
|
) : (
|
|
filteredIpdPatients.map(pat => (
|
|
<div key={pat.ipd_id} className="mobile-patient-card" style={{borderLeft: '5px solid #198754'}}>
|
|
<div className="d-flex justify-content-between align-items-center mb-2 pb-2 border-bottom">
|
|
<span className="badge bg-success text-uppercase px-2 py-1">Current Admission</span>
|
|
<strong className="text-dark small">{pat.ipd_code}</strong>
|
|
</div>
|
|
|
|
<div className="mb-3">
|
|
<h6 className="fw-bold text-primary mb-1 fs-5">{pat.patient_display_name}</h6>
|
|
<small className="text-muted d-block mb-2">
|
|
{pat.rname ? `S/D/W of ${pat.rname}` : `(${pat.age_display} / ${pat.gender_label})`}
|
|
</small>
|
|
|
|
<div className="d-flex flex-wrap gap-2 mb-2">
|
|
<span className="badge bg-primary-subtle text-primary border">
|
|
<i className="bi bi-door-closed me-1"></i>{pat.bed_label}
|
|
</span>
|
|
<span className="badge bg-light text-danger border">
|
|
<i className="bi bi-card-text me-1"></i>{pat.uhid}
|
|
</span>
|
|
<span className="badge bg-light text-dark border">
|
|
<i className="bi bi-person-badge me-1"></i>{pat.assigned_doctors || pat.r_doc_name || `Dr. ${authenticatedDoctor.name}`}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<button
|
|
className="btn btn-success w-100 fw-bold py-2 shadow-sm"
|
|
onClick={() => openIpdWorkspace(pat)}
|
|
>
|
|
<i className="bi bi-heart-pulse-fill me-1"></i> 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">
|
|
<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-primary fw-bold"
|
|
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" onClick={() => setActivePatient(null)}>
|
|
<i className="bi bi-arrow-left me-1"></i> Back to Admitted IPDs
|
|
</button>
|
|
<span className="badge bg-success fs-6">Status: CURRENT ADMISSION</span>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3 border-0 bg-primary text-white">
|
|
<div className="card-body p-3">
|
|
<h5 className="fw-bold mb-1">{activePatient.patient_display_name}</h5>
|
|
<small className="opacity-90 me-3">Demographics: {activePatient.age_display} / {activePatient.gender_label}</small>
|
|
<small className="opacity-90 me-3">UHID: {activePatient.uhid}</small>
|
|
<small className="opacity-90 me-3">IPD Code: {activePatient.ipd_code}</small>
|
|
<small className="opacity-90">Bed: {activePatient.bed_label}</small>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Write Doctor Round Note / Treatment Order */}
|
|
<div className="card p-3 shadow-sm border-0 mb-3">
|
|
<label className="form-label fw-bold small text-dark mb-1"><i className="bi bi-pencil-square text-primary me-1"></i> Write Doctor Round Clinical Note / Treatment Order</label>
|
|
<textarea className="form-control form-control-sm mb-2" 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-primary btn-sm fw-bold align-self-end" onClick={handleSaveTreatmentNote}>
|
|
<i className="bi bi-send-fill me-1"></i> Save Round Note
|
|
</button>
|
|
</div>
|
|
|
|
{/* Bedside Log Entries */}
|
|
<div className="card p-3 shadow-sm border-0">
|
|
<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-2 border rounded 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>
|
|
<div className="small fw-semibold">{e.treatment_text || e.general_note}</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : activeOpd ? (
|
|
/* -------------------------------------------------------------
|
|
OPD CONSULTATION & PRESCRIPTION FORM (NATIVE CONTROLS COPY)
|
|
------------------------------------------------------------- */
|
|
<div>
|
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
|
<button className="btn btn-sm btn-outline-dark" 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" 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-success fw-bold" onClick={handleSaveOpdPrescription}>
|
|
<i className="bi bi-check-circle-fill me-1"></i> Save & Mark Visited
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Patient Header Card */}
|
|
<div className="card shadow-sm mb-3 border-0 bg-primary text-white">
|
|
<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>
|
|
<small className="opacity-90 me-2">Age / Gender: {activeOpd.age_display || '47 Year'} / {activeOpd.gender_label}</small>
|
|
<small className="opacity-90 me-2">UHID: {activeOpd.uhid}</small>
|
|
<small className="opacity-90">OPD No: {activeOpd.opd_code}</small>
|
|
</div>
|
|
<span className="badge bg-white text-primary fs-6">Q #{activeOpd.queue_no || '1'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Vitals Entry Bar (Native HMS Controls) */}
|
|
<div className="card p-3 shadow-sm border-0 mb-3">
|
|
<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">
|
|
<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" 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">
|
|
<label className="form-label fw-bold small text-dark mb-1"><i className="bi bi-clipboard2-pulse text-info me-1"></i> Clinical Examination & 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" 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">
|
|
<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" rows="2" placeholder="Enter diagnosis..." value={prescForm.provisional_diagnosis} onChange={e => setPrescForm({...prescForm, provisional_diagnosis: e.target.value})}></textarea>
|
|
</div>
|
|
|
|
{/* Medicines Rx Repeater (Native HMS Controls) */}
|
|
<div className="card p-3 shadow-sm border-0 mb-3">
|
|
<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" 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 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">
|
|
<label className="form-label fw-bold small text-dark mb-1"><i className="bi bi-file-earmark-medical text-warning me-1"></i> Lab & 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" 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 & Instructions</label>
|
|
<textarea className="form-control form-control-sm mb-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" 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 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-success btn-lg w-50 fw-bold py-2 shadow-sm" onClick={handleSaveOpdPrescription}>
|
|
<i className="bi bi-check-circle-fill me-1"></i> Save & Mark Visited
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
root.render(<DoctorApp />);
|
|
</script>
|
|
</body>
|
|
</html>
|