2025-10-18 13:49:53 -04:00

369 lines
14 KiB
HTML

import React, { useState, useEffect } from 'react';
import { Activity, AlertCircle, Clock, Users, Bell, Heart, Thermometer, Wind, CheckCircle, UserX } from 'lucide-react';
const StaffDashboard = () => {
const [patients, setPatients] = useState([]);
const [stats, setStats] = useState({
total_patients: 0,
active_patients: 0,
tier_breakdown: { EMERGENCY: 0, ALERT: 0, NORMAL: 0 },
average_wait_minutes: 0
});
const [selectedPatient, setSelectedPatient] = useState(null);
const [filter, setFilter] = useState('all');
useEffect(() => {
const generateMockPatients = () => {
const mockPatients = [
{
patient_id: 'P100001',
band_id: 'VitalLink-A1B2',
name: 'John Smith',
tier: 'NORMAL',
priority_score: 15.2,
wait_time_minutes: 22,
last_hr: 76,
last_spo2: 98,
last_temp: 36.8,
symptoms: ['Chest Pain', 'Nausea']
},
{
patient_id: 'P100002',
band_id: 'VitalLink-C3D4',
name: 'Sarah Johnson',
tier: 'ALERT',
priority_score: 68.5,
wait_time_minutes: 45,
last_hr: 118,
last_spo2: 93,
last_temp: 38.4,
symptoms: ['Fever', 'Difficulty Breathing']
},
{
patient_id: 'P100003',
band_id: 'VitalLink-E5F6',
name: 'Michael Chen',
tier: 'EMERGENCY',
priority_score: 142.8,
wait_time_minutes: 8,
last_hr: 148,
last_spo2: 86,
last_temp: 39.7,
symptoms: ['Severe Headache', 'Chest Pain']
},
{
patient_id: 'P100004',
band_id: 'VitalLink-G7H8',
name: 'Emily Davis',
tier: 'NORMAL',
priority_score: 18.0,
wait_time_minutes: 35,
last_hr: 82,
last_spo2: 97,
last_temp: 37.1,
symptoms: ['Abdominal Pain']
},
{
patient_id: 'P100005',
band_id: 'VitalLink-I9J0',
name: 'Robert Williams',
tier: 'ALERT',
priority_score: 72.3,
wait_time_minutes: 52,
last_hr: 124,
last_spo2: 91,
last_temp: 38.8,
symptoms: ['Fever', 'Dizziness']
}
];
setPatients(mockPatients);
setStats({
total_patients: 5,
active_patients: 5,
tier_breakdown: { EMERGENCY: 1, ALERT: 2, NORMAL: 2 },
average_wait_minutes: 32.4
});
};
generateMockPatients();
const interval = setInterval(() => {
setPatients(prev => prev.map(p => ({
...p,
wait_time_minutes: p.wait_time_minutes + 1,
last_hr: Math.max(40, Math.min(180, p.last_hr + Math.floor(Math.random() * 5) - 2)),
last_spo2: Math.max(70, Math.min(100, p.last_spo2 + Math.floor(Math.random() * 3) - 1)),
last_temp: Math.max(35, Math.min(41, p.last_temp + (Math.random() * 0.2 - 0.1)))
})));
}, 3000);
return () => clearInterval(interval);
}, []);
const getTierColor = (tier) => {
switch(tier) {
case 'EMERGENCY': return 'bg-red-100 text-red-800 border-red-300';
case 'ALERT': return 'bg-yellow-100 text-yellow-800 border-yellow-300';
case 'NORMAL': return 'bg-green-100 text-green-800 border-green-300';
default: return 'bg-gray-100 text-gray-800 border-gray-300';
}
};
const getTierIcon = (tier) => {
switch(tier) {
case 'EMERGENCY': return <AlertCircle className="w-5 h-5" />;
case 'ALERT': return <Bell className="w-5 h-5" />;
case 'NORMAL': return <CheckCircle className="w-5 h-5" />;
default: return <Activity className="w-5 h-5" />;
}
};
const getVitalStatus = (type, value) => {
if (type === 'hr') {
if (value > 110 || value < 50) return 'text-red-600 font-bold';
if (value > 100 || value < 60) return 'text-yellow-600 font-semibold';
return 'text-green-600';
}
if (type === 'spo2') {
if (value < 88) return 'text-red-600 font-bold';
if (value < 92) return 'text-yellow-600 font-semibold';
return 'text-green-600';
}
if (type === 'temp') {
if (value > 39.5 || value < 35.5) return 'text-red-600 font-bold';
if (value > 38.3 || value < 36.0) return 'text-yellow-600 font-semibold';
return 'text-green-600';
}
return 'text-gray-700';
};
const handleDischarge = (patientId) => {
setPatients(prev => prev.filter(p => p.patient_id !== patientId));
setSelectedPatient(null);
};
const filteredPatients = patients
.filter(p => filter === 'all' || p.tier === filter)
.sort((a, b) => b.priority_score - a.priority_score);
return (
<div className="min-h-screen bg-gray-100">
<div className="bg-gradient-to-r from-blue-600 to-blue-700 text-white p-6 shadow-lg">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold mb-1">VitalLink Dashboard</h1>
<p className="text-blue-100">Emergency Department Patient Monitoring</p>
</div>
<div className="flex items-center gap-3">
<div className="bg-white bg-opacity-20 rounded-lg px-4 py-2">
<p className="text-sm text-blue-100">Last Update</p>
<p className="text-lg font-semibold">{new Date().toLocaleTimeString()}</p>
</div>
</div>
</div>
</div>
</div>
<div className="bg-white border-b shadow-sm">
<div className="max-w-7xl mx-auto p-6">
<div className="grid grid-cols-4 gap-6">
<div className="flex items-center gap-4">
<div className="bg-blue-100 p-3 rounded-lg">
<Users className="w-6 h-6 text-blue-600" />
</div>
<div>
<p className="text-sm text-gray-600">Active Patients</p>
<p className="text-2xl font-bold text-gray-800">{stats.active_patients}</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="bg-red-100 p-3 rounded-lg">
<AlertCircle className="w-6 h-6 text-red-600" />
</div>
<div>
<p className="text-sm text-gray-600">Emergency</p>
<p className="text-2xl font-bold text-red-600">{stats.tier_breakdown.EMERGENCY}</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="bg-yellow-100 p-3 rounded-lg">
<Bell className="w-6 h-6 text-yellow-600" />
</div>
<div>
<p className="text-sm text-gray-600">Alert</p>
<p className="text-2xl font-bold text-yellow-600">{stats.tier_breakdown.ALERT}</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="bg-green-100 p-3 rounded-lg">
<Clock className="w-6 h-6 text-green-600" />
</div>
<div>
<p className="text-sm text-gray-600">Avg Wait Time</p>
<p className="text-2xl font-bold text-gray-800">{stats.average_wait_minutes} min</p>
</div>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto p-6">
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
<div className="flex gap-2">
<button
onClick={() => setFilter('all')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'all'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
All Patients ({patients.length})
</button>
<button
onClick={() => setFilter('EMERGENCY')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'EMERGENCY'
? 'bg-red-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Emergency ({stats.tier_breakdown.EMERGENCY})
</button>
<button
onClick={() => setFilter('ALERT')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'ALERT'
? 'bg-yellow-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Alert ({stats.tier_breakdown.ALERT})
</button>
<button
onClick={() => setFilter('NORMAL')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'NORMAL'
? 'bg-green-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Stable ({stats.tier_breakdown.NORMAL})
</button>
</div>
</div>
<div className="space-y-4">
{filteredPatients.map((patient, index) => (
<div
key={patient.patient_id}
className="bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow"
>
<div className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-start gap-4">
<div className="text-2xl font-bold text-gray-400 min-w-12 text-center pt-1">
#{index + 1}
</div>
<div>
<h3 className="text-xl font-bold text-gray-800 mb-1">{patient.name}</h3>
<div className="flex items-center gap-3 text-sm text-gray-600">
<span className="font-mono">{patient.patient_id}</span>
<span></span>
<span className="font-mono">{patient.band_id}</span>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{patient.symptoms.map(symptom => (
<span key={symptom} className="bg-gray-100 text-gray-700 px-3 py-1 rounded-full text-xs font-medium">
{symptom}
</span>
))}
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className={`px-4 py-2 rounded-lg border-2 flex items-center gap-2 font-semibold ${getTierColor(patient.tier)}`}>
{getTierIcon(patient.tier)}
{patient.tier}
</div>
<button
onClick={() => handleDischarge(patient.patient_id)}
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors flex items-center gap-2 font-semibold"
>
<UserX className="w-4 h-4" />
Discharge
</button>
</div>
</div>
<div className="grid grid-cols-4 gap-4 pt-4 border-t">
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<Heart className="w-4 h-4 text-gray-600" />
<span className="text-xs text-gray-600 font-medium">Heart Rate</span>
</div>
<p className={`text-2xl font-bold ${getVitalStatus('hr', patient.last_hr)}`}>
{patient.last_hr}
</p>
<p className="text-xs text-gray-500 mt-1">bpm</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<Wind className="w-4 h-4 text-gray-600" />
<span className="text-xs text-gray-600 font-medium">SpO₂</span>
</div>
<p className={`text-2xl font-bold ${getVitalStatus('spo2', patient.last_spo2)}`}>
{patient.last_spo2}
</p>
<p className="text-xs text-gray-500 mt-1">%</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<Thermometer className="w-4 h-4 text-gray-600" />
<span className="text-xs text-gray-600 font-medium">Temperature</span>
</div>
<p className={`text-2xl font-bold ${getVitalStatus('temp', patient.last_temp)}`}>
{patient.last_temp.toFixed(1)}
</p>
<p className="text-xs text-gray-500 mt-1">°C</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<Clock className="w-4 h-4 text-gray-600" />
<span className="text-xs text-gray-600 font-medium">Wait Time</span>
</div>
<p className="text-2xl font-bold text-gray-700">
{patient.wait_time_minutes}
</p>
<p className="text-xs text-gray-500 mt-1">minutes</p>
</div>
</div>
</div>
</div>
))}
</div>
{filteredPatients.length === 0 && (
<div className="bg-white rounded-lg shadow-sm p-12 text-center">
<Activity className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-600 mb-2">No patients in this category</h3>
<p className="text-gray-500">Patients will appear here as they check in</p>
</div>
)}
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<StaffDashboard />);