feat: integrate hospital management module with advanced UI styling and analytics components

This commit is contained in:
2026-05-11 11:03:52 +05:30
parent 8dc773d205
commit a1930c1bab
36 changed files with 10084 additions and 1844 deletions

22
scratch/tag_counter.py Normal file
View File

@@ -0,0 +1,22 @@
import sys
def count_tags(file_path):
with open(file_path, 'r') as f:
content = f.read()
div_open = content.count('<div')
div_close = content.count('</div>')
motion_div_open = content.count('<motion.div')
motion_div_close = content.count('</motion.div>')
paren_open = content.count('(')
paren_close = content.count(')')
brace_open = content.count('{')
brace_close = content.count('}')
print(f"div: open={div_open}, close={div_close}, diff={div_open - div_close}")
print(f"motion.div: open={motion_div_open}, close={motion_div_close}, diff={motion_div_open - motion_div_close}")
print(f"parentheses: open={paren_open}, close={paren_close}, diff={paren_open - paren_close}")
print(f"braces: open={brace_open}, close={brace_close}, diff={brace_open - brace_close}")
if __name__ == "__main__":
count_tags(sys.argv[1])

View File

@@ -21,6 +21,7 @@ import { FleetLogin } from './pages/FleetLogin';
import { FleetOperatorDashboard } from './pages/FleetOperatorDashboard';
import { PerspectiveLauncher } from './pages/PerspectiveLauncher';
import { RoleLogin } from './pages/RoleLogin';
import { HospitalLogin } from './pages/HospitalLogin';
import { ComingSoonPortal } from './pages/ComingSoonPortal';
import {
Building2,
@@ -43,10 +44,13 @@ const RoleProtectedRoute: React.FC<{
if (!isAuthenticated) return <Navigate to="/login" replace />;
const userRoles = Array.isArray(user?.roles) ? user.roles : [];
const userRoles = Array.isArray(user?.roles)
? user.roles.map((r: any) => String(r).toUpperCase().replace(/\s+/g, '_'))
: [];
const hasAccess = allowedRoles.some(role => userRoles.includes(role)) || userRoles.includes('CURESELECT_ADMIN');
if (!hasAccess) {
console.log('[RBAC] Access Denied:', { required: allowedRoles, current: userRoles });
// Redirect to their respective "home" if they don't have access
if (userRoles.includes('FLEET_OPERATOR')) return <Navigate to="/fleet-operator" replace />;
return <Navigate to="/" replace />;
@@ -121,6 +125,7 @@ function AppContent() {
<Routes>
<Route path="/" element={<PerspectiveLauncher />} />
<Route path="/login" element={<Login />} />
<Route path="/login/hospital" element={<HospitalLogin />} />
<Route path="/login/:role" element={<RoleLogin />} />
<Route path="/fleet-login" element={<FleetLogin />} />
<Route path="/launcher" element={<PerspectiveLauncher />} />

View File

@@ -25,60 +25,9 @@ export const apiClient = {
defaultHeaders['Authorization'] = `Bearer ${authToken}`;
}
// --- MOCK BYPASS FOR DEMO SESSIONS ---
if (authToken && (
authToken.startsWith('mock-') ||
authToken.startsWith('dev-token-') ||
authToken === 'dev-super-token-2026'
)) {
return new Promise((resolve) => {
setTimeout(() => {
if (endpoint.includes('/v1/incidents')) {
resolve({
status: 200,
data: [
{
id: 'INC-MOCK-001',
category: 'MEDICAL',
severity: 'CRITICAL',
status: 'PENDING',
address: 'Sector 7G, Tactical Hub',
notes: 'High-priority mock incident for system validation.',
createdAt: new Date().toISOString(),
gps_lat: 13.0827,
gps_lon: 80.2707,
patients: [{ name: 'Tactical Test', age: 34, gender: 'Male', symptoms: ['None'], triage_code: 'RED' }]
}
]
});
} else if (endpoint.includes('/v1/auth/users')) {
resolve({
status: 200,
data: [
{ id: 'u1', username: 'admin', roles: ['CURESELECT_ADMIN'], status: 'ACTIVE', email: 'admin@teleems.com' },
{ id: 'u2', username: 'fleet_op', roles: ['FLEET_OPERATOR'], status: 'ACTIVE', email: 'fleet@teleems.com' }
]
});
} else if (endpoint.includes('/v1/auth/audit-logs')) {
resolve({
status: 200,
data: {
logs: [
{ id: 'l1', action: 'LOGIN_SUCCESS', createdAt: new Date().toISOString(), ipAddress: '127.0.0.1', user: { username: 'admin' } },
{ id: 'l2', action: 'INCIDENT_VIEW', createdAt: new Date().toISOString(), ipAddress: '127.0.0.1', user: { username: 'admin' } }
],
total: 2
}
});
} else {
resolve({ status: 200, data: [] });
}
}, 500);
});
}
const url = endpoint.startsWith('http') ? endpoint : `${BASE_URL}${endpoint}`;
console.log(`[API] ${options.method || 'GET'} ${url}`);
try {
const response = await fetch(url, {
headers: { ...defaultHeaders, ...headers },
@@ -86,8 +35,8 @@ export const apiClient = {
});
// Handle session expiration
if (response.status === 401 || response.status === 403) {
console.warn('Unauthorized request detected. Triggering auto-logout...');
if (response.status === 401 && !url.includes('/auth/login')) {
console.warn('Token expired or invalid. Triggering auto-logout...');
logout();
return null; // Return null as the app will redirect
}

View File

@@ -21,6 +21,10 @@ export const authApi = {
return apiClient.post('/v1/auth/mfa/totp/verify', { totp_code: totpCode }, { token });
},
getProfile: async (token: string): Promise<AuthUser> => {
return apiClient.get('/v1/auth/me', { token });
},
getAuditLogs: async (token: string, limit = 20, offset = 0) => {
return apiClient.get(`/v1/auth/audit-logs?limit=${limit}&offset=${offset}`, { token });
},

86
src/api/hospital.ts Normal file
View File

@@ -0,0 +1,86 @@
import { apiClient } from './apiClient';
import { authApi } from './auth';
import type { LoginResponse } from './types';
/**
* Hospital-specific API module.
* Wraps auth endpoints with hospital-context logic.
*/
export const hospitalApi = {
/**
* Authenticate a hospital user via the real auth API.
*/
login: async (username: string, password: string): Promise<LoginResponse> => {
return authApi.login(username, password);
},
/**
* Verify MFA for hospital user.
*/
verifyMfa: async (mfaSessionToken: string, totpCode: string): Promise<LoginResponse> => {
return authApi.verifyMfa(mfaSessionToken, totpCode);
},
/**
* Get hospital profile data from the authenticated user metadata.
*/
getProfile: async (token: string) => {
return apiClient.get('/v1/hospital/ops/profile', { token });
},
/**
* Get departments scoped to a specific hospital.
*/
getDepartments: async (token: string, hospitalId?: string) => {
const endpoint = hospitalId
? `/v1/hospital/departments?hospitalId=${hospitalId}`
: '/v1/hospital/departments';
return apiClient.get(endpoint, { token });
},
/**
* Get staff members scoped to a specific hospital.
*/
getStaff: async (token: string, hospitalId?: string) => {
const res = await authApi.getUsers(token);
if (res?.data && hospitalId) {
const filtered = (Array.isArray(res.data) ? res.data : []).filter(
(u: any) => u.hospitalId === hospitalId || u.organisationId === hospitalId
);
return { ...res, data: filtered };
}
return res;
},
/**
* Create a new department under a hospital.
*/
createDepartment: async (deptData: any, token: string) => {
return authApi.createDepartment(deptData, token);
},
/**
* Register a staff member under a hospital.
*/
registerStaff: async (staffData: any, token: string) => {
return authApi.registerUser(staffData, token);
},
updateProfile: async (payload: any, token: string) => {
return apiClient.patch('/v1/hospital/ops/profile', payload, { token });
},
/**
* Get incoming operations/dispatches for the hospital.
*/
getIncomingOperations: async (token: string, hospitalId?: string) => {
const url = hospitalId
? `/v1/hospital/ops/incoming?hospitalId=${hospitalId}`
: '/v1/hospital/ops/incoming';
return apiClient.get(url, { token });
},
admitPatient: async (patientId: string, departmentId: string, token: string) => {
return apiClient.post(`/v1/hospital/ops/incoming/${patientId}/admit`, { departmentId }, { token });
},
};

View File

@@ -1,4 +1,4 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import React, { Component, type ErrorInfo, type ReactNode } from 'react';
import { AlertTriangle, RefreshCw } from 'lucide-react';
interface Props {

View File

@@ -25,7 +25,7 @@ export const Sidebar: React.FC = () => {
const parsed = JSON.parse(stored);
if (parsed && typeof parsed === 'object') {
parsed.roles = Array.isArray(parsed.roles)
? parsed.roles.map((r: any) => String(r).toUpperCase())
? parsed.roles.map((r: any) => String(r).toUpperCase().replace(/\s+/g, '_'))
: [];
return parsed;
}
@@ -54,7 +54,7 @@ export const Sidebar: React.FC = () => {
const filteredNavItems = useMemo(() => {
const userRoles = Array.isArray(user.roles) ? user.roles : [];
const adminRoles = ['CURESELECT_ADMIN', 'ADMIN', 'SUPER_ADMIN', 'SUPERADMIN'];
const hasAdminRole = userRoles.some(r => adminRoles.includes(r));
const hasAdminRole = userRoles.some((r: string) => adminRoles.includes(r));
const filterItems = (items: NavItem[]): NavItem[] => {
return items.filter(item => {
@@ -66,7 +66,9 @@ export const Sidebar: React.FC = () => {
}));
};
return filterItems(NAVIGATION_CONFIG);
const result = filterItems(NAVIGATION_CONFIG);
console.log('[DEBUG] userRoles:', userRoles, 'filteredItems:', result);
return result;
}, [user.roles]);
const renderNavItem = (item: NavItem, isSubItem = false) => {
@@ -79,34 +81,23 @@ export const Sidebar: React.FC = () => {
<div key={item.id} style={{ display: 'flex', flexDirection: 'column' }}>
<NavLink
to={item.path}
style={({ isActive: linkActive }) => ({
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: isSubItem ? '8px 20px 8px 48px' : '10px 20px',
textDecoration: 'none',
color: (linkActive || isActive) ? 'var(--accent-cyan)' : 'var(--text-secondary)',
borderLeft: !isSubItem && (linkActive || isActive) ? '3px solid var(--accent-cyan)' : '3px solid transparent',
background: (linkActive || isActive) ? 'rgba(59, 130, 246, 0.08)' : 'transparent',
transition: 'all 0.25s ease',
textShadow: (linkActive || isActive) ? '0 0 10px rgba(59, 130, 246, 0.4)' : 'none',
minWidth: 0,
position: 'relative'
})}
className={({ isActive: linkActive }) =>
`${isSubItem ? 'sidebar-sub-item' : 'sidebar-nav-item'} ${(linkActive || isActive) ? 'active' : ''}`
}
>
{!isSubItem && <Icon size={18} style={{ flexShrink: 0 }} />}
<span style={{
fontWeight: (isActive || isParentActive) ? 700 : 500,
fontSize: isSubItem ? '0.8rem' : '0.875rem',
<span className="sidebar-label" style={{
flex: 1,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
flex: 1
textOverflow: 'ellipsis'
}}>
{item.label}
</span>
{hasSubItems && (
isParentActive ? <ChevronDown size={14} /> : <ChevronRight size={14} />
<span className="sidebar-label">
{isParentActive ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</span>
)}
</NavLink>
@@ -117,7 +108,7 @@ export const Sidebar: React.FC = () => {
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
style={{ overflow: 'hidden', background: 'rgba(255,255,255,0.02)' }}
style={{ overflow: 'hidden', background: 'transparent' }}
>
{item.subItems?.map(sub => renderNavItem(sub, true))}
</motion.div>
@@ -128,7 +119,7 @@ export const Sidebar: React.FC = () => {
};
return (
<aside className="glass" style={{
<aside className="sidebar-premium" style={{
width: 'var(--sidebar-width)',
minWidth: 'var(--sidebar-width)',
flexBasis: 'var(--sidebar-width)',
@@ -136,33 +127,30 @@ export const Sidebar: React.FC = () => {
height: '100vh',
display: 'flex',
flexDirection: 'column',
borderRight: '1px solid var(--card-border)',
background: 'var(--glass-bg)',
zIndex: 1100,
position: 'relative'
}}>
<div style={{
padding: '20px 20px',
borderBottom: '1px solid var(--card-border)',
padding: '16px 20px',
borderBottom: '1px solid #edf2f7',
display: 'flex',
alignItems: 'center',
gap: '10px',
flexShrink: 0,
}}>
<div style={{
width: '30px',
height: '30px',
background: 'var(--accent-cyan)',
borderRadius: '6px',
width: '28px',
height: '28px',
background: 'linear-gradient(135deg, #0ea5e9, #3b82f6)',
borderRadius: '7px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 0 12px var(--accent-cyan)',
flexShrink: 0,
}}>
<Zap size={18} color="#000" />
<Zap size={16} color="#ffffff" />
</div>
<h2 style={{ fontSize: '1.1rem', fontWeight: 800, color: 'var(--accent-cyan)', margin: 0, letterSpacing: '-0.3px' }}>CureSelect</h2>
<h2 className="sidebar-label sidebar-logo-text" style={{ fontSize: '1.1rem', margin: 0 }}>CureSelect</h2>
</div>
<nav style={{ flex: 1, padding: '12px 0', overflowY: 'auto', minHeight: 0 }} className="no-scrollbar">
@@ -171,7 +159,7 @@ export const Sidebar: React.FC = () => {
<div style={{
padding: '12px 16px',
borderTop: '1px solid var(--card-border)',
borderTop: '1px solid #edf2f7',
display: 'flex',
flexDirection: 'column',
gap: '10px',
@@ -189,42 +177,41 @@ export const Sidebar: React.FC = () => {
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', minWidth: 0, overflow: 'hidden' }}>
<div style={{
width: '32px',
height: '32px',
borderRadius: '50%',
background: 'linear-gradient(135deg, var(--accent-cyan), var(--accent-green))',
width: '30px',
height: '30px',
borderRadius: '8px',
background: 'linear-gradient(135deg, var(--accent-cyan), #3b82f6)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '0.75rem',
fontSize: '0.7rem',
fontWeight: 700,
color: '#000',
color: '#fff',
flexShrink: 0,
}}>{initials}</div>
<div style={{ minWidth: 0, overflow: 'hidden' }}>
<div style={{ fontSize: '0.78rem', fontWeight: 700, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{displayName}</div>
<div style={{ fontSize: '0.62rem', color: 'var(--text-secondary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>ID: {displayId}</div>
<div className="sidebar-label" style={{ minWidth: 0, overflow: 'hidden' }}>
<div style={{ fontSize: '0.8rem', fontWeight: 600, color: '#1e293b', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{displayName}</div>
<div style={{ fontSize: '0.68rem', color: '#94a3b8', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>ID: {displayId}</div>
</div>
</div>
<button
onClick={handleLogout}
className="hover-glow"
style={{
background: 'rgba(239, 68, 68, 0.1)',
border: '1px solid rgba(239, 68, 68, 0.2)',
borderRadius: '8px',
padding: '7px',
color: 'var(--alert-red)',
background: '#fef2f2',
border: '1px solid #fecaca',
borderRadius: '7px',
padding: '6px',
color: '#ef4444',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s',
transition: 'all 0.15s',
flexShrink: 0,
}}
title="Sign Out"
>
<LogOut size={15} />
<LogOut size={14} />
</button>
</div>
</div>

203
src/components/TopBar.css Normal file
View File

@@ -0,0 +1,203 @@
/* --- PROFESSIONAL RESPONSIVE TOPBAR --- */
.topbar-container {
height: var(--topbar-height);
margin: 12px 12px 0 12px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
gap: 16px;
border: 1px solid var(--card-border);
background: #ffffff;
box-shadow: var(--shadow-sm);
z-index: 900;
border-radius: 10px;
flex-shrink: 0;
min-width: 0;
}
.topbar-left {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
min-width: 0;
}
.nav-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.nav-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 7px 12px;
color: var(--text-secondary);
cursor: pointer;
transition: var(--transition-snappy);
font-weight: 600;
font-size: 0.75rem;
letter-spacing: 0.02em;
}
.nav-btn.home {
background: var(--accent-cyan-soft);
border-color: transparent;
color: var(--accent-cyan);
}
.nav-btn:hover {
background: #f1f5f9;
border-color: #cbd5e1;
color: var(--text-primary);
}
.search-wrap {
position: relative;
flex-shrink: 1;
width: clamp(160px, 20vw, 280px);
}
.search-wrap input {
width: 100%;
background: #f8fafc;
border: 1px solid var(--card-border);
border-radius: 8px;
padding: 7px 10px 7px 34px;
color: var(--text-primary);
font-size: 0.8rem;
outline: none;
transition: var(--transition-snappy);
}
.search-wrap input:focus {
border-color: var(--accent-cyan);
background: #fff;
box-shadow: 0 0 0 3px var(--accent-cyan-soft);
}
.topbar-right {
display: flex;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
.clock-wrap {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-muted);
}
.clock-time {
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
color: var(--text-primary);
font-weight: 500;
}
.notification-bell {
position: relative;
cursor: pointer;
color: var(--text-muted);
transition: color 0.15s;
padding: 4px;
}
.notification-bell:hover { color: var(--text-primary); }
.bell-badge {
position: absolute;
top: 0px;
right: -2px;
min-width: 16px;
height: 16px;
background: var(--alert-red);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6rem;
font-weight: 700;
color: #fff;
border: 2px solid #fff;
}
.user-profile {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
padding: 5px 8px;
border-radius: 10px;
transition: var(--transition-snappy);
border: 1px solid transparent;
}
.user-profile:hover {
background: #f8fafc;
border-color: var(--card-border);
}
.avatar-circle {
width: 32px;
height: 32px;
border-radius: 8px;
background: linear-gradient(135deg, var(--accent-cyan), #3b82f6);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
font-size: 0.7rem;
letter-spacing: 0.03em;
}
.user-info {
display: flex;
flex-direction: column;
min-width: 0;
}
.user-name {
font-size: 0.8rem;
font-weight: 600;
color: var(--text-primary);
line-height: 1.2;
}
.user-role {
font-size: 0.65rem;
font-weight: 500;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* Responsive */
@media (max-width: 1024px) {
.nav-btn span { display: none; }
.clock-wrap { display: none; }
.search-wrap { width: 180px; }
}
@media (max-width: 768px) {
.topbar-container { margin: 8px 8px 0; padding: 0 12px; }
.search-wrap { display: none; }
.user-info { display: none; }
.nav-actions { gap: 4px; }
.topbar-right { gap: 12px; }
}
@media (max-width: 480px) {
.nav-actions { display: none; }
}

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Search, Bell, Clock, LogOut, Home, ArrowLeft } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { logout } from '../utils/auth';
import './TopBar.css';
export const TopBar: React.FC = () => {
const [time, setTime] = useState(new Date());
@@ -29,256 +30,87 @@ export const TopBar: React.FC = () => {
const displayName = String(user.username || 'Admin');
const rawRole = Array.isArray(user.roles) ? (user.roles[0] || 'Administrator') : 'Administrator';
// Shorten long role names for the header
const roleLabel = rawRole
.replace(/_/g, ' ')
.replace('CURESELECT ADMIN', 'CS ADMIN')
.replace('HOSPITAL ADMIN', 'H. ADMIN')
.replace('FLEET OPERATOR', 'FLEET OPS')
.replace('STATION INCHARGE', 'STATION IC');
const initials = displayName.substring(0, 2).toUpperCase() || 'AD';
const formattedTime = time.toLocaleTimeString('en-US', {
hour12: false,
timeZone: 'Asia/Kolkata',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
minute: '2-digit'
});
const tzLabel = 'IST';
return (
<header
className="glass"
style={{
height: 'var(--topbar-height)',
margin: '16px 16px 0 16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 20px',
gap: '16px',
border: '1px solid var(--card-border)',
background: 'var(--glass-bg)',
zIndex: 900,
borderRadius: '12px',
flexShrink: 0,
minWidth: 0,
overflow: 'hidden',
}}
>
{/* ── LEFT: Search + status ────────────────────────────────── */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flex: 1, minWidth: 0, overflow: 'hidden' }}>
{/* Global Navigation Controls */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}>
{/* Back Navigation Button */}
<header className="topbar-container">
<div className="topbar-left">
<div className="nav-actions">
<button
onClick={() => navigate(-1)}
className="hover-glow"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px',
background: 'rgba(0, 0, 0, 0.03)',
border: '1px solid rgba(0, 0, 0, 0.08)',
borderRadius: '8px',
padding: '8px 14px',
color: 'var(--text-primary)',
cursor: 'pointer',
transition: 'all 0.2s ease',
fontWeight: 700,
fontSize: '0.8rem'
}}
title="Go to Previous Screen"
className="nav-btn"
title="Go Back"
>
<ArrowLeft size={16} />
<span>BACK</span>
</button>
{/* Home Navigation Button */}
<button
onClick={() => navigate('/')}
className="hover-glow"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px',
background: 'rgba(0, 209, 255, 0.1)',
border: '1px solid rgba(0, 209, 255, 0.3)',
borderRadius: '8px',
padding: '8px 14px',
color: 'var(--accent-cyan)',
cursor: 'pointer',
transition: 'all 0.2s ease',
fontWeight: 700,
fontSize: '0.8rem'
}}
title="Return to Dashboard"
className="nav-btn home"
title="Return Home"
>
<Home size={16} />
<span>HOME</span>
</button>
</div>
{/* Search bar */}
<div style={{ position: 'relative', flexShrink: 0, width: 'clamp(160px, 22vw, 320px)' }}>
<Search
size={16}
style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-secondary)', pointerEvents: 'none' }}
/>
<div className="search-wrap">
<Search size={14} className="search-icon" style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', opacity: 0.5 }} />
<input
type="text"
placeholder="Search operators, hospitals, incidents..."
style={{
width: '100%',
background: 'rgba(0, 0, 0, 0.03)',
border: '1px solid var(--card-border)',
borderRadius: '8px',
padding: '8px 10px 8px 36px',
color: 'var(--text-primary)',
fontSize: '0.8rem',
outline: 'none',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
placeholder="Search resources..."
/>
</div>
{/* Status pill — flexible, shrinks if needed */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '7px',
background: 'rgba(0, 255, 136, 0.08)',
padding: '5px 11px',
borderRadius: '20px',
border: '1px solid rgba(0, 255, 136, 0.18)',
flexShrink: 1,
minWidth: 0,
overflow: 'hidden',
maxWidth: '260px',
}}
>
<div style={{ width: '7px', height: '7px', background: 'var(--accent-green)', borderRadius: '50%', boxShadow: '0 0 6px var(--accent-green)', flexShrink: 0 }} />
<span style={{ fontSize: '0.68rem', fontWeight: 700, color: 'var(--accent-green)', letterSpacing: '0.03em', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
SYSTEM CONTROL PLANE HEALTHY
</span>
</div>
</div>
{/* ── RIGHT: Clock + Bell + Profile ───────────────────────── */}
<div style={{ display: 'flex', alignItems: 'center', gap: '20px', flexShrink: 0 }}>
{/* Clock */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--text-secondary)', flexShrink: 0 }}>
<Clock size={15} />
<span
className="mono"
style={{ fontSize: '0.92rem', color: 'var(--text-primary)', fontWeight: 600, whiteSpace: 'nowrap' }}
>
{formattedTime}{' '}
<span style={{ fontSize: '0.65rem', color: 'var(--text-secondary)' }}>{tzLabel}</span>
<div className="topbar-right">
<div className="clock-wrap">
<Clock size={14} />
<span className="clock-time">
{formattedTime} <small style={{ fontSize: '0.6rem', opacity: 0.6 }}>{tzLabel}</small>
</span>
</div>
{/* Bell */}
<div style={{ position: 'relative', cursor: 'pointer', flexShrink: 0 }}>
<Bell size={18} style={{ color: 'var(--text-secondary)' }} />
<div
style={{
position: 'absolute',
top: '-4px',
right: '-5px',
width: '15px',
height: '15px',
background: 'var(--alert-red)',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '0.6rem',
fontWeight: 700,
color: '#fff',
boxShadow: '0 0 8px var(--alert-red)',
}}
>
3
</div>
<div className="notification-bell">
<Bell size={18} />
<div className="bell-badge">3</div>
</div>
{/* Divider */}
<div style={{ height: '20px', width: '1px', background: 'var(--card-border)', flexShrink: 0 }} />
<div style={{ height: '20px', width: '1px', background: 'var(--card-border)' }} />
{/* Profile */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
cursor: 'pointer',
padding: '4px 8px',
borderRadius: '8px',
transition: 'background 0.2s',
border: '1px solid transparent',
flexShrink: 0,
maxWidth: '220px',
}}
className="hover-glow"
className="user-profile"
onClick={handleLogout}
title="Click to logout"
>
{/* Avatar */}
<div
style={{
width: '30px',
height: '30px',
borderRadius: '50%',
background: 'linear-gradient(135deg, var(--accent-cyan), #0066ff)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontWeight: 700,
fontSize: '0.75rem',
flexShrink: 0,
}}
>
<div className="avatar-circle">
{initials}
</div>
{/* Name + Role */}
<div style={{ display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}>
<span
style={{
fontSize: '0.82rem',
fontWeight: 700,
color: 'var(--text-primary)',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{displayName}
</span>
<span
style={{
fontSize: '0.65rem',
color: 'var(--accent-cyan)',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
textTransform: 'uppercase',
letterSpacing: '0.03em',
}}
>
{roleLabel} Logout
</span>
<div className="user-info">
<span className="user-name">{displayName}</span>
<span className="user-role">{roleLabel} Logout</span>
</div>
<LogOut size={14} style={{ color: 'var(--text-secondary)', flexShrink: 0 }} />
<LogOut size={14} style={{ opacity: 0.5 }} />
</div>
</div>
</header>

View File

@@ -14,7 +14,10 @@ import {
PhoneCall,
Navigation,
ShoppingCart,
LayoutGrid
LayoutGrid,
Video,
FileText,
TrendingUp
} from 'lucide-react';
export interface NavItem {
@@ -37,10 +40,10 @@ export const NAVIGATION_CONFIG: NavItem[] = [
},
{
id: 'overview',
label: 'Admin Dashboard',
label: 'Dashboard',
icon: LayoutDashboard,
path: '/',
roles: ['CURESELECT_ADMIN']
roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN']
},
{
id: 'incidents',
@@ -90,7 +93,18 @@ export const NAVIGATION_CONFIG: NavItem[] = [
label: 'Hospital Ops',
icon: Monitor,
path: '/hospital-console',
roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'COORDINATOR', 'EMT']
roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'COORDINATOR', 'EMT'],
subItems: [
{ id: 'hosp-ed', label: 'ED Monitor', icon: Monitor, path: '/hospital-console?tab=ED_MONITOR', roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'COORDINATOR', 'EMT'] },
{ id: 'hosp-bookings', label: 'Trip Management', icon: Activity, path: '/hospital-console?tab=BOOKINGS', roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'COORDINATOR', 'EMT'] },
{ id: 'hosp-fleet', label: 'Fleet Visibility', icon: Truck, path: '/hospital-console?tab=FLEET', roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'COORDINATOR', 'EMT'] },
{ id: 'hosp-telelink', label: 'TeleLink Hub', icon: Video, path: '/hospital-console?tab=TELELINK', roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'COORDINATOR', 'EMT'] },
{ id: 'hosp-epcr', label: 'ePCR Records', icon: FileText, path: '/hospital-console?tab=EPCR', roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'COORDINATOR', 'EMT'] },
{ id: 'hosp-history', label: 'Patient Archive', icon: Database, path: '/hospital-console?tab=HISTORY', roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'COORDINATOR', 'EMT'] },
{ id: 'hosp-reports', label: 'Analytics', icon: TrendingUp, path: '/hospital-console?tab=REPORTS', roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'COORDINATOR', 'EMT'] },
{ id: 'hosp-referrals', label: 'Referral Hub', icon: Hospital, path: '/hospital-console?tab=REFERRALS', roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'COORDINATOR', 'EMT'] },
{ id: 'hosp-setup', label: 'Account & Setup', icon: Settings, path: '/hospital-console?tab=SETUP', roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'COORDINATOR', 'EMT'] },
]
},
{
id: 'master-data',

View File

@@ -1,19 +1,62 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
:root {
--base-bg: #F8FAFC;
--card-bg: #FFFFFF;
--card-border: rgba(59, 130, 246, 0.15);
--accent-cyan: #3B82F6;
--accent-green: #10B981;
--alert-red: #EF4444;
--warning-amber: #F59E0B;
--text-primary: #1E293B;
--text-secondary: #64748B;
--glass-bg: rgba(255, 255, 255, 0.8);
--glass-blur: blur(12px);
/* --- HSL DESIGN TOKENS (PROFESSIONAL MEDICAL PALETTE) --- */
--hull-h: 220;
--hull-s: 16%;
--hull-l: 97%;
--hull-dark-h: 222;
--hull-dark-s: 47%;
--hull-dark-l: 11%;
/* Accents */
--accent-cyan-h: 199;
--accent-cyan-s: 89%;
--accent-cyan-l: 48%;
--accent-purple-h: 262;
--accent-purple-s: 83%;
--accent-purple-l: 58%;
--accent-green-h: 160;
--accent-green-s: 84%;
--accent-green-l: 39%;
/* Semantic Mappings */
--base-bg: #f8f9fb;
--card-bg: #ffffff;
--card-border: #e5e7eb;
--glass-bg: rgba(255, 255, 255, 0.6);
--accent-cyan: hsl(var(--accent-cyan-h), var(--accent-cyan-s), var(--accent-cyan-l));
--accent-cyan-soft: hsla(var(--accent-cyan-h), var(--accent-cyan-s), var(--accent-cyan-l), 0.07);
--accent-cyan-glow: hsla(var(--accent-cyan-h), var(--accent-cyan-s), var(--accent-cyan-l), 0.25);
--accent-purple: hsl(var(--accent-purple-h), var(--accent-purple-s), var(--accent-purple-l));
--accent-purple-soft: hsla(var(--accent-purple-h), var(--accent-purple-s), var(--accent-purple-l), 0.08);
--accent-green: hsl(var(--accent-green-h), var(--accent-green-s), var(--accent-green-l));
--accent-green-soft: hsla(var(--accent-green-h), var(--accent-green-s), var(--accent-green-l), 0.08);
--alert-red: hsl(4, 90%, 58%);
--warning-amber: hsl(38, 92%, 50%);
--text-primary: #1e293b;
--text-secondary: #64748b;
--text-muted: #94a3b8;
/* Effects */
--hull-glass: rgba(255, 255, 255, 0.85);
--glass-blur: blur(16px);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.08);
--shadow-premium: 0 20px 40px -12px rgba(0, 0, 0, 0.08);
--transition-snappy: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
--sidebar-width: 260px;
--topbar-height: 70px;
--topbar-height: 64px;
}
* {
@@ -23,16 +66,14 @@
}
body {
font-family: 'Inter', sans-serif;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background-color: var(--base-bg);
background-image:
linear-gradient(rgba(59, 130, 246, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(59, 130, 246, 0.05) 1px, transparent 1px);
background-size: 40px 40px;
color: var(--text-primary);
height: 100vh;
overflow: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
h1, h2, h3, h4 {
@@ -107,10 +148,17 @@ h1, h2, h3, h4 {
border-radius: 10px;
}
/* Grid Layouts */
/* 4K UI Limits Wrapper */
.page-content-wrapper {
max-width: 1920px;
margin: 0 auto;
width: 100%;
}
/* Responsive Grid Layouts */
.stats-bar {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
@@ -119,13 +167,49 @@ h1, h2, h3, h4 {
display: grid;
grid-template-columns: 1fr 1.5fr 1fr;
gap: 24px;
height: calc(100% - 140px);
height: auto;
min-height: calc(100% - 140px);
}
.main-grid > * {
min-width: 0;
}
/* Responsive Breakpoints */
@media (max-width: 1400px) {
.main-grid {
grid-template-columns: 1fr 1fr;
}
.main-grid > *:last-child {
grid-column: span 2;
}
}
@media (max-width: 1024px) {
:root {
--sidebar-width: 72px;
}
.sidebar-label {
display: none !important;
}
.main-grid {
grid-template-columns: 1fr;
}
.main-grid > *:last-child {
grid-column: span 1;
}
}
@media (max-width: 768px) {
.page-container {
padding: 16px;
}
.stats-bar {
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
}
}
/* Pulse Animation */
@keyframes pulse-red {
0% { transform: scale(1); opacity: 1; }
@@ -250,3 +334,214 @@ select, select option {
background-color: var(--card-bg);
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* ─── PREMIUM ADMIN DASHBOARD STYLES ─── */
/* ═══════════════════════════════════════════════════════════════════════════ */
.dashboard-header-premium {
position: relative;
padding: 24px;
border-radius: 16px;
background: linear-gradient(135deg, hsla(210, 40%, 98%, 0.8), hsla(0, 0%, 100%, 0.95));
backdrop-filter: blur(20px);
border: 1px solid var(--card-border);
box-shadow: 0 4px 24px -6px rgba(0, 0, 0, 0.03);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 24px;
}
.dashboard-header-premium h2 {
font-size: 1.8rem;
font-weight: 850;
letter-spacing: -0.02em;
background: linear-gradient(to right, var(--text-primary), var(--accent-cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 4px;
}
.premium-stat-card {
position: relative;
background: #fff;
border-radius: 16px;
padding: 20px;
border: 1px solid var(--card-border);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
display: flex;
flex-direction: column;
transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
overflow: hidden;
}
.premium-stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.06);
border-color: rgba(59, 130, 246, 0.2);
}
.premium-stat-icon-wrap {
width: 42px;
height: 42px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
}
.premium-stat-label {
font-size: 0.75rem;
color: var(--text-secondary);
text-transform: uppercase;
font-weight: 750;
letter-spacing: 0.02em;
margin-bottom: 4px;
}
.premium-stat-value {
font-size: 2rem;
font-weight: 850;
color: var(--text-primary);
line-height: 1;
letter-spacing: -0.03em;
display: flex;
align-items: baseline;
gap: 6px;
}
.premium-stat-sub {
font-size: 0.9rem;
color: var(--text-muted);
font-weight: 600;
}
.dashboard-primary-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 24px;
margin-bottom: 24px;
}
.dashboard-secondary-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 24px;
}
.premium-health-card {
background: hsla(210, 40%, 98%, 0.8);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 24px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.premium-health-card:hover {
background: #fff;
border-color: var(--accent-cyan);
box-shadow: 0 8px 32px rgba(59, 130, 246, 0.08);
}
.health-status-badge {
padding: 4px 10px;
border-radius: 20px;
font-size: 0.65rem;
font-weight: 850;
letter-spacing: 0.05em;
display: flex;
align-items: center;
gap: 6px;
}
@media (max-width: 1200px) {
.dashboard-primary-grid, .dashboard-secondary-grid {
grid-template-columns: 1fr;
}
}
/* ==========================================================================
GLOBAL SIDEBAR (PROFESSIONAL LIGHT DESIGN)
========================================================================== */
.sidebar-premium {
background: #ffffff !important;
border-right: 1px solid #edf2f7 !important;
box-shadow: 1px 0 8px rgba(0, 0, 0, 0.03);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.sidebar-logo-text {
background: linear-gradient(135deg, #0ea5e9, #2563eb);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 800;
letter-spacing: -0.5px;
}
.sidebar-nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 20px;
margin: 2px 12px;
text-decoration: none;
color: #475569;
font-weight: 500;
font-size: 0.875rem;
border-radius: 8px;
border: none;
border-left: none;
transition: all 0.15s ease;
position: relative;
}
.sidebar-nav-item:hover {
background: #f1f5f9;
color: #1e293b;
}
.sidebar-nav-item.active {
background: var(--accent-cyan-soft);
color: var(--accent-cyan);
font-weight: 600;
}
.sidebar-nav-item.active::before {
display: none;
}
.sidebar-sub-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 20px 8px 48px;
margin: 1px 12px;
text-decoration: none;
font-size: 0.825rem;
font-weight: 500;
color: #64748b;
border-radius: 6px;
transition: all 0.15s ease;
border: none;
border-left: none;
}
.sidebar-sub-item:hover {
background: #f1f5f9;
color: #1e293b;
}
.sidebar-sub-item.active {
background: var(--accent-cyan-soft);
color: var(--accent-cyan);
font-weight: 600;
}
.sidebar-sub-item.active::before {
display: none;
}

View File

@@ -285,6 +285,54 @@ const CustomChartTooltip = ({ active, payload }: any) => {
return null;
};
// --- PREMIUM COMPONENTS ---
const PremiumStatCard: React.FC<{
label: string;
value: string | number;
subValue?: string;
icon: any;
trend?: { value: string; isUp: boolean };
glowColor: 'cyan' | 'green' | 'red' | 'amber';
pulse?: boolean;
}> = ({ label, value, subValue, icon: Icon, trend, glowColor, pulse }) => {
const colors = {
cyan: { bg: 'rgba(59, 130, 246, 0.1)', text: 'var(--accent-cyan)' },
green: { bg: 'rgba(16, 185, 129, 0.1)', text: 'var(--accent-green)' },
red: { bg: 'rgba(239, 68, 68, 0.1)', text: 'var(--alert-red)' },
amber: { bg: 'rgba(245, 158, 11, 0.1)', text: 'var(--warning-amber)' },
};
const theme = colors[glowColor];
return (
<div className="premium-stat-card">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div className="premium-stat-icon-wrap" style={{ background: theme.bg, color: theme.text }}>
<Icon size={22} />
</div>
{pulse && <div className="status-pulse" style={{ background: theme.text }}></div>}
</div>
<div className="premium-stat-label">{label}</div>
<div className="premium-stat-value">
{value}
{subValue && <span className="premium-stat-sub">/ {subValue}</span>}
</div>
{trend && (
<div style={{
marginTop: '12px',
fontSize: '0.75rem',
fontWeight: 700,
color: trend.isUp ? 'var(--accent-green)' : 'var(--alert-red)',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}>
{trend.isUp ? '↑' : '↓'} {trend.value} <span style={{ color: 'var(--text-secondary)' }}>vs last hour</span>
</div>
)}
</div>
);
};
export const Dashboard: React.FC = () => {
const [incidents, setIncidents] = useState<Incident[]>([]);
const [users, setUsers] = useState<any[]>([]);
@@ -306,7 +354,8 @@ export const Dashboard: React.FC = () => {
const token = localStorage.getItem('teleems_token') || '';
const roles = Array.isArray(user.roles) ? user.roles : [];
const isFleetOp = roles.includes('FLEET_OPERATOR');
const orgName = user.metadata?.organization?.company_name || 'Fleet Operator';
const isHospitalAdmin = roles.some((r: string) => r.toUpperCase() === 'HOSPITAL_ADMIN' || r.toUpperCase() === 'HOSPITAL ADMIN');
const orgName = user.metadata?.organization?.company_name || (isHospitalAdmin ? 'Hospital' : 'Fleet Operator');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [clickCoords, setClickCoords] = useState<{ lat: number, lng: number } | null>(null);
@@ -418,34 +467,34 @@ export const Dashboard: React.FC = () => {
})) : [];
return (
<div className="page-container" style={{ display: 'flex', flexDirection: 'column', gap: '24px', paddingBottom: '40px' }}>
<div className="page-container" style={{ display: 'flex', flexDirection: 'column', gap: '0', paddingBottom: '40px' }}>
{/* Header Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
<div className="dashboard-header-premium">
<div>
<h2 style={{ fontSize: '1.8rem', fontWeight: 800, background: 'linear-gradient(to right, var(--text-primary), var(--accent-cyan))', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>
{isFleetOp ? `${orgName} Command` : 'Super Admin Command Center'}
<h2>
{isFleetOp ? `${orgName} Command` : isHospitalAdmin ? `${orgName} Administration` : 'Super Admin Command Center'}
</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span className="status-pulse" style={{ background: 'var(--accent-green)' }}></span>
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', fontWeight: 600 }}>
Live platform telemetry synchronized at {lastUpdated.toLocaleTimeString()}
</p>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
<button onClick={fetchData} className="glass hover-glow" style={{ padding: '8px 16px', display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', background: 'rgba(59,130,246,0.02)', color: 'var(--accent-cyan)', fontWeight: 600, fontSize: '0.8rem' }}>
<RefreshCw size={14} className={isLoading ? 'spin' : ''} /> REFRESH LIVE
<button onClick={fetchData} className="glass hover-glow" style={{ padding: '10px 20px', borderRadius: '8px', display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', background: 'rgba(59,130,246,0.04)', color: 'var(--accent-cyan)', fontWeight: 700, fontSize: '0.8rem', border: '1px solid rgba(59,130,246,0.1)' }}>
<RefreshCw size={16} className={isLoading ? 'spin' : ''} /> REFRESH LIVE
</button>
<div className="glass mono" style={{ padding: '8px 16px', fontSize: '0.75rem', color: 'var(--accent-green)', display: 'flex', alignItems: 'center', gap: '6px' }}>
<UsersIcon size={14} /> {fleetOperators.length} OPERATORS ACTIVE
<div className="glass mono" style={{ padding: '10px 20px', borderRadius: '8px', fontSize: '0.8rem', color: 'var(--accent-green)', display: 'flex', alignItems: 'center', gap: '8px', fontWeight: 700, border: '1px solid rgba(16,185,129,0.1)' }}>
<UsersIcon size={16} /> {fleetOperators.length} OPERATORS ACTIVE
</div>
</div>
</div>
{/* Primary Stats Bar */}
<div className="stats-bar" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', marginBottom: 0 }}>
<StatCard
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: '20px', marginBottom: '24px' }}>
<PremiumStatCard
label="Active Incidents"
value={activeIncidents.length}
icon={Activity}
@@ -453,110 +502,73 @@ export const Dashboard: React.FC = () => {
pulse={activeIncidents.length > 0}
trend={{ value: '14%', isUp: true }}
/>
<StatCard
<PremiumStatCard
label="Operational Fleet"
value={fleetOperators.length}
subValue={users.length.toString()}
icon={Truck}
glowColor="cyan"
/>
<StatCard
<PremiumStatCard
label="Dispatch SLA"
value="1.4s"
icon={Zap}
glowColor="green"
trend={{ value: '0.2s', isUp: false }}
/>
<StatCard
<PremiumStatCard
label="Critical Cases"
value={criticalIssues.length}
icon={HeartPulse}
glowColor="amber"
/>
<StatCard
<PremiumStatCard
label="Live CCE nodes"
value={users.filter(u => u.roles?.includes('CCE')).length || 4}
icon={Video}
glowColor="cyan"
/>
<StatCard
label="Node Integrity"
value="100%"
icon={ShieldCheck}
glowColor="green"
/>
</div>
{/* Main Operational Grid */}
<div className="main-grid" style={{ gridTemplateColumns: '1.5fr 1fr 1fr', height: 'auto', alignItems: 'start' }}>
{/* Main Operational Grid: 2 Columns */}
<div className="dashboard-primary-grid">
{/* Real-time Heatmap */}
<Card title="Global Incident Explorer" subtitle="System-wide incident history and live telemetry">
<Card title="Global Incident Explorer" subtitle="System-wide incident history and live telemetry" className="premium-health-card" style={{ padding: 0 }}>
<div style={{ padding: '24px', paddingBottom: 0 }}>
<h3 style={{ fontSize: '1.1rem', fontWeight: 800 }}>Global Incident Explorer</h3>
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginBottom: '16px' }}>System-wide incident history and live telemetry</p>
</div>
<LiveIncidentMap incidents={incidents} />
{/* Legend Overlay (Absolute in Card) */}
<div style={{ position: 'absolute', bottom: '32px', right: '32px', zIndex: 1000, background: 'rgba(255,255,255,0.85)', padding: '12px', borderRadius: '10px', border: '1px solid var(--card-border)', fontSize: '0.7rem', backdropFilter: 'blur(10px)', boxShadow: '0 8px 32px rgba(0,0,0,0.05)', pointerEvents: 'none' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '8px' }}>
<div style={{ position: 'absolute', bottom: '24px', right: '24px', zIndex: 1000, background: 'rgba(255,255,255,0.9)', padding: '12px 16px', borderRadius: '12px', border: '1px solid var(--card-border)', fontSize: '0.75rem', backdropFilter: 'blur(10px)', boxShadow: '0 8px 32px rgba(0,0,0,0.08)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px' }}>
<div style={{ width: '10px', height: '10px', borderRadius: '50%', background: '#EF4444', boxShadow: '0 0 10px #EF4444' }}></div>
<span style={{ fontWeight: 700, letterSpacing: '0.05em' }}>CRITICAL</span>
<span style={{ fontWeight: 750, letterSpacing: '0.05em', color: 'var(--text-primary)' }}>CRITICAL</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px' }}>
<div style={{ width: '10px', height: '10px', borderRadius: '50%', background: '#F59E0B', boxShadow: '0 0 10px #F59E0B' }}></div>
<span style={{ fontWeight: 700, letterSpacing: '0.05em' }}>ACTIVE (L/M)</span>
<span style={{ fontWeight: 750, letterSpacing: '0.05em', color: 'var(--text-primary)' }}>ACTIVE (L/M)</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div style={{ width: '10px', height: '10px', borderRadius: '50%', background: '#10B981', boxShadow: '0 0 10px #10B981' }}></div>
<span style={{ fontWeight: 700, letterSpacing: '0.05em' }}>RESOLVED</span>
<span style={{ fontWeight: 750, letterSpacing: '0.05em', color: 'var(--text-primary)' }}>RESOLVED</span>
</div>
</div>
</Card>
{/* Governance Feed */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<Card title="Governance Feed" subtitle="Real-time dispatch audit trail" style={{ height: '450px', display: 'flex', flexDirection: 'column' }}>
<div className="no-scrollbar" style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '12px' }}>
{incidents.slice(0, 8).map((inc) => (
<div key={inc.id} className="hover-glow" style={{
padding: '14px',
background: 'rgba(0,0,0,0.01)',
borderRadius: '10px',
borderLeft: `4px solid ${inc.severity === 'CRITICAL' ? 'var(--alert-red)' : inc.severity === 'HIGH' ? 'var(--warning-amber)' : 'var(--accent-cyan)'}`,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
transition: 'all 0.2s'
}}>
<div style={{ minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span className="mono" style={{ fontWeight: 800, fontSize: '0.85rem', color: 'var(--accent-cyan)' }}>{inc.id.split('-').pop()}</span>
<span style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{inc.address}</span>
</div>
<div style={{ fontSize: '0.75rem', marginTop: '4px', textTransform: 'uppercase', fontWeight: 600 }}>{inc.status}</div>
</div>
<div style={{ textAlign: 'right', flexShrink: 0 }}>
<div style={{ fontSize: '0.75rem', fontWeight: 800 }}>{inc.eta_seconds ? `${Math.floor(inc.eta_seconds / 60)}m` : '--'}</div>
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)' }}>ETA</div>
</div>
</div>
))}
{incidents.length === 0 && <div style={{ textAlign: 'center', color: 'var(--text-secondary)', padding: '40px 0' }}>No active incidents</div>}
</div>
</Card>
</div>
{/* Global Distribution & Health */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px', minHeight: 0 }}>
<Card title="Fleet Distribution" subtitle="Real-time system asset availability">
<Card title="Fleet Distribution" subtitle="Real-time system asset availability" className="premium-health-card">
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', justifyContent: 'space-between', minHeight: 0 }}>
<div style={{ height: '180px', position: 'relative', margin: '10px 0', minWidth: 0 }}>
<ResponsiveContainer width="100%" height="180">
<div style={{ height: '220px', position: 'relative', margin: '20px 0', minWidth: 0 }}>
<ResponsiveContainer width="100%" height={220}>
<PieChart>
<Pie
data={fleetStatusData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={8}
innerRadius={70}
outerRadius={95}
paddingAngle={6}
dataKey="value"
stroke="none"
animationBegin={0}
@@ -566,7 +578,7 @@ export const Dashboard: React.FC = () => {
<Cell
key={`cell-${index}`}
fill={entry.color}
style={{ filter: `drop-shadow(0 0 8px ${entry.color}44)` }}
style={{ filter: `drop-shadow(0 0 12px ${entry.color}33)` }}
/>
))}
</Pie>
@@ -583,35 +595,71 @@ export const Dashboard: React.FC = () => {
textAlign: 'center',
pointerEvents: 'none'
}}>
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em' }}>Total</div>
<div style={{ fontSize: '1.6rem', fontWeight: 900, color: 'var(--text-primary)', lineHeight: 1 }}>
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', fontWeight: 800, textTransform: 'uppercase', letterSpacing: '0.1em' }}>Total</div>
<div style={{ fontSize: '2.5rem', fontWeight: 900, color: 'var(--text-primary)', lineHeight: 1, margin: '4px 0' }}>
{fleetStatusData.reduce((acc, curr) => acc + curr.value, 0)}
</div>
<div style={{ fontSize: '0.5rem', color: 'var(--accent-cyan)', fontWeight: 800, marginTop: '2px' }}>ASSETS</div>
<div style={{ fontSize: '0.6rem', color: 'var(--accent-cyan)', fontWeight: 850 }}>ASSETS</div>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', padding: '12px', background: 'rgba(0,0,0,0.01)', borderRadius: '12px', border: '1px solid var(--card-border)' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', padding: '16px', background: 'rgba(0,0,0,0.02)', borderRadius: '12px', border: '1px solid rgba(0,0,0,0.05)' }}>
{fleetStatusData.map(item => {
const total = fleetStatusData.reduce((acc, curr) => acc + curr.value, 0);
const percentage = total > 0 ? Math.round((item.value / total) * 100) : 0;
return (
<div key={item.name} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 8px' }}>
<div key={item.name} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '6px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '8px', height: '8px', background: item.color, borderRadius: '2px', boxShadow: `0 0 10px ${item.color}66` }}></div>
<span style={{ fontSize: '0.65rem', fontWeight: 700, color: 'var(--text-secondary)' }}>{item.name}</span>
<div style={{ width: '10px', height: '10px', background: item.color, borderRadius: '3px', boxShadow: `0 0 12px ${item.color}66` }}></div>
<span style={{ fontSize: '0.75rem', fontWeight: 750, color: 'var(--text-secondary)' }}>{item.name}</span>
</div>
<span className="mono" style={{ fontSize: '0.7rem', fontWeight: 800, color: 'var(--text-primary)' }}>{percentage}%</span>
<span className="mono" style={{ fontSize: '0.85rem', fontWeight: 850, color: 'var(--text-primary)' }}>{percentage}%</span>
</div>
);
})}
</div>
</div>
</Card>
</Card>
</div>
<Card title="System Performance" subtitle="Transaction density (30s avg)">
<div style={{ height: '140px', minWidth: 0, position: 'relative' }}>
<ResponsiveContainer width="100%" height={140}>
{/* Secondary Grid: Governance Feed and Performance */}
<div className="dashboard-secondary-grid">
{/* Governance Feed */}
<Card title="Governance Feed" subtitle="Real-time dispatch audit trail" className="premium-health-card" style={{ height: '400px', display: 'flex', flexDirection: 'column' }}>
<div className="no-scrollbar" style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '12px', marginTop: '16px' }}>
{incidents.slice(0, 8).map((inc) => (
<div key={inc.id} className="hover-glow" style={{
padding: '16px',
background: '#fff',
borderRadius: '12px',
border: '1px solid var(--card-border)',
borderLeft: `4px solid ${inc.severity === 'CRITICAL' ? 'var(--alert-red)' : inc.severity === 'HIGH' ? 'var(--warning-amber)' : 'var(--accent-cyan)'}`,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
transition: 'all 0.2s',
boxShadow: '0 2px 8px rgba(0,0,0,0.02)'
}}>
<div style={{ minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<span className="mono" style={{ fontWeight: 850, fontSize: '0.9rem', color: 'var(--text-primary)' }}>#{inc.id.split('-').pop()}</span>
<span style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{inc.address}</span>
</div>
<div style={{ fontSize: '0.75rem', marginTop: '6px', textTransform: 'uppercase', fontWeight: 750, color: 'var(--accent-cyan)' }}>{inc.status}</div>
</div>
<div style={{ textAlign: 'right', flexShrink: 0 }}>
<div style={{ fontSize: '0.9rem', fontWeight: 850, color: 'var(--text-primary)' }}>{inc.eta_seconds ? `${Math.floor(inc.eta_seconds / 60)}m` : '--'}</div>
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', fontWeight: 700 }}>ETA</div>
</div>
</div>
))}
{incidents.length === 0 && <div style={{ textAlign: 'center', color: 'var(--text-secondary)', padding: '40px 0', fontWeight: 600 }}>No active incidents</div>}
</div>
</Card>
<Card title="System Performance" subtitle="Transaction density (30s avg)" className="premium-health-card" style={{ height: '400px', display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: 1, minWidth: 0, position: 'relative', marginTop: '16px' }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={activityData}>
<defs>
<linearGradient id="colorSessions" x1="0" y1="0" x2="0" y2="1">
@@ -622,78 +670,89 @@ export const Dashboard: React.FC = () => {
<Tooltip
contentStyle={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: '8px', fontSize: '12px' }}
/>
<Area type="monotone" dataKey="count" stroke="var(--accent-cyan)" strokeWidth={2} fillOpacity={1} fill="url(#colorSessions)" />
<Area type="monotone" dataKey="count" stroke="var(--accent-cyan)" strokeWidth={3} fillOpacity={1} fill="url(#colorSessions)" />
</AreaChart>
</ResponsiveContainer>
</div>
</Card>
</div>
</Card>
</div>
{/* Platform DNA Section */}
<section style={{ display: 'grid', gridTemplateColumns: '2.5fr 1fr', gap: '24px' }}>
<Card title="Platform Architecture & Compliance" subtitle="Global oversight of system DNA, security flags and ABDM synchronization.">
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px', marginTop: '8px' }}>
<div className="glass hover-glow" style={{ padding: '20px', borderRadius: '12px', border: '1px solid var(--card-border)' }}>
<section className="dashboard-primary-grid" style={{ marginBottom: '24px' }}>
<Card title="Platform Architecture & Compliance" subtitle="Global oversight of system DNA, security flags and ABDM synchronization." className="premium-health-card" style={{ paddingBottom: '16px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '16px', marginTop: '16px' }}>
<div style={{ padding: '20px', borderRadius: '12px', background: '#0B1120', border: '1px solid #1E293B', color: '#fff', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, transparent, var(--accent-cyan), transparent)' }}></div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Database size={22} color="var(--accent-cyan)" />
<span className="mono" style={{ fontSize: '0.6rem', color: 'var(--accent-green)' }}>SYNCED</span>
<Database size={20} color="var(--accent-cyan)" />
<span className="health-status-badge" style={{ background: 'rgba(16,185,129,0.15)', color: 'var(--accent-green)' }}>
<span className="status-pulse" style={{ background: 'var(--accent-green)', width: '6px', height: '6px' }}></span> SYNCED
</span>
</div>
<div style={{ fontSize: '0.9rem', fontWeight: 800, marginTop: '16px' }}>Master Data</div>
<p style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', marginTop: '6px' }}>482 Triage rules active.</p>
<div style={{ height: '2px', background: 'rgba(0,0,0,0.02)', margin: '12px 0' }}></div>
<button className="mono" style={{ width: '100%', padding: '8px', background: 'rgba(59,130,246,0.1)', border: 'none', color: 'var(--accent-cyan)', fontSize: '0.6rem', borderRadius: '4px', cursor: 'pointer', fontWeight: 700 }}>MANAGE DNA</button>
<div style={{ fontSize: '1rem', fontWeight: 850, marginTop: '16px', letterSpacing: '-0.02em' }}>Master Data</div>
<p style={{ fontSize: '0.75rem', color: '#94A3B8', marginTop: '4px' }}>482 Triage rules active.</p>
<div style={{ height: '1px', background: '#1E293B', margin: '16px 0' }}></div>
<button className="mono" style={{ width: '100%', padding: '10px', background: 'rgba(59,130,246,0.1)', border: '1px solid rgba(59,130,246,0.2)', color: '#60A5FA', fontSize: '0.7rem', borderRadius: '6px', cursor: 'pointer', fontWeight: 750, transition: 'all 0.2s' }}>MANAGE DNA</button>
</div>
<div className="glass hover-glow" style={{ padding: '20px', borderRadius: '12px', border: '1px solid var(--card-border)' }}>
<div style={{ padding: '20px', borderRadius: '12px', background: '#0B1120', border: '1px solid #1E293B', color: '#fff', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, transparent, var(--accent-green), transparent)' }}></div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<ShieldCheck size={22} color="var(--accent-green)" />
<span className="mono" style={{ fontSize: '0.6rem', color: 'var(--accent-green)' }}>100%</span>
<ShieldCheck size={20} color="var(--accent-green)" />
<span className="health-status-badge" style={{ background: 'rgba(16,185,129,0.15)', color: 'var(--accent-green)' }}>
<span className="status-pulse" style={{ background: 'var(--accent-green)', width: '6px', height: '6px' }}></span> 100%
</span>
</div>
<div style={{ fontSize: '0.9rem', fontWeight: 800, marginTop: '16px' }}>Compliance</div>
<p style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', marginTop: '6px' }}>HIPAA / ABDM verified.</p>
<div style={{ height: '2px', background: 'rgba(0,0,0,0.02)', margin: '12px 0' }}></div>
<button className="mono" style={{ width: '100%', padding: '8px', background: 'rgba(16,185,129,0.1)', border: 'none', color: 'var(--accent-green)', fontSize: '0.6rem', borderRadius: '4px', cursor: 'pointer', fontWeight: 700 }}>AUDIT LOGS</button>
<div style={{ fontSize: '1rem', fontWeight: 850, marginTop: '16px', letterSpacing: '-0.02em' }}>Compliance</div>
<p style={{ fontSize: '0.75rem', color: '#94A3B8', marginTop: '4px' }}>HIPAA / ABDM verified.</p>
<div style={{ height: '1px', background: '#1E293B', margin: '16px 0' }}></div>
<button className="mono" style={{ width: '100%', padding: '10px', background: 'rgba(16,185,129,0.1)', border: '1px solid rgba(16,185,129,0.2)', color: '#34D399', fontSize: '0.7rem', borderRadius: '6px', cursor: 'pointer', fontWeight: 750, transition: 'all 0.2s' }}>AUDIT LOGS</button>
</div>
<div className="glass hover-glow" style={{ padding: '20px', borderRadius: '12px', border: '1px solid var(--card-border)' }}>
<div style={{ padding: '20px', borderRadius: '12px', background: '#0B1120', border: '1px solid #1E293B', color: '#fff', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, transparent, var(--warning-amber), transparent)' }}></div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Settings size={22} color="var(--warning-amber)" />
<span className="mono" style={{ fontSize: '0.6rem', color: 'var(--warning-amber)' }}>STABLE</span>
<Settings size={20} color="var(--warning-amber)" />
<span className="health-status-badge" style={{ background: 'rgba(245,158,11,0.15)', color: 'var(--warning-amber)' }}>
<span className="status-pulse" style={{ background: 'var(--warning-amber)', width: '6px', height: '6px' }}></span> STABLE
</span>
</div>
<div style={{ fontSize: '0.9rem', fontWeight: 800, marginTop: '16px' }}>System Logic</div>
<p style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', marginTop: '6px' }}>SLA thresholds active.</p>
<div style={{ height: '2px', background: 'rgba(0,0,0,0.02)', margin: '12px 0' }}></div>
<button className="mono" style={{ width: '100%', padding: '8px', background: 'rgba(245,158,0,0.1)', border: 'none', color: 'var(--warning-amber)', fontSize: '0.6rem', borderRadius: '4px', cursor: 'pointer', fontWeight: 700 }}>CONFIGURE</button>
<div style={{ fontSize: '1rem', fontWeight: 850, marginTop: '16px', letterSpacing: '-0.02em' }}>System Logic</div>
<p style={{ fontSize: '0.75rem', color: '#94A3B8', marginTop: '4px' }}>SLA thresholds active.</p>
<div style={{ height: '1px', background: '#1E293B', margin: '16px 0' }}></div>
<button className="mono" style={{ width: '100%', padding: '10px', background: 'rgba(245,158,11,0.1)', border: '1px solid rgba(245,158,11,0.2)', color: '#FBBF24', fontSize: '0.7rem', borderRadius: '6px', cursor: 'pointer', fontWeight: 750, transition: 'all 0.2s' }}>CONFIGURE</button>
</div>
<div className="glass hover-glow" style={{ padding: '20px', borderRadius: '12px', border: '1px solid var(--card-border)' }}>
<div style={{ padding: '20px', borderRadius: '12px', background: '#0B1120', border: '1px solid #1E293B', color: '#fff', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, transparent, var(--accent-cyan), transparent)' }}></div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Navigation size={22} color="var(--accent-cyan)" />
<span className="mono" style={{ fontSize: '0.6rem', color: 'var(--accent-cyan)' }}>ACTIVE</span>
<Navigation size={20} color="var(--accent-cyan)" />
<span className="health-status-badge" style={{ background: 'rgba(59,130,246,0.15)', color: '#60A5FA' }}>
<span className="status-pulse" style={{ background: '#60A5FA', width: '6px', height: '6px' }}></span> ACTIVE
</span>
</div>
<div style={{ fontSize: '0.9rem', fontWeight: 800, marginTop: '16px' }}>Network Hub</div>
<p style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', marginTop: '6px' }}>Multi-zone sync active.</p>
<div style={{ height: '2px', background: 'rgba(0,0,0,0.02)', margin: '12px 0' }}></div>
<button className="mono" style={{ width: '100%', padding: '8px', background: 'rgba(59,130,246,0.1)', border: 'none', color: 'var(--accent-cyan)', fontSize: '0.6rem', borderRadius: '4px', cursor: 'pointer', fontWeight: 700 }}>NODES MAP</button>
<div style={{ fontSize: '1rem', fontWeight: 850, marginTop: '16px', letterSpacing: '-0.02em' }}>Network Hub</div>
<p style={{ fontSize: '0.75rem', color: '#94A3B8', marginTop: '4px' }}>Multi-zone sync active.</p>
<div style={{ height: '1px', background: '#1E293B', margin: '16px 0' }}></div>
<button className="mono" style={{ width: '100%', padding: '10px', background: 'rgba(59,130,246,0.1)', border: '1px solid rgba(59,130,246,0.2)', color: '#60A5FA', fontSize: '0.7rem', borderRadius: '6px', cursor: 'pointer', fontWeight: 750, transition: 'all 0.2s' }}>NODES MAP</button>
</div>
</div>
</Card>
<Card title="Critical Task Cluster" style={{ background: 'rgba(255, 59, 59, 0.03)' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '14px' }}>
<Card title="Critical Task Cluster" className="premium-health-card" style={{ background: 'linear-gradient(to bottom, #fff, hsla(0, 80%, 98%, 0.5))', border: '1px solid rgba(239,68,68,0.2)' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', marginTop: '16px' }}>
{[
{ label: 'Blood Link', status: 'Healthy', color: 'var(--accent-green)' },
{ label: 'Organ Registry', status: 'Healthy', color: 'var(--accent-green)' },
{ label: 'Mortuary Sync', status: 'Delayed', color: 'var(--warning-amber)' },
{ label: 'Police V-Link', status: 'Healthy', color: 'var(--accent-green)' },
].map((r, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingBottom: '8px', borderBottom: '1px solid rgba(0,0,0,0.02)' }}>
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingBottom: '12px', borderBottom: '1px dashed var(--card-border)' }}>
<div>
<div style={{ fontSize: '0.8rem', fontWeight: 700 }}>{r.label}</div>
<div style={{ fontSize: '0.6rem', color: r.color }}>{r.status}</div>
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: 'var(--text-primary)' }}>{r.label}</div>
<div style={{ fontSize: '0.7rem', color: r.color, fontWeight: 700, marginTop: '2px' }}>{r.status}</div>
</div>
<div className="status-pulse" style={{ background: r.color }}></div>
<div className="status-pulse" style={{ background: r.color, width: '10px', height: '10px' }}></div>
</div>
))}
</div>
@@ -702,8 +761,8 @@ export const Dashboard: React.FC = () => {
{/* SLA Ticker & Progress */}
<div style={{ display: 'flex', gap: '24px', alignItems: 'stretch', flexWrap: 'wrap' }}>
<Card style={{ flex: 1, padding: '16px 24px' }}>
<div style={{ display: 'grid', gap: '16px', gridTemplateColumns: 'repeat(auto-fit, minmax(130px, 1fr))' }}>
<Card className="premium-health-card" style={{ flex: 1, padding: '24px' }}>
<div style={{ display: 'grid', gap: '24px', gridTemplateColumns: 'repeat(auto-fit, minmax(130px, 1fr))' }}>
{[
{ label: 'Foundation', progress: 100 },
{ label: 'MVP Core', progress: 100 },
@@ -712,11 +771,11 @@ export const Dashboard: React.FC = () => {
{ label: 'Compliance', progress: 100 },
].map((phase, i) => (
<div key={phase.label} style={{ minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.65rem', marginBottom: '6px' }}>
<span style={{ fontWeight: 600 }}>{phase.label}</span>
<span className="mono">{phase.progress}%</span>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.75rem', marginBottom: '8px' }}>
<span style={{ fontWeight: 750, color: 'var(--text-primary)' }}>{phase.label}</span>
<span className="mono" style={{ fontWeight: 850 }}>{phase.progress}%</span>
</div>
<div style={{ height: '4px', background: 'rgba(0,0,0,0.02)', borderRadius: '2px', overflow: 'hidden' }}>
<div style={{ height: '6px', background: 'rgba(0,0,0,0.05)', borderRadius: '3px', overflow: 'hidden' }}>
<motion.div
initial={{ width: 0 }}
animate={{ width: `${phase.progress}%` }}
@@ -724,7 +783,7 @@ export const Dashboard: React.FC = () => {
style={{
height: '100%',
background: phase.progress === 100 ? 'var(--accent-green)' : 'var(--accent-cyan)',
boxShadow: `0 0 10px ${phase.progress === 100 ? 'rgba(0,255,136,0.3)' : 'rgba(0,212,255,0.3)'}`
boxShadow: `0 0 10px ${phase.progress === 100 ? 'rgba(0,255,136,0.5)' : 'rgba(0,212,255,0.5)'}`
}}
></motion.div>
</div>
@@ -733,26 +792,35 @@ export const Dashboard: React.FC = () => {
</div>
</Card>
<div className="glass" style={{
<div style={{
minWidth: '300px',
flex: '1 1 320px',
padding: '16px',
border: '1px solid var(--card-border)',
padding: '16px 24px',
background: '#0B1120',
border: '1px solid #1E293B',
borderRadius: '16px',
display: 'flex',
alignItems: 'center',
gap: '12px',
gap: '16px',
overflow: 'hidden',
whiteSpace: 'nowrap'
whiteSpace: 'nowrap',
boxShadow: '0 8px 32px rgba(0,0,0,0.1)'
}}>
<div className="mono" style={{ fontSize: '0.7rem', fontWeight: 800, color: 'var(--accent-cyan)', padding: '4px 8px', background: 'rgba(59,130,246,0.1)', borderRadius: '4px' }}>SLA TICKER</div>
<div className="no-scrollbar" style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', overflow: 'hidden', flex: 1, position: 'relative' }}>
<div className="mono" style={{ fontSize: '0.75rem', fontWeight: 850, color: '#38BDF8', padding: '6px 12px', background: 'rgba(56, 189, 248, 0.15)', borderRadius: '6px', border: '1px solid rgba(56, 189, 248, 0.3)' }}>SLA TICKER</div>
<div className="no-scrollbar" style={{ fontSize: '0.85rem', color: '#94A3B8', overflow: 'hidden', flex: 1, position: 'relative' }}>
<motion.div
animate={{ x: [400, -800] }}
transition={{ duration: 25, repeat: Infinity, ease: "linear" }}
style={{ display: 'inline-block', whiteSpace: 'nowrap', fontWeight: 700 }}
animate={{ x: [500, -1000] }}
transition={{ duration: 30, repeat: Infinity, ease: "linear" }}
style={{ display: 'inline-block', whiteSpace: 'nowrap', fontWeight: 750, letterSpacing: '0.02em' }}
>
HIPAA COMPLIANT | ABDM SYNCED | ISO 27001 AUDIT PASSED | PHI ENCRYPTED | DPDP ACT ALIGNED | END-TO-END TLS 1.3 ACTIVE
<span style={{ color: '#fff' }}>HIPAA COMPLIANT</span> <span style={{ color: '#34D399' }}></span> &nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
<span style={{ color: '#fff' }}>ABDM SYNCED</span> <span style={{ color: '#34D399' }}></span> &nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
<span style={{ color: '#fff' }}>ISO 27001 AUDIT PASSED</span> <span style={{ color: '#34D399' }}></span> &nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
<span style={{ color: '#fff' }}>PHI ENCRYPTED</span> <span style={{ color: '#34D399' }}></span> &nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
<span style={{ color: '#fff' }}>DPDP ACT ALIGNED</span> <span style={{ color: '#34D399' }}></span> &nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
<span style={{ color: '#fff' }}>END-TO-END TLS 1.3 ACTIVE</span> <span style={{ color: '#34D399' }}></span>
</motion.div>
</div>
</div>
</div>

View File

@@ -469,7 +469,7 @@ const LocationPickerMap: React.FC<{
setIsLocating(false);
if (accuracy > 100) {
notify('GPS Warning', 'Location accuracy is low. Please manually adjust the pin.', 'info');
console.log('GPS Warning', 'Location accuracy is low. Please manually adjust the pin.', 'info');
}
},
(error) => {
@@ -478,7 +478,7 @@ const LocationPickerMap: React.FC<{
if (error.code === 1) msg = 'Location permission denied.';
else if (error.code === 3) msg = 'Location request timed out.';
notify('GPS Error', msg, 'error');
console.log('GPS Error', msg, 'error');
setIsLocating(false);
},
{
@@ -1931,7 +1931,7 @@ const BrandRegistrationForm: React.FC<{ onSubmit: (data: any) => void; loading?:
});
const [showPassword, setShowPassword] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
@@ -2079,7 +2079,7 @@ const StationRegistrationForm: React.FC<{
phone: ''
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
@@ -2440,7 +2440,7 @@ export const FleetDispatch: React.FC = () => {
const roles = u.roles?.map((r: any) => String(r).trim().toUpperCase()) || [];
const isOp = roles.includes('FLEET_OPERATOR') ||
roles.includes('FLEET OPERATOR') ||
roles.some(r => r.includes('FLEET') && r.includes('OPERATOR'));
roles.some((r: string) => r.includes('FLEET') && r.includes('OPERATOR'));
if (!isOp) return false;
@@ -2559,13 +2559,13 @@ export const FleetDispatch: React.FC = () => {
}
console.log('Registration Success:', result);
notify('Brand Registered', `Organisation "${data.metadata.organization.company_name}" has been onboarded.`, 'success');
console.log('Brand Registered', `Organisation "${data.metadata.organization.company_name}" has been onboarded.`, 'success');
setRefreshKey(prev => prev + 1);
setIsModalOpen(false);
} catch (error: any) {
console.error('Registration failed:', error);
const isExpired = error.message.includes('Token has expired');
notify('Registration Failed', error.message, 'error');
console.log('Registration Failed', error.message, 'error');
if (isExpired) {
localStorage.removeItem('teleems_auth');
@@ -2596,13 +2596,13 @@ export const FleetDispatch: React.FC = () => {
}
console.log('Dispatch: Station Created Successfully:', stationId);
notify('Station Created', `Station "${data.name}" is now active in the system.`, 'success');
console.log('Station Created', `Station "${data.name}" is now active in the system.`, 'success');
setRefreshKey(prev => prev + 1); // Trigger re-fetch of fleet owners/stations
setIsModalOpen(false);
} catch (error: any) {
console.error('Dispatch: Station Creation Failed:', error);
notify('Station Error', error.message, 'error');
console.log('Station Error', error.message, 'error');
} finally {
setIsSubmitting(false);
}
@@ -2628,13 +2628,13 @@ export const FleetDispatch: React.FC = () => {
throw new Error(result.message || `Failed to ${data.id ? 'update' : 'create'} vehicle.`);
}
notify('Vehicle Updated', `Vehicle "${data.registration_number}" telemetry is now synced.`, 'success');
console.log('Vehicle Updated', `Vehicle "${data.registration_number}" telemetry is now synced.`, 'success');
setRefreshKey(prev => prev + 1);
setIsModalOpen(false);
setEditingVehicle(null);
} catch (error: any) {
console.error(`Dispatch: Vehicle ${data.id ? 'Update' : 'Creation'} Failed:`, error);
notify('Vehicle Error', error.message, 'error');
console.log('Vehicle Error', error.message, 'error');
} finally {
setIsSubmitting(false);
}
@@ -2658,12 +2658,12 @@ export const FleetDispatch: React.FC = () => {
}
console.log('Dispatch: Staff Registered Successfully');
notify('Staff Registered', `Personnel file for "${data.name}" has been created.`, 'success');
console.log('Staff Registered', `Personnel file for "${data.name}" has been created.`, 'success');
setRefreshKey(prev => prev + 1);
setIsModalOpen(false);
} catch (error: any) {
console.error('Dispatch: Staff Registration Failed:', error);
notify('Staff Error', error.message, 'error');
console.log('Staff Error', error.message, 'error');
} finally {
setIsSubmitting(false);
}
@@ -2682,11 +2682,11 @@ export const FleetDispatch: React.FC = () => {
throw new Error(result.message || 'Failed to create roster.');
}
notify('Roster Created', 'Crew assignment has been finalized.', 'success');
console.log('Roster Created', 'Crew assignment has been finalized.', 'success');
setIsModalOpen(false);
} catch (error: any) {
console.error('Dispatch: Roster Creation Failed:', error);
notify('Roster Error', error.message, 'error');
console.log('Roster Error', error.message, 'error');
} finally {
setIsSubmitting(false);
}
@@ -2705,12 +2705,12 @@ export const FleetDispatch: React.FC = () => {
throw new Error(result.message || 'Failed to start shift.');
}
notify('Shift Started', 'Vehicle is now LIVE and tracking telemetry.', 'success');
console.log('Shift Started', 'Vehicle is now LIVE and tracking telemetry.', 'success');
setIsModalOpen(false);
setRefreshKey(prev => prev + 1);
} catch (error: any) {
console.error('Dispatch: Shift Start Failed:', error);
notify('Shift Error', error.message, 'error');
console.log('Shift Error', error.message, 'error');
} finally {
setIsSubmitting(false);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

634
src/pages/HospitalLogin.css Normal file
View File

@@ -0,0 +1,634 @@
/* ═══════════════════════════════════════════════════════════════════════════════
HOSPITAL LOGIN — Premium Medical Theme
═══════════════════════════════════════════════════════════════════════════════ */
/* ─── PAGE SHELL ─────────────────────────────────────────────────────────────── */
.hosp-login-page {
min-height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(145deg, #F0FDFA 0%, #F8FAFC 40%, #ECFDF5 100%);
position: relative;
overflow: hidden;
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
-webkit-font-smoothing: antialiased;
}
/* ─── BACKGROUND EFFECTS ─────────────────────────────────────────────────────── */
.hosp-bg-grid {
position: absolute;
inset: 0;
background-image:
radial-gradient(ellipse at 30% 20%, rgba(16, 185, 129, 0.06) 0%, transparent 55%),
radial-gradient(ellipse at 70% 80%, rgba(13, 148, 136, 0.04) 0%, transparent 55%),
linear-gradient(rgba(16, 185, 129, 0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(16, 185, 129, 0.02) 1px, transparent 1px);
background-size: 100% 100%, 100% 100%, 40px 40px, 40px 40px;
pointer-events: none;
}
.hosp-bg-orb {
position: absolute;
border-radius: 50%;
filter: blur(100px);
pointer-events: none;
z-index: 0;
}
.orb-1 {
width: 600px;
height: 600px;
top: -200px;
left: -100px;
background: radial-gradient(circle, rgba(16, 185, 129, 0.12) 0%, transparent 70%);
animation: orb-float 18s ease-in-out infinite alternate;
}
.orb-2 {
width: 500px;
height: 500px;
bottom: -150px;
right: -100px;
background: radial-gradient(circle, rgba(13, 148, 136, 0.1) 0%, transparent 70%);
animation: orb-float 22s ease-in-out infinite alternate-reverse;
}
.orb-3 {
width: 350px;
height: 350px;
top: 50%;
left: 60%;
background: radial-gradient(circle, rgba(59, 130, 246, 0.06) 0%, transparent 70%);
animation: orb-float 15s ease-in-out infinite alternate;
}
@keyframes orb-float {
0% { transform: translate(0, 0) scale(1); }
50% { transform: translate(60px, -40px) scale(1.1); }
100% { transform: translate(-30px, 30px) scale(0.95); }
}
.hosp-scan-line {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(16, 185, 129, 0.4), transparent);
animation: hosp-scan 4s linear infinite;
z-index: 1;
pointer-events: none;
}
@keyframes hosp-scan {
0% { top: -2px; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { top: 100vh; opacity: 0; }
}
/* ─── FLOATING MEDICAL ICONS ─────────────────────────────────────────────────── */
.hosp-floating-icons {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 1;
}
.float-icon {
position: absolute;
color: rgba(16, 185, 129, 0.12);
}
.fi-1 { top: 15%; left: 12%; }
.fi-2 { top: 60%; right: 15%; }
.fi-3 { bottom: 20%; left: 20%; }
/* ─── LOGIN CARD ─────────────────────────────────────────────────────────────── */
.hosp-login-card {
width: 460px;
max-width: calc(100vw - 32px);
padding: 48px 44px 40px;
z-index: 2;
position: relative;
display: flex;
flex-direction: column;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(40px) saturate(1.6);
border: 1px solid rgba(16, 185, 129, 0.1);
border-radius: 28px;
box-shadow:
0 0 0 1px rgba(16, 185, 129, 0.04),
0 30px 80px rgba(0, 0, 0, 0.07),
0 0 60px rgba(16, 185, 129, 0.04),
0 2px 6px rgba(0, 0, 0, 0.03);
transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.hosp-login-card.success-glow {
border-color: rgba(16, 185, 129, 0.4);
box-shadow:
0 0 0 2px rgba(16, 185, 129, 0.15),
0 30px 80px rgba(16, 185, 129, 0.1),
0 0 100px rgba(16, 185, 129, 0.08);
}
.hosp-card-accent {
position: absolute;
top: 0;
left: 15%;
right: 15%;
height: 3px;
background: linear-gradient(90deg, transparent, #10b981, #0d9488, transparent);
border-radius: 0 0 8px 8px;
opacity: 0.8;
}
.hosp-card-accent::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 60%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.8), transparent);
animation: accent-shimmer 3s infinite;
}
@keyframes accent-shimmer {
0% { left: -60%; }
100% { left: 160%; }
}
/* ─── HEADER ─────────────────────────────────────────────────────────────────── */
.hosp-login-header {
text-align: center;
margin-bottom: 32px;
}
.hosp-logo-container {
position: relative;
width: 68px;
height: 68px;
margin: 0 auto 20px;
display: flex;
align-items: center;
justify-content: center;
}
.hosp-logo-inner {
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba(16, 185, 129, 0.12), rgba(13, 148, 136, 0.08));
border: 1.5px solid rgba(16, 185, 129, 0.35);
border-radius: 18px;
color: #0d9488;
position: relative;
z-index: 2;
box-shadow: 0 0 30px rgba(16, 185, 129, 0.15);
}
.hosp-logo-ring {
position: absolute;
inset: -4px;
border: 1.5px solid rgba(16, 185, 129, 0.15);
border-radius: 22px;
animation: ring-pulse 3s ease-in-out infinite;
}
@keyframes ring-pulse {
0%, 100% { opacity: 0.5; transform: scale(1); }
50% { opacity: 0.15; transform: scale(1.1); }
}
.hosp-logo-pulse {
position: absolute;
inset: -8px;
border: 1px solid rgba(16, 185, 129, 0.08);
border-radius: 26px;
animation: ring-pulse 3s ease-in-out infinite 0.5s;
}
.hosp-title {
font-size: 28px;
font-weight: 800;
color: #0f172a;
margin: 0 0 8px;
letter-spacing: -0.7px;
}
.hosp-subtitle {
color: #64748b;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 600;
margin: 0;
}
/* ─── FORM ───────────────────────────────────────────────────────────────────── */
.hosp-form {
display: flex;
flex-direction: column;
gap: 0;
}
.hosp-input-group {
margin-bottom: 20px;
}
.hosp-label {
display: block;
font-size: 11px;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 1.2px;
margin-bottom: 8px;
}
.hosp-input-wrap {
position: relative;
display: flex;
align-items: center;
}
.hosp-input-icon {
position: absolute;
left: 16px;
color: #94a3b8;
transition: color 0.3s;
pointer-events: none;
z-index: 2;
}
.hosp-input {
width: 100%;
background: rgba(0, 0, 0, 0.02);
border: 1.5px solid rgba(0, 0, 0, 0.06);
border-radius: 14px;
padding: 14px 16px 14px 48px;
color: #0f172a;
font-family: 'Outfit', 'Inter', sans-serif;
font-size: 14px;
font-weight: 500;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
outline: none;
position: relative;
}
.hosp-input.mono {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
letter-spacing: 4px;
text-align: center;
font-size: 20px;
padding-left: 48px;
}
.hosp-input::placeholder {
color: rgba(0, 0, 0, 0.25);
font-size: 13px;
letter-spacing: 0.5px;
}
.hosp-input:focus {
background: rgba(16, 185, 129, 0.03);
border-color: rgba(16, 185, 129, 0.45);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.08), 0 0 20px rgba(16, 185, 129, 0.05);
}
.hosp-input:focus ~ .hosp-input-icon,
.hosp-input-wrap:focus-within .hosp-input-icon {
color: #10b981;
}
.hosp-input-focus-line {
position: absolute;
bottom: 0;
left: 50%;
right: 50%;
height: 2px;
background: linear-gradient(90deg, #10b981, #0d9488);
border-radius: 0 0 14px 14px;
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
opacity: 0;
}
.hosp-input:focus ~ .hosp-input-focus-line {
left: 0;
right: 0;
opacity: 1;
}
.hosp-eye-btn {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #94a3b8;
cursor: pointer;
padding: 4px;
display: flex;
z-index: 2;
transition: color 0.2s;
}
.hosp-eye-btn:hover { color: #10b981; }
/* ─── EXTRAS ─────────────────────────────────────────────────────────────────── */
.hosp-extras {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
font-size: 12.5px;
}
.hosp-remember {
display: flex;
align-items: center;
gap: 10px;
color: #64748b;
cursor: pointer;
user-select: none;
font-weight: 500;
position: relative;
}
.hosp-remember input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 16px;
height: 16px;
border: 1.5px solid rgba(0, 0, 0, 0.12);
border-radius: 5px;
background: rgba(0, 0, 0, 0.02);
cursor: pointer;
transition: all 0.25s;
flex-shrink: 0;
}
.hosp-remember input[type="checkbox"]:checked {
background: rgba(16, 185, 129, 0.15);
border-color: rgba(16, 185, 129, 0.5);
}
.hosp-remember input[type="checkbox"]:checked::after {
content: '';
display: block;
width: 100%;
height: 100%;
background: #10b981;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
.hosp-forgot {
color: #0d9488;
text-decoration: none;
font-weight: 600;
font-size: 12.5px;
transition: color 0.2s;
}
.hosp-forgot:hover { color: #10b981; }
/* ─── SUBMIT BUTTON ──────────────────────────────────────────────────────────── */
.hosp-submit-btn {
width: 100%;
height: 54px;
background: linear-gradient(135deg, #10b981, #0d9488);
border: none;
border-radius: 14px;
color: #fff;
font-weight: 800;
font-size: 14px;
letter-spacing: 1.2px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow:
0 8px 30px rgba(16, 185, 129, 0.25),
0 0 0 1px rgba(16, 185, 129, 0.2);
font-family: 'Outfit', sans-serif;
text-transform: uppercase;
position: relative;
overflow: hidden;
}
.hosp-submit-btn::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, transparent 40%, rgba(255,255,255,0.15) 50%, transparent 60%);
transform: translateX(-100%);
transition: transform 0.6s;
}
.hosp-submit-btn:hover:not(:disabled)::before {
transform: translateX(100%);
}
.hosp-submit-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow:
0 14px 40px rgba(16, 185, 129, 0.35),
0 0 0 1px rgba(16, 185, 129, 0.3);
}
.hosp-submit-btn:active:not(:disabled) {
transform: translateY(0);
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.2);
}
.hosp-submit-btn:disabled {
opacity: 0.8;
cursor: not-allowed;
}
.hosp-loader {
display: flex;
align-items: center;
gap: 10px;
}
.hosp-success-check {
display: flex;
align-items: center;
gap: 10px;
}
/* ─── MFA ────────────────────────────────────────────────────────────────────── */
.hosp-mfa-hint {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 12px;
color: #64748b;
margin: 0 0 20px;
text-align: center;
}
.hosp-back-btn {
background: transparent;
border: none;
color: #64748b;
font-size: 13px;
font-weight: 600;
cursor: pointer;
margin-top: 12px;
transition: color 0.2s;
font-family: 'Outfit', sans-serif;
}
.hosp-back-btn:hover { color: #0d9488; }
/* ─── ERROR BADGE ────────────────────────────────────────────────────────────── */
.hosp-error-badge {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
color: #EF4444;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.3px;
background: rgba(239, 68, 68, 0.06);
border: 1px solid rgba(239, 68, 68, 0.15);
border-radius: 12px;
overflow: hidden;
}
/* ─── SECURITY BADGE ─────────────────────────────────────────────────────────── */
.hosp-security-badge {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
margin-top: 20px;
color: #10b981;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.8px;
text-transform: uppercase;
background: rgba(16, 185, 129, 0.06);
border: 1px solid rgba(16, 185, 129, 0.12);
border-radius: 12px;
}
/* ─── FOOTER ─────────────────────────────────────────────────────────────────── */
.hosp-footer {
text-align: center;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid rgba(0, 0, 0, 0.04);
}
.hosp-portal-link {
color: #64748b;
text-decoration: none;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.5px;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
text-transform: uppercase;
}
.hosp-portal-link:hover {
color: #0d9488;
}
/* ─── STATUS BAR ─────────────────────────────────────────────────────────────── */
.hosp-status-bar {
position: absolute;
bottom: 28px;
right: 32px;
display: flex;
align-items: center;
gap: 12px;
z-index: 2;
pointer-events: none;
opacity: 0.3;
}
.hosp-status-item {
display: flex;
align-items: center;
gap: 6px;
color: #10b981;
font-size: 10px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
font-family: 'JetBrains Mono', monospace;
}
.hosp-status-dot {
width: 4px;
height: 4px;
background: #10b981;
border-radius: 50%;
animation: dot-blink 2s ease-in-out infinite;
}
@keyframes dot-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
/* ─── RESPONSIVE ─────────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
.hosp-login-card {
padding: 40px 28px 32px;
border-radius: 24px;
}
.hosp-title { font-size: 24px; }
.hosp-subtitle { font-size: 11px; letter-spacing: 1.5px; }
.float-icon { display: none; }
}
@media (max-width: 480px) {
.hosp-login-card {
padding: 32px 20px 28px;
border-radius: 20px;
}
.hosp-title { font-size: 22px; }
.hosp-subtitle { font-size: 10px; }
.hosp-status-bar { display: none; }
.hosp-submit-btn {
height: 50px;
font-size: 13px;
}
.hosp-extras {
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
}
@media (max-width: 360px) {
.hosp-login-card {
padding: 28px 16px 24px;
}
.hosp-logo-container { width: 56px; height: 56px; }
.hosp-logo-inner { width: 46px; height: 46px; border-radius: 14px; }
}

471
src/pages/HospitalLogin.tsx Normal file
View File

@@ -0,0 +1,471 @@
import React, { useState } from 'react';
import { useNavigate, NavLink } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import {
ShieldCheck,
Lock,
User,
ArrowRight,
Cpu,
Activity,
KeyRound,
ShieldAlert,
Eye,
EyeOff,
Building,
HeartPulse,
Stethoscope,
Monitor,
Wifi,
CheckCircle2,
AlertCircle
} from 'lucide-react';
import { hospitalApi } from '../api/hospital';
import './HospitalLogin.css';
export const HospitalLogin: React.FC = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [mfaCode, setMfaCode] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [showError, setShowError] = useState('');
const [mfaSessionToken, setMfaSessionToken] = useState('');
const [tempUser, setTempUser] = useState<any>(null);
const [loginStep, setLoginStep] = useState<'login' | 'mfa'>('login');
const [showPassword, setShowPassword] = useState(false);
const [loginSuccess, setLoginSuccess] = useState(false);
const navigate = useNavigate();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setShowError('');
try {
const response = await hospitalApi.login(username, password);
if (response.status === 201 || response.status === 200) {
if (response.data?.mfa_required) {
setMfaSessionToken(response.data.mfa_session_token || '');
setTempUser(response.data.user || null);
setLoginStep('mfa');
} else {
// Verify hospital role
const user = response.data?.user;
const roles = Array.isArray(user?.roles)
? user.roles.map((r: string) => r.toUpperCase())
: [];
const hasHospitalAccess = roles.some((r: string) =>
['HOSPITAL_ADMIN', 'HOSPITAL ADMIN', 'ED_DOCTOR', 'COORDINATOR', 'EMT', 'CURESELECT_ADMIN', 'ADMIN'].includes(r)
);
if (!hasHospitalAccess) {
setShowError('This account does not have hospital access privileges');
setIsLoading(false);
return;
}
// Success animation then redirect
setLoginSuccess(true);
localStorage.setItem('teleems_auth', 'true');
localStorage.setItem('teleems_token', response.data.access_token || '');
localStorage.setItem('teleems_user', JSON.stringify(user || {}));
setTimeout(() => navigate('/'), 800);
}
} else {
let apiError = 'Invalid credentials. Please try again.';
const resAny = response as any;
if (resAny.error && typeof resAny.error === 'object') {
if (Array.isArray(resAny.error.details) && resAny.error.details.length > 0) {
apiError = resAny.error.details.join('; ');
} else {
apiError = resAny.error.message || apiError;
}
} else if (typeof resAny.error === 'string') {
apiError = resAny.error;
} else if (resAny.message) {
apiError = resAny.message;
} else if (resAny.detail) {
apiError = resAny.detail;
} else if (resAny.data) {
apiError = resAny.data.message || resAny.data.detail || resAny.data.error || apiError;
}
setShowError(apiError);
}
} catch (err) {
setShowError('Unable to reach hospital authentication server');
} finally {
if (!loginSuccess) setIsLoading(false);
}
};
const handleMfaVerify = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setShowError('');
try {
const response = await hospitalApi.verifyMfa(mfaSessionToken, mfaCode);
if (response.status === 201 || response.status === 200) {
const userToStore = response.data?.user || tempUser || {};
userToStore.mfa_enabled = true;
setLoginSuccess(true);
localStorage.setItem('teleems_auth', 'true');
localStorage.setItem('teleems_token', response.data.access_token || '');
localStorage.setItem('teleems_user', JSON.stringify(userToStore));
setTimeout(() => navigate('/'), 800);
} else {
let apiError = 'Invalid verification code';
const resAny = response as any;
if (resAny.error && typeof resAny.error === 'object') {
if (Array.isArray(resAny.error.details) && resAny.error.details.length > 0) {
apiError = resAny.error.details.join('; ');
} else {
apiError = resAny.error.message || apiError;
}
} else if (typeof resAny.error === 'string') {
apiError = resAny.error;
} else if (resAny.message) {
apiError = resAny.message;
} else if (resAny.detail) {
apiError = resAny.detail;
} else if (resAny.data) {
apiError = resAny.data.message || resAny.data.detail || resAny.data.error || apiError;
}
setShowError(apiError);
}
} catch (err) {
setShowError('MFA verification failed. Please try again.');
} finally {
if (!loginSuccess) setIsLoading(false);
}
};
return (
<div className="hosp-login-page">
{/* Background effects */}
<div className="hosp-bg-grid" />
<div className="hosp-bg-orb orb-1" />
<div className="hosp-bg-orb orb-2" />
<div className="hosp-bg-orb orb-3" />
<div className="hosp-scan-line" />
{/* Floating medical icons */}
<div className="hosp-floating-icons">
<motion.div
className="float-icon fi-1"
animate={{ y: [-10, 10, -10], rotate: [0, 5, -5, 0] }}
transition={{ duration: 6, repeat: Infinity, ease: 'easeInOut' }}
>
<HeartPulse size={20} />
</motion.div>
<motion.div
className="float-icon fi-2"
animate={{ y: [10, -10, 10], rotate: [0, -5, 5, 0] }}
transition={{ duration: 8, repeat: Infinity, ease: 'easeInOut' }}
>
<Stethoscope size={18} />
</motion.div>
<motion.div
className="float-icon fi-3"
animate={{ y: [-8, 12, -8] }}
transition={{ duration: 7, repeat: Infinity, ease: 'easeInOut' }}
>
<Activity size={16} />
</motion.div>
</div>
{/* Login card */}
<AnimatePresence mode="wait">
<motion.div
key={loginStep + (loginSuccess ? '-success' : '')}
initial={{ opacity: 0, y: 30, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.96 }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
className={`hosp-login-card ${loginSuccess ? 'success-glow' : ''}`}
>
{/* Card top accent */}
<div className="hosp-card-accent" />
{/* Header */}
<div className="hosp-login-header">
<motion.div
className="hosp-logo-container"
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.2, duration: 0.6, type: 'spring', stiffness: 200 }}
>
<div className="hosp-logo-ring" />
<div className="hosp-logo-inner">
<Building size={26} strokeWidth={2} />
</div>
<div className="hosp-logo-pulse" />
</motion.div>
<motion.h1
className="hosp-title"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
{loginStep === 'login' ? 'Hospital Console' : 'Verify Identity'}
</motion.h1>
<motion.p
className="hosp-subtitle"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
>
{loginStep === 'login'
? 'Secure Clinical Operations Portal'
: 'Enter your authenticator code'}
</motion.p>
</div>
{/* Login form */}
{loginStep === 'login' ? (
<form onSubmit={handleLogin} className="hosp-form">
<motion.div
className="hosp-input-group"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.35 }}
>
<label className="hosp-label">Hospital ID</label>
<div className="hosp-input-wrap">
<User className="hosp-input-icon" size={18} />
<input
id="hospital-username"
type="text"
className="hosp-input"
placeholder="Enter username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
autoComplete="username"
/>
<div className="hosp-input-focus-line" />
</div>
</motion.div>
<motion.div
className="hosp-input-group"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.45 }}
>
<label className="hosp-label">Access Key</label>
<div className="hosp-input-wrap">
<Lock className="hosp-input-icon" size={18} />
<input
id="hospital-password"
type={showPassword ? 'text' : 'password'}
className="hosp-input"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
autoComplete="current-password"
/>
<button
type="button"
className="hosp-eye-btn"
onClick={() => setShowPassword(!showPassword)}
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
<div className="hosp-input-focus-line" />
</div>
</motion.div>
<motion.div
className="hosp-extras"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
>
<label className="hosp-remember">
<input type="checkbox" />
<span className="hosp-check-mark" />
Keep session active
</label>
<a href="#" className="hosp-forgot">Reset credentials?</a>
</motion.div>
<motion.button
type="submit"
className="hosp-submit-btn"
disabled={isLoading || loginSuccess}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.55 }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{loginSuccess ? (
<motion.div
className="hosp-success-check"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 300 }}
>
<CheckCircle2 size={22} />
<span>ACCESS GRANTED</span>
</motion.div>
) : isLoading ? (
<div className="hosp-loader">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
>
<Cpu size={20} />
</motion.div>
<span>AUTHENTICATING...</span>
</div>
) : (
<>
<span>SECURE LOGIN</span>
<ArrowRight size={18} />
</>
)}
</motion.button>
</form>
) : (
/* MFA Form */
<form onSubmit={handleMfaVerify} className="hosp-form">
<motion.div
className="hosp-input-group"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
>
<label className="hosp-label">TOTP Security Code</label>
<div className="hosp-input-wrap">
<KeyRound className="hosp-input-icon" size={18} />
<input
id="hospital-mfa-code"
type="text"
className="hosp-input mono"
placeholder="000 000"
maxLength={6}
value={mfaCode}
onChange={(e) => setMfaCode(e.target.value.replace(/\D/g, ''))}
required
autoFocus
/>
<div className="hosp-input-focus-line" />
</div>
</motion.div>
<p className="hosp-mfa-hint">
<AlertCircle size={14} />
Enter the 6-digit code from your authenticator app
</p>
<motion.button
type="submit"
className="hosp-submit-btn"
disabled={isLoading || loginSuccess}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{loginSuccess ? (
<motion.div
className="hosp-success-check"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
>
<CheckCircle2 size={22} />
<span>VERIFIED</span>
</motion.div>
) : isLoading ? (
<div className="hosp-loader">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
>
<Cpu size={20} />
</motion.div>
<span>VERIFYING...</span>
</div>
) : (
<>
<ShieldCheck size={18} />
<span>VERIFY IDENTITY</span>
</>
)}
</motion.button>
<button
type="button"
className="hosp-back-btn"
onClick={() => { setLoginStep('login'); setShowError(''); setMfaCode(''); }}
>
Back to Login
</button>
</form>
)}
{/* Error display */}
<AnimatePresence>
{showError && (
<motion.div
className="hosp-error-badge"
initial={{ opacity: 0, height: 0, marginTop: 0 }}
animate={{ opacity: 1, height: 'auto', marginTop: 16 }}
exit={{ opacity: 0, height: 0, marginTop: 0 }}
>
<ShieldAlert size={14} />
<span>{showError}</span>
</motion.div>
)}
</AnimatePresence>
{/* Security badge */}
{!showError && (
<motion.div
className="hosp-security-badge"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
>
<ShieldCheck size={14} />
<span>HIPAA-COMPLIANT SECURE CONNECTION</span>
</motion.div>
)}
{/* Footer */}
<div className="hosp-footer">
<NavLink to="/launcher" className="hosp-portal-link">
<Monitor size={14} />
VIEW ALL PORTALS
</NavLink>
</div>
</motion.div>
</AnimatePresence>
{/* Status indicators */}
<div className="hosp-status-bar">
<div className="hosp-status-item">
<Wifi size={12} />
<span>UPLINK ACTIVE</span>
</div>
<div className="hosp-status-dot" />
<div className="hosp-status-item">
<Activity size={12} />
<span>NODE ONLINE</span>
</div>
</div>
</div>
);
};
export default HospitalLogin;

View File

@@ -113,7 +113,7 @@ export const HospitalsNetwork: React.FC = () => {
setRealHospitals(hospitalNodes);
// Derive current issues
const newIssues = hospitalNodes.map(h => {
const newIssues = hospitalNodes.map((h: any) => {
const [available] = (h.beds || '0/0').split('/').map((n: string) => parseInt(n) || 0);
if (available === 0) return { type: 'CRITICAL', msg: `${h.name}: Zero bed capacity`, hospital: h.name };
if (available < 5) return { type: 'WARNING', msg: `${h.name}: Low bed availability`, hospital: h.name };
@@ -582,7 +582,7 @@ export const HospitalsNetwork: React.FC = () => {
onSubmit={triggerSubmit}
loading={isSubmitting}
>
<HospitalRegistrationForm onSubmit={handleHospitalSubmit} loading={isSubmitting} />
<HospitalRegistrationForm onSubmit={handleHospitalSubmit} />
</Modal>
{/* EDIT HOSPITAL MODAL */}

View File

@@ -146,7 +146,7 @@ export const LiveIncidents: React.FC = () => {
// Patient Modal states
const [isAddPatientsModalOpen, setIsAddPatientsModalOpen] = useState(false);
const [bulkPatients, setBulkPatients] = useState<any[]>([
{ gender: 'Male', triage_code: 'RED', symptoms: [] }
{ gender: 'Male', triage_level: 'RED', symptoms: [] }
]);
const [isMapPickerOpen, setIsMapPickerOpen] = useState(false);
@@ -166,7 +166,7 @@ export const LiveIncidents: React.FC = () => {
age: 0,
gender: 'Male',
symptoms: [],
triage_code: 'GREEN'
triage_level: 'GREEN'
}
],
caller_id: JSON.parse(localStorage.getItem('teleems_user') || '{}').id || '',
@@ -272,7 +272,7 @@ export const LiveIncidents: React.FC = () => {
notes: formData.notes,
patients: formData.patients?.map(p => ({
...p,
triage_code: p.triage_code || 'GREEN'
triage_level: p.triage_level || 'GREEN'
}))
};
const response = await incidentsApi.createIncident(payload, token);
@@ -317,7 +317,7 @@ export const LiveIncidents: React.FC = () => {
age: 0,
gender: 'Male',
symptoms: [],
triage_code: 'GREEN'
triage_level: 'GREEN'
}],
caller_id: JSON.parse(localStorage.getItem('teleems_user') || '{}').id || '',
organisationId: null
@@ -386,7 +386,7 @@ export const LiveIncidents: React.FC = () => {
if (response.status === 200 || response.status === 201) {
setIsAddPatientsModalOpen(false);
fetchIncidentDetails(selectedIncident.id);
setBulkPatients([{ gender: 'Male', triage_code: 'RED', symptoms: [] }]);
setBulkPatients([{ gender: 'Male', triage_level: 'RED', symptoms: [] }]);
}
} catch (error) {
console.error('Bulk add patients failed:', error);
@@ -767,23 +767,23 @@ export const LiveIncidents: React.FC = () => {
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{selectedIncident.patients.map((p, idx) => (
<div key={idx} className="glass" style={{ padding: '16px', background: 'rgba(0,0,0,0.03)', border: '1px solid var(--card-border)', borderRadius: '12px', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 0, left: 0, bottom: 0, width: '4px', background: (p.triage_level || p.triage_code) === 'RED' ? 'var(--alert-red)' : (p.triage_level || p.triage_code) === 'YELLOW' ? 'var(--warning-amber)' : 'var(--accent-green)' }}></div>
<div style={{ position: 'absolute', top: 0, left: 0, bottom: 0, width: '4px', background: (p.triage_level || p.triage_level) === 'RED' ? 'var(--alert-red)' : (p.triage_level || p.triage_level) === 'YELLOW' ? 'var(--warning-amber)' : 'var(--accent-green)' }}></div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 800, fontSize: '0.9rem', color: 'var(--text-primary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.name || 'ANONYMOUS-SUBJECT'}</div>
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', marginTop: '2px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.age || '?'}Y · {p.gender} · TRIAGE: {(p.triage_level || p.triage_code)?.toUpperCase()}</div>
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', marginTop: '2px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.age || '?'}Y · {p.gender} · TRIAGE: {(p.triage_level || p.triage_level)?.toUpperCase()}</div>
</div>
<div style={{
padding: '4px 8px',
borderRadius: '6px',
fontSize: '0.6rem',
fontWeight: 900,
background: (p.triage_level || p.triage_code) === 'RED' ? 'rgba(239, 68, 68, 0.1)' : 'rgba(0,0,0,0.02)',
color: (p.triage_level || p.triage_code) === 'RED' ? 'var(--alert-red)' : 'var(--text-primary)',
background: (p.triage_level || p.triage_level) === 'RED' ? 'rgba(239, 68, 68, 0.1)' : 'rgba(0,0,0,0.02)',
color: (p.triage_level || p.triage_level) === 'RED' ? 'var(--alert-red)' : 'var(--text-primary)',
border: '1px solid',
borderColor: (p.triage_level || p.triage_code) === 'RED' ? 'rgba(239, 68, 68, 0.2)' : 'var(--card-border)'
borderColor: (p.triage_level || p.triage_level) === 'RED' ? 'rgba(239, 68, 68, 0.2)' : 'var(--card-border)'
}}>
CODE {(p.triage_level || p.triage_code) || 'PENDING'}
CODE {(p.triage_level || p.triage_level) || 'PENDING'}
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
@@ -1098,7 +1098,7 @@ export const LiveIncidents: React.FC = () => {
type="button"
onClick={() => {
const newPatients = [...(formData.patients || [])];
newPatients.push({ name: '', age: 0, gender: 'Male', symptoms: [], triage_code: 'GREEN' });
newPatients.push({ name: '', age: 0, gender: 'Male', symptoms: [], triage_level: 'GREEN' });
setFormData({ ...formData, patients: newPatients });
}}
style={{ background: 'rgba(59, 130, 246, 0.1)', border: '1px solid var(--accent-cyan)', color: 'var(--accent-cyan)', padding: '4px 10px', borderRadius: '6px', fontSize: '0.65rem', fontWeight: 800, cursor: 'pointer' }}
@@ -1168,10 +1168,10 @@ export const LiveIncidents: React.FC = () => {
<div>
<label style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', display: 'block', marginBottom: '6px', fontWeight: 700 }}>TRIAGE</label>
<select
value={patient.triage_code}
value={patient.triage_level}
onChange={(e) => {
const newPatients = [...(formData.patients || [])];
newPatients[index] = { ...newPatients[index], triage_code: e.target.value };
newPatients[index] = { ...newPatients[index], triage_level: e.target.value };
setFormData({ ...formData, patients: newPatients });
}}
style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)', fontSize: '0.8rem' }}
@@ -1190,7 +1190,7 @@ export const LiveIncidents: React.FC = () => {
value={patient.symptoms?.map((s: any) => typeof s === 'string' ? s : s?.name || '').join(', ')}
onChange={(e) => {
const newPatients = [...(formData.patients || [])];
newPatients[index] = { ...newPatients[index], symptoms: e.target.value.split(',').map(s => s.trim()).filter(s => s !== '') };
newPatients[index] = { ...newPatients[index], symptoms: e.target.value.split(',').map(s => s.trim()).filter(s => s !== '').map(s => ({ name: s, duration_minutes: 0 })) };
setFormData({ ...formData, patients: newPatients });
}}
style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)', fontSize: '0.8rem' }}
@@ -1320,10 +1320,10 @@ export const LiveIncidents: React.FC = () => {
<div>
<label style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', display: 'block', marginBottom: '6px' }}>TRIAGE CODE</label>
<select
value={patient.triage_code}
value={patient.triage_level}
onChange={(e) => {
const newPatients = [...bulkPatients];
newPatients[index].triage_code = e.target.value;
newPatients[index].triage_level = e.target.value;
setBulkPatients(newPatients);
}}
style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)', fontSize: '0.8rem' }}
@@ -1354,7 +1354,7 @@ export const LiveIncidents: React.FC = () => {
))}
<button
onClick={() => setBulkPatients([...bulkPatients, { gender: 'Male', triage_code: 'GREEN', symptoms: [] }])}
onClick={() => setBulkPatients([...bulkPatients, { gender: 'Male', triage_level: 'GREEN', symptoms: [] }])}
style={{ padding: '12px', background: 'rgba(0,0,0,0.02)', border: '1px dashed var(--card-border)', borderRadius: '12px', color: 'var(--text-secondary)', fontSize: '0.8rem', fontWeight: 700, cursor: 'pointer' }}
>
+ ADD ANOTHER SUBJECT

View File

@@ -1,9 +1,8 @@
.launcher-page {
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
background: #020617;
color: #f8fafc;
min-height: 100vh;
background: var(--hull-dark-l);
background-color: hsl(var(--hull-dark-h), var(--hull-dark-s), 4%);
color: #fff;
font-family: 'Inter', system-ui, sans-serif;
position: relative;
display: flex;
@@ -27,74 +26,37 @@
background: rgba(59, 130, 246, 0.4);
}
/* Background Effects */
/* Background Effects - Clean Clinical Finish */
.launcher-bg {
position: absolute;
inset: 0;
z-index: 0;
overflow: hidden;
background: hsl(var(--hull-dark-h), var(--hull-dark-s), 2%);
}
.launcher-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(to right, rgba(59, 130, 246, 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(59, 130, 246, 0.05) 1px, transparent 1px);
background-size: 50px 50px;
mask-image: radial-gradient(circle at center, black, transparent 80%);
}
.launcher-blob {
position: absolute;
width: 500px;
height: 500px;
filter: blur(100px);
opacity: 0.15;
border-radius: 50%;
z-index: -1;
}
.blob-1 {
top: -100px;
left: -100px;
background: #3b82f6;
animation: float 20s infinite alternate;
}
.blob-2 {
bottom: -100px;
right: -100px;
background: #8b5cf6;
animation: float 25s infinite alternate-reverse;
}
.blob-3 {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #10b981;
width: 300px;
height: 300px;
opacity: 0.05;
}
@keyframes float {
from { transform: translate(0, 0); }
to { transform: translate(100px, 50px); }
linear-gradient(to right, hsla(var(--accent-cyan-h), 100%, 50%, 0.015) 1px, transparent 1px),
linear-gradient(to bottom, hsla(var(--accent-cyan-h), 100%, 50%, 0.015) 1px, transparent 1px);
background-size: 80px 80px;
mask-image: radial-gradient(circle at 0% 0%, black, transparent 100%);
}
/* Header */
.launcher-header {
position: relative;
z-index: 10;
padding: 24px 40px;
position: sticky;
top: 0;
z-index: 100;
padding: 20px 40px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
background: rgba(2, 6, 23, 0.5);
backdrop-filter: blur(10px);
border-bottom: 1px solid hsla(0, 0%, 100%, 0.05);
background: hsla(220, 30%, 5%, 0.8);
backdrop-filter: blur(20px);
}
.launcher-brand {
@@ -227,17 +189,22 @@
/* Cards */
.portal-card {
position: relative;
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 24px;
background: hsla(220, 30%, 10%, 0.4);
border: 1px solid hsla(0, 0%, 100%, 0.05);
border-radius: 28px;
padding: 32px;
cursor: pointer;
overflow: hidden;
transition: border-color 0.3s;
transition: var(--transition-snappy);
display: flex;
flex-direction: column;
}
.portal-card:hover {
border-color: var(--accent-color);
background: hsla(220, 30%, 15%, 0.6);
transform: translateY(-8px);
box-shadow: 0 30px 60px -12px hsla(0, 0%, 0%, 0.5);
}
.portal-card-glow {
@@ -246,14 +213,14 @@
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at top right, var(--accent-color), transparent 70%);
background: radial-gradient(circle at top right, var(--accent-color), transparent 60%);
opacity: 0;
transition: opacity 0.3s;
transition: opacity 0.4s;
pointer-events: none;
}
.portal-card:hover .portal-card-glow {
opacity: 0.1;
opacity: 0.15;
}
.portal-card-inner {
@@ -285,18 +252,21 @@
}
.portal-subtitle {
font-size: 0.75rem;
font-weight: 800;
letter-spacing: 0.1em;
font-size: 0.65rem;
font-weight: 950;
letter-spacing: 0.15em;
color: var(--accent-color);
filter: brightness(1.2);
margin-bottom: 8px;
display: block;
text-transform: uppercase;
}
.portal-title {
font-size: 1.5rem;
font-weight: 800;
font-size: 1.6rem;
font-weight: 900;
margin: 0 0 12px 0;
color: #fff;
}
.portal-description {
@@ -399,21 +369,20 @@
color: #f8fafc;
}
@media (max-width: 768px) {
.launcher-main-title {
font-size: 2.5rem;
}
.portal-grid {
grid-template-columns: 1fr;
}
.launcher-header {
padding: 16px 20px;
}
.launcher-content {
padding: 40px 20px;
}
.footer-info {
gap: 20px;
flex-wrap: wrap;
}
@media (max-width: 1024px) {
.launcher-main-title { font-size: 3rem; }
.portal-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 768px) {
.launcher-header { padding: 16px 20px; }
.launcher-actions { display: none; }
.launcher-main-title { font-size: 2.2rem; }
.launcher-intro { margin-bottom: 40px; }
.portal-grid { grid-template-columns: 1fr; gap: 16px; }
.launcher-content { padding: 40px 20px; }
.portal-card { padding: 24px; }
.footer-info { gap: 20px; flex-direction: column; align-items: center; text-align: center; }
.footer-links { justify-content: center; width: 100%; margin-top: 24px; }
.launcher-footer { flex-direction: column; gap: 24px; padding: 40px 20px; }
}

View File

@@ -61,76 +61,28 @@ const PortalCard: React.FC<PortalCardProps> = ({ title, subtitle, icon: Icon, co
export const PerspectiveLauncher: React.FC = () => {
const portals = [
{
title: 'Admin Control',
subtitle: 'SYSTEM ADMINISTRATION',
title: 'Hospital Admin',
subtitle: 'FACILITY MANAGEMENT',
icon: Shield,
color: '#f8fafc',
path: '/login/admin',
description: 'Global system configuration, user management, and infrastructure monitoring.'
},
{
title: 'Hospital Group',
subtitle: 'REGIONAL MANAGEMENT',
icon: Building2,
color: '#3b82f6',
path: '/login/hospital-group',
description: 'Centralized oversight for multiple healthcare facilities and resource allocation.'
path: '/login/hospital',
description: 'Full administrative control over facility configuration, department setup, and staff management.'
},
{
title: 'Hospital',
subtitle: 'FACILITY OPERATIONS',
icon: Building,
title: 'ED Doctor (ERCP)',
subtitle: 'CLINICAL OPERATIONS',
icon: Stethoscope,
color: '#10b981',
path: '/login/hospital',
description: 'End-to-end management of emergency department operations and bed tracking.'
description: 'Manage active patient triage, teleconsultations, and real-time vital monitoring from the mission control.'
},
{
title: 'Provider',
subtitle: 'CLINICAL CARE',
icon: Stethoscope,
title: 'Hospital Coordinator',
subtitle: 'PATIENT LOGISTICS',
icon: Building,
color: '#8b5cf6',
path: '/login/provider',
description: 'Dedicated interface for healthcare professionals to manage patient care.'
},
{
title: 'Provider React',
subtitle: 'ACTIVE MONITORING',
icon: Activity,
color: '#f59e0b',
path: '/login/provider-react',
description: 'Real-time physiological data monitoring and emergency response triggers.'
},
{
title: 'Patient',
subtitle: 'PERSONAL HEALTH',
icon: User,
color: '#ec4899',
path: '/login/patient',
description: 'Access to medical records, treatment plans, and direct communication with care teams.'
},
{
title: 'Scan Centre',
subtitle: 'DIAGNOSTICS & IMAGING',
icon: Scan,
color: '#06b6d4',
path: '/login/scan-centre',
description: 'Radiology and diagnostic workflow management with high-fidelity imaging.'
},
{
title: 'Cart / Mobile',
subtitle: 'EMERGENCY RESPONSE',
icon: ShoppingCart,
color: '#f43f5e',
path: '/login/cart',
description: 'On-the-go medical equipment tracking and mobile incident management.'
},
{
title: 'Fleet Command',
subtitle: 'TACTICAL LOGISTICS',
icon: Truck,
color: '#f59e0b',
path: '/fleet-login',
description: 'Real-time vehicle tracking, tactical dispatch, and fleet telemetry monitoring.'
path: '/login/hospital',
description: 'Coordinate with incoming ambulances, manage bed capacity, and oversee patient documentation handovers.'
}
];

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import {
import { Lock,
Plus,
Search,
Filter,
@@ -60,7 +60,7 @@ export const FleetInventory: React.FC = () => {
</div>
<div className="glass" style={{ padding: '20px', borderRadius: '16px', border: '1px solid rgba(245, 158, 11, 0.2)', background: 'rgba(245, 158, 11, 0.02)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ color: '#F59E0B' }}><Clock size={20} /></div>
<div style={{ color: '#F59E0B' }}><Lock size={20} /></div>
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#F59E0B' }}>8</div>
<div style={{ fontSize: '0.7rem', opacity: 0.5, textTransform: 'uppercase' }}>EXPIRING (30D)</div>

View File

@@ -0,0 +1,161 @@
import React from 'react';
import { motion } from 'framer-motion';
import { X, ChevronDown } from 'lucide-react';
interface DeptModalProps {
isOpen: boolean;
onClose: () => void;
deptFormData: {
name: string;
headOfDepartment: string;
totalBedsCapacity: number;
contactPhone: string;
isActive: boolean;
};
setDeptFormData: (data: any) => void;
allUsers: any[];
selectedHospital: any;
onSubmit: () => void;
}
export const DeptModal: React.FC<DeptModalProps> = ({
isOpen,
onClose,
deptFormData,
setDeptFormData,
allUsers,
selectedHospital,
onSubmit,
}) => {
if (!isOpen) return null;
return (
<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={onClose}>
<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={onClose}>
CANCEL
</button>
<button className="btn-primary-glass" onClick={onSubmit}>
ADD DEPARTMENT
</button>
</div>
</motion.div>
</div>
);
};

View File

@@ -0,0 +1,633 @@
import React, { useState, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { motion, AnimatePresence } from 'framer-motion';
import {
Search,
Activity,
Wind,
Video,
Zap,
ShieldAlert,
Database,
Radio,
Clock,
ChevronRight,
TrendingUp,
Filter,
AlertCircle,
Truck,
CheckCircle2,
X,
LayoutDashboard
} from 'lucide-react';
import { hospitalApi } from '../../api/hospital';
import {
ResponsiveContainer,
PieChart,
Pie,
Cell,
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
AreaChart,
Area
} from 'recharts';
interface EDMonitorProps {
incomingPatients: any[];
selectedHospital: any;
departments: any[];
onRefresh?: () => void;
onOpenBooking: () => void;
onOpenTelelink: () => void;
}
export const EDMonitor: React.FC<EDMonitorProps> = ({
incomingPatients,
selectedHospital,
departments,
onRefresh,
onOpenBooking,
onOpenTelelink,
}) => {
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState('ALL');
const [expandedId, setExpandedId] = useState<string | null>(null);
const [assigningPatientId, setAssigningPatientId] = useState<string | null>(null);
const [isAdmitting, setIsAdmitting] = useState(false);
const toggleExpand = (id: string) => {
setExpandedId(expandedId === id ? null : id);
};
const handleAdmit = async (patientId: string, departmentId: string) => {
setIsAdmitting(true);
try {
const patient = incomingPatients.find(p => p.id === patientId);
const realId = patient?.originalId || patientId;
const token = localStorage.getItem('teleems_token') || '';
const res = await hospitalApi.admitPatient(realId, departmentId, token);
if (res.status === 200 || res.status === 201) {
setAssigningPatientId(null);
if (onRefresh) onRefresh();
} else {
alert('Admission failed: ' + (res.message || 'Unknown error'));
}
} catch (err: any) {
alert('Error admitting patient: ' + err.message);
} finally {
setIsAdmitting(false);
}
};
const stats = useMemo(() => {
return {
total: incomingPatients.filter(p => p.status === 'IN_TRANSIT').length,
critical: incomingPatients.filter(p => (p.triage === 'RED' || p.triage === 'CRITICAL') && p.status !== 'HANDOFF_COMPLETE').length,
onsite: incomingPatients.filter(p => p.status === 'ARRIVED').length,
handoff: incomingPatients.filter(p => p.status === 'HANDOFF_COMPLETE').length,
};
}, [incomingPatients]);
const filteredPatients = useMemo(() => {
return incomingPatients.filter(patient => {
const matchesSearch =
patient.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
patient.mrn.toLowerCase().includes(searchQuery.toLowerCase()) ||
patient.ambulanceId.toLowerCase().includes(searchQuery.toLowerCase()) ||
patient.complaint.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = statusFilter === 'ALL' || patient.status === statusFilter;
return matchesSearch && matchesStatus;
});
}, [incomingPatients, searchQuery, statusFilter]);
const statuses = ['ALL', 'IN_TRANSIT', 'ARRIVED', 'HANDOFF_COMPLETE'];
const triageData = useMemo(() => {
const counts: any = { RED: 0, ORANGE: 0, YELLOW: 0, GREEN: 0, WHITE: 0 };
incomingPatients.forEach(p => {
const t = String(p.triage).toUpperCase();
if (counts[t] !== undefined) counts[t]++;
else counts.GREEN++; // fallback
});
return [
{ name: 'Critical', value: counts.RED, color: '#ef4444' },
{ name: 'Urgent', value: counts.ORANGE, color: '#f97316' },
{ name: 'Stable', value: counts.YELLOW, color: '#eab308' },
{ name: 'Minimal', value: counts.GREEN, color: '#22c55e' },
{ name: 'Routine', value: counts.WHITE, color: '#94a3b8' },
].filter(d => d.value > 0);
}, [incomingPatients]);
const loadData = useMemo(() => {
return [
{ time: '08:00', load: Math.max(0, stats.total - 2) },
{ time: '10:00', load: Math.max(0, stats.total - 1) },
{ time: '12:00', load: stats.total + 1 },
{ time: 'NOW', load: stats.total },
];
}, [stats.total]);
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="module-content ed-monitor-tactical"
style={{ background: 'var(--tactical-bg)', minHeight: '100%', padding: '24px' }}
>
{/* Metrics Row */}
<div className="metrics-row-modern">
{[
{ label: 'Incoming', count: stats.total, icon: <Activity size={20} />, color: '#0ea5e9' },
{ label: 'Critical', count: stats.critical, icon: <AlertCircle size={20} />, color: '#ef4444' },
{ label: 'On-Site', count: stats.onsite, icon: <Truck size={20} />, color: '#f59e0b' },
{ label: 'Completed', count: stats.handoff, icon: <CheckCircle2 size={20} />, color: '#10b981' },
].map((m, i) => (
<div key={i} className="metric-card-premium">
<div className="metric-icon-wrap" style={{ color: m.color }}>
{m.icon}
</div>
<div>
<div style={{ fontSize: '1.5rem', fontWeight: 800, color: '#0f172a', lineHeight: 1 }}>{m.count}</div>
<div style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.05em', marginTop: '4px' }}>{m.label}</div>
</div>
</div>
))}
</div>
<div className="tactical-board-container">
<div className="board-header-tactical" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '32px' }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', color: '#ef4444', fontWeight: 700, fontSize: '0.75rem', letterSpacing: '0.05em' }}>
<Radio size={14} /> LIVE TRIAGE DATA STREAM
</div>
<h2 style={{ margin: '8px 0 0 0', fontSize: '1.5rem', fontWeight: 700, color: '#1e293b', letterSpacing: '-0.02em' }}>Emergent Intake Board</h2>
</div>
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
<div className="search-mini" style={{ width: '300px', background: '#f1f5f9', border: '1px solid #e2e8f0' }}>
<Search size={16} color="#64748b" />
<input
type="text"
placeholder="Filter by Name, MRN, Unit..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{ fontWeight: 600 }}
/>
</div>
<div className="setup-nav" style={{ background: '#f1f5f9', padding: '4px', borderRadius: '12px' }}>
{statuses.map(status => (
<button
key={status}
className={`setup-nav-item ${statusFilter === status ? 'active' : ''}`}
onClick={() => setStatusFilter(status)}
style={{ fontSize: '0.7rem', fontWeight: 700, padding: '6px 14px' }}
>
{status.replace('_', ' ')}
</button>
))}
</div>
</div>
</div>
<div className="ed-grid-modern">
<div className="triage-feed-container">
<AnimatePresence mode="popLayout">
{filteredPatients.length > 0 ? (
filteredPatients.map((patient) => (
<motion.div
key={patient.id}
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
className="tactical-patient-card"
>
<div className={`card-status-indicator ${patient.triage}`} />
<div className="card-main-tactical">
{/* Unit & ETA */}
<div className="unit-info-hex">
<div className="unit-label-premium">UNIT: {patient.ambulanceId}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '8px' }}>
<div style={{ width: 10, height: 10, background: patient.triage === 'RED' ? '#ef4444' : '#22c55e', borderRadius: '50%' }} />
<span style={{ fontSize: '0.9rem', fontWeight: 700, color: '#1e293b' }}>ETA {patient.eta}</span>
</div>
<div style={{ fontSize: '0.7rem', color: '#64748b', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.02em' }}>{patient.status.replace('_', ' ')}</div>
</div>
{/* Identity & Complaint */}
<div className="p-identity-block">
<div className="p-name-premium">{patient.name}</div>
<div className="p-meta-inline">
<span style={{ color: 'var(--accent-cyan)', background: 'rgba(14, 165, 233, 0.08)', padding: '2px 8px', borderRadius: '6px', fontSize: '0.7rem' }}>{patient.mrn}</span>
<span></span>
<span>{patient.age}Y</span>
<span></span>
<span>{patient.gender}</span>
</div>
<div style={{ marginTop: '10px' }}>
<div className={`complaint-badge-tactical ${patient.triage === 'RED' ? 'CRITICAL' : ''}`}>
<TrendingUp size={14} />
{patient.complaint}
</div>
</div>
</div>
{/* Vitals Monitor */}
<div className="vitals-monitor-block">
<div className="v-node-tactical hr">
<span className="v-val">{patient.vitals.hr}</span>
<span className="v-lbl">HR</span>
</div>
<div style={{ width: '1px', height: '30px', background: 'rgba(255,255,255,0.08)' }} />
<div className="v-node-tactical spo2">
<span className="v-val">{patient.vitals.spo2}</span>
<span className="v-lbl">SPO2</span>
</div>
<div style={{ width: '1px', height: '30px', background: 'rgba(255,255,255,0.08)' }} />
<div className="v-node-tactical bp">
<span className="v-val">{patient.vitals.bp}</span>
<span className="v-lbl">BP</span>
</div>
</div>
{/* Actions */}
<div className="tactical-actions">
<div style={{ display: 'flex', gap: '8px' }}>
<button className="action-btn-tactical primary" onClick={onOpenTelelink} style={{ flex: 1 }}>
<Video size={14} /> JOIN
</button>
<button
className="action-btn-tactical"
onClick={() => toggleExpand(patient.id)}
style={{ flex: 1 }}
>
VIEW DATA
</button>
</div>
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
<button className="action-btn-tactical" onClick={() => setAssigningPatientId(patient.id)} style={{ flex: 1, background: '#f8fafc', color: 'var(--accent-cyan)', border: '1px solid var(--accent-cyan-soft)' }}>
<LayoutDashboard size={14} /> ASSIGN
</button>
{/* <button className="action-btn-tactical" onClick={onOpenBooking} style={{ flex: 1 }}>
TRIAGE REG.
</button> */}
</div>
</div>
</div>
</motion.div>
))
) : (
<div style={{ padding: '100px 40px', textAlign: 'center', background: '#f8fafc', borderRadius: '32px', border: '2px dashed #e2e8f0' }}>
<div style={{ width: 80, height: 80, borderRadius: '50%', background: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 24px auto', boxShadow: '0 10px 25px rgba(0,0,0,0.05)' }}>
<Filter size={32} color="#94a3b8" />
</div>
<h3 style={{ color: '#1e293b', margin: '0 0 8px 0', fontWeight: 800 }}>Board Stabilized</h3>
<p style={{ color: '#64748b', fontSize: '0.9rem', maxWidth: '300px', margin: '8px auto 0 auto' }}>All dispatches accounted for. No incoming units match current filters.</p>
</div>
)}
</AnimatePresence>
</div>
<div className="ed-intel-sidebar" style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div className="hub-card premium-ops" style={{ background: '#fff', border: '1px solid #e2e8f0', borderRadius: '24px', padding: '24px' }}>
<div style={{ fontSize: '0.65rem', fontWeight: 900, color: '#64748b', letterSpacing: '0.15em', textTransform: 'uppercase', marginBottom: '20px' }}>Triage Distribution</div>
<div style={{ height: '180px', width: '100%' }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={triageData}
innerRadius={55}
outerRadius={75}
paddingAngle={5}
dataKey="value"
>
{triageData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}
itemStyle={{ fontSize: '12px', fontWeight: 600 }}
/>
</PieChart>
</ResponsiveContainer>
<div style={{ display: 'flex', justifyContent: 'center', gap: '12px', marginTop: '10px', flexWrap: 'wrap' }}>
{triageData.map((d, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: d.color }} />
<span style={{ fontSize: '0.65rem', fontWeight: 700, color: '#64748b' }}>{d.name}</span>
</div>
))}
</div>
</div>
</div>
<div className="hub-card premium-ops" style={{ background: '#fff', border: '1px solid #e2e8f0', borderRadius: '24px', padding: '24px' }}>
<div style={{ fontSize: '0.65rem', fontWeight: 900, color: '#64748b', letterSpacing: '0.15em', textTransform: 'uppercase', marginBottom: '20px' }}>Inflow Velocity</div>
<div style={{ height: '120px', width: '100%' }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={loadData}>
<defs>
<linearGradient id="colorLoad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#0ea5e9" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#0ea5e9" stopOpacity={0}/>
</linearGradient>
</defs>
<Tooltip
contentStyle={{ borderRadius: '10px', border: 'none', boxShadow: '0 2px 8px rgba(0,0,0,0.05)' }}
labelStyle={{ fontSize: '10px', color: '#94a3b8' }}
/>
<Area type="monotone" dataKey="load" stroke="#0ea5e9" fillOpacity={1} fill="url(#colorLoad)" strokeWidth={3} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
<div className="hub-card premium-ops" style={{ background: '#fff', border: '1px solid #e2e8f0', borderRadius: '24px', padding: '24px' }}>
<div style={{ fontSize: '0.65rem', fontWeight: 900, color: '#ef4444', letterSpacing: '0.15em', textTransform: 'uppercase', marginBottom: '20px' }}>System Alerts</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<button className="code-btn-modern red" style={{ height: '60px' }}>
<div className="c-info">
<strong style={{ fontSize: '1rem' }}>CODE RED</strong>
<span>STAT Response Required</span>
</div>
<Zap size={20} fill="#fff" />
</button>
<button className="code-btn-modern trauma" style={{ height: '60px', background: 'linear-gradient(135deg, #f59e0b, #d97706)' }}>
<div className="c-info">
<strong style={{ fontSize: '1rem' }}>TRAUMA STAT</strong>
<span>Activation Sequence</span>
</div>
<ShieldAlert size={20} fill="#fff" />
</button>
</div>
</div>
</div>
</div>
</div>
{createPortal(
<AnimatePresence>
{expandedId && (
<div style={{ position: 'fixed', inset: 0, zIndex: 10000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px' }}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setExpandedId(null)}
style={{ position: 'absolute', inset: 0, background: 'rgba(15, 23, 42, 0.8)', backdropFilter: 'blur(4px)' }}
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
style={{
position: 'relative',
background: '#fff',
borderRadius: '24px',
width: '100%',
maxWidth: '1000px',
maxHeight: '90vh',
overflow: 'hidden',
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.5)'
}}
>
{(() => {
const patient = incomingPatients.find(p => p.id === expandedId);
if (!patient) return null;
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{/* Modal Header */}
<div style={{ padding: '24px', borderBottom: '1px solid #f1f5f9', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#f8fafc' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
<div className={`card-status-indicator ${patient.triage}`} style={{ width: '4px', height: '40px', borderRadius: '4px' }} />
<div>
<h3 style={{ margin: 0, fontSize: '1.25rem', fontWeight: 800, color: '#1e293b' }}>{patient.name} <span style={{ color: '#94a3b8', fontWeight: 500, fontSize: '1rem' }}> {patient.mrn}</span></h3>
<div style={{ fontSize: '0.85rem', color: '#64748b', marginTop: '4px' }}>
{patient.age}Y {patient.gender} Unit {patient.ambulanceId} Status: {patient.status.replace('_', ' ')}
</div>
</div>
</div>
<button
onClick={() => setExpandedId(null)}
style={{ width: '40px', height: '40px', borderRadius: '12px', border: '1px solid #e2e8f0', background: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
>
<X size={20} color="#64748b" />
</button>
</div>
{/* Modal Body */}
<div style={{ padding: '32px', overflowY: 'auto' }}>
<div className="detail-module-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '32px' }}>
{/* Clinical Module */}
<div className="detail-module">
<h5 style={{ fontSize: '0.7rem', color: '#94a3b8', textTransform: 'uppercase', marginBottom: '16px', letterSpacing: '0.1em', fontWeight: 800 }}>Clinical Assessment</h5>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div style={{ background: '#f8fafc', padding: '16px', borderRadius: '12px', border: '1px solid #e2e8f0' }}>
<div style={{ fontSize: '0.65rem', color: '#64748b', marginBottom: '6px' }}>GCS TOTAL</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#1e293b' }}>{patient.gcs?.total || '--'}</div>
<div style={{ fontSize: '0.65rem', color: '#94a3b8', marginTop: '4px' }}>E:{patient.gcs?.eye} V:{patient.gcs?.verbal} M:{patient.gcs?.motor}</div>
</div>
<div style={{ background: '#f8fafc', padding: '16px', borderRadius: '12px', border: '1px solid #e2e8f0' }}>
<div style={{ fontSize: '0.65rem', color: '#64748b', marginBottom: '6px' }}>AVPU</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#1e293b' }}>{patient.avpu}</div>
</div>
<div style={{ background: '#f8fafc', padding: '16px', borderRadius: '12px', border: '1px solid #e2e8f0', gridColumn: 'span 2' }}>
<div style={{ fontSize: '0.65rem', color: '#64748b', marginBottom: '8px' }}>PUPILS</div>
<div style={{ display: 'flex', gap: '30px' }}>
<div style={{ fontSize: '1rem', fontWeight: 700 }}><span style={{ color: '#94a3b8' }}>LEFT:</span> {patient.pupils?.left || '--'}</div>
<div style={{ fontSize: '1rem', fontWeight: 700 }}><span style={{ color: '#94a3b8' }}>RIGHT:</span> {patient.pupils?.right || '--'}</div>
</div>
</div>
</div>
</div>
{/* HPI Module */}
<div className="detail-module">
<h5 style={{ fontSize: '0.7rem', color: '#94a3b8', textTransform: 'uppercase', marginBottom: '16px', letterSpacing: '0.1em', fontWeight: 800 }}>History of Illness (HPI)</h5>
<div style={{ background: '#fff', padding: '20px', borderRadius: '16px', border: '1px solid #e2e8f0', boxShadow: '0 4px 6px -1px rgba(0,0,0,0.05)' }}>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '0.7rem', color: '#94a3b8' }}>CHIEF COMPLAINT</div>
<div style={{ fontSize: '1.1rem', fontWeight: 800, color: '#1e293b' }}>{patient.complaint}</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div>
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>ONSET</div>
<div style={{ fontSize: '0.85rem', fontWeight: 700 }}>{patient.hpi?.onset || '--'}</div>
</div>
<div>
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>DURATION</div>
<div style={{ fontSize: '0.85rem', fontWeight: 700 }}>{patient.hpi?.duration || '--'}m</div>
</div>
<div>
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>SEVERITY</div>
<div style={{ fontSize: '0.85rem', fontWeight: 700 }}>{patient.hpi?.severity || '--'}/10</div>
</div>
<div>
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>CHARACTER</div>
<div style={{ fontSize: '0.85rem', fontWeight: 700 }}>{patient.hpi?.character || '--'}</div>
</div>
</div>
</div>
</div>
{/* Medical Background */}
<div className="detail-module">
<h5 style={{ fontSize: '0.7rem', color: '#94a3b8', textTransform: 'uppercase', marginBottom: '16px', letterSpacing: '0.1em', fontWeight: 800 }}>Medical Records</h5>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{[
{ label: 'ALLERGIES', val: patient.allergies.map((a: any) => a.name).join(', '), color: '#fee2e2', textColor: '#b91c1c' },
{ label: 'MEDICATIONS', val: patient.medications.join(', '), color: '#f1f5f9', textColor: '#475569' },
{ label: 'CONDITIONS', val: patient.conditions.join(', '), color: '#f1f5f9', textColor: '#475569' },
{ label: 'SURGERIES', val: patient.surgeries.join(', '), color: '#f1f5f9', textColor: '#475569' },
].map((item, idx) => (
<div key={idx} style={{ background: item.val ? item.color : 'transparent', padding: '12px 16px', borderRadius: '10px', border: item.val ? 'none' : '1px dashed #e2e8f0' }}>
<div style={{ fontSize: '0.6rem', color: '#64748b', fontWeight: 800, marginBottom: '4px' }}>{item.label}</div>
<div style={{ fontSize: '0.9rem', color: item.textColor, fontWeight: 700 }}>{item.val || '--'}</div>
</div>
))}
</div>
</div>
{/* Footer Info Row */}
<div style={{ gridColumn: '1 / -1', borderTop: '1px solid #f1f5f9', paddingTop: '32px', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(350px, 1fr))', gap: '24px' }}>
<div style={{ background: '#f8fafc', padding: '20px', borderRadius: '16px', border: '1px solid #e2e8f0' }}>
<div style={{ fontSize: '0.7rem', color: '#94a3b8', marginBottom: '12px', fontWeight: 800 }}>INFORMER CONTACT</div>
<div style={{ display: 'flex', gap: '40px' }}>
<div><span style={{ fontSize: '0.65rem', color: '#94a3b8' }}>NAME:</span> <div style={{ fontSize: '0.9rem', fontWeight: 700 }}>{patient.informer?.name}</div></div>
<div><span style={{ fontSize: '0.65rem', color: '#94a3b8' }}>RELATION:</span> <div style={{ fontSize: '0.9rem', fontWeight: 700 }}>{patient.informer?.relation}</div></div>
<div><span style={{ fontSize: '0.65rem', color: '#94a3b8' }}>PHONE:</span> <div style={{ fontSize: '0.9rem', fontWeight: 700 }}>{patient.informer?.phone}</div></div>
</div>
</div>
<div style={{ background: patient.mlc?.is_mlc ? '#fffef2' : '#f8fafc', padding: '20px', borderRadius: '16px', border: `1px solid ${patient.mlc?.is_mlc ? '#fef08a' : '#e2e8f0'}` }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ fontSize: '0.7rem', color: '#94a3b8', fontWeight: 800 }}>MLC RECORD</div>
{patient.mlc?.is_mlc && <span style={{ background: '#fef9c3', color: '#854d0e', fontSize: '0.6rem', fontWeight: 900, padding: '4px 8px', borderRadius: '6px' }}>LEGAL CLEARANCE REQ.</span>}
</div>
<div style={{ display: 'flex', gap: '40px' }}>
<div><span style={{ fontSize: '0.65rem', color: '#94a3b8' }}>FIR:</span> <div style={{ fontSize: '0.9rem', fontWeight: 700 }}>{patient.mlc?.fir || '--'}</div></div>
<div><span style={{ fontSize: '0.65rem', color: '#94a3b8' }}>STATION:</span> <div style={{ fontSize: '0.9rem', fontWeight: 700 }}>{patient.mlc?.station || '--'}</div></div>
<div><span style={{ fontSize: '0.65rem', color: '#94a3b8' }}>OFFICER:</span> <div style={{ fontSize: '0.9rem', fontWeight: 700 }}>{patient.mlc?.officer || '--'}</div></div>
</div>
</div>
</div>
</div>
</div>
{/* Modal Actions */}
<div style={{ padding: '24px', borderTop: '1px solid #f1f5f9', background: '#f8fafc', display: 'flex', justifyContent: 'flex-end', gap: '16px' }}>
<button className="action-btn-tactical" onClick={() => setExpandedId(null)} style={{ padding: '0 24px', height: '44px' }}>CLOSE REPORT</button>
<button className="action-btn-tactical primary" onClick={onOpenTelelink} style={{ padding: '0 32px', height: '44px' }}><Video size={18} /> INITIATE CALL</button>
</div>
</div>
);
})()}
</motion.div>
</div>
)}
</AnimatePresence>,
document.body
)}
{/* Assignment Modal */}
{createPortal(
<AnimatePresence>
{assigningPatientId && (
<div style={{ position: 'fixed', inset: 0, zIndex: 10001, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px' }}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setAssigningPatientId(null)}
style={{ position: 'absolute', inset: 0, background: 'rgba(15, 23, 42, 0.4)', backdropFilter: 'blur(2px)' }}
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
style={{
position: 'relative',
background: '#fff',
borderRadius: '20px',
width: '100%',
maxWidth: '450px',
overflow: 'hidden',
boxShadow: '0 20px 40px rgba(0,0,0,0.2)'
}}
>
<div style={{ padding: '20px 24px', borderBottom: '1px solid #f1f5f9', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#f8fafc' }}>
<h3 style={{ margin: 0, fontSize: '1.1rem', fontWeight: 800, color: '#1e293b' }}>Assign Department</h3>
<button
onClick={() => setAssigningPatientId(null)}
style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: '#94a3b8' }}
>
<X size={20} />
</button>
</div>
<div style={{ padding: '24px', maxHeight: '400px', overflowY: 'auto' }}>
<div style={{ marginBottom: '16px', fontSize: '0.85rem', color: '#64748b' }}>
Select target clinical unit for: <strong>{incomingPatients.find(p => p.id === assigningPatientId)?.name}</strong>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{departments.length > 0 ? departments.map(dept => (
<button
key={dept.id}
disabled={isAdmitting}
onClick={() => handleAdmit(assigningPatientId, dept.id)}
style={{
width: '100%',
padding: '14px 18px',
borderRadius: '12px',
border: '1px solid #e2e8f0',
background: '#fff',
textAlign: 'left',
cursor: isAdmitting ? 'not-allowed' : 'pointer',
transition: 'all 0.2s',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
onMouseEnter={(e) => e.currentTarget.style.borderColor = 'var(--accent-cyan)'}
onMouseLeave={(e) => e.currentTarget.style.borderColor = '#e2e8f0'}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: dept.isActive ? '#10b981' : '#cbd5e1' }} />
<div>
<div style={{ fontSize: '0.9rem', fontWeight: 700, color: '#1e293b' }}>{dept.name}</div>
<div style={{ fontSize: '0.7rem', color: '#94a3b8' }}>Available: {dept.availableBeds} Units</div>
</div>
</div>
<ChevronRight size={16} color="#94a3b8" />
</button>
)) : (
<div style={{ textAlign: 'center', padding: '20px', color: '#94a3b8', fontSize: '0.9rem' }}>
No active departments found.
</div>
)}
</div>
</div>
{isAdmitting && (
<div style={{ position: 'absolute', inset: 0, background: 'rgba(255,255,255,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10 }}>
<div className="spin" style={{ width: 24, height: 24, border: '2px solid #e2e8f0', borderTop: '2px solid var(--accent-cyan)', borderRadius: '50%' }} />
</div>
)}
</motion.div>
</div>
)}
</AnimatePresence>,
document.body
)}
</motion.div>
);
};

View File

@@ -0,0 +1,304 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
FileText,
Search,
Filter,
Download,
Printer,
Clock,
User,
Activity,
CheckCircle2,
ShieldCheck,
ExternalLink,
ChevronRight,
TrendingUp,
Pill,
Syringe,
X,
Stethoscope
} from 'lucide-react';
interface PCRRecord {
id: string;
date: string;
ambulance: string;
patient: string;
age: string;
gender: string;
triage: 'RED' | 'YELLOW' | 'GREEN';
status: 'RECEIVED' | 'PENDING_SIGN';
emt: string;
hospitalSign: string | null;
vitals: { hr: string; spo2: string; bp: string; rr: string };
meds: string[];
interventions: string[];
}
const PCR_DETAIL_VIEW = ({ record, onClose }: { record: PCRRecord; onClose: () => void }) => {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="pcr-modal-overlay"
onClick={onClose}
>
<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="pcr-detail-card"
onClick={e => e.stopPropagation()}
>
<div className="pcr-detail-header">
<div className="pcr-id-badge">
<FileText size={18} />
<span>RECORD {record.id}</span>
</div>
<button className="close-pcr-btn" onClick={onClose}><X size={20} /></button>
</div>
<div className="pcr-detail-content">
<section className="pcr-section patient-bio">
<div className="section-label">PATIENT INFORMATION</div>
<div className="bio-grid">
<div className="bio-item"><span className="label">Full Name:</span> <span className="value">{record.patient}</span></div>
<div className="bio-item"><span className="label">Age/Gender:</span> <span className="value">{record.age} / {record.gender}</span></div>
<div className="bio-item"><span className="label">Ambulance:</span> <span className="value">{record.ambulance} (ALS)</span></div>
<div className="bio-item"><span className="label">Lead EMT:</span> <span className="value">{record.emt}</span></div>
</div>
</section>
<div className="pcr-row-split">
<section className="pcr-section vitals-trends">
<div className="section-label"><TrendingUp size={14} /> VITAL SIGN TRENDS</div>
<div className="vitals-trend-grid">
<div className="trend-item">
<span className="label">HR (BPM)</span>
<div className="sparkline-placeholder" />
<span className="value">{record.vitals.hr}</span>
</div>
<div className="trend-item">
<span className="label">SpO2 (%)</span>
<div className="sparkline-placeholder green" />
<span className="value">{record.vitals.spo2}</span>
</div>
<div className="trend-item">
<span className="label">BP (mmHg)</span>
<span className="value">{record.vitals.bp}</span>
</div>
</div>
</section>
<section className="pcr-section interventions">
<div className="section-label"><Stethoscope size={14} /> INTERVENTIONS & MEDS</div>
<div className="med-list">
{record.meds.map((med, i) => (
<div key={i} className="med-item"><Pill size={12} /> {med}</div>
))}
{record.interventions.map((int, i) => (
<div key={i} className="med-item highlight"><Activity size={12} /> {int}</div>
))}
</div>
</section>
</div>
<section className="pcr-section hms-integration">
<div className="section-label">TRANSFER OF CARE & SIGN-OFF</div>
<div className="sign-off-panel">
{record.status === 'PENDING_SIGN' ? (
<div className="pending-sign-area">
<p>I verify that the patient and the associated ePCR have been received by the clinical team.</p>
<button className="confirm-sign-btn">DIGITALLY SIGN & SYNC TO HMIS</button>
</div>
) : (
<div className="completed-sign-area">
<ShieldCheck size={24} className="text-green-500" />
<div>
<div className="signed-by">Signed by: {record.hospitalSign}</div>
<div className="signed-at">Timestamp: {record.date} (Synced)</div>
</div>
</div>
)}
</div>
</section>
</div>
<div className="pcr-detail-footer">
<button className="pcr-action-btn primary"><Download size={16} /> DOWNLOAD PDF</button>
<button className="pcr-action-btn"><Printer size={16} /> PRINT RECORD</button>
</div>
</motion.div>
</motion.div>
);
};
export const EPCRRecords: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedPCR, setSelectedPCR] = useState<PCRRecord | null>(null);
const records: PCRRecord[] = [
{
id: 'PCR-8821',
date: '2026-05-05 12:30',
ambulance: 'KA-01-AM-102',
patient: 'Rajesh Khanna',
age: '45',
gender: 'M',
triage: 'RED',
status: 'RECEIVED',
emt: 'Arjun K.',
hospitalSign: 'Dr. Ramesh',
vitals: { hr: '124', spo2: '88', bp: '100/60', rr: '24' },
meds: ['Epinephrine 1mg', 'Amiodarone 300mg'],
interventions: ['CPR Performed', 'Intubation', 'IV Access Established']
},
{
id: 'PCR-8815',
date: '2026-05-05 11:15',
ambulance: 'KA-05-MN-88',
patient: 'Unknown Male',
age: '28',
gender: 'M',
triage: 'RED',
status: 'PENDING_SIGN',
emt: 'Suman R.',
hospitalSign: null,
vitals: { hr: '142', spo2: '82', bp: '80/40', rr: '28' },
meds: ['Normal Saline 500ml', 'TXA 1g'],
interventions: ['C-Collar Applied', 'Hemorrhage Control', 'Oxygen (15L)']
},
{
id: 'PCR-8798',
date: '2026-05-04 22:40',
ambulance: 'KA-03-KL-44',
patient: 'Amit Shah',
age: '58',
gender: 'M',
triage: 'GREEN',
status: 'RECEIVED',
emt: 'Kiran P.',
hospitalSign: 'Nurse Meera',
vitals: { hr: '82', spo2: '98', bp: '120/80', rr: '16' },
meds: [],
interventions: ['Splinting (Right Leg)']
},
];
return (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
className="module-content"
>
<div className="module-header-modern">
<div className="title-wrap">
<h3>ePCR RECEIPT & ARCHIVE</h3>
<div className="live-pill">
<span className="pulse" /> DIGITAL HANDOVER REPOSITORY
</div>
</div>
<div className="action-area-header">
<div className="search-mini">
<Search size={14} />
<input
type="text"
placeholder="Search by Patient, MRN, Incident ID..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<button className="register-staff-btn-premium">
<Filter size={18} />
<span className="hide-mobile">FILTERS</span>
</button>
</div>
</div>
<div className="epcr-stats-strip">
<div className="mini-stat">
<span className="label">TODAY'S ePCRs</span>
<span className="value">12</span>
</div>
<div className="mini-stat">
<span className="label">AWAITING SIGNATURE</span>
<span className="value-alert">01</span>
</div>
<div className="mini-stat">
<span className="label">SYNCED TO HMIS</span>
<span className="value">11</span>
</div>
</div>
<div className="epcr-table-container">
<table className="staff-table-premium">
<thead>
<tr>
<th>Record ID & Date</th>
<th>Ambulance & EMT</th>
<th>Patient Details</th>
<th>Triage & Clinical Status</th>
<th>Handoff Signature</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{records.map(record => (
<tr key={record.id} className="table-row-hover" onClick={() => setSelectedPCR(record)}>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div className="record-icon-mini"><FileText size={14} /></div>
<div>
<div className="record-id">{record.id}</div>
<div className="record-date">{record.date}</div>
</div>
</div>
</td>
<td>
<div className="record-meta">
<span className="reg-no">{record.ambulance}</span>
<span className="emt-name">{record.emt}</span>
</div>
</td>
<td>
<div className="record-patient">{record.patient}</div>
</td>
<td>
<div className="record-status-wrap">
<span className={`triage-tag triage-${record.triage.toLowerCase()}`}>{record.triage}</span>
<span className={`receive-status ${record.status.toLowerCase()}`}>
{record.status === 'RECEIVED' ? <CheckCircle2 size={12} /> : <Clock size={12} />}
{record.status.replace('_', ' ')}
</span>
</div>
</td>
<td>
{record.hospitalSign ? (
<div className="sign-badge">
<ShieldCheck size={12} />
{record.hospitalSign}
</div>
) : (
<button className="sign-action-btn">PENDING SIGN</button>
)}
</td>
<td>
<div className="action-row-mini">
<button className="tag-btn" title="View ePCR"><ExternalLink size={14} /></button>
<button className="tag-btn" title="Download PDF"><Download size={14} /></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<AnimatePresence>
{selectedPCR && <PCR_DETAIL_VIEW record={selectedPCR} onClose={() => setSelectedPCR(null)} />}
</AnimatePresence>
</motion.div>
);
};

View File

@@ -0,0 +1,166 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import {
Truck,
MapPin,
Activity,
Droplet,
AlertTriangle,
Filter,
Eye,
EyeOff,
Navigation,
CheckCircle2,
Clock,
ShieldCheck,
Search,
Users
} from 'lucide-react';
export const FleetView: React.FC = () => {
const [filterType, setFilterType] = useState('ALL');
const [isPrivateOnly, setIsPrivateOnly] = useState(false);
const fleet = [
{ id: 'AMB-01', type: 'ALS', status: 'RUNNING', patient: 'TR-1082', fuel: '75%', crew: 'EMT Arjun, Driver Som', eta: '4m', coords: [12.9716, 77.5946], network: 'PUBLIC' },
{ id: 'AMB-02', type: 'BLS', status: 'IDLE', patient: null, fuel: '92%', crew: 'EMT Suman, Driver Ravi', eta: '-', coords: [12.935, 77.6245], network: 'PRIVATE' },
{ id: 'AMB-03', type: 'Transport', status: 'BREAKDOWN', patient: null, fuel: '15%', crew: 'None', eta: '-', coords: [12.95, 77.6], alert: 'Engine Overheat', network: 'PUBLIC' },
];
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>FLEET VISIBILITY DASHBOARD</h3>
<div className="live-pill">
<span className="pulse" /> {fleet.filter(f => f.status === 'RUNNING').length} ACTIVE UNITS
</div>
</div>
<div className="action-area-header">
<div className="filter-group-premium">
<button className={`filter-chip ${!isPrivateOnly ? 'active' : ''}`} onClick={() => setIsPrivateOnly(false)}>NETWORK FLEET</button>
<button className={`filter-chip ${isPrivateOnly ? 'active' : ''}`} onClick={() => setIsPrivateOnly(true)}>PRIVATE FLEET</button>
</div>
<div className="search-mini">
<Search size={14} />
<input type="text" placeholder="Search Vehicle ID..." />
</div>
</div>
</div>
<div className="fleet-visibility-grid-premium">
<div className="map-command-surface">
<div className="map-overlay-vignette" />
<div className="map-scan-lines" />
<div className="map-placeholder-premium">
<div className="map-coordinate-grid" />
{fleet.map(unit => (
<motion.div
key={unit.id}
initial={{ scale: 0 }}
animate={{ scale: 1 }}
whileHover={{ scale: 1.2, zIndex: 100 }}
className={`node-marker-premium status-${unit.status.toLowerCase()}`}
style={{
left: `${(unit.coords[1] - 77.5) * 600}px`,
top: `${(13.0 - unit.coords[0]) * 600}px`
}}
>
<div className="marker-core">
<Truck size={14} />
</div>
<div className="marker-ping" />
<div className="marker-label-premium">
<span className="id">{unit.id}</span>
<span className="type">{unit.type}</span>
</div>
</motion.div>
))}
<div className="map-hud-controls">
<div className="hud-group">
<button className="hud-btn"><Navigation size={18} /></button>
<div className="hud-divider" />
<button className="hud-btn"><MapPin size={18} /></button>
</div>
<div className="hud-group zoom">
<button className="hud-btn">+</button>
<button className="hud-btn">-</button>
</div>
</div>
</div>
</div>
<div className="fleet-mission-sidebar">
<div className="sidebar-head">
<div className="head-text">
<h4>ACTIVE ASSETS</h4>
<p>REGION: BENGALURU-NORTH-X2</p>
</div>
<div className="status-summary-mini">
<span className="dot active" />
<span className="val">{fleet.length}</span>
</div>
</div>
<div className="fleet-unit-stack">
{fleet.map(unit => (
<motion.div
key={unit.id}
whileHover={{ x: 6 }}
className={`unit-mission-card ${unit.alert ? 'critical' : ''}`}
>
<div className="unit-card-header">
<div className="type-badge">{unit.type} UNIT</div>
<div className={`status-pill ${unit.status.toLowerCase()}`}>{unit.status}</div>
</div>
<div className="unit-card-body">
<h3 className="unit-id-large">{unit.id}</h3>
<div className="unit-stats-row">
<div className="u-stat">
<Droplet size={12} />
<span>{unit.fuel}</span>
</div>
<div className="u-stat">
<Clock size={12} />
<span className="highlight-cyan">{unit.eta || '--'} ETA</span>
</div>
</div>
{unit.patient && (
<div className="mission-active-indicator">
<div className="m-label">ACTIVE MISSION</div>
<div className="m-id">{unit.patient}</div>
</div>
)}
</div>
<div className="unit-card-footer">
<div className="crew-mini">
<Users size={12} />
<span>{unit.crew}</span>
</div>
<button className="unit-action-btn"><Eye size={14} /></button>
</div>
{unit.alert && (
<div className="unit-card-alert">
<AlertTriangle size={14} />
<span>{unit.alert.toUpperCase()}</span>
</div>
)}
</motion.div>
))}
</div>
</div>
</div>
</motion.div>
);
};

View File

@@ -0,0 +1,255 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import {
TrendingUp,
BarChart,
PieChart,
Clock,
Calendar,
Download,
Filter,
Activity,
Users,
Video,
Zap,
ArrowUpRight,
ArrowDownRight,
Search,
FileBarChart,
BrainCircuit,
Database
} from 'lucide-react';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
BarChart as ReBarChart,
Bar,
Cell,
PieChart as RePieChart,
Pie,
LineChart,
Line
} from 'recharts';
export const HospitalAnalytics: React.FC = () => {
const [timeRange, setTimeRange] = useState('7D');
const volumeData = [
{ name: '00:00', count: 5, baseline: 4 },
{ name: '04:00', count: 12, baseline: 8 },
{ name: '08:00', count: 28, baseline: 22 },
{ name: '12:00', count: 42, baseline: 35 },
{ name: '16:00', count: 38, baseline: 40 },
{ name: '20:00', count: 22, baseline: 18 },
{ name: '23:59', count: 8, baseline: 6 },
];
const triageData = [
{ name: 'Red (Critical)', value: 25, color: 'hsl(0, 84%, 60%)' },
{ name: 'Yellow (Emergent)', value: 45, color: 'hsl(38, 92%, 50%)' },
{ name: 'Green (Standard)', value: 30, color: 'hsl(142, 70%, 45%)' },
];
const departmentEfficiency = [
{ dept: 'ER', load: 85, cap: 90 },
{ dept: 'ICU', load: 92, cap: 80 },
{ dept: 'Cardio', load: 60, cap: 75 },
{ dept: 'Trauma', load: 78, cap: 85 },
];
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="module-content analytics-v2"
>
<div className="module-header-modern">
<div className="title-wrap">
<div className="clinical-intel-badge">
<BrainCircuit size={16} />
CONSOLIDATED CLINICAL INTELLIGENCE
</div>
<h3>Hospital Command Insights</h3>
</div>
<div className="action-area-header">
<div className="intel-filters">
{['24H', '7D', '30D', 'ALL'].map(t => (
<button
key={t}
className={`intel-filter-btn ${timeRange === t ? 'active' : ''}`}
onClick={() => setTimeRange(t)}
>
{t}
</button>
))}
</div>
<button className="register-staff-btn-premium">
<FileBarChart size={18} />
<span className="hide-mobile">EXTRACT REPORT</span>
</button>
</div>
</div>
<div className="intelligence-grid">
{/* Row 1: High Level KPIs */}
<div className="kpi-strip">
<div className="kpi-card-modern neon-blue">
<div className="kpi-icon"><Activity size={20} /></div>
<div className="kpi-info">
<label>System Utilization</label>
<div className="kpi-value-wrap">
<span className="value">84.2%</span>
<span className="trend positive">+2.4%</span>
</div>
</div>
</div>
<div className="kpi-card-modern neon-purple">
<div className="kpi-icon"><Video size={20} /></div>
<div className="kpi-info">
<label>TeleLink Saturation</label>
<div className="kpi-value-wrap">
<span className="value">62/h</span>
<span className="trend positive">+18%</span>
</div>
</div>
</div>
<div className="kpi-card-modern neon-green">
<div className="kpi-icon"><Users size={20} /></div>
<div className="kpi-info">
<label>Total Intake (v24)</label>
<div className="kpi-value-wrap">
<span className="value">342</span>
<span className="trend neutral">STABLE</span>
</div>
</div>
</div>
<div className="kpi-card-modern neon-amber">
<div className="kpi-icon"><Clock size={20} /></div>
<div className="kpi-info">
<label>P-ETA Variance</label>
<div className="kpi-value-wrap">
<span className="value">-1.2m</span>
<span className="trend positive">IMPROVED</span>
</div>
</div>
</div>
</div>
{/* Row 2: Main Traffic & Triage */}
<div className="analytics-body-layout">
<div className="intel-card large">
<div className="card-top">
<div className="card-title">
<TrendingUp size={16} />
<span>Patient Inflow / Capacity Overlap</span>
</div>
<div className="legend-mini">
<span className="dot current" /> Live Intake
<span className="dot base" /> Expected
</div>
</div>
<div className="viewport-chart">
<ResponsiveContainer width="100%" height={320}>
<AreaChart data={volumeData}>
<defs>
<linearGradient id="colorIntake" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--accent-cyan)" stopOpacity={0.1}/>
<stop offset="95%" stopColor="var(--accent-cyan)" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsla(215, 32%, 17%, 0.05)" />
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fontSize: 10, fontWeight: 750, fill: 'var(--text-muted)' }} />
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 10, fontWeight: 750, fill: 'var(--text-muted)' }} />
<Tooltip
contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: 'var(--shadow-premium)', background: '#fff' }}
/>
<Area type="monotone" dataKey="count" stroke="var(--accent-cyan)" strokeWidth={3} fillOpacity={1} fill="url(#colorIntake)" />
<Line type="monotone" dataKey="baseline" stroke="var(--text-muted)" strokeDasharray="5 5" strokeWidth={1} dot={false} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
<div className="intel-card side">
<div className="card-top">
<div className="card-title">
<PieChart size={16} />
<span>Urgency Index</span>
</div>
</div>
<div className="viewport-chart donut">
<ResponsiveContainer width="100%" height={240}>
<RePieChart>
<Pie
data={triageData}
innerRadius={65}
outerRadius={85}
paddingAngle={8}
dataKey="value"
>
{triageData.map((e, idx) => <Cell key={idx} fill={e.color} stroke="none" />)}
</Pie>
<Tooltip />
</RePieChart>
</ResponsiveContainer>
<div className="donut-center-label">
<span className="v">126</span>
<span className="l">TOTAL</span>
</div>
</div>
<div className="pie-legend-v2">
{triageData.map(t => (
<div key={t.name} className="legend-row">
<div className="l-side">
<div className="l-dot" style={{ background: t.color }} />
<span className="l-name">{t.name.split(' ')[0]}</span>
</div>
<span className="l-val">{t.value}%</span>
</div>
))}
</div>
</div>
</div>
{/* Row 3: Dept Load & Alerts */}
<div className="intelligence-footer-grid">
<div className="intel-card medium no-bg">
<div className="card-title small">DEPARTMENTAL LOAD (HEATMAP)</div>
<div className="dept-load-stack">
{departmentEfficiency.map(d => (
<div key={d.dept} className="dept-progress-block">
<div className="d-info">
<span className="d-name">{d.dept}</span>
<span className="d-val">{d.load}% Load</span>
</div>
<div className="d-bar-bg">
<div className="d-bar-fill" style={{ width: `${d.load}%`, background: d.load > 85 ? 'var(--alert-red)' : 'var(--accent-cyan)' }} />
</div>
</div>
))}
</div>
</div>
<div className="intel-card medium glass-alert">
<div className="card-title small alert-mode"><Activity size={14} /> LIVE ANOMALY DETECTION</div>
<div className="anomaly-list">
<div className="anomaly-item">
<span className="time">14:02</span>
<p>Suboptimal triage time detected in ER South Cluster.</p>
</div>
<div className="anomaly-item warning">
<span className="time">13:45</span>
<p>TeleLink latency spike (+45ms) reported in Hub 4.</p>
</div>
</div>
</div>
</div>
</div>
</motion.div>
);
};

View File

@@ -0,0 +1,141 @@
import React from 'react';
import { motion } from 'framer-motion';
import {
Activity,
Building2,
Users,
ChevronRight,
AlertTriangle,
} from 'lucide-react';
interface HospitalSelectorProps {
hospitals: any[];
isLoading: boolean;
onSelect: (hospital: any) => void;
}
export const HospitalSelector: React.FC<HospitalSelectorProps> = ({
hospitals,
isLoading,
onSelect,
}) => {
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 className="hospital-grid">
{isLoading ? (
<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.95, y: 30 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{
delay: idx * 0.08,
duration: 0.6,
ease: [0.16, 1, 0.3, 1]
}}
whileHover={{ y: -8, boxShadow: '0 20px 40px hsla(215, 32%, 17%, 0.12)' }}
className="hospital-select-card-premium"
onClick={() => onSelect(h)}
>
<div className="card-top-accent" />
<div className="h-icon-cluster">
<div className="h-icon-orb">
<Building2 size={32} />
</div>
<div className="h-status-indicator pulse-slow" data-status={h.status.toLowerCase().replace(' ', '-')} />
</div>
<div className="h-card-content">
<span className="h-type-tag">{h.type.toUpperCase()} NODE</span>
<h3 className="h-name-premium">{h.name}</h3>
<div className="h-meta-grid">
<div className="h-meta-item">
<Users size={14} />
<span>{h.admin || 'Dr. Admin'}</span>
</div>
<div className="h-meta-item">
<Activity size={14} />
<span>{h.status}</span>
</div>
</div>
<div className="capacity-section">
<div className="capacity-header">
<span>LOAD CAPACITY</span>
<span className="capacity-val">{h.beds}</span>
</div>
<div className="capacity-track">
<motion.div
className="capacity-fill"
initial={{ width: 0 }}
animate={{
width: (parseInt(h.beds) / parseInt(h.beds.split('/')[1] || '100')) * 100 + '%'
}}
transition={{ delay: 0.5 + idx * 0.1, duration: 1 }}
/>
</div>
</div>
</div>
<button className="h-access-btn">
INITIALIZE SESSION <ChevronRight size={16} />
</button>
</motion.div>
))
)}
</div>
</motion.div>
</div>
);
};

View File

@@ -0,0 +1,136 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import {
Database,
Search,
Calendar,
TrendingUp,
Clock,
FileText,
User,
Activity,
MapPin,
ChevronRight,
Filter,
CheckCircle2,
Video
} from 'lucide-react';
export const PatientArchive: React.FC = () => {
const [activeTab, setActiveTab] = useState<'LIST' | 'TIMELINE'>('LIST');
const archive = [
{ id: 'INC-9012', date: '2026-05-04', name: 'Rajit Bose', age: 52, gender: 'M', triage: 'RED', category: 'CARDIAC', outcome: 'DISCHARGED', ambulance: 'ALS-02', teleLink: true },
{ id: 'INC-8955', date: '2026-05-03', name: 'Sana Khan', age: 24, gender: 'F', triage: 'YELLOW', category: 'TRAUMA', outcome: 'ADMITTED', ambulance: 'BLS-09', teleLink: false },
{ id: 'INC-8940', date: '2026-05-03', name: 'Unknown Male', age: 0, gender: 'M', triage: 'RED', category: 'STROKE', outcome: 'DECEASED', ambulance: 'ALS-01', teleLink: true },
];
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="module-content"
>
<div className="module-header-modern">
<div className="title-wrap">
<h3>PATIENT & INCIDENT HISTORY</h3>
<div className="live-pill">
<span className="pulse" /> COMPREHENSIVE CARE RECORDS
</div>
</div>
<div className="action-area-header">
<div className="date-picker-mini">
<Calendar size={14} />
<span>LAST 30 DAYS</span>
</div>
<div className="search-mini">
<Search size={14} />
<input type="text" placeholder="Search by ID, Name, MRN..." />
</div>
<button className="register-staff-btn-premium">
<Filter size={18} />
</button>
</div>
</div>
<div className="archive-grid">
<div className="archive-list-panel">
<table className="staff-table-premium">
<thead>
<tr>
<th>Patient ID & Date</th>
<th>Category</th>
<th>Provider Details</th>
<th>Outcome</th>
<th>History</th>
</tr>
</thead>
<tbody>
{archive.map(p => (
<tr key={p.id} className="archive-row-hover">
<td>
<div className="patient-main-ident">
<strong>{p.name}</strong>
<span>{p.id} · {p.date}</span>
</div>
</td>
<td>
<div className="cat-badge-wrap">
<span className={`cat-pill cat-${p.category.toLowerCase()}`}>{p.category}</span>
<span className={`triage-tag triage-${p.triage.toLowerCase()}`}>{p.triage}</span>
</div>
</td>
<td>
<div className="amb-emt-meta">
<span>{p.ambulance}</span>
{p.teleLink && <Video size={10} style={{ color: 'var(--accent-cyan)' }} />}
</div>
</td>
<td>
<span className={`outcome-pill outcome-${p.outcome.toLowerCase()}`}>{p.outcome}</span>
</td>
<td>
<button className="tag-btn"><ChevronRight size={14} /></button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="archive-metrics-side">
<div className="side-metric-card">
<h4>Outcome Distribution</h4>
<div className="metric-bar-chart">
{/* Simple Mock Bars */}
<div className="bar-row">
<span>Admitted</span>
<div className="bar-container"><div className="bar-val" style={{ width: '65%', background: 'var(--accent-cyan)' }} /></div>
<span className="perc">65%</span>
</div>
<div className="bar-row">
<span>Discharged</span>
<div className="bar-container"><div className="bar-val" style={{ width: '25%', background: 'var(--accent-green)' }} /></div>
<span className="perc">25%</span>
</div>
<div className="bar-row">
<span>Referred</span>
<div className="bar-container"><div className="bar-val" style={{ width: '10%', background: 'var(--warning-amber)' }} /></div>
<span className="perc">10%</span>
</div>
</div>
</div>
<div className="side-metric-card">
<h4>Timeline Insight (Scene to ED)</h4>
<div className="avg-time-display">
<Clock size={24} style={{ color: 'var(--accent-cyan)' }} />
<div className="time-val">28.4 <small>min</small></div>
<span className="trend down"> 2m vs Last Month</span>
</div>
</div>
</div>
</div>
</motion.div>
);
};

View File

@@ -0,0 +1,121 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import {
Hospital,
MapPin,
PhoneCall,
Mail,
Award,
Plus,
Settings,
ShieldCheck,
User,
ChevronRight,
Search,
Building2,
Trash2,
Edit2
} from 'lucide-react';
export const ReferralsSetup: React.FC = () => {
const [isAddOpen, setIsAddOpen] = useState(false);
const referrals = [
{ id: 'REF-01', name: 'Narayana Health City', type: 'Tertiary Care', specialty: 'CARDIAC, ONCOLOGY', contact: 'Dr. Vikram - 9845011223', address: 'Hosur Road, Bangalore', status: 'ACTIVE' },
{ id: 'REF-02', name: 'St. Johns Medical College', type: 'Teaching Hospital', specialty: 'TRAUMA, PEDIATRICS', contact: 'Coordinator Ravi - 9845099881', address: 'Koramangala, Bangalore', status: 'ACTIVE' },
{ id: 'REF-03', name: 'Nimhans', type: 'Psychiatric/Neuro', specialty: 'NEUROLOGY, NEUROSURGERY', contact: 'ER Helpdesk - 080-26995000', address: 'Hosur Road, Bangalore', status: 'ACTIVE' },
];
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>REFERRAL HOSPITAL NETWORK</h3>
<div className="live-pill">
<span className="pulse" /> {referrals.length} TRUSTED PARTNERS
</div>
</div>
<div className="action-area-header">
<button className="register-staff-btn-premium" onClick={() => setIsAddOpen(true)}>
<Plus size={18} />
<span>ADD REFERRAL NODE</span>
</button>
</div>
</div>
<div className="referral-grid-layout">
<div className="referral-list-main">
{referrals.map(ref => (
<div key={ref.id} className="referral-node-card">
<div className="node-icon-bubble">
<Building2 size={24} />
</div>
<div className="node-info-content">
<div className="node-top-row">
<div className="node-title-stack">
<h4>{ref.name}</h4>
<span className="node-type">{ref.type}</span>
</div>
<div className="node-badge-active">PARTNERED</div>
</div>
<div className="node-details-grid">
<div className="d-item">
<Award size={14} />
<span>Specialties: {ref.specialty}</span>
</div>
<div className="d-item">
<MapPin size={14} />
<span>{ref.address}</span>
</div>
<div className="d-item">
<PhoneCall size={14} />
<span>{ref.contact}</span>
</div>
</div>
<div className="node-footer-actions">
<button className="node-action"><Edit2 size={14} /> Configure Notifications</button>
<button className="node-action-danger"><Trash2 size={14} /> Remove Node</button>
</div>
</div>
</div>
))}
</div>
<div className="referral-settings-side">
<div className="side-card-premium">
<div className="card-header-mini">
<Settings size={16} />
<h4>Global Referral Preferences</h4>
</div>
<div className="setting-control-row">
<div className="setting-info">
<strong>Auto-Pre-Alert</strong>
<span>Notify referral hub immediately on booking</span>
</div>
<input type="checkbox" defaultChecked />
</div>
<div className="setting-control-row">
<div className="setting-info">
<strong>Mortuary Referral</strong>
<span>Enable deceased-outcome routing logic</span>
</div>
<input type="checkbox" defaultChecked />
</div>
</div>
<div className="side-card-premium dark-accent">
<ShieldCheck size={32} style={{ opacity: 0.5, marginBottom: '16px' }} />
<h4>Protocol Compliance</h4>
<p style={{ fontSize: '0.8rem', opacity: 0.8, lineHeight: 1.5 }}>
All outgoing referrals are logged and linked to active ePCR data for legal handoff audit.
</p>
</div>
</div>
</div>
</motion.div>
);
};

View File

@@ -0,0 +1,502 @@
import React from 'react';
import { motion } from 'framer-motion';
import {
Building2,
Users,
LayoutDashboard,
Stethoscope,
Search,
Plus,
Edit2,
Phone,
Settings,
MoreVertical,
Activity
} from 'lucide-react';
import { Card } from '../../components/Common';
interface SetupPanelProps {
setupSubTab: string;
onSubTabChange: (tab: string) => void;
selectedHospital: any;
setSelectedHospital: (h: any) => void;
departments: any[];
setDepartments: (d: any[]) => void;
allUsers: any[];
onSaveProfile: () => void;
onOpenStaffModal: () => void;
onEditStaff: (user: any) => void;
onOpenDeptModal: () => void;
}
export const SetupPanel: React.FC<SetupPanelProps> = ({
setupSubTab,
onSubTabChange,
selectedHospital,
setSelectedHospital,
departments,
setDepartments,
allUsers,
onSaveProfile,
onOpenStaffModal,
onEditStaff,
onOpenDeptModal,
}) => {
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={() => onSubTabChange('PROFILE')}
>
<Building2 size={14} /> <span>Profile</span>
</button>
<button
className={`setup-nav-item ${
setupSubTab === 'USERS' ? 'active' : ''
}`}
onClick={() => onSubTabChange('USERS')}
>
<Users size={14} /> <span>Users</span>
</button>
<button
className={`setup-nav-item ${
setupSubTab === 'DEPTS' ? 'active' : ''
}`}
onClick={() => onSubTabChange('DEPTS')}
>
<LayoutDashboard size={14} /> <span>Departments</span>
</button>
<button
className={`setup-nav-item ${
setupSubTab === 'BEDS' ? 'active' : ''
}`}
onClick={() => onSubTabChange('BEDS')}
>
<Stethoscope size={14} /> <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 }}
style={{ display: 'block' }}
>
<Card title="Hospital Profile & Configuration">
<div className="setup-form-modern" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
<div className="form-group-premium" style={{ gridColumn: '1 / -1' }}>
<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" style={{ gridColumn: '1 / -1' }}>
<label>Physical Address</label>
<input
type="text"
className="setup-input-premium"
value={selectedHospital?.address || ''}
onChange={(e) =>
setSelectedHospital({
...selectedHospital,
address: e.target.value,
})
}
/>
</div>
<div className="form-group-premium">
<label>Emergency Phone</label>
<input
type="text"
className="setup-input-premium"
value={selectedHospital?.emergency_phone || ''}
onChange={(e) =>
setSelectedHospital({
...selectedHospital,
emergency_phone: e.target.value,
})
}
/>
</div>
<div className="form-group-premium">
<label>Contact Phone</label>
<input
type="text"
className="setup-input-premium"
value={selectedHospital?.contact_phone || ''}
onChange={(e) =>
setSelectedHospital({
...selectedHospital,
contact_phone: e.target.value,
})
}
/>
</div>
<div className="form-group-premium" style={{ gridColumn: '1 / -1' }}>
<label>Contact Email</label>
<input
type="email"
className="setup-input-premium"
value={selectedHospital?.contact_email || ''}
onChange={(e) =>
setSelectedHospital({
...selectedHospital,
contact_email: e.target.value,
})
}
/>
</div>
<div className="form-group-premium">
<label>GPS Latitude</label>
<input
type="number"
className="setup-input-premium"
value={selectedHospital?.gps_lat || ''}
onChange={(e) =>
setSelectedHospital({
...selectedHospital,
gps_lat: e.target.value,
})
}
/>
</div>
<div className="form-group-premium">
<label>GPS Longitude</label>
<input
type="number"
className="setup-input-premium"
value={selectedHospital?.gps_lon || ''}
onChange={(e) =>
setSelectedHospital({
...selectedHospital,
gps_lon: e.target.value,
})
}
/>
</div>
<div className="form-group-premium" style={{ gridColumn: '1 / -1', display: 'flex', alignItems: 'center', gap: '12px', padding: '10px 0' }}>
<input
type="checkbox"
id="nabh_status"
checked={selectedHospital?.nabh_status || false}
onChange={(e) =>
setSelectedHospital({
...selectedHospital,
nabh_status: e.target.checked,
})
}
style={{ width: '18px', height: '18px', cursor: 'pointer' }}
/>
<label htmlFor="nabh_status" style={{ margin: 0, cursor: 'pointer', fontSize: '1rem', fontWeight: 'bold' }}>NABH Accredited Institution</label>
</div>
<div style={{ gridColumn: '1 / -1', marginTop: '16px' }}>
<button
className="setup-confirm-btn"
onClick={onSaveProfile}
>
Save Configuration
</button>
</div>
</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={onOpenStaffModal}
>
<Plus size={16} /> Add 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>
<th style={{ textAlign: 'right' }}>Action</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>
<td style={{ textAlign: 'right' }}>
<button
className="btn-icon"
onClick={() => onEditStaff(u)}
style={{ background: 'transparent', border: 'none', color: 'var(--accent-cyan)', cursor: 'pointer' }}
title="Edit Staff"
>
<Edit2 size={16} />
</button>
</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 Node Registry">
<div className="setup-actions-bar">
<div className="search-mini">
<Search size={16} />
<input type="text" placeholder="Search departments..." />
</div>
<button
className="register-staff-btn-premium"
onClick={onOpenDeptModal}
>
<Plus size={16} /> ADD DEPARTMENT
</button>
</div>
<div className="table-responsive-premium">
<table className="staff-table-premium">
<thead>
<tr>
<th>Department / Clinical Node</th>
<th>Leadership</th>
<th>Resource Allocation</th>
<th>Communication</th>
<th>Ops Status</th>
<th style={{ textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody>
{departments.map((d, i) => {
const occupancy = ((d.occupiedBeds || 0) / (d.totalBedsCapacity || 1)) * 100;
const isHighOccupancy = occupancy > 85;
return (
<motion.tr
key={i}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.05 }}
>
<td style={{ fontWeight: 800 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{
width: '36px',
height: '36px',
background: 'var(--accent-cyan-soft)',
borderRadius: '10px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--accent-cyan)'
}}>
<Activity size={18} />
</div>
<div>
<div style={{ fontSize: '0.95rem', color: '#1e293b' }}>{d.name}</div>
<div style={{ fontSize: '0.65rem', color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Node: {d.id?.substring(0, 8)}</div>
</div>
</div>
</td>
<td>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={{ fontWeight: 700, fontSize: '0.85rem' }}>{d.headOfDepartment}</span>
<span style={{ fontSize: '0.7rem', color: '#94a3b8' }}>Chief of Medicine</span>
</div>
</td>
<td>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', minWidth: '140px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.75rem', fontWeight: 700 }}>
<span style={{ color: isHighOccupancy ? '#ef4444' : '#64748b' }}>{d.occupiedBeds}/{d.totalBedsCapacity} Units</span>
<span style={{ color: '#94a3b8' }}>{Math.round(occupancy)}%</span>
</div>
<div style={{ height: '6px', width: '100%', background: '#f1f5f9', borderRadius: '3px', overflow: 'hidden' }}>
<div style={{
height: '100%',
width: `${occupancy}%`,
background: isHighOccupancy ? '#ef4444' : 'var(--accent-cyan)',
borderRadius: '3px'
}} />
</div>
<div style={{ fontSize: '0.65rem', color: '#10b981', fontWeight: 600 }}>{d.availableBeds} Units Available</div>
</div>
</td>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#475569', fontSize: '0.85rem' }}>
<Phone size={14} style={{ color: '#94a3b8' }} />
{d.contactPhone || 'No direct line'}
</div>
</td>
<td>
<span className={`node-status-text ${d.isActive ? 'ON' : 'OFF'}`} style={{ padding: '4px 10px', borderRadius: '20px', fontSize: '0.65rem', display: 'inline-flex', alignItems: 'center', gap: '6px' }}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: d.isActive ? '#10b981' : '#94a3b8' }} />
{d.isActive ? 'OPERATIONAL' : 'INACTIVE'}
</span>
</td>
<td style={{ textAlign: 'right' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
<button
className={`toggle-btn-modern ${d.isActive ? 'disable' : 'enable'}`}
style={{ padding: '6px 12px', fontSize: '0.65rem', height: '30px' }}
onClick={() => {
const n = [...departments];
n[i].isActive = !n[i].isActive;
setDepartments(n);
}}
>
{d.isActive ? 'OFFLINE' : 'ONLINE'}
</button>
<button className="btn-icon" style={{ background: '#f8fafc', borderRadius: '8px', padding: '6px' }}>
<Settings size={14} color="#64748b" />
</button>
</div>
</td>
</motion.tr>
);
})}
</tbody>
</table>
</div>
</Card>
</motion.div>
)}
{setupSubTab === 'BEDS' && (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
>
<Card title="Inventory &amp; 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;
const tot = b.totalBedsCapacity || 1;
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>
</td>
</tr>
);
})}
</tbody>
</table>
</Card>
</motion.div>
)}
</div>
</div>
</motion.div>
);
};

View File

@@ -0,0 +1,389 @@
import React from 'react';
import { motion } from 'framer-motion';
import { X, Eye, EyeOff, User, Shield, Briefcase, FileText } from 'lucide-react';
interface StaffModalProps {
isOpen: boolean;
onClose: () => void;
staffFormData: any;
setStaffFormData: (data: any) => void;
rolesList: any[];
showPassword: boolean;
setShowPassword: (show: boolean) => void;
onSubmit: () => void;
isEditing?: boolean;
}
export const StaffModal: React.FC<StaffModalProps> = ({
isOpen,
onClose,
staffFormData,
setStaffFormData,
rolesList,
showPassword,
setShowPassword,
onSubmit,
isEditing = false,
}) => {
if (!isOpen) return null;
const role = staffFormData.role || 'ED_DOCTOR';
const renderRoleSpecificFields = () => {
switch (role) {
case 'ED_DOCTOR':
return (
<>
<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="e.g. Emergency Medicine"
/>
</div>
<div className="form-group-premium">
<label>License Number</label>
<input
type="text"
className="setup-input-premium"
value={staffFormData.license_number || ''}
onChange={(e) => setStaffFormData({ ...staffFormData, license_number: e.target.value })}
placeholder="e.g. MC-998877"
/>
</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="e.g. Emergency Operations"
/>
</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="e.g. ERCP Specialist"
/>
</div>
<div className="form-group-premium">
<label>Shift Schedule</label>
<input
type="text"
className="setup-input-premium"
value={staffFormData.shift || ''}
onChange={(e) => setStaffFormData({ ...staffFormData, shift: e.target.value })}
placeholder="e.g. Rotational"
/>
</div>
</>
);
case 'Hospital Coordinator':
return (
<>
<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="e.g. Triage Section"
/>
</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="e.g. Quick Response Lead"
/>
</div>
<div className="form-group-premium">
<label>Shift Schedule</label>
<input
type="text"
className="setup-input-premium"
value={staffFormData.shift || ''}
onChange={(e) => setStaffFormData({ ...staffFormData, shift: e.target.value })}
placeholder="e.g. DAY"
/>
</div>
<div className="form-group-premium">
<label>Languages Spoken (comma separated)</label>
<input
type="text"
className="setup-input-premium"
value={staffFormData.languages || ''}
onChange={(e) => setStaffFormData({ ...staffFormData, languages: e.target.value })}
placeholder="e.g. English, Tamil, Hindi"
/>
</div>
</>
);
case 'Hospital Nurse':
return (
<>
<div className="form-group-premium">
<label>Employee ID</label>
<input
type="text"
className="setup-input-premium"
value={staffFormData.employee_id || ''}
onChange={(e) => setStaffFormData({ ...staffFormData, employee_id: e.target.value })}
placeholder="e.g. MIO-N005"
/>
</div>
<div className="form-group-premium">
<label>Organization ID</label>
<input
type="text"
className="setup-input-premium"
value={staffFormData.org_id || ''}
onChange={(e) => setStaffFormData({ ...staffFormData, org_id: e.target.value })}
placeholder="Auto-populated if left blank"
/>
</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="e.g. Emergency Room (ER)"
/>
</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="e.g. Staff Nurse (Grade II)"
/>
</div>
<div className="form-group-premium">
<label>Shift Schedule</label>
<input
type="text"
className="setup-input-premium"
value={staffFormData.shift || ''}
onChange={(e) => setStaffFormData({ ...staffFormData, shift: e.target.value })}
placeholder="e.g. NIGHT"
/>
</div>
<div className="form-group-premium">
<label>Assigned Floor / Ward</label>
<input
type="text"
className="setup-input-premium"
value={staffFormData.assigned_floor || ''}
onChange={(e) => setStaffFormData({ ...staffFormData, assigned_floor: e.target.value })}
placeholder="e.g. 2nd Floor - Trauma Wing"
/>
</div>
<div className="form-group-premium">
<label>Languages Spoken (comma separated)</label>
<input
type="text"
className="setup-input-premium"
value={staffFormData.languages || ''}
onChange={(e) => setStaffFormData({ ...staffFormData, languages: e.target.value })}
placeholder="e.g. English, Tamil"
/>
</div>
</>
);
default:
// Generic fallback fields
return (
<>
<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 })}
/>
</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 })}
/>
</div>
<div className="form-group-premium">
<label>Shift Schedule</label>
<input
type="text"
className="setup-input-premium"
value={staffFormData.shift || ''}
onChange={(e) => setStaffFormData({ ...staffFormData, shift: e.target.value })}
/>
</div>
</>
);
}
};
return (
<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"
style={{ maxWidth: '800px', width: '90%' }} // Made wider for two columns
>
<div className="modal-header-premium">
<h3>{isEditing ? 'Update Personnel' : 'New Staff Registration'}</h3>
<button className="modal-close-btn" onClick={onClose}>
<X size={18} />
</button>
</div>
<div className="setup-form-modern" style={{ maxHeight: '65vh', overflowY: 'auto', padding: '24px' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '28px' }}>
{/* LEFT COLUMN: Identity & Auth */}
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '14px', paddingBottom: '8px', borderBottom: '1px solid var(--card-border)' }}>
<User size={16} style={{ color: 'var(--accent-cyan)' }} /> <h4 style={{ margin: 0, fontSize: '0.8rem', textTransform: 'uppercase', color: 'var(--text-secondary)', fontWeight: 600, letterSpacing: '0.04em' }}>Identity & Access</h4>
</div>
<div style={{ display: 'flex', flexDirection: 'column', 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.length > 0 ? (
rolesList.map((r: any) => (
<option key={r.id} value={r.name}>
{r.name}
</option>
))
) : (
<>
<option value="ED_DOCTOR">ED_DOCTOR</option>
<option value="Hospital Coordinator">Hospital Coordinator</option>
<option value="Hospital Nurse">Hospital Nurse</option>
</>
)}
</select>
</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. +919876543210"
/>
</div>
<div className="form-group-premium">
<label>System Username</label>
<input
type="text"
className="setup-input-premium"
value={staffFormData.username}
onChange={(e) => setStaffFormData({ ...staffFormData, username: e.target.value })}
placeholder="e.g. jsmith.ed"
autoComplete="new-password"
/>
</div>
<div className="form-group-premium">
<label>{isEditing ? 'New Password (leave blank to keep current)' : '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={isEditing ? 'Enter new password...' : '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>
</div>
{/* RIGHT COLUMN: Role Specific Data */}
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '14px', paddingBottom: '8px', borderBottom: '1px solid var(--card-border)' }}>
<Briefcase size={16} style={{ color: 'var(--accent-cyan)' }} /> <h4 style={{ margin: 0, fontSize: '0.8rem', textTransform: 'uppercase', color: 'var(--text-secondary)', fontWeight: 600, letterSpacing: '0.04em' }}>Professional Details</h4>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{renderRoleSpecificFields()}
</div>
</div>
</div>
</div>
<div className="modal-footer-premium">
<button className="btn-secondary-glass" onClick={onClose}>
Cancel
</button>
<button className="btn-primary-glass" onClick={onSubmit}>
{isEditing ? 'Update Personnel' : 'Register Personnel'}
</button>
</div>
</motion.div>
</div>
);
};

View File

@@ -0,0 +1,218 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Video,
VideoOff,
Mic,
MicOff,
PhoneOff,
User,
Activity,
MessageSquare,
ClipboardList,
AlertCircle,
MoreVertical,
Shield,
Clock,
CheckCircle2,
XCircle,
MessageCircle
} from 'lucide-react';
import { Card } from '../../components/Common';
export const TeleLinkHub: React.FC = () => {
const [activeCall, setActiveCall] = useState<any>(null);
const [activeTab, setActiveTab] = useState<'CALL' | 'HISTORY'>('CALL');
const incomingRequests = [
{ id: 'TL-101', patient: 'Rajesh Khanna', triage: 'RED', wait: '1:45', complaint: 'Cardiac Arrest', symptoms: 'Ongoing CPR, 2 shocks delivered' },
{ id: 'TL-102', patient: 'Unknown Male', triage: 'RED', wait: '0:30', complaint: 'Potential Stroke', symptoms: 'GCS 12, unilateral weakness' },
];
const callHistory = [
{ id: 'TL-098', date: '2026-05-04', duration: '12m 45s', patient: 'Amit Shah', outcome: 'Admission Admitted', emt: 'Arjun K.' },
{ id: 'TL-095', date: '2026-05-04', duration: '08m 20s', patient: 'Priya Verma', outcome: 'Consult Completed', emt: 'Suman R.' },
];
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="module-content"
>
<div className="module-header-modern">
<div className="title-wrap">
<h3>TELELINK COMMAND CENTER</h3>
<div className="live-pill">
<span className="pulse" /> ERCP PHYSICIAN CONSOLE
</div>
</div>
<div className="tab-switcher-premium">
<button
className={`tab-btn ${activeTab === 'CALL' ? 'active' : ''}`}
onClick={() => setActiveTab('CALL')}
>
ACTIVE SESSIONS
</button>
<button
className={`tab-btn ${activeTab === 'HISTORY' ? 'active' : ''}`}
onClick={() => setActiveTab('HISTORY')}
>
CONSULT HISTORY
</button>
</div>
</div>
{activeTab === 'CALL' ? (
<div className="telelink-main-grid">
{/* Call Queue */}
<div className="call-queue-panel">
<div className="panel-header-mini">
<h4>INCOMING REQUESTS</h4>
<span className="count-badge">{incomingRequests.length}</span>
</div>
<div className="queue-list">
{incomingRequests.map(req => (
<div key={req.id} className={`queue-item triage-${req.triage.toLowerCase()}`}>
<div className="q-header">
<span className="q-id">{req.id}</span>
<span className="q-wait"><Clock size={12} /> {req.wait}</span>
</div>
<div className="q-body">
<div className="q-patient">{req.patient}</div>
<div className="q-complaint">{req.complaint}</div>
</div>
<div className="q-actions">
<button className="q-btn accept" onClick={() => setActiveCall(req)}>ACCEPT</button>
<button className="q-btn decline">DECLINE</button>
</div>
</div>
))}
</div>
</div>
{/* Active Call Surface */}
<div className="call-surface-panel-premium">
<div className="surface-glass-glow" />
{activeCall ? (
<div className="active-call-grid">
<div className="video-workspace">
<div className="video-feed-main">
<div className="feed-header-overlay">
<div className="secure-badge">
<Shield size={14} /> 256-BIT ENCRYPTED
</div>
<div className="call-duration">12:45</div>
</div>
<div className="video-placeholder-premium">
<div className="video-pulse-icon">
<Video size={48} />
</div>
<p>INITIALIZING SECURE VIDEO FEED...</p>
<div className="patient-id-overlay">
<span className="p-triage" data-triage={activeCall.triage.toLowerCase()}>{activeCall.triage}</span>
<div className="p-name-stack">
<span className="name">{activeCall.patient}</span>
<span className="id">CASE: {activeCall.id}</span>
</div>
</div>
</div>
<div className="video-actions-floating">
<button className="v-float-btn"><Mic size={20} /></button>
<button className="v-float-btn active"><Video size={20} /></button>
<button className="v-float-btn end-session" onClick={() => setActiveCall(null)}><PhoneOff size={20} /></button>
<div className="v-divider-vertical" />
<button className="v-float-btn"><MessageCircle size={20} /></button>
</div>
</div>
</div>
<div className="clinical-data-rail">
<div className="rail-section">
<div className="rail-head">
<Activity size={16} /> <h4>LIVE VITALS</h4>
</div>
<div className="vitals-mini-grid">
<div className="v-stat-card">
<label>HEART RATE</label>
<div className="val">102<small>BPM</small></div>
</div>
<div className="v-stat-card">
<label>SPO2</label>
<div className="val">94%</div>
</div>
<div className="v-stat-card">
<label>BP</label>
<div className="val">130/85</div>
</div>
<div className="v-stat-card warning">
<label>TEMP</label>
<div className="val">101.4<small>°F</small></div>
</div>
</div>
</div>
<div className="rail-section grow">
<div className="rail-head">
<ClipboardList size={16} /> <h4>TELE-CONSULT NOTES</h4>
</div>
<textarea className="clinical-input" placeholder="Type clinical advice, prescriptions, or observations..." />
<button className="commit-btn">COMMIT TO EPCR</button>
</div>
</div>
</div>
) : (
<div className="idle-surface-placeholder">
<div className="idle-icon-wrap">
<VideoOff size={64} />
<div className="idle-ring" />
</div>
<h3>READY FOR UPLINK</h3>
<p>Select a pending triage request from the queue to bridge TeleLink session</p>
</div>
)}
</div>
</div>
) : (
<div className="history-panel-full">
<table className="staff-table-premium">
<thead>
<tr>
<th>Session ID & Date</th>
<th>Patient</th>
<th>Duration</th>
<th>EMT / Field Provider</th>
<th>Clinical Outcome</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{callHistory.map(h => (
<tr key={h.id}>
<td>
<div style={{ fontWeight: 700 }}>{h.id}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{h.date}</div>
</td>
<td>{h.patient}</td>
<td>{h.duration}</td>
<td>{h.emt}</td>
<td>
<span className="status-badge" style={{ background: 'rgba(16, 185, 129, 0.1)', color: 'var(--accent-green)' }}>
{h.outcome}
</span>
</td>
<td>
<button className="tag-btn"><ClipboardList size={14} /></button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</motion.div>
);
};

View File

@@ -0,0 +1,240 @@
import React from 'react';
import { motion } from 'framer-motion';
import {
Search,
Plus,
TrendingUp,
Clock,
MapPin,
Navigation,
Truck,
Video,
FileText,
XCircle,
} from 'lucide-react';
interface TripManagementProps {
tripsData: any[];
onDeleteTrip: (id: string) => void;
onOpenBooking: () => void;
}
export const TripManagement: React.FC<TripManagementProps> = ({
tripsData,
onDeleteTrip,
onOpenBooking,
}) => {
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" /> 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={onOpenBooking}
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 &amp; 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) => (
<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 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
className={`ts-line ${t.step >= 2 ? 'active' : ''}`}
/>
<div
className={`ts-step ${
t.step >= 2
? 'completed'
: t.step === 1
? 'active'
: ''
}`}
/>
<div
className={`ts-line ${t.step >= 3 ? 'active' : ''}`}
/>
<div
className={`ts-step ${
t.step >= 3
? 'completed'
: t.step === 2
? 'active'
: ''
}`}
/>
</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={() => onDeleteTrip(t.id)}
style={{ color: 'var(--alert-red)' }}
>
<XCircle size={14} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</motion.div>
);
};

View File

@@ -16,8 +16,8 @@
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},

View File

@@ -15,8 +15,8 @@
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},