724 lines
29 KiB
TypeScript
724 lines
29 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useSearchParams } from 'react-router-dom';
|
|
import {
|
|
Building2,
|
|
Activity,
|
|
Truck,
|
|
FileText,
|
|
Settings,
|
|
CheckCircle2,
|
|
Monitor,
|
|
TrendingUp,
|
|
Database,
|
|
Video,
|
|
Hospital,
|
|
ChevronDown,
|
|
} from 'lucide-react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { Card } from '../components/Common';
|
|
import { authApi } from '../api/auth';
|
|
import { hospitalApi } from '../api/hospital';
|
|
import { HospitalSelector } from './hospital/HospitalSelector';
|
|
import { EDMonitor } from './hospital/EDMonitor';
|
|
import { TripManagement } from './hospital/TripManagement';
|
|
import { SetupPanel } from './hospital/SetupPanel';
|
|
import { StaffModal } from './hospital/StaffModal';
|
|
import { DeptModal } from './hospital/DeptModal';
|
|
import { TeleLinkHub } from './hospital/TeleLinkHub';
|
|
import { FleetView } from './hospital/FleetView';
|
|
import { EPCRRecords } from './hospital/EPCRRecords';
|
|
import { PatientArchive } from './hospital/PatientArchive';
|
|
import { ReferralsSetup } from './hospital/ReferralsSetup';
|
|
import { HospitalAnalytics } from './hospital/HospitalAnalytics';
|
|
import './HospitalConsole.css';
|
|
|
|
type ConsoleModule =
|
|
| 'ED_MONITOR'
|
|
| 'BOOKINGS'
|
|
| 'FLEET'
|
|
| 'TELELINK'
|
|
| 'EPCR'
|
|
| 'HISTORY'
|
|
| 'REPORTS'
|
|
| 'SETUP'
|
|
| 'REFERRALS';
|
|
|
|
// ─── Static data ─────────────────────────────────────────────────────────────
|
|
|
|
import { incidentsApi } from '../api/incidents';
|
|
|
|
const INITIAL_TRIPS: any[] = [];
|
|
const FLEET_DATA: any[] = [];
|
|
const EPCR_DATA: any[] = [];
|
|
const TELELINK_QUEUE: any[] = [];
|
|
|
|
// ─── Component ───────────────────────────────────────────────────────────────
|
|
export const HospitalConsole: React.FC = () => {
|
|
// ── Core state ──
|
|
const [hospitals, setHospitals] = useState<any[]>([]);
|
|
const [isLoadingHospitals, setIsLoadingHospitals] = useState(true);
|
|
const [selectedHospital, setSelectedHospital] = useState<any>(null);
|
|
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const activeModule = (searchParams.get('tab') as ConsoleModule) || 'ED_MONITOR';
|
|
const setActiveModule = (key: ConsoleModule) => {
|
|
setSearchParams({ tab: key });
|
|
};
|
|
|
|
// activeModule defaults to ED_MONITOR when tab param exists
|
|
// When no tab is set, we show the card landing page
|
|
|
|
const [successMessage, setSuccessMessage] = useState('');
|
|
|
|
// ── Data state ──
|
|
const [incomingPatients, setIncomingPatients] = useState<any[]>([]);
|
|
const [tripsData, setTripsData] = useState(INITIAL_TRIPS);
|
|
const [departments, setDepartments] = useState<any[]>([]);
|
|
const [allUsers, setAllUsers] = useState<any[]>([]);
|
|
const [rolesList, setRolesList] = useState<any[]>([]);
|
|
const [user, setUser] = useState<any>(null);
|
|
|
|
// ── Modal state ──
|
|
const [isBookingModalOpen, setIsBookingModalOpen] = useState(false);
|
|
const [isStaffModalOpen, setIsStaffModalOpen] = useState(false);
|
|
const [isEditingStaff, setIsEditingStaff] = useState(false);
|
|
const [editingStaffId, setEditingStaffId] = useState<string | null>(null);
|
|
const [isDeptModalOpen, setIsDeptModalOpen] = useState(false);
|
|
const [isUpdating, setIsUpdating] = useState(false);
|
|
const [setupSubTab, setSetupSubTab] = useState('PROFILE');
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
// ── Form state ──
|
|
const [staffFormData, setStaffFormData] = useState({
|
|
name: '', email: '', phone: '', username: '', password: '',
|
|
role: 'ED_DOCTOR', department: '', specialization: '',
|
|
license_number: '', designation: '', shift: 'General',
|
|
employee_id: '', org_id: '', assigned_floor: '', languages: ''
|
|
});
|
|
const [deptFormData, setDeptFormData] = useState({
|
|
name: '', headOfDepartment: '', totalBedsCapacity: 0,
|
|
contactPhone: '', isActive: true,
|
|
});
|
|
|
|
// ── Load user from localStorage ──
|
|
useEffect(() => {
|
|
const userData = localStorage.getItem('teleems_user');
|
|
if (userData) setUser(JSON.parse(userData));
|
|
}, []);
|
|
|
|
// ── Fetch roles ──
|
|
useEffect(() => {
|
|
const loadRoles = async () => {
|
|
try {
|
|
const token = localStorage.getItem('teleems_token') || '';
|
|
if (!token) return;
|
|
const res = await authApi.getRoles(token);
|
|
if (res.data?.data) setRolesList(res.data.data);
|
|
} catch (err) {
|
|
console.error('Failed to fetch roles:', err);
|
|
}
|
|
};
|
|
loadRoles();
|
|
}, []);
|
|
|
|
// ── Fetch trips ──
|
|
useEffect(() => {
|
|
const loadTrips = async () => {
|
|
try {
|
|
const token = localStorage.getItem('teleems_token') || '';
|
|
if (!token) return;
|
|
const res = await incidentsApi.getIncidents({}, token);
|
|
if (res.data) {
|
|
const formatted = (Array.isArray(res.data) ? res.data : []).map((inc: any) => ({
|
|
id: inc.id.substring(0, 8).toUpperCase(),
|
|
patient: inc.patients?.[0]?.name || 'Unknown',
|
|
mrn: `MRN-${inc.id.substring(8, 12).toUpperCase()}`,
|
|
type: inc.category || 'EMERGENCY',
|
|
origin: inc.address || 'Field Location',
|
|
destination: selectedHospital?.name || 'Hospital',
|
|
vehicle: 'Pending',
|
|
crew: 'Pending',
|
|
status: inc.status || 'PENDING',
|
|
eta: 'TBD',
|
|
urgency: inc.severity || 'YELLOW',
|
|
step: Math.max(1, inc.status === 'COMPLETED' ? 4 : (inc.status === 'ACTIVE' ? 2 : 1))
|
|
}));
|
|
setTripsData(formatted);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch trips:', err);
|
|
setTripsData([]);
|
|
}
|
|
};
|
|
loadTrips();
|
|
const interval = setInterval(loadTrips, 15000); // 15-sec poll
|
|
return () => clearInterval(interval);
|
|
}, [selectedHospital]);
|
|
|
|
// ── Fetch incoming patients for ED Monitor ──
|
|
useEffect(() => {
|
|
const loadIncoming = async () => {
|
|
try {
|
|
const token = localStorage.getItem('teleems_token') || '';
|
|
if (!token) return;
|
|
const orgId = user?.organisationId || user?.hospitalId || selectedHospital?.rawUser?.organisationId || selectedHospital?.id;
|
|
const res = await hospitalApi.getIncomingOperations(token, orgId);
|
|
console.log('[DEBUG] Incoming Operations Full Response:', res);
|
|
|
|
let items = [];
|
|
if (res.data?.data?.items && Array.isArray(res.data.data.items)) {
|
|
items = res.data.data.items;
|
|
} else if (res.data?.items && Array.isArray(res.data.items)) {
|
|
items = res.data.items;
|
|
} else if (Array.isArray(res.data?.data)) {
|
|
items = res.data.data;
|
|
} else if (Array.isArray(res.data)) {
|
|
items = res.data;
|
|
} else if (Array.isArray(res.items)) {
|
|
items = res.items;
|
|
}
|
|
|
|
console.log('[DEBUG] Extracted Items:', items);
|
|
|
|
if (items.length > 0) {
|
|
const flattenedPatients: any[] = [];
|
|
items.forEach((dispatch: any) => {
|
|
const rawPatients = dispatch.patients || dispatch.patient_data;
|
|
const patientsArr = Array.isArray(rawPatients) ? rawPatients : (rawPatients ? [rawPatients] : [{}]);
|
|
|
|
patientsArr.forEach((patient: any, idx: number) => {
|
|
const pId = patient.id || dispatch.dispatch_id || `idx-${idx}`;
|
|
flattenedPatients.push({
|
|
id: `${pId}-${idx}`,
|
|
originalId: pId,
|
|
mrn: patient.mrn || String(pId).substring(0, 8).toUpperCase(),
|
|
name: patient.name || dispatch.patient_name || 'Unknown Patient',
|
|
age: patient.age || dispatch.patient_age || '--',
|
|
gender: patient.gender || dispatch.patient_gender || '--',
|
|
triage: patient.triage_level || dispatch.severity || 'GREEN',
|
|
complaint: patient.chief_complaint || patient.symptoms?.[0]?.name || dispatch.category || 'Observation',
|
|
eta: dispatch.eta_seconds ? Math.floor(dispatch.eta_seconds / 60) + 'm' : 'TBD',
|
|
ambulanceId: (dispatch.dispatch_id || 'UNIT').substring(0, 8).toUpperCase(),
|
|
status: dispatch.status || 'IN_TRANSIT',
|
|
vitals: { hr: '--', spo2: '--', bp: '--/--' },
|
|
location: 'In Transit',
|
|
gcs: patient.gcs || null,
|
|
hpi: patient.hpi || null,
|
|
avpu: patient.avpu || '--',
|
|
pupils: patient.pupils || null,
|
|
trauma: patient.trauma || null,
|
|
allergies: patient.allergies || [],
|
|
surgeries: patient.surgeries || [],
|
|
conditions: patient.conditions || [],
|
|
medications: patient.medications || [],
|
|
informer: {
|
|
name: patient.informer_name || '--',
|
|
relation: patient.informer_relation || '--',
|
|
phone: patient.informer_phone || '--'
|
|
},
|
|
mlc: {
|
|
is_mlc: patient.is_mlc || false,
|
|
fir: patient.mlc_fir_number || '--',
|
|
station: patient.mlc_police_station || '--',
|
|
officer: patient.mlc_officer_contact || '--'
|
|
}
|
|
});
|
|
});
|
|
});
|
|
console.log('[DEBUG] Final Flattened Patients:', flattenedPatients);
|
|
setIncomingPatients(flattenedPatients);
|
|
} else {
|
|
setIncomingPatients([]);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch incoming patients:', err);
|
|
}
|
|
};
|
|
loadIncoming();
|
|
const interval = setInterval(loadIncoming, 15000);
|
|
return () => clearInterval(interval);
|
|
}, [selectedHospital]);
|
|
|
|
// ── Fetch departments ──
|
|
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]);
|
|
|
|
// ── Fetch hospitals ──
|
|
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);
|
|
|
|
// Auto-select first hospital if none selected
|
|
if (!selectedHospital && mappedHospitals.length > 0) {
|
|
setSelectedHospital(mappedHospitals[0]);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch hospitals:', error);
|
|
setHospitals([]);
|
|
} finally {
|
|
setIsLoadingHospitals(false);
|
|
}
|
|
};
|
|
|
|
fetchHospitals();
|
|
}, []);
|
|
|
|
// ── Fetch specific profile ──
|
|
useEffect(() => {
|
|
const fetchProfile = async () => {
|
|
const token = localStorage.getItem('teleems_token');
|
|
if (!token) return;
|
|
try {
|
|
const res = await hospitalApi.getProfile(token);
|
|
console.log("PROFILE API RESPONSE:", res);
|
|
|
|
if (res.data) {
|
|
const apiData = res.data.metadata?.hospital || res.data.profile || res.data;
|
|
|
|
setSelectedHospital((prev: any) => ({
|
|
...prev,
|
|
...apiData,
|
|
name: apiData.hospitalName || apiData.name || prev?.name || '',
|
|
contact_phone: apiData.contact_phone || apiData.phone || prev?.contact_phone || '',
|
|
contact_email: apiData.contact_email || apiData.email || prev?.contact_email || '',
|
|
address: apiData.address || prev?.address || '',
|
|
emergency_phone: apiData.emergency_phone || prev?.emergency_phone || '',
|
|
gps_lat: apiData.gps_lat || prev?.gps_lat || '',
|
|
gps_lon: apiData.gps_lon || prev?.gps_lon || '',
|
|
nabh_status: apiData.nabh_status || prev?.nabh_status || false,
|
|
// Keep the raw user object around just in case
|
|
rawUser: res.data
|
|
}));
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch hospital profile:', err);
|
|
}
|
|
};
|
|
fetchProfile();
|
|
}, []);
|
|
|
|
|
|
// ── Handlers ──
|
|
const handleSaveProfile = async () => {
|
|
if (!selectedHospital) return;
|
|
setIsUpdating(true);
|
|
try {
|
|
const token = localStorage.getItem('teleems_token');
|
|
if (!token) throw new Error('Not authenticated');
|
|
|
|
const payload = {
|
|
name: selectedHospital.name || '',
|
|
address: selectedHospital.address || '',
|
|
emergency_phone: selectedHospital.emergency_phone || '',
|
|
contact_phone: selectedHospital.contact_phone || '',
|
|
contact_email: selectedHospital.contact_email || '',
|
|
gps_lat: parseFloat(selectedHospital.gps_lat) || 0,
|
|
gps_lon: parseFloat(selectedHospital.gps_lon) || 0,
|
|
nabh_status: selectedHospital.nabh_status || false
|
|
};
|
|
|
|
const result = await hospitalApi.updateProfile(payload, token);
|
|
console.log('PATCH RESPONSE:', result);
|
|
|
|
if (result.status === 200 || result.status === 201 || !result.error) {
|
|
setSuccessMessage('Configuration Updated!');
|
|
setTimeout(() => setSuccessMessage(''), 3000);
|
|
} else {
|
|
console.error('PATCH FAILED:', result);
|
|
alert(result.message || 'Update failed');
|
|
}
|
|
} catch (err: any) {
|
|
alert(err.message);
|
|
} finally {
|
|
setIsUpdating(false);
|
|
}
|
|
};
|
|
|
|
const handleRegisterStaff = async () => {
|
|
try {
|
|
const token = localStorage.getItem('teleems_token');
|
|
|
|
const basePayload: any = {
|
|
phone: staffFormData.phone,
|
|
email: staffFormData.email,
|
|
name: staffFormData.name,
|
|
username: staffFormData.username,
|
|
role: staffFormData.role,
|
|
};
|
|
|
|
if (staffFormData.password) {
|
|
basePayload.password = staffFormData.password;
|
|
}
|
|
|
|
let finalPayload: any = { ...basePayload };
|
|
|
|
if (staffFormData.role === 'ED_DOCTOR') {
|
|
finalPayload.metadata = {
|
|
specialization: staffFormData.specialization,
|
|
license_number: staffFormData.license_number,
|
|
department: staffFormData.department,
|
|
designation: staffFormData.designation,
|
|
shift: staffFormData.shift,
|
|
};
|
|
} else if (staffFormData.role === 'Hospital Coordinator') {
|
|
finalPayload.department = staffFormData.department;
|
|
finalPayload.designation = staffFormData.designation;
|
|
finalPayload.metadata = {
|
|
shift: staffFormData.shift,
|
|
languages: staffFormData.languages ? staffFormData.languages.split(',').map(s => s.trim()) : [],
|
|
};
|
|
} else if (staffFormData.role === 'Hospital Nurse') {
|
|
finalPayload.department = staffFormData.department;
|
|
finalPayload.designation = staffFormData.designation;
|
|
finalPayload.employee_id = staffFormData.employee_id;
|
|
finalPayload.org_id = staffFormData.org_id || selectedHospital?.rawUser?.organisationId;
|
|
finalPayload.metadata = {
|
|
shift: staffFormData.shift,
|
|
languages: staffFormData.languages ? staffFormData.languages.split(',').map(s => s.trim()) : [],
|
|
assigned_floor: staffFormData.assigned_floor,
|
|
};
|
|
} else {
|
|
// Fallback for other roles
|
|
finalPayload.org_id = selectedHospital?.rawUser?.organisationId;
|
|
finalPayload.metadata = {
|
|
department: staffFormData.department,
|
|
designation: staffFormData.designation,
|
|
shift: staffFormData.shift,
|
|
};
|
|
}
|
|
|
|
let res;
|
|
if (isEditingStaff && editingStaffId) {
|
|
// Backend forbids updating username and password via this endpoint
|
|
delete finalPayload.username;
|
|
delete finalPayload.password;
|
|
res = await authApi.updateUser(editingStaffId, finalPayload, token || '');
|
|
} else {
|
|
res = await hospitalApi.registerStaff(finalPayload, token || '');
|
|
}
|
|
|
|
if (res.status === 201 || res.status === 200 || !res.error) {
|
|
setSuccessMessage(`Staff ${isEditingStaff ? 'updated' : 'registered'} successfully.`);
|
|
setIsStaffModalOpen(false);
|
|
setIsEditingStaff(false);
|
|
setEditingStaffId(null);
|
|
|
|
// Refresh users list
|
|
const usersRes = await authApi.getUsers(token || '');
|
|
if (usersRes.data) {
|
|
setAllUsers(usersRes.data);
|
|
}
|
|
} else {
|
|
alert(res.message || 'Operation failed');
|
|
}
|
|
} catch (e: any) {
|
|
alert(e.message || 'Failed');
|
|
}
|
|
};
|
|
|
|
const handleEditStaff = (user: any) => {
|
|
setIsEditingStaff(true);
|
|
setEditingStaffId(user.id);
|
|
|
|
// Parse role-specific metadata
|
|
const role = user.roles?.[0] || 'ED_DOCTOR';
|
|
const metadata = user.metadata || {};
|
|
|
|
setStaffFormData({
|
|
name: user.name || '',
|
|
email: user.email || '',
|
|
phone: user.phone || '',
|
|
username: user.username || '',
|
|
password: '', // Leave blank so it doesn't update unless typed
|
|
role: role,
|
|
department: user.department || metadata.department || '',
|
|
designation: user.designation || metadata.designation || '',
|
|
specialization: metadata.specialization || '',
|
|
license_number: metadata.license_number || '',
|
|
shift: metadata.shift || '',
|
|
employee_id: user.employee_id || user.employeeId || '',
|
|
org_id: user.org_id || user.organisationId || '',
|
|
assigned_floor: metadata.assigned_floor || '',
|
|
languages: metadata.languages ? metadata.languages.join(', ') : '',
|
|
});
|
|
|
|
setIsStaffModalOpen(true);
|
|
};
|
|
|
|
const handleCreateDepartment = 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 hospitalApi.createDepartment(payload, token || '');
|
|
if (res.status === 200 || res.status === 201) {
|
|
setSuccessMessage('Department verified and registered.');
|
|
setIsDeptModalOpen(false);
|
|
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);
|
|
}
|
|
};
|
|
|
|
// ── Module renderer ──
|
|
const renderModule = () => {
|
|
switch (activeModule) {
|
|
case 'ED_MONITOR':
|
|
return (
|
|
<EDMonitor
|
|
incomingPatients={incomingPatients}
|
|
selectedHospital={selectedHospital}
|
|
departments={departments}
|
|
onRefresh={() => {
|
|
// Trigger reload in console
|
|
window.dispatchEvent(new CustomEvent('refresh_incoming'));
|
|
}}
|
|
onOpenBooking={() => setIsBookingModalOpen(true)}
|
|
onOpenTelelink={() => setActiveModule('TELELINK')}
|
|
/>
|
|
);
|
|
|
|
case 'BOOKINGS':
|
|
return (
|
|
<TripManagement
|
|
tripsData={tripsData}
|
|
onDeleteTrip={(id) => setTripsData(tripsData.filter((t) => t.id !== id))}
|
|
onOpenBooking={() => setIsBookingModalOpen(true)}
|
|
/>
|
|
);
|
|
|
|
case 'FLEET':
|
|
return <FleetView />;
|
|
|
|
case 'TELELINK':
|
|
return <TeleLinkHub />;
|
|
|
|
case 'EPCR':
|
|
return <EPCRRecords />;
|
|
|
|
case 'HISTORY':
|
|
return <PatientArchive />;
|
|
|
|
case 'REPORTS':
|
|
return <HospitalAnalytics />;
|
|
|
|
case 'SETUP':
|
|
return (
|
|
<SetupPanel
|
|
setupSubTab={setupSubTab}
|
|
onSubTabChange={setSetupSubTab}
|
|
selectedHospital={selectedHospital}
|
|
setSelectedHospital={setSelectedHospital}
|
|
departments={departments}
|
|
setDepartments={setDepartments}
|
|
allUsers={allUsers}
|
|
onSaveProfile={handleSaveProfile}
|
|
onOpenStaffModal={() => {
|
|
setIsEditingStaff(false);
|
|
setEditingStaffId(null);
|
|
setStaffFormData({
|
|
name: '', email: '', phone: '', username: '', password: '',
|
|
role: 'ED_DOCTOR', department: '', specialization: '',
|
|
license_number: '', designation: '', shift: 'General',
|
|
employee_id: '', org_id: '', assigned_floor: '', languages: ''
|
|
});
|
|
setIsStaffModalOpen(true);
|
|
}}
|
|
onEditStaff={handleEditStaff}
|
|
onOpenDeptModal={() => setIsDeptModalOpen(true)}
|
|
/>
|
|
);
|
|
|
|
case 'REFERRALS':
|
|
return <ReferralsSetup />;
|
|
|
|
default:
|
|
return <div>Module Under Development</div>;
|
|
}
|
|
};
|
|
|
|
// Hospital selection is now handled inline — no full-screen overlay
|
|
|
|
// ── Main console ──
|
|
// Module card definitions with descriptions and color accents
|
|
const MODULE_CARDS: { key: ConsoleModule; icon: any; label: string; description: string; accent: string; accentBg: string }[] = [
|
|
{ key: 'ED_MONITOR', icon: Monitor, label: 'ED Monitor', description: 'Live triage stream, incoming patients & emergency broadcasts', accent: 'hsl(0, 84%, 60%)', accentBg: 'hsla(0, 84%, 60%, 0.08)' },
|
|
{ key: 'BOOKINGS', icon: Activity, label: 'Trip Management', description: 'Ambulance bookings, dispatches & trip tracking', accent: 'hsl(199, 89%, 48%)', accentBg: 'hsla(199, 89%, 48%, 0.08)' },
|
|
{ key: 'FLEET', icon: Truck, label: 'Fleet Visibility', description: 'Real-time fleet map, vehicle status & route monitoring', accent: 'hsl(262, 83%, 58%)', accentBg: 'hsla(262, 83%, 58%, 0.08)' },
|
|
{ key: 'TELELINK', icon: Video, label: 'TeleLink Hub', description: 'Video consults, physician console & remote triage', accent: 'hsl(152, 69%, 40%)', accentBg: 'hsla(152, 69%, 40%, 0.08)' },
|
|
{ key: 'EPCR', icon: FileText, label: 'ePCR Records', description: 'Electronic patient care reports & documentation', accent: 'hsl(38, 92%, 50%)', accentBg: 'hsla(38, 92%, 50%, 0.08)' },
|
|
{ key: 'HISTORY', icon: Database, label: 'Patient Archive', description: 'Historical patient data, discharge records & search', accent: 'hsl(215, 60%, 50%)', accentBg: 'hsla(215, 60%, 50%, 0.08)' },
|
|
{ key: 'REPORTS', icon: TrendingUp, label: 'Analytics', description: 'Performance metrics, KPIs & operational insights', accent: 'hsl(280, 65%, 55%)', accentBg: 'hsla(280, 65%, 55%, 0.08)' },
|
|
{ key: 'REFERRALS', icon: Hospital, label: 'Referral Hub', description: 'Inter-hospital referrals, network & transfer protocols', accent: 'hsl(170, 60%, 42%)', accentBg: 'hsla(170, 60%, 42%, 0.08)' },
|
|
{ key: 'SETUP', icon: Settings, label: 'Account & Setup', description: 'Hospital profile, departments, staff & configuration', accent: 'hsl(220, 15%, 50%)', accentBg: 'hsla(220, 15%, 50%, 0.08)' },
|
|
];
|
|
|
|
return (
|
|
<div className="page-container" style={{ padding: '24px 32px', height: '100%', overflowY: 'auto' }}>
|
|
{successMessage && (
|
|
<div className="success-toast-premium" style={{ position: 'fixed', top: 30, left: '50%', transform: 'translateX(-50%)', zIndex: 2000 }}>
|
|
<CheckCircle2 size={18} /> {successMessage}
|
|
</div>
|
|
)}
|
|
|
|
<AnimatePresence mode="wait">
|
|
{isLoadingHospitals ? (
|
|
<motion.div
|
|
key="loading"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: '16px' }}
|
|
>
|
|
<div className="spin" style={{ width: 32, height: 32, border: '3px solid var(--card-border)', borderTop: '3px solid var(--accent-cyan)', borderRadius: '50%' }} />
|
|
<span style={{ fontSize: '0.85rem', color: 'var(--text-muted)', fontWeight: 600 }}>Loading infrastructure...</span>
|
|
</motion.div>
|
|
) : (
|
|
<motion.div
|
|
key={`module-${activeModule}`}
|
|
initial={{ opacity: 0, y: 15 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -15 }}
|
|
transition={{ duration: 0.25, ease: 'easeOut' }}
|
|
className="ops-active-module"
|
|
>
|
|
<div className="ops-module-topbar">
|
|
<div className="ops-module-breadcrumb">
|
|
<span className="ops-breadcrumb-dot" style={{ background: MODULE_CARDS.find(c => c.key === activeModule)?.accent || 'var(--accent-cyan)' }} />
|
|
<span className="ops-breadcrumb-label">
|
|
{MODULE_CARDS.find(c => c.key === activeModule)?.label || 'Hospital Ops'}
|
|
{activeModule === 'ED_MONITOR' && incomingPatients.length > 0 && (
|
|
<span style={{
|
|
marginLeft: '8px',
|
|
background: 'var(--alert-red)',
|
|
color: '#fff',
|
|
padding: '1px 6px',
|
|
borderRadius: '10px',
|
|
fontSize: '0.65rem',
|
|
fontWeight: 700,
|
|
boxShadow: '0 2px 4px rgba(239, 68, 68, 0.2)'
|
|
}}>
|
|
{incomingPatients.length}
|
|
</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
{hospitals.length > 0 && (
|
|
<div className="ops-hospital-picker" style={{ marginLeft: 'auto' }}>
|
|
<div className="ops-picker-indicator">
|
|
<div style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--accent-green)', flexShrink: 0 }} />
|
|
<select
|
|
className="ops-hospital-select"
|
|
value={selectedHospital?.id || ''}
|
|
onChange={(e) => {
|
|
const h = hospitals.find((h: any) => h.id === e.target.value);
|
|
if (h) setSelectedHospital(h);
|
|
}}
|
|
>
|
|
{hospitals.map((h: any) => (
|
|
<option key={h.id} value={h.id}>{h.name}</option>
|
|
))}
|
|
</select>
|
|
<ChevronDown size={12} style={{ flexShrink: 0, color: 'var(--text-muted)' }} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{renderModule()}
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
<AnimatePresence>
|
|
<StaffModal
|
|
isOpen={isStaffModalOpen}
|
|
onClose={() => setIsStaffModalOpen(false)}
|
|
staffFormData={staffFormData}
|
|
setStaffFormData={setStaffFormData}
|
|
rolesList={rolesList}
|
|
showPassword={showPassword}
|
|
setShowPassword={setShowPassword}
|
|
onSubmit={handleRegisterStaff}
|
|
isEditing={isEditingStaff}
|
|
/>
|
|
<DeptModal
|
|
isOpen={isDeptModalOpen}
|
|
onClose={() => setIsDeptModalOpen(false)}
|
|
deptFormData={deptFormData}
|
|
setDeptFormData={setDeptFormData}
|
|
allUsers={allUsers}
|
|
selectedHospital={selectedHospital}
|
|
onSubmit={handleCreateDepartment}
|
|
/>
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default HospitalConsole;
|