1157 lines
61 KiB
TypeScript
1157 lines
61 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|||
|
|
import {
|
||
|
|
Building2,
|
||
|
|
Activity,
|
||
|
|
Truck,
|
||
|
|
PhoneCall,
|
||
|
|
FileText,
|
||
|
|
Settings,
|
||
|
|
Plus,
|
||
|
|
Search,
|
||
|
|
MapPin,
|
||
|
|
Clock,
|
||
|
|
ShieldAlert,
|
||
|
|
CheckCircle2,
|
||
|
|
XCircle,
|
||
|
|
ChevronRight,
|
||
|
|
ChevronDown,
|
||
|
|
HeartPulse,
|
||
|
|
Share2,
|
||
|
|
AlertTriangle,
|
||
|
|
Monitor,
|
||
|
|
Users,
|
||
|
|
LayoutDashboard,
|
||
|
|
TrendingUp,
|
||
|
|
Printer,
|
||
|
|
Download,
|
||
|
|
Filter,
|
||
|
|
Map as MapIcon,
|
||
|
|
Video,
|
||
|
|
Mic,
|
||
|
|
MoreVertical,
|
||
|
|
ExternalLink,
|
||
|
|
Save,
|
||
|
|
Bell,
|
||
|
|
Navigation,
|
||
|
|
Calendar,
|
||
|
|
AlertCircle,
|
||
|
|
Heart,
|
||
|
|
Wind,
|
||
|
|
Thermometer,
|
||
|
|
Zap,
|
||
|
|
Database,
|
||
|
|
Lock,
|
||
|
|
Stethoscope,
|
||
|
|
Phone,
|
||
|
|
Trash2,
|
||
|
|
Timer,
|
||
|
|
CheckCircle,
|
||
|
|
Menu,
|
||
|
|
X,
|
||
|
|
Eye,
|
||
|
|
EyeOff
|
||
|
|
} from 'lucide-react';
|
||
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
||
|
|
import { Card, StatCard } from '../components/Common';
|
||
|
|
import { authApi } from '../api/auth';
|
||
|
|
import './HospitalConsole.css';
|
||
|
|
|
||
|
|
type ConsoleModule = 'ED_MONITOR' | 'BOOKINGS' | 'FLEET' | 'TELELINK' | 'EPCR' | 'HISTORY' | 'REPORTS' | 'SETUP' | 'REFERRALS';
|
||
|
|
|
||
|
|
export const HospitalConsole: React.FC = () => {
|
||
|
|
const [hospitals, setHospitals] = useState<any[]>([]);
|
||
|
|
const [isLoadingHospitals, setIsLoadingHospitals] = useState(true);
|
||
|
|
const [selectedHospital, setSelectedHospital] = useState<any>(null);
|
||
|
|
const [editingHospital, setEditingHospital] = useState<any>(null);
|
||
|
|
const [isUpdating, setIsUpdating] = useState(false);
|
||
|
|
const [activeModule, setActiveModule] = useState<ConsoleModule>('ED_MONITOR');
|
||
|
|
const [isBookingModalOpen, setIsBookingModalOpen] = useState(false);
|
||
|
|
const [successMessage, setSuccessMessage] = useState<string>('');
|
||
|
|
const [setupSubTab, setSetupSubTab] = useState('PROFILE');
|
||
|
|
const [departments, setDepartments] = useState<any[]>([]);
|
||
|
|
const [isDeptModalOpen, setIsDeptModalOpen] = useState(false);
|
||
|
|
const [deptFormData, setDeptFormData] = useState({
|
||
|
|
name: '',
|
||
|
|
headOfDepartment: '',
|
||
|
|
totalBedsCapacity: 0,
|
||
|
|
contactPhone: '',
|
||
|
|
isActive: true
|
||
|
|
});
|
||
|
|
const [newBedUnit, setNewBedUnit] = useState({ dept: '', total: 10, occupied: 0 });
|
||
|
|
const [isAddingBed, setIsAddingBed] = useState(false);
|
||
|
|
const [bedUnits, setBedUnits] = useState([
|
||
|
|
{ id: 1, dept: 'Emergency Room', total: 12, occupied: 8, status: 'LIVE' },
|
||
|
|
{ id: 2, dept: 'ICU - Level 1', total: 8, occupied: 7, status: 'LIVE' },
|
||
|
|
{ id: 3, dept: 'Trauma Unit', total: 4, occupied: 2, status: 'MANUAL' }
|
||
|
|
]);
|
||
|
|
const [incomingPatients] = useState([
|
||
|
|
{ id: 'T-9821', name: 'Rajesh Khanna', age: 45, gender: 'M', triage: 'RED', complaint: 'Cardiac Arrest', eta: '4m', reg: 'KA-01-EF-2291', ambulanceId: 'CURE-ALS-01', vitals: { hr: '124', spo2: '88', bp: '90/60', temp: '98.6' }, location: 'Electronic City' },
|
||
|
|
{ id: 'T-9825', name: 'Sunita Rao', age: 32, gender: 'F', triage: 'YELLOW', complaint: 'Respiratory Distress', eta: '7m', reg: 'KA-05-MN-8832', ambulanceId: 'CURE-BLS-09', vitals: { hr: '98', spo2: '94', bp: '130/85', temp: '101.2' }, location: 'HSR Layout' },
|
||
|
|
{ id: 'T-9830', name: 'Unknown Male', age: 28, gender: 'M', triage: 'RED', complaint: 'Multiple Trauma', eta: '2m', reg: 'KA-03-GH-1102', ambulanceId: 'CURE-ALS-04', vitals: { hr: '142', spo2: '82', bp: '70/40', temp: '97.4' }, location: 'Koramangala' },
|
||
|
|
{ id: 'T-9835', name: 'Amit Shah', age: 58, gender: 'M', triage: 'GREEN', complaint: 'Fracture - Tibia', eta: '12m', reg: 'KA-02-KL-4451', ambulanceId: 'CURE-PTS-12', vitals: { hr: '82', spo2: '98', bp: '120/80', temp: '98.8' }, location: 'Indiranagar' }
|
||
|
|
]);
|
||
|
|
const [tripsData, setTripsData] = useState([
|
||
|
|
{ id: 'TR-4022', patient: 'Anil Kapur', mrn: 'MRN-9022', type: 'IFT-CRITICAL', origin: 'District Hospital', destination: 'Apollo Main', vehicle: 'ALS-04', crew: 'Dr. Arjun, EMT Som', status: 'IN TRANSIT', eta: '6m', urgency: 'RED', step: 2 },
|
||
|
|
{ id: 'TR-4025', patient: 'Priya Verma', mrn: 'MRN-1102', type: 'EMERGENCY', origin: 'Indiranagar', destination: 'Apollo Main', vehicle: 'BLS-12', crew: 'EMT Suman, Pilot Ravi', status: 'ARRIVED', eta: '0m', urgency: 'YELLOW', step: 3 },
|
||
|
|
{ id: 'TR-4030', patient: 'Sarah Khan', mrn: 'MRN-4451', type: 'IFT', origin: 'Apollo Main', destination: 'Narayana Health', vehicle: 'ALS-09', crew: 'EMT Kiran, Pilot Dev', status: 'DISPATCHED', eta: '15m', urgency: 'GREEN', step: 1 },
|
||
|
|
]);
|
||
|
|
const [isAddingDept, setIsAddingDept] = useState(false);
|
||
|
|
const [newDeptName, setNewDeptName] = useState('');
|
||
|
|
const [allUsers, setAllUsers] = useState<any[]>([]);
|
||
|
|
const [userSearchTerm, setUserSearchTerm] = useState('');
|
||
|
|
const [isStaffModalOpen, setIsStaffModalOpen] = useState(false);
|
||
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||
|
|
const [staffFormData, setStaffFormData] = useState({
|
||
|
|
name: '',
|
||
|
|
email: '',
|
||
|
|
phone: '',
|
||
|
|
username: '',
|
||
|
|
password: '',
|
||
|
|
role: 'ED_DOCTOR',
|
||
|
|
department: '',
|
||
|
|
specialization: '',
|
||
|
|
license: '',
|
||
|
|
designation: '',
|
||
|
|
shift: 'General'
|
||
|
|
});
|
||
|
|
|
||
|
|
const [user, setUser] = useState<any>(null);
|
||
|
|
const [rolesList, setRolesList] = useState<any[]>([]);
|
||
|
|
const [showPassword, setShowPassword] = useState(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const userData = localStorage.getItem('teleems_user');
|
||
|
|
if (userData) {
|
||
|
|
setUser(JSON.parse(userData));
|
||
|
|
}
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const loadRoles = async () => {
|
||
|
|
try {
|
||
|
|
const token = localStorage.getItem('teleems_token') || '';
|
||
|
|
if (!token) return;
|
||
|
|
const res = await authApi.getRoles(token);
|
||
|
|
if (res.data && res.data.data) {
|
||
|
|
setRolesList(res.data.data);
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to fetch roles:', err);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
loadRoles();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const fetchDepartments = async () => {
|
||
|
|
const token = localStorage.getItem('teleems_token');
|
||
|
|
if (token && selectedHospital) {
|
||
|
|
try {
|
||
|
|
const res = await authApi.getDepartments(token);
|
||
|
|
if (res.data) {
|
||
|
|
const hospId = selectedHospital.rawUser?.hospitalId || selectedHospital.id;
|
||
|
|
setDepartments(res.data.filter((d: any) => d.hospitalId === hospId));
|
||
|
|
}
|
||
|
|
} catch(err) {
|
||
|
|
console.error('Failed to fetch departments:', err);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
fetchDepartments();
|
||
|
|
}, [selectedHospital]);
|
||
|
|
|
||
|
|
const userRoles = user?.roles?.map((r: any) => String(r).toUpperCase()) || [];
|
||
|
|
const isAdmin = userRoles.includes('ADMIN') || userRoles.includes('HOSPITAL_ADMIN') || userRoles.includes('CURESELECT_ADMIN');
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const fetchHospitals = async () => {
|
||
|
|
const token = localStorage.getItem('teleems_token');
|
||
|
|
if (!token) {
|
||
|
|
setIsLoadingHospitals(false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await authApi.getUsers(token);
|
||
|
|
if (response.status === 200 || response.status === 201) {
|
||
|
|
const users = response.data || [];
|
||
|
|
setAllUsers(users);
|
||
|
|
|
||
|
|
const admins = users.filter((u: any) => {
|
||
|
|
const roles = Array.isArray(u.roles) ? u.roles.map((r: any) => String(r).toUpperCase()) : [];
|
||
|
|
return roles.includes('HOSPITAL ADMIN') || roles.includes('HOSPITAL_ADMIN');
|
||
|
|
});
|
||
|
|
|
||
|
|
const mappedHospitals = admins.map((u: any) => {
|
||
|
|
const h = u.metadata?.hospital || {};
|
||
|
|
const org = u.metadata?.organization || {};
|
||
|
|
return {
|
||
|
|
id: u.id,
|
||
|
|
name: h.name || org.company_name || u.name || u.username || "Unknown Hospital",
|
||
|
|
type: h.specialization || h.type || "General Care",
|
||
|
|
address: h.city || org.city || "Bangalore",
|
||
|
|
floor: h.floor || 0,
|
||
|
|
lat: h.lat || 13.0827,
|
||
|
|
lon: h.lon || 80.2707,
|
||
|
|
status: u.status === 'ACTIVE' ? 'Active' : 'Occupied',
|
||
|
|
beds: h.beds || "12/50",
|
||
|
|
icon: <Building2 />,
|
||
|
|
specialty: h.specialization ? [h.specialization] : ['General'],
|
||
|
|
admin: u.name,
|
||
|
|
email: u.email,
|
||
|
|
phone: u.phone,
|
||
|
|
rawUser: u
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
setHospitals(mappedHospitals);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to fetch hospitals:", error);
|
||
|
|
setHospitals([
|
||
|
|
{
|
||
|
|
id: 'hosp-001',
|
||
|
|
name: 'Apollo Main Hospital',
|
||
|
|
type: 'Multispecialty',
|
||
|
|
address: 'Greams Road, Chennai',
|
||
|
|
status: 'Active',
|
||
|
|
beds: '42/150',
|
||
|
|
admin: 'Dr. Ramesh R.',
|
||
|
|
rawUser: { id: 'hosp-001', metadata: { hospital: { id: 'hosp-001' } } }
|
||
|
|
}
|
||
|
|
]);
|
||
|
|
} finally {
|
||
|
|
setIsLoadingHospitals(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
fetchHospitals();
|
||
|
|
}, [isUpdating]);
|
||
|
|
|
||
|
|
const handleUpdateInternal = async () => {
|
||
|
|
if (!selectedHospital) return;
|
||
|
|
|
||
|
|
setIsUpdating(true);
|
||
|
|
try {
|
||
|
|
const token = localStorage.getItem('teleems_token');
|
||
|
|
if (!token) throw new Error('Not authenticated');
|
||
|
|
|
||
|
|
const u = selectedHospital.rawUser || selectedHospital.raw;
|
||
|
|
if (!u) throw new Error('Hospital metadata missing');
|
||
|
|
|
||
|
|
const payload = {
|
||
|
|
...u,
|
||
|
|
name: selectedHospital.name,
|
||
|
|
metadata: {
|
||
|
|
...u.metadata,
|
||
|
|
hospital: {
|
||
|
|
...u.metadata.hospital,
|
||
|
|
name: selectedHospital.name,
|
||
|
|
city: selectedHospital.address,
|
||
|
|
specialization: selectedHospital.type,
|
||
|
|
floor: parseInt(selectedHospital.floor) || 0,
|
||
|
|
lat: parseFloat(selectedHospital.lat) || 0,
|
||
|
|
lon: parseFloat(selectedHospital.lon) || 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const result = await authApi.updateUser(u.id, payload, token);
|
||
|
|
if (result.status === 200 || result.status === 201 || !result.error) {
|
||
|
|
setSuccessMessage('Configuration Updated!');
|
||
|
|
setTimeout(() => setSuccessMessage(''), 3000);
|
||
|
|
} else {
|
||
|
|
alert(result.message || 'Update failed');
|
||
|
|
}
|
||
|
|
} catch (err: any) {
|
||
|
|
alert(err.message);
|
||
|
|
} finally {
|
||
|
|
setIsUpdating(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleUpdateHospital = async (e: React.FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
if (!editingHospital) return;
|
||
|
|
|
||
|
|
setIsUpdating(true);
|
||
|
|
const token = localStorage.getItem('teleems_token');
|
||
|
|
if (!token) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const u = editingHospital.rawUser;
|
||
|
|
const payload = {
|
||
|
|
...u,
|
||
|
|
name: editingHospital.name,
|
||
|
|
metadata: {
|
||
|
|
...u.metadata,
|
||
|
|
hospital: {
|
||
|
|
...u.metadata.hospital,
|
||
|
|
name: editingHospital.name,
|
||
|
|
city: editingHospital.address,
|
||
|
|
specialization: editingHospital.type,
|
||
|
|
floor: parseInt(editingHospital.floor) || 0,
|
||
|
|
lat: parseFloat(editingHospital.lat) || 0,
|
||
|
|
lon: parseFloat(editingHospital.lon) || 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const result = await authApi.updateUser(u.id, payload, token);
|
||
|
|
if (result.status === 200 || result.status === 201 || !result.error) {
|
||
|
|
setSuccessMessage('Node updated successfully!');
|
||
|
|
setTimeout(() => setSuccessMessage(''), 3000);
|
||
|
|
setEditingHospital(null);
|
||
|
|
} else {
|
||
|
|
alert(result.message || 'Update failed');
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Update failed:', error);
|
||
|
|
} finally {
|
||
|
|
setIsUpdating(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const telelinkQueue = [
|
||
|
|
{ id: 'TL-9921', patient: 'TR-1082', triage: 'RED', wait: '1:45', complaint: 'Cardiac Arrest', symptoms: 'Ongoing CPR, 2 shocks delivered' },
|
||
|
|
{ id: 'TL-9925', patient: 'TR-1089', triage: 'RED', wait: '0:30', complaint: 'Potential Stroke', symptoms: 'GCS 12, unilateral weakness' },
|
||
|
|
];
|
||
|
|
|
||
|
|
const fleetDataList = [
|
||
|
|
{ id: 'AMB-01', type: 'ALS', status: 'RUNNING', patient: 'TR-1082', fuel: '75%', crew: 'EMT Arjun, Driver Som', eta: '4m', coords: [12.9716, 77.5946] },
|
||
|
|
{ id: 'AMB-02', type: 'BLS', status: 'IDLE', patient: null, fuel: '92%', crew: 'EMT Suman, Driver Ravi', eta: '-', coords: [12.9350, 77.6245] },
|
||
|
|
{ id: 'AMB-03', type: 'Transport', status: 'BREAKDOWN', patient: null, fuel: '15%', crew: 'None', eta: '-', coords: [12.9500, 77.6000], alert: 'Engine Overheat' },
|
||
|
|
];
|
||
|
|
|
||
|
|
const epcrs = [
|
||
|
|
{ id: 'PCR-7721', date: '2026-04-16', ambulance: 'KA-01-AM-22', patient: 'Anil Kapur', triage: 'RED', status: 'PENDING_SIGN' },
|
||
|
|
{ id: 'PCR-7718', date: '2026-04-16', ambulance: 'KA-05-BX-44', patient: 'Priya Verma', triage: 'YELLOW', status: 'RECEIVED' },
|
||
|
|
];
|
||
|
|
|
||
|
|
const renderModule = () => {
|
||
|
|
switch (activeModule) {
|
||
|
|
case 'ED_MONITOR':
|
||
|
|
return (
|
||
|
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="module-content">
|
||
|
|
<div className="ed-grid-modern">
|
||
|
|
<div className="triage-feed-container">
|
||
|
|
<div className="module-header-modern">
|
||
|
|
<div className="title-wrap">
|
||
|
|
<h3>REAL-TIME TRIAGE BOARD</h3>
|
||
|
|
<div className="live-pill"><span className="pulse"></span> 4 CRITICAL • 2 ENROUTE</div>
|
||
|
|
</div>
|
||
|
|
<div className="header-filters">
|
||
|
|
<div className="search-mini">
|
||
|
|
<Search size={16} color="var(--accent-cyan)" />
|
||
|
|
<input type="text" placeholder="Search by name, ambulance..." />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<motion.div layout className="triage-scroll-area no-scrollbar">
|
||
|
|
{incomingPatients.map((patient, idx) => (
|
||
|
|
<motion.div
|
||
|
|
key={patient.id}
|
||
|
|
initial={{ opacity: 0, x: -20 }}
|
||
|
|
animate={{ opacity: 1, x: 0 }}
|
||
|
|
transition={{ delay: idx * 0.1 }}
|
||
|
|
className="patient-alert-card premium"
|
||
|
|
>
|
||
|
|
<div className={`triage-accent ${patient.triage}`}></div>
|
||
|
|
<div className="alert-grid">
|
||
|
|
<div className="unit-info">
|
||
|
|
<div className="ambulance-badge">{patient.ambulanceId || patient.reg}</div>
|
||
|
|
<div className="eta-container">
|
||
|
|
<div className="eta-value">{patient.eta}</div>
|
||
|
|
<div className="eta-label">ARRIVAL</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="patient-details">
|
||
|
|
<h4>{patient.name}</h4>
|
||
|
|
<div className="p-sub">{patient.age}Y · {patient.gender} · {patient.id}</div>
|
||
|
|
<div className={`t-status ${patient.triage}`}>
|
||
|
|
<AlertCircle size={14} /> {patient.triage} · {patient.complaint}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="vital-telemetry-grid">
|
||
|
|
<div className={`vital-cell ${parseInt(patient.vitals.hr) > 110 ? 'critical' : ''}`}>
|
||
|
|
<Activity size={16} color="#00D1FF" style={{ flexShrink: 0 }} />
|
||
|
|
<div className="v-data" style={{ display: 'flex', flexDirection: 'column' }}>
|
||
|
|
<span className="v-val">{patient.vitals.hr}</span>
|
||
|
|
<span className="v-label">HR / BPM</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className={`vital-cell ${parseInt(patient.vitals.spo2) < 90 ? 'critical' : ''}`}>
|
||
|
|
<Wind size={16} color="#00E272" style={{ flexShrink: 0 }} />
|
||
|
|
<div className="v-data" style={{ display: 'flex', flexDirection: 'column' }}>
|
||
|
|
<span className="v-val">{patient.vitals.spo2}</span>
|
||
|
|
<span className="v-label">SpO2 / %</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="vital-cell">
|
||
|
|
<Heart size={16} color="#FF4B4B" style={{ flexShrink: 0 }} />
|
||
|
|
<div className="v-data" style={{ display: 'flex', flexDirection: 'column' }}>
|
||
|
|
<span className="v-val">{patient.vitals.bp}</span>
|
||
|
|
<span className="v-label">BP / mmHg</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="vital-cell">
|
||
|
|
<Thermometer size={16} color="#FFB800" style={{ flexShrink: 0 }} />
|
||
|
|
<div className="v-data" style={{ display: 'flex', flexDirection: 'column' }}>
|
||
|
|
<span className="v-val">{patient.vitals.temp}</span>
|
||
|
|
<span className="v-label">TEMP / °F</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="card-actions-area">
|
||
|
|
<button className="telelink-btn-premium" onClick={() => setActiveModule('TELELINK')}>
|
||
|
|
<Video size={16} /> VIDEO LINK
|
||
|
|
</button>
|
||
|
|
<button className="assign-btn-premium" onClick={() => setIsBookingModalOpen(true)}>
|
||
|
|
<Plus size={16} /> ADMIT UNIT
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
))}
|
||
|
|
</motion.div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="ed-ops-side-panel">
|
||
|
|
<div className="hub-card premium-ops">
|
||
|
|
<h4>EMERGENCY RESPONSE HUB</h4>
|
||
|
|
<p className="hub-desc">Broadcast high-priority emergency alerts throughout hospital network nodes.</p>
|
||
|
|
<div className="codes-stack">
|
||
|
|
<button className="code-btn-modern red">
|
||
|
|
<div className="c-info"><strong>CODE RED</strong><span>Cardiac Arrest</span></div>
|
||
|
|
<Zap size={24} />
|
||
|
|
</button>
|
||
|
|
<button className="code-btn-modern blue">
|
||
|
|
<div className="c-info"><strong>CODE BLUE</strong><span>Medical Emergency</span></div>
|
||
|
|
<Activity size={24} />
|
||
|
|
</button>
|
||
|
|
<button className="code-btn-modern trauma">
|
||
|
|
<div className="c-info"><strong>CODE TRAUMA</strong><span>Severe Surgical</span></div>
|
||
|
|
<ShieldAlert size={24} />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Card title="Bed Inventory Control" className="inventory-card">
|
||
|
|
<div className="inventory-stats">
|
||
|
|
<div className="inv-item" style={{ marginBottom: '24px' }}>
|
||
|
|
<div className="inv-label" style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 800, fontSize: '0.85rem', marginBottom: '8px' }}>
|
||
|
|
<span>EMERGENCY BAY</span>
|
||
|
|
<span style={{ color: 'var(--accent-cyan)' }}>{selectedHospital?.beds || '4/12'}</span>
|
||
|
|
</div>
|
||
|
|
<div className="inv-bar" style={{ height: '8px', background: 'rgba(0,0,0,0.03)', borderRadius: '4px', overflow: 'hidden' }}>
|
||
|
|
<div style={{ width: '33%', height: '100%', background: 'var(--accent-cyan)' }}></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="inv-item">
|
||
|
|
<div className="inv-label" style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 800, fontSize: '0.85rem', marginBottom: '8px' }}>
|
||
|
|
<span>ICU - LEVEL 1</span>
|
||
|
|
<span style={{ color: 'var(--alert-red)' }}>1/8</span>
|
||
|
|
</div>
|
||
|
|
<div className="inv-bar" style={{ height: '8px', background: 'rgba(0,0,0,0.03)', borderRadius: '4px', overflow: 'hidden' }}>
|
||
|
|
<div style={{ width: '87%', height: '100%', background: 'var(--alert-red)' }}></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<button className="sync-btn-modern">
|
||
|
|
<Database size={14} style={{ marginRight: '8px' }} /> REFRESH FROM HMS
|
||
|
|
</button>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<div className="operational-clock-card">
|
||
|
|
<div className="o-time">14:55:02</div>
|
||
|
|
<div className="o-status"><span className="pulse status-sync"></span> NODE STATUS: SYNCED</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
);
|
||
|
|
|
||
|
|
case 'BOOKINGS':
|
||
|
|
return (
|
||
|
|
<motion.div initial={{ opacity: 0, scale: 0.98 }} animate={{ opacity: 1, scale: 1 }} className="module-content">
|
||
|
|
<div className="module-header-modern">
|
||
|
|
<div className="title-wrap">
|
||
|
|
<h3>LIVE TRIP MANAGEMENT</h3>
|
||
|
|
<div className="live-pill"><span className="pulse"></span> 8 ACTIVE TRANSPORTS</div>
|
||
|
|
</div>
|
||
|
|
<div className="action-area-header">
|
||
|
|
<div className="search-mini">
|
||
|
|
<Search size={14} />
|
||
|
|
<input type="text" placeholder="Search Patient, MRN..." />
|
||
|
|
</div>
|
||
|
|
<button onClick={() => setIsBookingModalOpen(true)} className="register-staff-btn-premium">
|
||
|
|
<Plus size={18} /> <span className="hide-mobile">NEW DISPATCH</span>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="stats-strip stats-grid-responsive">
|
||
|
|
<div className="stat-card-premium">
|
||
|
|
<span className="stat-label">Active Trips</span>
|
||
|
|
<div className="stat-value">{tripsData.length}</div>
|
||
|
|
<div className="stat-trend up"><TrendingUp size={12} /> 25% vs last hour</div>
|
||
|
|
</div>
|
||
|
|
<div className="stat-card-premium">
|
||
|
|
<span className="stat-label">Avg Arrival E.T.A</span>
|
||
|
|
<div className="stat-value">9.4m</div>
|
||
|
|
<div className="stat-trend down"><Clock size={12} /> 10% vs last hour</div>
|
||
|
|
</div>
|
||
|
|
<div className="stat-card-premium">
|
||
|
|
<span className="stat-label">I.F.T Pending Nodes</span>
|
||
|
|
<div className="stat-value">03</div>
|
||
|
|
<div className="stat-trend" style={{ color: 'var(--text-secondary)' }}>Status: Operational</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="trip-mgmt-table-container">
|
||
|
|
<table className="staff-table-premium">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Accession & Patient</th>
|
||
|
|
<th>Trip Parameters</th>
|
||
|
|
<th>Fleet Asset</th>
|
||
|
|
<th>Live Progress Timeline</th>
|
||
|
|
<th>Operational Actions</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
{tripsData.map((t, idx) => (
|
||
|
|
<tr key={t.id}>
|
||
|
|
<td>
|
||
|
|
<div className="trip-patient-ident">
|
||
|
|
<div className="trip-patient-main">{t.patient}</div>
|
||
|
|
<div className="trip-id-mono">{t.id} · MRNID: {t.mrn}</div>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
<div className="route-cell" style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', fontWeight: 700 }}>
|
||
|
|
<div className="r-point"><MapPin size={12} /> {t.origin}</div>
|
||
|
|
<div className="route-line" style={{ height: '20px', borderLeft: '2px dashed var(--card-border)', margin: '4px 5px' }}></div>
|
||
|
|
<div className="r-point"><Navigation size={12} /> {t.destination}</div>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
<div className="vehicle-crew-stack">
|
||
|
|
<div className="vc-unit" style={{ fontWeight: 900, color: 'var(--text-primary)' }}><Truck size={14} /> {t.vehicle}</div>
|
||
|
|
<div className="vc-crew" style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '4px' }}>{t.crew}</div>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
<div className="progress-cell" style={{ width: '240px' }}>
|
||
|
|
<div className="eta-container" style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||
|
|
<span className={`v-status ${t.status.replace(' ', '') === 'ARRIVED' ? 'RUNNING' : 'IDLE'}`} style={{ fontSize: '0.65rem' }}>{t.status}</span>
|
||
|
|
<span style={{ fontSize: '0.7rem', fontWeight: 900, color: 'var(--accent-cyan)' }}>{t.eta} REMAINING</span>
|
||
|
|
</div>
|
||
|
|
<div className="trip-status-timeline">
|
||
|
|
<div className={`ts-step ${t.step >= 1 ? 'completed' : ''}`}></div>
|
||
|
|
<div className={`ts-line ${t.step >= 2 ? 'active' : ''}`}></div>
|
||
|
|
<div className={`ts-step ${t.step >= 2 ? 'completed' : t.step === 1 ? 'active' : ''}`}></div>
|
||
|
|
<div className={`ts-line ${t.step >= 3 ? 'active' : ''}`}></div>
|
||
|
|
<div className={`ts-step ${t.step >= 3 ? 'completed' : t.step === 2 ? 'active' : ''}`}></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
<div className="trip-action-group" style={{ display: 'flex', gap: '8px' }}>
|
||
|
|
<button className="tag-btn"><Video size={14} /></button>
|
||
|
|
<button className="tag-btn"><FileText size={14} /></button>
|
||
|
|
<button className="tag-btn" onClick={() => setTripsData(tripsData.filter(item => item.id !== t.id))} style={{ color: 'var(--alert-red)' }}><XCircle size={14} /></button>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
))}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
);
|
||
|
|
case 'FLEET':
|
||
|
|
return (
|
||
|
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="module-content">
|
||
|
|
<div className="fleet-dashboard-layout">
|
||
|
|
<div className="fleet-map-container">
|
||
|
|
<div className="mock-map-view"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="fleet-sidebar">
|
||
|
|
<div className="fleet-list no-scrollbar">
|
||
|
|
{fleetDataList.map(v => (
|
||
|
|
<div key={v.id} className={`fleet-item ${v.status}`}>
|
||
|
|
<div className="f-header">
|
||
|
|
<div className="v-info">
|
||
|
|
<span className="v-tag">{v.id}</span>
|
||
|
|
<span className="v-type">{v.type}</span>
|
||
|
|
</div>
|
||
|
|
<span className={`v-status ${v.status}`}>{v.status}</span>
|
||
|
|
</div>
|
||
|
|
<div className="f-body">{v.crew}</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
);
|
||
|
|
|
||
|
|
case 'TELELINK':
|
||
|
|
return (
|
||
|
|
<motion.div initial={{ opacity: 0, scale: 0.98 }} animate={{ opacity: 1, scale: 1 }} className="module-content">
|
||
|
|
<div className="telelink-workspace">
|
||
|
|
<div className="telelink-queues">
|
||
|
|
<div className="queue-section" style={{ marginTop: '40px' }}>
|
||
|
|
<div className="q-title">ACTIVE INBOUND <span className="q-count">2 PENDING</span></div>
|
||
|
|
<div className="queue-list-items">
|
||
|
|
{telelinkQueue.map(q => (
|
||
|
|
<div key={q.id} className={`q-card ${q.triage}`}>
|
||
|
|
<div className="q-patient-info">
|
||
|
|
<div className="q-name">{q.patient}</div>
|
||
|
|
<div className="q-desc">EMT: Arjun</div>
|
||
|
|
</div>
|
||
|
|
<button className="telelink-btn-premium">ACCEPT UPLINK</button>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="video-central-hub shadow-premium">
|
||
|
|
<div className="remote-feed">
|
||
|
|
<div className="placeholder-video">
|
||
|
|
<Video size={80} color="rgba(255,255,255,0.05)" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
);
|
||
|
|
case 'EPCR':
|
||
|
|
return (
|
||
|
|
<motion.div initial={{ opacity: 0, scale: 0.98 }} animate={{ opacity: 1, scale: 1 }} className="module-content">
|
||
|
|
<div className="module-header-modern"><h3>CLINICAL RECORDS (ePCR)</h3></div>
|
||
|
|
<div className="trip-mgmt-table-container">
|
||
|
|
<table className="staff-table-premium">
|
||
|
|
<thead><tr><th>Record ID</th><th>Patient</th><th>Status</th><th>Action</th></tr></thead>
|
||
|
|
<tbody>
|
||
|
|
{epcrs.map(e => (
|
||
|
|
<tr key={e.id}><td>{e.id}</td><td>{e.patient}</td><td>{e.status}</td><td><button className="tag-btn">VIEW</button></td></tr>
|
||
|
|
))}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
);
|
||
|
|
|
||
|
|
case 'HISTORY':
|
||
|
|
return (
|
||
|
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="module-content">
|
||
|
|
<div className="module-header-modern"><h3>PATIENT ARCHIVE</h3></div>
|
||
|
|
<div className="archive-table-container">
|
||
|
|
<table className="staff-table-premium">
|
||
|
|
<thead><tr><th>Date</th><th>Patient</th><th>Triage</th><th>Status</th></tr></thead>
|
||
|
|
<tbody>
|
||
|
|
{[
|
||
|
|
{ date: '16 Apr 2026', patient: 'Meera Jain', triage: 'RED', status: 'ADMITTED' }
|
||
|
|
].map((item, i) => (
|
||
|
|
<tr key={i}><td>{item.date}</td><td>{item.patient}</td><td>{item.triage}</td><td>{item.status}</td></tr>
|
||
|
|
))}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
);
|
||
|
|
|
||
|
|
case 'REPORTS':
|
||
|
|
return (
|
||
|
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="module-content">
|
||
|
|
<div className="module-header-modern"><h3>ANALYTICS</h3></div>
|
||
|
|
<div className="stats-strip stats-grid-responsive">
|
||
|
|
<div className="stat-card-premium"><span className="stat-label">Admissions</span><span className="stat-value">1,284</span></div>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
);
|
||
|
|
|
||
|
|
case 'SETUP':
|
||
|
|
return (
|
||
|
|
<motion.div initial={{ opacity: 0, scale: 0.99 }} animate={{ opacity: 1, scale: 1 }} className="module-content">
|
||
|
|
<div className="setup-layout">
|
||
|
|
<aside className="setup-nav">
|
||
|
|
<button className={`setup-nav-item ${setupSubTab === 'PROFILE' ? 'active' : ''}`} onClick={() => setSetupSubTab('PROFILE')}>
|
||
|
|
<Building2 size={18} /> <span>Profile</span>
|
||
|
|
</button>
|
||
|
|
<button className={`setup-nav-item ${setupSubTab === 'USERS' ? 'active' : ''}`} onClick={() => setSetupSubTab('USERS')}>
|
||
|
|
<Users size={18} /> <span>Users</span>
|
||
|
|
</button>
|
||
|
|
<button className={`setup-nav-item ${setupSubTab === 'DEPTS' ? 'active' : ''}`} onClick={() => setSetupSubTab('DEPTS')}>
|
||
|
|
<LayoutDashboard size={18} /> <span>Departments</span>
|
||
|
|
</button>
|
||
|
|
<button className={`setup-nav-item ${setupSubTab === 'BEDS' ? 'active' : ''}`} onClick={() => setSetupSubTab('BEDS')}>
|
||
|
|
<Stethoscope size={18} /> <span>Bed Management</span>
|
||
|
|
</button>
|
||
|
|
</aside>
|
||
|
|
|
||
|
|
<div className="setup-content-modern no-scrollbar">
|
||
|
|
{setupSubTab === 'PROFILE' && (
|
||
|
|
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }}>
|
||
|
|
<Card title="Hospital Profile">
|
||
|
|
<div className="setup-form-modern">
|
||
|
|
<div className="form-group-premium">
|
||
|
|
<label>Institution Name</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
className="setup-input-premium"
|
||
|
|
value={selectedHospital?.name}
|
||
|
|
onChange={(e) => setSelectedHospital({...selectedHospital, name: e.target.value})}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div className="form-group-premium">
|
||
|
|
<label>Operational Status</label>
|
||
|
|
<div className="setup-input-premium" style={{ color: 'var(--accent-green)', fontWeight: 800 }}>ACTIVE • FULLY OPERATIONAL</div>
|
||
|
|
</div>
|
||
|
|
<button className="setup-confirm-btn" onClick={handleUpdateInternal}>
|
||
|
|
SAVE CONFIGURATION
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
</motion.div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{setupSubTab === 'USERS' && (
|
||
|
|
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }}>
|
||
|
|
<Card title="Personnel Registry">
|
||
|
|
<div className="setup-actions-bar">
|
||
|
|
<div className="search-mini">
|
||
|
|
<Search size={16} />
|
||
|
|
<input type="text" placeholder="Filter by name or role..." />
|
||
|
|
</div>
|
||
|
|
<button className="register-staff-btn-premium" onClick={() => setIsStaffModalOpen(true)}>
|
||
|
|
<Plus size={16} /> REGISTER NEW STAFF
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<table className="staff-table-premium">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Operator Name</th>
|
||
|
|
<th>Designation</th>
|
||
|
|
<th>Node Alignment</th>
|
||
|
|
<th>Auth Status</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
{allUsers.filter(u => u.hospitalId === selectedHospital?.rawUser?.hospitalId || u.organisationId === selectedHospital?.rawUser?.organisationId).map((u, i) => (
|
||
|
|
<tr key={i}>
|
||
|
|
<td style={{ fontWeight: 800 }}>{u.name || u.username}</td>
|
||
|
|
<td><span className="v-status IDLE" style={{ fontSize: '0.6rem' }}>{u.roles[0]?.replace('_', ' ')}</span></td>
|
||
|
|
<td style={{ opacity: 0.7 }}>{selectedHospital?.name}</td>
|
||
|
|
<td><span className="node-status-text ON">AUTHORIZED</span></td>
|
||
|
|
</tr>
|
||
|
|
))}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</Card>
|
||
|
|
</motion.div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{setupSubTab === 'DEPTS' && (
|
||
|
|
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }}>
|
||
|
|
<Card title="Emergency Departments">
|
||
|
|
<div className="setup-actions-bar">
|
||
|
|
<p style={{ margin: 0, color: 'var(--text-secondary)', fontWeight: 600 }}>Active Departments: {departments.length}</p>
|
||
|
|
<button className="register-staff-btn-premium" onClick={() => setIsDeptModalOpen(true)}>
|
||
|
|
<Plus size={16} /> ADD DEPARTMENT
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div className="dept-service-grid">
|
||
|
|
{departments.map((d, i) => (
|
||
|
|
<div key={i} className={`service-node-card ${d.isActive ? 'active' : 'inactive'}`}>
|
||
|
|
<div className="node-info-text">
|
||
|
|
<strong>{d.name}</strong>
|
||
|
|
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginBottom: '8px' }}>
|
||
|
|
Head: {d.headOfDepartment} <br/>
|
||
|
|
Beds: {d.totalBedsCapacity} | Tel: {d.contactPhone}
|
||
|
|
</div>
|
||
|
|
<span className={`node-status-text ${d.isActive ? 'ON' : 'OFF'}`}>
|
||
|
|
{d.isActive ? '• OPERATIONAL' : '• OFFLINE'}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
className={`toggle-btn-modern ${d.isActive ? 'disable' : 'enable'}`}
|
||
|
|
onClick={() => {
|
||
|
|
// Local toggle preview for now, normally would PATCH to API
|
||
|
|
const n = [...departments];
|
||
|
|
n[i].isActive = !n[i].isActive;
|
||
|
|
setDepartments(n);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{d.isActive ? 'OFFLINE' : 'ONLINE'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
</motion.div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{setupSubTab === 'BEDS' && (
|
||
|
|
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }}>
|
||
|
|
<Card title="Inventory & Capacity Management">
|
||
|
|
<div className="setup-actions-bar">
|
||
|
|
<p style={{ margin: 0, color: 'var(--text-secondary)', fontWeight: 600 }}>Active Care Units: {departments.length}</p>
|
||
|
|
<button className="register-staff-btn-premium"><Plus size={16} /> ADD UNIT</button>
|
||
|
|
</div>
|
||
|
|
<table className="staff-table-premium">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Care Unit / Dept</th>
|
||
|
|
<th>Total Capacity</th>
|
||
|
|
<th>Occupied</th>
|
||
|
|
<th>Availability</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
{departments.map((b, i) => {
|
||
|
|
const occ = b.occupiedBeds || 0; // Fallback if API hasn't mapped occupied yet
|
||
|
|
const tot = b.totalBedsCapacity || 1; // Prevent division by zero
|
||
|
|
return (
|
||
|
|
<tr key={i}>
|
||
|
|
<td style={{ fontWeight: 800 }}>{b.name}</td>
|
||
|
|
<td>{b.totalBedsCapacity} Units</td>
|
||
|
|
<td style={{ color: 'var(--accent-cyan)', fontWeight: 700 }}>{occ}</td>
|
||
|
|
<td>
|
||
|
|
<div className="bed-progress" style={{ width: '100px', marginBottom: 0 }}>
|
||
|
|
<div className="progress-fill" style={{ width: `${(occ/tot)*100}%` }}></div>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
)})}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</Card>
|
||
|
|
</motion.div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
);
|
||
|
|
|
||
|
|
case 'REFERRALS':
|
||
|
|
return (
|
||
|
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="module-content">
|
||
|
|
<div className="module-header-modern"><h3>Referral Network</h3></div>
|
||
|
|
<Card title="Active Outgoing Referrals">
|
||
|
|
<p>No active outgoing referrals at this time.</p>
|
||
|
|
</Card>
|
||
|
|
</motion.div>
|
||
|
|
);
|
||
|
|
|
||
|
|
default:
|
||
|
|
return <div>Module Under Development</div>;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!selectedHospital) {
|
||
|
|
return (
|
||
|
|
<div className="hospital-selection-overlay">
|
||
|
|
<div className="selection-background">
|
||
|
|
<div className="glow-sphere top-left" />
|
||
|
|
<div className="glow-sphere bottom-right" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, y: 20 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
className="selection-content"
|
||
|
|
>
|
||
|
|
<div className="selection-header">
|
||
|
|
<div className="brand-logo large">
|
||
|
|
<Activity size={48} className="pulse-aura" />
|
||
|
|
CURESELECT <span style={{ fontWeight: 400, color: 'var(--text-secondary)' }}>EMS</span>
|
||
|
|
</div>
|
||
|
|
<h1>Command Node Protocol</h1>
|
||
|
|
<p className="selection-subtitle">Regional Emergency Service Network · Authorized Node Access Only</p>
|
||
|
|
<div className="security-scan-line"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="hospital-grid">
|
||
|
|
{isLoadingHospitals ? (
|
||
|
|
<div className="loading-hospitals">
|
||
|
|
<motion.div animate={{ rotate: 360 }} transition={{ repeat: Infinity, duration: 1, ease: "linear" }}>
|
||
|
|
<Activity size={48} color="var(--accent-cyan)" />
|
||
|
|
</motion.div>
|
||
|
|
<p style={{ letterSpacing: '0.2em', fontWeight: 800 }}>ESTABLISHING SECURE UPLINK...</p>
|
||
|
|
</div>
|
||
|
|
) : hospitals.length === 0 ? (
|
||
|
|
<div className="no-hospitals">
|
||
|
|
<AlertTriangle size={48} color="var(--alert-amber)" />
|
||
|
|
<p>No active hospital nodes detected within the regional network grid.</p>
|
||
|
|
<button className="select-hospital-btn" onClick={() => window.location.reload()}>RESCAN NETWORK</button>
|
||
|
|
</div>
|
||
|
|
) : hospitals.map((h, idx) => (
|
||
|
|
<motion.div
|
||
|
|
key={h.id}
|
||
|
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
|
|
transition={{ delay: idx * 0.1, duration: 0.5 }}
|
||
|
|
whileHover={{ y: -12, scale: 1.02 }}
|
||
|
|
className="hospital-select-card"
|
||
|
|
onClick={() => setSelectedHospital(h)}
|
||
|
|
>
|
||
|
|
<div className="h-icon-bg">
|
||
|
|
<Building2 size={40} />
|
||
|
|
</div>
|
||
|
|
<h3>{h.name}</h3>
|
||
|
|
<div className="h-meta">
|
||
|
|
<span className="h-type">{h.type} Node</span>
|
||
|
|
<span className={`h-status-pulse ${h.status.replace(' ', '-')}`}>{h.status}</span>
|
||
|
|
</div>
|
||
|
|
<div className="h-beds">
|
||
|
|
<div className="bed-info">
|
||
|
|
<Users size={16} color="var(--accent-cyan)" />
|
||
|
|
<span>Admin: {h.admin || 'Dr. Admin'}</span>
|
||
|
|
</div>
|
||
|
|
<div className="bed-progress">
|
||
|
|
<div className="progress-fill" style={{ width: (parseInt(h.beds) / parseInt(h.beds.split('/')[1] || '100') * 100) + '%' }}></div>
|
||
|
|
</div>
|
||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.75rem', fontWeight: 700, opacity: 0.7 }}>
|
||
|
|
<span>BED CAPACITY</span>
|
||
|
|
<span>{h.beds} AVAIL</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<button className="select-hospital-btn">SECURE ACCESS <ChevronRight size={18} /></button>
|
||
|
|
</motion.div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="hospital-console-container">
|
||
|
|
<header className="console-header premium">
|
||
|
|
<div className="header-left" onClick={() => setSelectedHospital(null)} style={{ cursor: 'pointer' }}>
|
||
|
|
<div className="hospital-brand">
|
||
|
|
<div className="hospital-info-stack">
|
||
|
|
<h2 className="hospital-name">{selectedHospital?.name}</h2>
|
||
|
|
<span className="status-badge online">OPERATIONAL</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* ── CENTER: All nav tabs in their own grid column ── */}
|
||
|
|
<nav className={`console-nav-modern ${isMobileMenuOpen ? 'mobile-open' : ''}`}>
|
||
|
|
<button className="mobile-close-btn show-mobile" onClick={() => setIsMobileMenuOpen(false)}><X size={22} /></button>
|
||
|
|
<div className="nav-tab-container">
|
||
|
|
<button title="ED Monitor" className={`nav-tab ${activeModule === 'ED_MONITOR' ? 'active' : ''}`} onClick={() => { setActiveModule('ED_MONITOR'); setIsMobileMenuOpen(false); }}><Monitor size={16} /> <span>ED MONITOR</span></button>
|
||
|
|
<button title="Trips" className={`nav-tab ${activeModule === 'BOOKINGS' ? 'active' : ''}`} onClick={() => { setActiveModule('BOOKINGS'); setIsMobileMenuOpen(false); }}><Activity size={16} /> <span>TRIPS</span></button>
|
||
|
|
<button title="Fleet" className={`nav-tab ${activeModule === 'FLEET' ? 'active' : ''}`} onClick={() => { setActiveModule('FLEET'); setIsMobileMenuOpen(false); }}><Truck size={16} /> <span>FLEET</span></button>
|
||
|
|
<button title="TeleLink" className={`nav-tab ${activeModule === 'TELELINK' ? 'active' : ''}`} onClick={() => { setActiveModule('TELELINK'); setIsMobileMenuOpen(false); }}><Video size={16} /> <span>TELELINK</span></button>
|
||
|
|
<button title="ePCR Records" className={`nav-tab ${activeModule === 'EPCR' ? 'active' : ''}`} onClick={() => { setActiveModule('EPCR'); setIsMobileMenuOpen(false); }}><FileText size={16} /> <span>ePCR</span></button>
|
||
|
|
<button title="Archive" className={`nav-tab ${activeModule === 'HISTORY' ? 'active' : ''}`} onClick={() => { setActiveModule('HISTORY'); setIsMobileMenuOpen(false); }}><Database size={16} /> <span>ARCHIVE</span></button>
|
||
|
|
<div className="nav-divider"></div>
|
||
|
|
<button title="Reports & Analytics" className={`nav-tab ${activeModule === 'REPORTS' ? 'active' : ''}`} onClick={() => { setActiveModule('REPORTS'); setIsMobileMenuOpen(false); }}><TrendingUp size={16} /> <span>REPORTS</span></button>
|
||
|
|
<button title="Setup & Configuration" className={`nav-tab ${activeModule === 'SETUP' ? 'active' : ''}`} onClick={() => { setActiveModule('SETUP'); setIsMobileMenuOpen(false); }}><Settings size={16} /> <span>SETUP</span></button>
|
||
|
|
</div>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
{/* ── RIGHT: Actions (hamburger on mobile) ── */}
|
||
|
|
<div className="header-actions">
|
||
|
|
<button className="mobile-menu-toggle show-mobile" onClick={() => setIsMobileMenuOpen(true)}>
|
||
|
|
<Menu size={22} />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
{successMessage && <div className="success-toast-premium"><CheckCircle2 size={18} /> {successMessage}</div>}
|
||
|
|
|
||
|
|
<main className="console-main">
|
||
|
|
<AnimatePresence mode="wait">{renderModule()}</AnimatePresence>
|
||
|
|
</main>
|
||
|
|
|
||
|
|
<AnimatePresence>
|
||
|
|
{isStaffModalOpen && (
|
||
|
|
<div className="premium-modal-overlay">
|
||
|
|
<motion.div initial={{ scale: 0.95, opacity: 0, y: 20 }} animate={{ scale: 1, opacity: 1, y: 0 }} exit={{ scale: 0.95, opacity: 0, y: 20 }} className="premium-modal-container">
|
||
|
|
<div className="modal-header-premium">
|
||
|
|
<h3>New Staff Registration</h3>
|
||
|
|
<button className="modal-close-btn" onClick={() => setIsStaffModalOpen(false)}><X size={18} /></button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="setup-form-modern">
|
||
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
||
|
|
<div className="form-group-premium">
|
||
|
|
<label>Full Name</label>
|
||
|
|
<input type="text" className="setup-input-premium" value={staffFormData.name} onChange={e => setStaffFormData({...staffFormData, name: e.target.value})} placeholder="e.g. Dr. John Smith" />
|
||
|
|
</div>
|
||
|
|
<div className="form-group-premium">
|
||
|
|
<label>Organizational Role</label>
|
||
|
|
<select className="setup-input-premium" value={staffFormData.role} onChange={e => setStaffFormData({...staffFormData, role: e.target.value})}>
|
||
|
|
{rolesList.map((r: any) => (
|
||
|
|
<option key={r.id} value={r.name}>{r.name}</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div className="form-group-premium">
|
||
|
|
<label>Username</label>
|
||
|
|
<input type="text" className="setup-input-premium" value={staffFormData.username} onChange={e => setStaffFormData({...staffFormData, username: e.target.value})} placeholder="e.g. jsmith" autoComplete="new-password" />
|
||
|
|
</div>
|
||
|
|
<div className="form-group-premium">
|
||
|
|
<label>Temporary Password</label>
|
||
|
|
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
|
||
|
|
<input type={showPassword ? "text" : "password"} className="setup-input-premium" style={{ width: '100%', paddingRight: '40px' }} value={staffFormData.password} onChange={e => setStaffFormData({...staffFormData, password: e.target.value})} placeholder="Password" autoComplete="new-password" />
|
||
|
|
<button type="button" onClick={() => setShowPassword(!showPassword)} style={{ position: 'absolute', right: '14px', top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', color: 'var(--text-secondary)', cursor: 'pointer', padding: 0, display: 'flex' }}>
|
||
|
|
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="form-group-premium">
|
||
|
|
<label>Email Address</label>
|
||
|
|
<input type="email" className="setup-input-premium" value={staffFormData.email} onChange={e => setStaffFormData({...staffFormData, email: e.target.value})} placeholder="e.g. jsmith@hospital.com" />
|
||
|
|
</div>
|
||
|
|
<div className="form-group-premium">
|
||
|
|
<label>Phone Number</label>
|
||
|
|
<input type="text" className="setup-input-premium" value={staffFormData.phone} onChange={e => setStaffFormData({...staffFormData, phone: e.target.value})} placeholder="e.g. +91 9876543210" />
|
||
|
|
</div>
|
||
|
|
<div className="form-group-premium">
|
||
|
|
<label>Department</label>
|
||
|
|
<input type="text" className="setup-input-premium" value={staffFormData.department} onChange={e => setStaffFormData({...staffFormData, department: e.target.value})} placeholder="Emergency" />
|
||
|
|
</div>
|
||
|
|
<div className="form-group-premium">
|
||
|
|
<label>Specialization</label>
|
||
|
|
<input type="text" className="setup-input-premium" value={staffFormData.specialization} onChange={e => setStaffFormData({...staffFormData, specialization: e.target.value})} placeholder="Trauma" />
|
||
|
|
</div>
|
||
|
|
<div className="form-group-premium">
|
||
|
|
<label>Designation</label>
|
||
|
|
<input type="text" className="setup-input-premium" value={staffFormData.designation} onChange={e => setStaffFormData({...staffFormData, designation: e.target.value})} placeholder="Resident" />
|
||
|
|
</div>
|
||
|
|
<div className="form-group-premium">
|
||
|
|
<label>License Number</label>
|
||
|
|
<input type="text" className="setup-input-premium" value={staffFormData.license} onChange={e => setStaffFormData({...staffFormData, license: e.target.value})} placeholder="MC-12345" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="modal-footer-premium">
|
||
|
|
<button className="btn-secondary-glass" onClick={() => setIsStaffModalOpen(false)}>CANCEL</button>
|
||
|
|
<button className="btn-primary-glass" onClick={async () => {
|
||
|
|
try {
|
||
|
|
const token = localStorage.getItem('teleems_token');
|
||
|
|
const payload = {
|
||
|
|
phone: staffFormData.phone,
|
||
|
|
email: staffFormData.email,
|
||
|
|
name: staffFormData.name,
|
||
|
|
username: staffFormData.username,
|
||
|
|
password: staffFormData.password,
|
||
|
|
role: staffFormData.role,
|
||
|
|
org_id: selectedHospital?.rawUser?.organisationId,
|
||
|
|
metadata: {
|
||
|
|
specialization: staffFormData.specialization,
|
||
|
|
license_number: staffFormData.license,
|
||
|
|
department: staffFormData.department,
|
||
|
|
designation: staffFormData.designation,
|
||
|
|
shift: staffFormData.shift
|
||
|
|
}
|
||
|
|
};
|
||
|
|
const res = await authApi.registerUser(payload, token || '');
|
||
|
|
if (res.status === 201 || res.status === 200) {
|
||
|
|
setSuccessMessage('Staff registered successfully.');
|
||
|
|
setIsStaffModalOpen(false);
|
||
|
|
setAllUsers(prev => [{ ...payload, roles: [payload.role], hospitalId: selectedHospital?.rawUser?.hospitalId }, ...prev]);
|
||
|
|
}
|
||
|
|
} catch (e) { alert('Failed'); }
|
||
|
|
}}>REGISTER PERSONNEL</button>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{isDeptModalOpen && (
|
||
|
|
<div className="premium-modal-overlay">
|
||
|
|
<motion.div initial={{ scale: 0.95, opacity: 0, y: 20 }} animate={{ scale: 1, opacity: 1, y: 0 }} exit={{ scale: 0.95, opacity: 0, y: 20 }} className="premium-modal-container">
|
||
|
|
<div className="modal-header-premium">
|
||
|
|
<h3>Add Department</h3>
|
||
|
|
<button className="modal-close-btn" onClick={() => setIsDeptModalOpen(false)}><X size={18} /></button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="setup-form-modern">
|
||
|
|
<div className="form-group-premium">
|
||
|
|
<label>Department Name</label>
|
||
|
|
<input type="text" className="setup-input-premium" value={deptFormData.name} onChange={e => setDeptFormData({...deptFormData, name: e.target.value})} placeholder="e.g. Emergency & Trauma" />
|
||
|
|
</div>
|
||
|
|
<div className="form-group-premium">
|
||
|
|
<label>Head of Department</label>
|
||
|
|
<div className="custom-select-wrapper" style={{ position: 'relative' }}>
|
||
|
|
<select
|
||
|
|
className="setup-input-premium custom-select"
|
||
|
|
value={deptFormData.headOfDepartment}
|
||
|
|
onChange={e => setDeptFormData({...deptFormData, headOfDepartment: e.target.value})}
|
||
|
|
style={{ width: '100%', appearance: 'none' }}
|
||
|
|
>
|
||
|
|
<option value="" disabled>Select Head of Department</option>
|
||
|
|
{allUsers
|
||
|
|
.filter(u => u.hospitalId === (selectedHospital?.rawUser?.hospitalId || selectedHospital?.id) || u.organisationId === selectedHospital?.rawUser?.organisationId)
|
||
|
|
.map((u, i) => (
|
||
|
|
<option key={i} value={u.name || u.username}>{u.name || u.username} ({u.roles[0]?.replace('_', ' ')})</option>
|
||
|
|
))
|
||
|
|
}
|
||
|
|
</select>
|
||
|
|
<ChevronDown size={14} style={{ position: 'absolute', right: '12px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-secondary)', pointerEvents: 'none' }} />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="form-group-premium" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
|
||
|
|
<div>
|
||
|
|
<label>Capacity (Beds)</label>
|
||
|
|
<input type="number" className="setup-input-premium" style={{ width: '100%', marginTop: '10px' }} value={deptFormData.totalBedsCapacity} onChange={e => setDeptFormData({...deptFormData, totalBedsCapacity: parseInt(e.target.value) || 0})} />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label>Contact Phone</label>
|
||
|
|
<input type="text" className="setup-input-premium" style={{ width: '100%', marginTop: '10px' }} value={deptFormData.contactPhone} onChange={e => setDeptFormData({...deptFormData, contactPhone: e.target.value})} placeholder="+91-..." />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="modal-footer-premium">
|
||
|
|
<button className="btn-secondary-glass" onClick={() => setIsDeptModalOpen(false)}>CANCEL</button>
|
||
|
|
<button className="btn-primary-glass" onClick={async () => {
|
||
|
|
try {
|
||
|
|
const token = localStorage.getItem('teleems_token');
|
||
|
|
const payload = {
|
||
|
|
name: deptFormData.name,
|
||
|
|
headOfDepartment: deptFormData.headOfDepartment,
|
||
|
|
totalBedsCapacity: deptFormData.totalBedsCapacity,
|
||
|
|
contactPhone: deptFormData.contactPhone,
|
||
|
|
hospitalId: selectedHospital?.rawUser?.hospitalId || selectedHospital?.id,
|
||
|
|
isActive: true
|
||
|
|
};
|
||
|
|
const res = await authApi.createDepartment(payload, token || '');
|
||
|
|
if (res.status === 200 || res.status === 201) {
|
||
|
|
setSuccessMessage('Department verified and registered.');
|
||
|
|
setIsDeptModalOpen(false);
|
||
|
|
// Refresh list
|
||
|
|
const updatedRes = await authApi.getDepartments(token || '');
|
||
|
|
if (updatedRes.data) {
|
||
|
|
const hospId = selectedHospital?.rawUser?.hospitalId || selectedHospital?.id;
|
||
|
|
setDepartments(updatedRes.data.filter((d: any) => d.hospitalId === hospId));
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
alert('Failed to register department. Code: ' + res.status);
|
||
|
|
}
|
||
|
|
} catch (e) { alert('Exception: ' + e); }
|
||
|
|
}}>ADD DEPARTMENT</button>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</AnimatePresence>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
export default HospitalConsole;
|