Files
TeleEms-Dashboard/src/pages/HospitalConsole.tsx
T

1157 lines
61 KiB
TypeScript
Raw Normal View History

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;