implement TeleEMS platform architecture with centralized API client and master data management system

This commit is contained in:
2026-05-04 15:27:35 +05:30
parent e165269e92
commit 8dc773d205
61 changed files with 22829 additions and 0 deletions

184
src/App.css Normal file
View File

@@ -0,0 +1,184 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

244
src/App.tsx Normal file
View File

@@ -0,0 +1,244 @@
import React, { useEffect } from 'react';
import { BrowserRouter, Routes, Route, useLocation, Navigate } from 'react-router-dom';
import { Sidebar } from './components/Sidebar';
import { TopBar } from './components/TopBar';
import { ErrorBoundary } from './components/ErrorBoundary';
import { Dashboard } from './pages/Dashboard';
import { LiveIncidents } from './pages/LiveIncidents';
import { FleetDispatch } from './pages/FleetDispatch';
import { PatientClinical } from './pages/PatientClinical';
import { HospitalsNetwork } from './pages/HospitalsNetwork';
import { AnalyticsReports } from './pages/AnalyticsReports';
import { UserManagement } from './pages/UserManagement';
import { PlatformConfig } from './pages/PlatformConfig';
import { AuditCompliance } from './pages/AuditCompliance';
import { SystemHealth } from './pages/SystemHealth';
import { HospitalConsole } from './pages/HospitalConsole';
import { MasterDataManagement } from './pages/MasterData';
import { CallerPortal } from './pages/CallerPortal';
import { Login } from './pages/Login';
import { FleetLogin } from './pages/FleetLogin';
import { FleetOperatorDashboard } from './pages/FleetOperatorDashboard';
import { PerspectiveLauncher } from './pages/PerspectiveLauncher';
import { RoleLogin } from './pages/RoleLogin';
import { ComingSoonPortal } from './pages/ComingSoonPortal';
import {
Building2,
Stethoscope,
Activity,
User,
Scan,
ShoppingCart
} from 'lucide-react';
import { isTokenExpired, logout } from './utils/auth';
// --- ROLE-BASED ACCESS CONTROL ---
const RoleProtectedRoute: React.FC<{
children: React.ReactNode,
allowedRoles: string[],
user: any
}> = ({ children, allowedRoles, user }) => {
const isAuthenticated = localStorage.getItem('teleems_auth') === 'true';
if (!isAuthenticated) return <Navigate to="/login" replace />;
const userRoles = Array.isArray(user?.roles) ? user.roles : [];
const hasAccess = allowedRoles.some(role => userRoles.includes(role)) || userRoles.includes('CURESELECT_ADMIN');
if (!hasAccess) {
// 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 />;
}
return <>{children}</>;
};
function AppContent() {
const location = useLocation();
// --- SESSION MONITORING ---
// Periodically check if the token has expired
useEffect(() => {
const checkSession = () => {
const token = localStorage.getItem('teleems_token');
const auth = localStorage.getItem('teleems_auth') === 'true';
if (auth && token && isTokenExpired(token)) {
console.warn('Session expired. Logging out...');
logout();
}
};
// Check on mount
checkSession();
// Check on every route change
checkSession();
// Periodically check every 30 seconds
const interval = setInterval(checkSession, 30000);
return () => clearInterval(interval);
}, [location.pathname]);
// --- DEVELOPMENT BYPASS ---
// In a real production app, this would be removed.
// For the user's request: "this admin so don't want login give me admin level access"
/* Commented out to allow testing of launcher and login flow
useEffect(() => {
const isAuth = localStorage.getItem('teleems_auth') === 'true';
const isCaller = window.location.pathname === '/caller';
const isLogin = window.location.pathname === '/login';
if (!isAuth && !isCaller && !isLogin) {
console.log('Dev Mode: Auto-authenticating as CureSelect Super Admin');
localStorage.setItem('teleems_auth', 'true');
localStorage.setItem('teleems_token', 'dev-super-token-2026');
localStorage.setItem('teleems_user', JSON.stringify({
id: 'admin-001',
username: 'CureSelect Super Admin',
roles: ['CURESELECT_ADMIN', 'ADMIN'],
metadata: {
organization: { company_name: 'CureSelect Healthcare LLP' }
}
}));
// Force reload to update navigation
window.location.reload();
}
}, []);
*/
const isLoginPage = location.pathname.startsWith('/login') || location.pathname === '/fleet-login' || location.pathname === '/launcher';
const isAuthenticated = localStorage.getItem('teleems_auth') === 'true';
const user = JSON.parse(localStorage.getItem('teleems_user') || '{}');
// --- PUBLIC ROUTES (No Auth Required) ---
if (isLoginPage || (location.pathname === '/' && !isAuthenticated)) {
return (
<Routes>
<Route path="/" element={<PerspectiveLauncher />} />
<Route path="/login" element={<Login />} />
<Route path="/login/:role" element={<RoleLogin />} />
<Route path="/fleet-login" element={<FleetLogin />} />
<Route path="/launcher" element={<PerspectiveLauncher />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
}
// --- PROTECTED ROUTES (Auth Required) ---
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return (
<div className="dashboard-container">
<ErrorBoundary>
<Sidebar />
</ErrorBoundary>
<main className="main-content">
<div className="scanline" />
<TopBar />
<div style={{ flex: 1, overflow: 'hidden', position: 'relative', display: 'flex', flexDirection: 'column' }}>
<ErrorBoundary>
<Routes>
<Route path="/" element={
isAuthenticated ? (
user?.roles?.includes('FLEET_OPERATOR') && !user?.roles?.includes('CURESELECT_ADMIN')
? <Navigate to="/fleet-operator" replace />
: <Dashboard />
) : (
<Navigate to="/launcher" replace />
)
} />
<Route path="/incidents" element={
<RoleProtectedRoute allowedRoles={['CURESELECT_ADMIN', 'PILOT', 'COORDINATOR']} user={user}>
<LiveIncidents />
</RoleProtectedRoute>
} />
<Route path="/fleet" element={
<RoleProtectedRoute allowedRoles={['CURESELECT_ADMIN', 'STATION_INCHARGE']} user={user}>
<FleetDispatch />
</RoleProtectedRoute>
} />
<Route path="/clinical" element={
<RoleProtectedRoute allowedRoles={['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'EMT']} user={user}>
<PatientClinical />
</RoleProtectedRoute>
} />
<Route path="/hospitals" element={
<RoleProtectedRoute allowedRoles={['CURESELECT_ADMIN']} user={user}>
<HospitalsNetwork />
</RoleProtectedRoute>
} />
<Route path="/analytics" element={
<RoleProtectedRoute allowedRoles={['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'STATION_INCHARGE']} user={user}>
<AnalyticsReports />
</RoleProtectedRoute>
} />
<Route path="/users" element={
<RoleProtectedRoute allowedRoles={['CURESELECT_ADMIN']} user={user}>
<UserManagement />
</RoleProtectedRoute>
} />
<Route path="/config" element={
<RoleProtectedRoute allowedRoles={['CURESELECT_ADMIN']} user={user}>
<PlatformConfig />
</RoleProtectedRoute>
} />
<Route path="/compliance" element={
<RoleProtectedRoute allowedRoles={['CURESELECT_ADMIN', 'HOSPITAL_ADMIN']} user={user}>
<AuditCompliance />
</RoleProtectedRoute>
} />
<Route path="/health" element={
<RoleProtectedRoute allowedRoles={['CURESELECT_ADMIN']} user={user}>
<SystemHealth />
</RoleProtectedRoute>
} />
<Route path="/hospital-console" element={
<RoleProtectedRoute allowedRoles={['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'COORDINATOR', 'EMT']} user={user}>
<HospitalConsole />
</RoleProtectedRoute>
} />
<Route path="/master-data" element={
<RoleProtectedRoute allowedRoles={['CURESELECT_ADMIN']} user={user}>
<MasterDataManagement />
</RoleProtectedRoute>
} />
<Route path="/caller" element={<CallerPortal />} />
<Route path="/fleet-operator" element={
<RoleProtectedRoute allowedRoles={['CURESELECT_ADMIN', 'FLEET_OPERATOR']} user={user}>
<FleetOperatorDashboard />
</RoleProtectedRoute>
} />
{/* --- NEW PERSPECTIVE PORTALS --- */}
<Route path="/launcher" element={<PerspectiveLauncher />} />
<Route path="/hospital-group" element={<ComingSoonPortal title="Hospital Group" icon={Building2} />} />
<Route path="/provider" element={<ComingSoonPortal title="Provider" icon={Stethoscope} />} />
<Route path="/provider-react" element={<ComingSoonPortal title="Provider React" icon={Activity} />} />
<Route path="/patient-portal" element={<ComingSoonPortal title="Patient" icon={User} />} />
<Route path="/scan-centre" element={<ComingSoonPortal title="Scan Centre" icon={Scan} />} />
<Route path="/cart" element={<ComingSoonPortal title="Cart / Mobile" icon={ShoppingCart} />} />
</Routes>
</ErrorBoundary>
</div>
</main>
</div>
);
}
function App() {
return (
<BrowserRouter>
<AppContent />
</BrowserRouter>
);
}
export default App;

134
src/api/apiClient.ts Normal file
View File

@@ -0,0 +1,134 @@
import { logout } from '../utils/auth';
const BASE_URL = 'https://teleems-api-gateway.onrender.com';
interface RequestOptions extends RequestInit {
token?: string;
}
/**
* Centralized API client that handles automatic token injection
* and global error handling (like 401 Unauthorized)
*/
export const apiClient = {
request: async (endpoint: string, options: RequestOptions = {}) => {
const { token, headers, ...rest } = options;
// Use provided token or get from localStorage
const authToken = token || localStorage.getItem('teleems_token');
const defaultHeaders: Record<string, string> = {
'Content-Type': 'application/json',
};
if (authToken) {
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}`;
try {
const response = await fetch(url, {
headers: { ...defaultHeaders, ...headers },
...rest,
});
// Handle session expiration
if (response.status === 401 || response.status === 403) {
console.warn('Unauthorized request detected. Triggering auto-logout...');
logout();
return null; // Return null as the app will redirect
}
const data = await response.json();
if (!response.ok) {
return { ...data, status: response.status };
}
return data;
} catch (error) {
console.error('API Request Error:', error);
throw error;
}
},
get: (endpoint: string, options: RequestOptions = {}) =>
apiClient.request(endpoint, { ...options, method: 'GET' }),
post: (endpoint: string, body: any, options: RequestOptions = {}) =>
apiClient.request(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(body)
}),
put: (endpoint: string, body: any, options: RequestOptions = {}) =>
apiClient.request(endpoint, {
...options,
method: 'PUT',
body: JSON.stringify(body)
}),
patch: (endpoint: string, body: any, options: RequestOptions = {}) =>
apiClient.request(endpoint, {
...options,
method: 'PATCH',
body: JSON.stringify(body)
}),
delete: (endpoint: string, options: RequestOptions = {}) =>
apiClient.request(endpoint, { ...options, method: 'DELETE' }),
};

55
src/api/auth.ts Normal file
View File

@@ -0,0 +1,55 @@
import { apiClient } from './apiClient';
import type { AuthUser, LoginResponse, MfaSetupResponse, MfaVerifyResponse } from './types';
export const authApi = {
login: async (username: string, password: string): Promise<LoginResponse> => {
return apiClient.post('/v1/auth/login', { username, password });
},
verifyMfa: async (mfaSessionToken: string, totpCode: string): Promise<LoginResponse> => {
return apiClient.post('/v1/auth/mfa/verify', {
mfa_session_token: mfaSessionToken,
totp_code: totpCode
});
},
setupMfa: async (token: string): Promise<MfaSetupResponse> => {
return apiClient.post('/v1/auth/mfa/totp/setup', {}, { token });
},
verifyTotpSetup: async (token: string, totpCode: string): Promise<MfaVerifyResponse> => {
return apiClient.post('/v1/auth/mfa/totp/verify', { totp_code: totpCode }, { token });
},
getAuditLogs: async (token: string, limit = 20, offset = 0) => {
return apiClient.get(`/v1/auth/audit-logs?limit=${limit}&offset=${offset}`, { token });
},
registerUser: async (userData: any, token: string) => {
return apiClient.post('/v1/auth/users', userData, { token });
},
getUsers: async (token: string) => {
return apiClient.get('/v1/auth/users', { token });
},
updateUser: async (userId: string, userData: any, token: string) => {
return apiClient.put(`/v1/auth/users/${userId}`, userData, { token });
},
disableMfa: async (password: string, token: string) => {
return apiClient.post('/v1/auth/mfa/disable', { password }, { token });
},
getDepartments: async (token: string) => {
return apiClient.get('/v1/hospital/departments', { token });
},
createDepartment: async (deptData: any, token: string) => {
return apiClient.post('/v1/hospital/departments', deptData, { token });
},
getRoles: async (token: string) => {
return apiClient.get('/v1/auth/roles', { token });
}
};

47
src/api/fleet.ts Normal file
View File

@@ -0,0 +1,47 @@
import { apiClient } from './apiClient';
export const fleetApi = {
createStation: async (stationData: any, token: string) => {
return apiClient.post('/v1/fleet/stations', stationData, { token });
},
getStations: async (token: string, organisationId?: string) => {
const url = organisationId
? `/v1/fleet/stations?organisationId=${organisationId}`
: `/v1/fleet/stations`;
return apiClient.get(url, { token });
},
createVehicle: async (vehicleData: any, token: string) => {
return apiClient.post('/v1/fleet/vehicles', vehicleData, { token });
},
getVehicles: async (token: string, orgId: string) => {
return apiClient.get(`/v1/fleet/vehicles?org_id=${orgId}`, { token });
},
updateVehicleDetails: async (vehicleId: string, vehicleData: any, token: string) => {
return apiClient.patch(`/v1/fleet/vehicles/${vehicleId}`, vehicleData, { token });
},
createStaff: async (staffData: any, token: string) => {
return apiClient.post('/v1/fleet/staff', staffData, { token });
},
getStaff: async (token: string, orgId: string) => {
return apiClient.get(`/v1/fleet/staff?organisationId=${orgId}`, { token });
},
createRoster: async (rosterData: any, token: string) => {
return apiClient.post('/v1/fleet/roster', rosterData, { token });
},
startShift: async (shiftData: any, token: string) => {
return apiClient.post('/v1/fleet/shifts/start', shiftData, { token });
},
getInventoryMaster: async (token: string) => {
return apiClient.get('/v1/fleet/inventory/master', { token });
}
};

45
src/api/incidents.ts Normal file
View File

@@ -0,0 +1,45 @@
import { apiClient } from './apiClient';
import type { ApiResponse, Incident } from './types';
export const incidentsApi = {
createIncident: async (incidentData: any, token: string): Promise<ApiResponse<Incident>> => {
return apiClient.post('/v1/incidents', incidentData, { token });
},
getIncidents: async (params: { status?: string; limit?: number; date_from?: string }, token: string): Promise<ApiResponse<Incident[]>> => {
const query = new URLSearchParams();
if (params.status) query.append('status', params.status);
if (params.limit) query.append('limit', params.limit.toString());
if (params.date_from) query.append('date_from', params.date_from);
return apiClient.get(`/v1/incidents?${query.toString()}`, { token });
},
getIncidentById: async (id: string, token: string): Promise<ApiResponse<Incident>> => {
return apiClient.get(`/v1/incidents/${id}`, { token });
},
updateIncident: async (id: string, incidentData: any, token: string): Promise<ApiResponse<Incident>> => {
return apiClient.patch(`/v1/incidents/${id}`, incidentData, { token });
},
getIncidentTimeline: async (id: string, token: string): Promise<ApiResponse<any[]>> => {
return apiClient.get(`/v1/incidents/${id}/timeline`, { token });
},
getIncidentAudit: async (id: string, token: string): Promise<ApiResponse<any[]>> => {
return apiClient.get(`/v1/incidents/${id}/audit`, { token });
},
recommendDispatch: async (payload: { gps_lat: number, gps_lon: number, vehicle_type_required: string }, token: string): Promise<ApiResponse<any>> => {
return apiClient.post('/v1/dispatch/recommend', payload, { token });
},
dispatchVehicle: async (incidentId: string, vehicleId: string, token: string): Promise<ApiResponse<any>> => {
return apiClient.post(`/v1/incidents/${incidentId}/dispatch`, { vehicle_id: vehicleId }, { token });
},
addPatientsToIncident: async (incidentId: string, patients: any[], token: string): Promise<ApiResponse<any>> => {
return apiClient.post(`/v1/incidents/${incidentId}/patients`, { patients }, { token });
}
};

81
src/api/types.ts Normal file
View File

@@ -0,0 +1,81 @@
export type AuthUser = {
id: string;
phone: string;
username: string;
roles: string[];
}
export type LoginResponse = {
status: number;
message: string;
data: {
mfa_required: boolean;
mfa_session_token?: string;
user?: AuthUser;
access_token?: string;
refresh_token?: string;
};
}
export type MfaSetupResponse = {
status: number;
message: string;
data: {
totp_uri: string;
secret: string;
qr_code_base64: string;
};
}
export type MfaVerifyResponse = {
status: number;
message: string;
data: {
backup_codes: string[];
};
meta: {
request_id: string;
timestamp: string;
};
}
export interface Patient {
name: string;
age: number;
gender: string;
triage_level: string;
symptoms: { name: string; duration_minutes: number }[];
}
export interface Incident {
id: string;
category: string;
triage_level: string;
severity: string;
caller_id: string;
organisationId: string | null;
gps_lat: number;
gps_lon: number;
address: string;
patients: Patient[];
notes: string;
guest_name?: string | null;
guest_phone?: string | null;
status: string;
assigned_vehicle: string | null;
eta_seconds: number | null;
createdAt: string;
updatedAt: string;
}
export interface ApiResponse<T> {
status: number;
message: string;
data: T;
meta: {
request_id: string;
timestamp: string;
next_cursor?: string | null;
total_count?: number;
};
}

BIN
src/assets/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

1
src/assets/react.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

1
src/assets/vite.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

126
src/components/Common.tsx Normal file
View File

@@ -0,0 +1,126 @@
import React from 'react';
import { motion } from 'framer-motion';
interface CardProps {
children?: React.ReactNode;
title?: React.ReactNode;
subtitle?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
glowColor?: 'cyan' | 'green' | 'red' | 'amber';
onClick?: () => void;
value?: string | number;
icon?: any;
color?: string;
}
export const Card: React.FC<CardProps> = ({
children,
title,
subtitle,
className = '',
style = {},
glowColor,
onClick
}) => {
const glowClass = glowColor ? `glow-${glowColor}` : '';
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className={`glass ${glowClass} ${className}`}
onClick={onClick}
style={{
padding: '20px',
border: '1px solid var(--card-border)',
position: 'relative',
overflow: 'hidden',
...style
}}
>
{title && (
<div style={{ marginBottom: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ width: '100%' }}>
{typeof title === 'string' ? (
<h3 style={{ fontSize: '1rem', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em' }}>{title}</h3>
) : title}
{subtitle && (
typeof subtitle === 'string' ? (
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '4px' }}>{subtitle}</p>
) : subtitle
)}
</div>
</div>
)}
{children}
</motion.div>
);
};
interface StatCardProps {
label: string;
value: string | number;
subValue?: string;
icon: any;
trend?: {
value: string;
isUp: boolean;
};
glowColor: 'cyan' | 'green' | 'red' | 'amber';
pulse?: boolean;
}
export const StatCard: React.FC<StatCardProps> = ({
label,
value,
subValue,
icon: Icon,
trend,
glowColor,
pulse
}) => {
const colorVar = `var(--accent-${glowColor === 'red' ? 'red' : glowColor === 'amber' ? 'warning-amber' : glowColor})`;
return (
<Card glowColor={glowColor} style={{ padding: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '12px' }}>
<div style={{
width: '36px',
height: '36px',
borderRadius: '8px',
background: `rgba(0, 0, 0, 0.3)`,
border: `1px solid var(--card-border)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: colorVar
}}>
<Icon size={20} />
</div>
{pulse && <div className="pulse-dot-red"></div>}
</div>
<div>
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', textTransform: 'uppercase', fontWeight: 600 }}>{label}</p>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '8px', marginTop: '4px' }}>
<h2 className="mono" style={{ fontSize: '1.5rem', fontWeight: 700 }}>{value}</h2>
{subValue && <span style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>/ {subValue}</span>}
</div>
{trend && (
<div style={{
marginTop: '8px',
fontSize: '0.75rem',
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>
</Card>
);
};

View File

@@ -0,0 +1,86 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { AlertTriangle, RefreshCw } from 'lucide-react';
interface Props {
children?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
public render() {
if (this.state.hasError) {
return (
<div style={{
height: '100vh',
width: '100vw',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--base-bg)',
color: 'var(--text-primary)',
padding: '40px',
textAlign: 'center',
zIndex: 9999
}}>
<AlertTriangle size={64} color="var(--alert-red)" style={{ marginBottom: '24px' }} />
<h1 style={{ fontSize: '1.5rem', marginBottom: '16px' }}>Terminal Error Detected</h1>
<p style={{ color: 'var(--text-secondary)', maxWidth: '500px', marginBottom: '32px' }}>
A critical failure occurred in the module hierarchy. The security perimeter remains intact, but the interface requires a full system reset.
</p>
{this.state.error && (
<div style={{
background: 'rgba(255, 59, 59, 0.05)',
border: '1px solid rgba(255, 59, 59, 0.2)',
borderRadius: '8px',
padding: '12px',
fontFamily: 'monospace',
fontSize: '0.8rem',
color: 'var(--alert-red)',
marginBottom: '32px',
maxWidth: '80%'
}}>
{this.state.error.toString()}
</div>
)}
<button
onClick={() => window.location.reload()}
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '12px 24px',
background: 'var(--accent-cyan)',
border: 'none',
borderRadius: '8px',
color: '#000',
fontWeight: 700,
cursor: 'pointer'
}}
>
<RefreshCw size={18} />
REBOOT SYSTEM
</button>
</div>
);
}
return this.props.children;
}
}

View File

@@ -0,0 +1,308 @@
import React, { useState, useEffect } from 'react';
import { ShieldCheck, Smartphone, CheckCircle, AlertCircle, Loader2, Eye, EyeOff } from 'lucide-react';
import { authApi } from '../api/auth';
import type { MfaSetupResponse } from '../api/types';
import { Card } from './Common';
export const MfaSettings: React.FC = () => {
const [setupData, setSetupData] = useState<MfaSetupResponse['data'] | null>(null);
const [totpCode, setTotpCode] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [status, setStatus] = useState<'idle' | 'setup' | 'enabled' | 'disable' | 'error'>('idle');
const [message, setMessage] = useState('');
const [backupCodes, setBackupCodes] = useState<string[]>([]);
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const token = localStorage.getItem('teleems_token') || '';
useEffect(() => {
const userJson = localStorage.getItem('teleems_user');
if (userJson) {
const user = JSON.parse(userJson);
if (user.mfa_enabled) {
setStatus('enabled');
}
}
}, []);
const initiateSetup = async () => {
setIsLoading(true);
try {
const response = await authApi.setupMfa(token);
if (response.status === 201) {
setSetupData(response.data);
setStatus('setup');
} else {
setMessage(response.message);
setStatus('error');
}
} catch (err) {
setMessage('Failed to initiate MFA setup');
setStatus('error');
} finally {
setIsLoading(false);
}
};
const verifyAndEnable = async () => {
setIsLoading(true);
try {
const response = await authApi.verifyTotpSetup(token, totpCode);
if (response.status === 201 || response.status === 200) {
setBackupCodes(response.data.backup_codes || []);
setStatus('enabled');
setMessage('MFA has been successfully enabled. Save your backup codes securely.');
const userJson = localStorage.getItem('teleems_user');
if (userJson) {
const user = JSON.parse(userJson);
user.mfa_enabled = true;
localStorage.setItem('teleems_user', JSON.stringify(user));
}
} else {
setMessage(response.message);
setStatus('error');
}
} catch (err) {
setMessage('Verification failed');
setStatus('error');
} finally {
setIsLoading(false);
}
};
const handleDisableMfa = async () => {
if (!password) {
setMessage('Password is required to disable MFA');
return;
}
setIsLoading(true);
try {
const response = await authApi.disableMfa(password, token);
if (response.status === 200 || response.status === 201) {
setStatus('idle');
setMessage('MFA protection has been disabled.');
setPassword('');
const userJson = localStorage.getItem('teleems_user');
if (userJson) {
const user = JSON.parse(userJson);
user.mfa_enabled = false;
localStorage.setItem('teleems_user', JSON.stringify(user));
}
} else {
setMessage(response.message || 'Failed to disable MFA. Check your password.');
setStatus('error');
}
} catch (err) {
setMessage('An error occurred while disabling MFA');
setStatus('error');
} finally {
setIsLoading(false);
}
};
return (
<Card title="Multi-Factor Authentication" subtitle="Enhance your nodal security with TOTP.">
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{status === 'idle' && (
<div style={{ textAlign: 'center', padding: '20px 0' }}>
<Smartphone size={48} style={{ color: 'var(--text-secondary)', marginBottom: '16px', opacity: 0.5 }} />
<p style={{ fontSize: '0.9rem', color: 'var(--text-secondary)', marginBottom: '20px' }}>
MFA adds an extra layer of protection to your account by requiring a code from your mobile device.
</p>
<button
onClick={initiateSetup}
disabled={isLoading}
className="login-button"
style={{ width: 'auto', padding: '0 32px' }}
>
{isLoading ? <Loader2 className="animate-spin" /> : 'BEGIN SECURE SETUP'}
</button>
</div>
)}
{status === 'setup' && setupData && (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '20px' }}>
<div style={{
padding: '12px',
background: '#fff',
borderRadius: '8px',
boxShadow: '0 0 20px rgba(0,212,255,0.2)'
}}>
<img src={setupData.qr_code_base64} alt="MFA QR Code" style={{ width: '180px', height: '180px' }} />
</div>
<div style={{ textAlign: 'center' }}>
<p style={{ fontSize: '0.85rem', fontWeight: 600 }}>Scan with Google Authenticator or Authy</p>
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '4px' }}>
Secret: <span className="mono" style={{ color: 'var(--accent-cyan)' }}>{setupData.secret}</span>
</p>
</div>
<div style={{ width: '100%' }}>
<label className="input-label">Verification Code</label>
<input
type="text"
className="login-input mono"
placeholder="000000"
value={totpCode}
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, ''))}
maxLength={6}
style={{ textAlign: 'center', fontSize: '1.2rem', letterSpacing: '8px' }}
/>
</div>
<button
onClick={verifyAndEnable}
disabled={isLoading || totpCode.length < 6}
className="login-button"
>
{isLoading ? <Loader2 className="animate-spin" /> : 'VERIFY & ENABLE MFA'}
</button>
</div>
)}
{status === 'enabled' && (
<div style={{ textAlign: 'center', padding: '10px 0' }}>
<div style={{ color: 'var(--accent-green)', marginBottom: '20px' }}>
<CheckCircle size={48} style={{ marginBottom: '16px' }} />
<p style={{ fontSize: '1rem', fontWeight: 800 }}>MFA PROTECTION ACTIVE</p>
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginTop: '8px' }}>
Your node is now secured with quantum-ready TOTP.
</p>
</div>
{backupCodes.length > 0 && (
<div style={{
background: 'rgba(59, 130, 246, 0.05)',
border: '1px solid var(--card-border)',
borderRadius: '12px',
padding: '20px',
marginTop: '20px',
textAlign: 'left'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px', color: 'var(--accent-blue)' }}>
<ShieldCheck size={18} />
<span style={{ fontSize: '0.85rem', fontWeight: 600 }}>Emergency Backup Codes</span>
</div>
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginBottom: '16px' }}>
Save these codes in a secure location. You can use them if you lose access to your authenticator device.
</p>
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '10px',
fontFamily: 'monospace',
fontSize: '0.9rem'
}}>
{backupCodes.map((code, index) => (
<div key={index} style={{
padding: '8px',
background: 'rgba(255,255,255,0.03)',
borderRadius: '4px',
color: 'var(--accent-cyan)',
letterSpacing: '1px'
}}>
{code}
</div>
))}
</div>
<button
onClick={() => {
const text = backupCodes.join('\n');
navigator.clipboard.writeText(text);
}}
style={{
marginTop: '20px',
width: '100%',
padding: '10px',
background: 'rgba(59, 130, 246, 0.1)',
border: '1px solid rgba(59, 130, 246, 0.2)',
borderRadius: '8px',
color: 'var(--accent-cyan)',
fontSize: '0.8rem',
cursor: 'pointer',
fontWeight: 600
}}
>
COPY ALL CODES
</button>
</div>
)}
<button
onClick={() => setStatus('disable')}
className="btn-ghost-sm"
style={{ width: '100%', marginTop: '24px', color: 'var(--alert-red)', borderColor: 'rgba(255, 59, 59, 0.2)' }}
>
DISABLE MFA PROTECTION
</button>
</div>
)}
{status === 'disable' && (
<div style={{ textAlign: 'center', padding: '10px 0' }}>
<AlertCircle size={48} style={{ color: 'var(--alert-red)', marginBottom: '16px' }} />
<p style={{ fontSize: '1rem', fontWeight: 800, color: '#fff' }}>DISABLE PROTECTION?</p>
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginTop: '8px', marginBottom: '24px' }}>
Removing MFA significantly increases the risk of unauthorized access. Please confirm your password to proceed.
</p>
<div style={{ textAlign: 'left', marginBottom: '20px' }}>
<label className="input-label">Operator Password</label>
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
<input
type={showPassword ? "text" : "password"}
className="login-input"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ paddingRight: '40px' }}
autoComplete="new-password"
autoFocus
/>
<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 style={{ display: 'flex', gap: '12px' }}>
<button
onClick={() => setStatus('enabled')}
className="btn-secondary"
style={{ flex: 1 }}
disabled={isLoading}
>
CANCEL
</button>
<button
onClick={handleDisableMfa}
className="login-button"
style={{ flex: 1.5, background: 'var(--alert-red)', boxShadow: '0 0 20px rgba(255, 59, 59, 0.3)' }}
disabled={isLoading || !password}
>
{isLoading ? <Loader2 className="animate-spin" /> : 'CONFIRM REMOVAL'}
</button>
</div>
</div>
)}
{status === 'error' && (
<div style={{ textAlign: 'center', padding: '20px 0', color: 'var(--alert-red)' }}>
<AlertCircle size={48} style={{ marginBottom: '16px' }} />
<p style={{ fontSize: '0.9rem' }}>{message}</p>
<button onClick={() => setStatus('idle')} style={{ color: 'var(--accent-cyan)', background: 'none', border: 'none', marginTop: '12px', cursor: 'pointer', fontWeight: 600 }}>Try Again</button>
</div>
)}
</div>
</Card>
);
};

View File

@@ -0,0 +1,246 @@
import React, { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
ShieldAlert,
Hospital,
Monitor,
HeartPulse,
Truck,
Zap,
Activity,
ChevronDown,
ChevronUp,
LayoutDashboard,
Navigation,
User
} from 'lucide-react';
interface Perspective {
id: string;
label: string;
group: string;
icon: React.ElementType;
description: string;
}
const perspectives: Perspective[] = [
{ id: 'CURESELECT_ADMIN', label: 'CureSelect Admin', group: 'System Access', icon: LayoutDashboard, description: 'Full Platform Access' },
{ id: 'HOSPITAL_ADMIN', label: 'Hospital Admin', group: 'Hospital Scope', icon: Hospital, description: 'Manage Hospital Assets' },
{ id: 'COORDINATOR', label: 'Hospital Coordinator', group: 'Hospital Scope', icon: Monitor, description: 'Manage Operations' },
{ id: 'ED_DOCTOR', label: 'Emergency Doctor', group: 'Hospital Scope', icon: HeartPulse, description: 'Clinical Intelligence' },
{ id: 'FLEET_OPERATOR', label: 'Fleet Operator', group: 'Fleet Scope', icon: Truck, description: 'Dispatch & Logistics' },
{ id: 'STATION_INCHARGE', label: 'Station Incharge', group: 'Fleet Scope', icon: Zap, description: 'Node Management' },
{ id: 'PILOT', label: 'Ambulance Pilot', group: 'Fleet Scope', icon: Navigation, description: 'Trip Management' },
{ id: 'EMT', label: 'EMT/Paramedic', group: 'Fleet Scope', icon: Activity, description: 'Patient Care' },
{ id: 'HOSPITAL_GROUP', label: 'Hospital Group', group: 'Regional Scope', icon: ShieldAlert, description: 'Regional Management' },
{ id: 'PROVIDER', label: 'Provider Portal', group: 'Clinical Scope', icon: Activity, description: 'Healthcare Provider' },
{ id: 'PATIENT', label: 'Patient Portal', group: 'Patient Scope', icon: User, description: 'Personal Health' },
{ id: 'SCAN_CENTRE', label: 'Scan Centre', group: 'Diagnostic Scope', icon: Monitor, description: 'Imaging & Diagnostics' },
{ id: 'CART', label: 'Mobile Cart', group: 'Field Scope', icon: Truck, description: 'Mobile Response' },
];
export const PerspectiveSwitcher: React.FC<{ currentRole: string; onSwitch: (role: string) => void }> = ({
currentRole,
onSwitch
}) => {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const currentPerspective = perspectives.find(p => p.id === currentRole) || perspectives[0];
const Icon = currentPerspective.icon;
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const groupedPerspectives = perspectives.reduce((acc, p) => {
if (!acc[p.group]) acc[p.group] = [];
acc[p.group].push(p);
return acc;
}, {} as Record<string, Perspective[]>);
return (
<div ref={containerRef} style={{ position: 'relative', width: '100%' }}>
<span style={{
fontSize: '0.6rem',
color: 'var(--text-secondary)',
textTransform: 'uppercase',
fontWeight: 800,
letterSpacing: '0.1em',
marginBottom: '6px',
display: 'block',
marginLeft: '4px'
}}>
Perspective Switcher
</span>
<motion.button
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.98 }}
onClick={() => setIsOpen(!isOpen)}
style={{
width: '100%',
padding: '10px 12px',
background: 'rgba(59, 130, 246, 0.05)',
border: '1px solid var(--card-border)',
borderRadius: '10px',
display: 'flex',
alignItems: 'center',
gap: '10px',
cursor: 'pointer',
color: 'var(--text-primary)',
textAlign: 'left',
transition: 'border-color 0.2s',
outline: 'none',
}}
>
<div style={{
width: '24px',
height: '24px',
borderRadius: '6px',
background: 'rgba(59, 130, 246, 0.1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--accent-cyan)',
}}>
<Icon size={14} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: '0.75rem', fontWeight: 700, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{currentPerspective.label}
</div>
</div>
{isOpen ? <ChevronUp size={14} color="var(--text-secondary)" /> : <ChevronDown size={14} color="var(--text-secondary)" />}
</motion.button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.95 }}
transition={{ duration: 0.15, ease: 'easeOut' }}
className="glass"
style={{
position: 'absolute',
bottom: '100%',
left: 0,
width: '240px',
marginBottom: '12px',
padding: '8px',
zIndex: 2000,
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
border: '1px solid var(--card-border)',
background: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(16px)',
maxHeight: '400px',
overflowY: 'auto'
}}
>
{Object.entries(groupedPerspectives).map(([group, items]) => (
<div key={group} style={{ marginBottom: '8px' }}>
<div style={{
fontSize: '0.55rem',
fontWeight: 800,
color: 'var(--text-secondary)',
textTransform: 'uppercase',
padding: '4px 8px',
letterSpacing: '0.05em'
}}>
{group}
</div>
{items.map((p) => {
const ItemIcon = p.icon;
const isActive = p.id === currentRole;
return (
<button
key={p.id}
onClick={() => {
onSwitch(p.id);
setIsOpen(false);
}}
style={{
width: '100%',
padding: '8px',
borderRadius: '6px',
border: 'none',
background: isActive ? 'rgba(59, 130, 246, 0.08)' : 'transparent',
display: 'flex',
alignItems: 'center',
gap: '10px',
cursor: 'pointer',
textAlign: 'left',
transition: 'all 0.2s',
color: isActive ? 'var(--accent-cyan)' : 'var(--text-primary)',
marginBottom: '2px'
}}
className={isActive ? '' : 'table-row-hover'}
>
<div style={{
width: '22px',
height: '22px',
borderRadius: '5px',
background: isActive ? 'rgba(59, 130, 246, 0.15)' : 'rgba(0,0,0,0.03)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<ItemIcon size={12} />
</div>
<div>
<div style={{ fontSize: '0.72rem', fontWeight: isActive ? 700 : 600 }}>{p.label}</div>
<div style={{ fontSize: '0.58rem', color: 'var(--text-secondary)', opacity: 0.8 }}>{p.description}</div>
</div>
</button>
);
})}
</div>
))}
<div style={{
marginTop: '8px',
paddingTop: '8px',
borderTop: '1px solid rgba(0,0,0,0.05)',
display: 'flex',
flexDirection: 'column',
gap: '4px'
}}>
<button
onClick={() => {
window.location.href = '/launcher';
}}
style={{
width: '100%',
padding: '10px',
borderRadius: '6px',
border: '1px dashed var(--accent-cyan)',
background: 'rgba(59, 130, 246, 0.03)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
cursor: 'pointer',
color: 'var(--accent-cyan)',
fontSize: '0.7rem',
fontWeight: 700,
transition: 'all 0.2s'
}}
className="table-row-hover"
>
<LayoutDashboard size={14} />
SWITCH SYSTEM PORTAL
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};

233
src/components/Sidebar.tsx Normal file
View File

@@ -0,0 +1,233 @@
import React, { useMemo } from 'react';
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
import {
LogOut,
Zap,
ChevronDown,
ChevronRight,
AlertCircle
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { PerspectiveSwitcher } from './PerspectiveSwitcher';
import { NAVIGATION_CONFIG } from '../config/navigation';
import type { NavItem } from '../config/navigation';
import { logout } from '../utils/auth';
export const Sidebar: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
// Safely parse user data
const user = useMemo(() => {
try {
const stored = localStorage.getItem('teleems_user');
if (!stored || stored === 'undefined' || stored === 'null') return { roles: [] };
const parsed = JSON.parse(stored);
if (parsed && typeof parsed === 'object') {
parsed.roles = Array.isArray(parsed.roles)
? parsed.roles.map((r: any) => String(r).toUpperCase())
: [];
return parsed;
}
return { roles: [] };
} catch (err) {
console.error('Failed to parse user data from localStorage', err);
return { roles: [] };
}
}, []);
const handleLogout = () => {
logout();
};
const handleRoleSwitch = (role: string) => {
const updatedUser = { ...user, roles: [role.toUpperCase()] };
localStorage.setItem('teleems_user', JSON.stringify(updatedUser));
window.location.reload();
};
const currentRole = user.roles?.[0] || 'CURESELECT_ADMIN';
const displayName = String(user.username || (currentRole === 'CURESELECT_ADMIN' ? 'CureSelect Admin' : 'Admin User'));
const displayId = user.id ? `#${String(user.id).substring(0, 6)}` : '#789022';
const initials = currentRole === 'CURESELECT_ADMIN' ? 'CA' : (displayName.substring(0, 2).toUpperCase() || 'AU');
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 filterItems = (items: NavItem[]): NavItem[] => {
return items.filter(item => {
const hasItemRole = item.roles.some(role => userRoles.includes(role.toUpperCase()));
return hasAdminRole || hasItemRole;
}).map(item => ({
...item,
subItems: item.subItems ? filterItems(item.subItems) : undefined
}));
};
return filterItems(NAVIGATION_CONFIG);
}, [user.roles]);
const renderNavItem = (item: NavItem, isSubItem = false) => {
const Icon = item.icon || AlertCircle;
const isActive = location.pathname === item.path || (item.path.includes('?') && location.pathname + location.search === item.path);
const hasSubItems = item.subItems && item.subItems.length > 0;
const isParentActive = hasSubItems && (isActive || item.subItems?.some(sub => location.pathname === sub.path.split('?')[0]));
return (
<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'
})}
>
{!isSubItem && <Icon size={18} style={{ flexShrink: 0 }} />}
<span style={{
fontWeight: (isActive || isParentActive) ? 700 : 500,
fontSize: isSubItem ? '0.8rem' : '0.875rem',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
flex: 1
}}>
{item.label}
</span>
{hasSubItems && (
isParentActive ? <ChevronDown size={14} /> : <ChevronRight size={14} />
)}
</NavLink>
<AnimatePresence>
{hasSubItems && isParentActive && (
<motion.div
initial={{ height: 0, opacity: 0 }}
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)' }}
>
{item.subItems?.map(sub => renderNavItem(sub, true))}
</motion.div>
)}
</AnimatePresence>
</div>
);
};
return (
<aside className="glass" style={{
width: 'var(--sidebar-width)',
minWidth: 'var(--sidebar-width)',
flexBasis: 'var(--sidebar-width)',
flexShrink: 0,
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)',
display: 'flex',
alignItems: 'center',
gap: '10px',
flexShrink: 0,
}}>
<div style={{
width: '30px',
height: '30px',
background: 'var(--accent-cyan)',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 0 12px var(--accent-cyan)',
flexShrink: 0,
}}>
<Zap size={18} color="#000" />
</div>
<h2 style={{ fontSize: '1.1rem', fontWeight: 800, color: 'var(--accent-cyan)', margin: 0, letterSpacing: '-0.3px' }}>CureSelect</h2>
</div>
<nav style={{ flex: 1, padding: '12px 0', overflowY: 'auto', minHeight: 0 }} className="no-scrollbar">
{filteredNavItems.map(item => renderNavItem(item))}
</nav>
<div style={{
padding: '12px 16px',
borderTop: '1px solid var(--card-border)',
display: 'flex',
flexDirection: 'column',
gap: '10px',
flexShrink: 0,
}}>
{/* Perspective Switcher */}
{(user.roles?.includes('CURESELECT_ADMIN') || user.roles?.includes('ADMIN')) && (
<PerspectiveSwitcher
currentRole={currentRole}
onSwitch={handleRoleSwitch}
/>
)}
{/* User row */}
<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))',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '0.75rem',
fontWeight: 700,
color: '#000',
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>
</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)',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s',
flexShrink: 0,
}}
title="Sign Out"
>
<LogOut size={15} />
</button>
</div>
</div>
</aside>
);
};

286
src/components/TopBar.tsx Normal file
View File

@@ -0,0 +1,286 @@
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';
export const TopBar: React.FC = () => {
const [time, setTime] = useState(new Date());
const navigate = useNavigate();
useEffect(() => {
const timer = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(timer);
}, []);
const handleLogout = () => {
logout();
};
const user = React.useMemo(() => {
try {
const stored = localStorage.getItem('teleems_user');
if (!stored || stored === 'undefined' || stored === 'null') return {};
const parsed = JSON.parse(stored);
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}, []);
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'
});
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 */}
<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"
>
<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"
>
<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' }}
/>
<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',
}}
/>
</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>
</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>
{/* Divider */}
<div style={{ height: '20px', width: '1px', background: 'var(--card-border)', flexShrink: 0 }} />
{/* 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"
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,
}}
>
{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>
<LogOut size={14} style={{ color: 'var(--text-secondary)', flexShrink: 0 }} />
</div>
</div>
</header>
);
};

137
src/config/navigation.ts Normal file
View File

@@ -0,0 +1,137 @@
import {
Activity,
Truck,
HeartPulse,
Hospital,
PieChart,
Users,
Settings,
ShieldCheck,
LayoutDashboard,
Monitor,
Database,
Zap,
PhoneCall,
Navigation,
ShoppingCart,
LayoutGrid
} from 'lucide-react';
export interface NavItem {
id: string;
label: string;
icon: any;
path: string;
roles: string[];
subItems?: NavItem[];
portalContext?: string; // If set, this item and its sub-items belong to a specific portal
}
export const NAVIGATION_CONFIG: NavItem[] = [
{
id: 'launcher',
label: 'Portal Hub',
icon: Monitor,
path: '/launcher',
roles: ['CURESELECT_ADMIN']
},
{
id: 'overview',
label: 'Admin Dashboard',
icon: LayoutDashboard,
path: '/',
roles: ['CURESELECT_ADMIN']
},
{
id: 'incidents',
label: 'Incident Command',
icon: Activity,
path: '/incidents',
roles: ['CURESELECT_ADMIN', 'PILOT', 'COORDINATOR']
},
{
id: 'caller',
label: 'Caller Portal',
icon: PhoneCall,
path: '/caller',
roles: ['CURESELECT_ADMIN', 'CALLER']
},
{
id: 'fleet-operator',
label: 'Fleet Command',
icon: Zap,
path: '/fleet-operator',
roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'],
subItems: [
{ id: 'fleet-overview', label: 'Command Center', icon: LayoutGrid, path: '/fleet-operator?tab=overview', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] },
{ id: 'fleet-assets', label: 'Fleet Assets', icon: Truck, path: '/fleet-operator?tab=assets', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] },
{ id: 'fleet-personnel', label: 'Personnel Hub', icon: Users, path: '/fleet-operator?tab=personnel', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] },
{ id: 'fleet-mission', label: 'Mission Control', icon: Navigation, path: '/fleet-operator?tab=scheduling', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] },
{ id: 'fleet-inventory', label: 'Supply Chain', icon: ShoppingCart, path: '/fleet-operator?tab=inventory', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] },
{ id: 'fleet-intel', label: 'Fleet Intel', icon: Activity, path: '/fleet-operator?tab=analytics', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] },
]
},
{
id: 'clinical',
label: 'Clinical Intelligence',
icon: HeartPulse,
path: '/clinical',
roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'EMT']
},
{
id: 'hospitals',
label: 'Hospital Network',
icon: Hospital,
path: '/hospitals',
roles: ['CURESELECT_ADMIN']
},
{
id: 'hospital-console',
label: 'Hospital Ops',
icon: Monitor,
path: '/hospital-console',
roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'ED_DOCTOR', 'COORDINATOR', 'EMT']
},
{
id: 'master-data',
label: 'Platform Masters',
icon: Database,
path: '/master-data',
roles: ['CURESELECT_ADMIN']
},
{
id: 'analytics',
label: 'Business Intelligence',
icon: PieChart,
path: '/analytics',
roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN', 'STATION_INCHARGE']
},
{
id: 'users',
label: 'Identity & Access',
icon: Users,
path: '/users',
roles: ['CURESELECT_ADMIN']
},
{
id: 'config',
label: 'System Config',
icon: Settings,
path: '/config',
roles: ['CURESELECT_ADMIN']
},
{
id: 'compliance',
label: 'Audit & Compliance',
icon: ShieldCheck,
path: '/compliance',
roles: ['CURESELECT_ADMIN', 'HOSPITAL_ADMIN']
},
{
id: 'health',
label: 'Infrastructure Health',
icon: Zap,
path: '/health',
roles: ['CURESELECT_ADMIN']
},
];

252
src/index.css Normal file
View File

@@ -0,0 +1,252 @@
@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');
: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);
--sidebar-width: 260px;
--topbar-height: 70px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', 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;
}
h1, h2, h3, h4 {
font-family: 'Space Grotesk', sans-serif;
letter-spacing: -0.02em;
}
.mono {
font-family: 'JetBrains Mono', monospace;
}
/* Glassmorphism Utility */
.glass {
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
border: 1px solid var(--card-border);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03);
}
/* Glow Effects */
.glow-cyan {
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.15);
}
.glow-green {
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.15);
}
.glow-red {
box-shadow: 0 8px 24px rgba(239, 68, 68, 0.15);
}
.glow-amber {
box-shadow: 0 8px 24px rgba(245, 158, 11, 0.15);
}
.glow-text-cyan {
text-shadow: 0 0 4px rgba(59, 130, 246, 0.2);
}
/* Common Layout Components */
.dashboard-container {
display: flex;
height: 100vh;
width: 100vw;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
height: 100vh;
position: relative;
overflow: hidden;
}
.page-container {
flex: 1;
padding: 24px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--card-border) transparent;
}
.page-container::-webkit-scrollbar {
width: 6px;
}
.page-container::-webkit-scrollbar-thumb {
background: var(--card-border);
border-radius: 10px;
}
/* Grid Layouts */
.stats-bar {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.main-grid {
display: grid;
grid-template-columns: 1fr 1.5fr 1fr;
gap: 24px;
height: calc(100% - 140px);
}
.main-grid > * {
min-width: 0;
}
/* Pulse Animation */
@keyframes pulse-red {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0.5; }
100% { transform: scale(1); opacity: 1; }
}
.pulse-dot-red {
width: 8px;
height: 8px;
background-color: var(--alert-red);
border-radius: 50%;
box-shadow: 0 0 10px rgba(239, 68, 68, 0.3);
animation: pulse-red 2s infinite ease-in-out;
}
@keyframes pulse-cyan {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0.5; }
100% { transform: scale(1); opacity: 1; }
}
.pulse-dot-cyan {
width: 8px;
height: 8px;
background-color: var(--accent-cyan);
border-radius: 50%;
box-shadow: 0 0 10px rgba(59, 130, 246, 0.3);
animation: pulse-cyan 2s infinite ease-in-out;
}
@keyframes scanline {
0% { transform: translateY(-100%); }
100% { transform: translateY(100vh); }
}
.scanline {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(to bottom, transparent, rgba(59, 130, 246, 0.05), transparent);
animation: scanline 8s linear infinite;
pointer-events: none;
z-index: 100;
}
/* Scrollbar Hide for specific panels if needed */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.hover-glow {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.hover-glow:hover {
border-color: var(--accent-cyan) !important;
box-shadow: 0 12px 32px rgba(59, 130, 246, 0.12);
transform: translateY(-2px);
}
.spin-slow {
animation: spin 8s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.status-pulse {
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
}
.status-pulse::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 50%;
background: inherit;
animation: pulse-ring 1.5s cubic-bezier(0.24, 0, 0.38, 1) infinite;
}
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 0.8; }
100% { transform: scale(3.5); opacity: 0; }
}
.spin {
animation: spin 1s linear infinite;
}
.table-row-hover {
transition: all 0.2s ease;
}
.table-row-hover:hover {
background: rgba(59, 130, 246, 0.04) !important;
}
.input-glow:focus {
outline: none;
border-color: var(--accent-cyan) !important;
box-shadow: 0 0 10px rgba(59, 130, 246, 0.15);
}
/* Global Dropdown Styling */
select, select option {
color: var(--text-primary);
background-color: var(--card-bg);
}

10
src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,259 @@
import React, { useState } from 'react';
import {
Calendar,
Download,
FileText,
ChevronDown,
TrendingUp,
Activity,
Map as MapIcon,
BarChart3,
PieChart as PieChartIcon,
Clock,
Filter,
Users,
Building2,
Package,
Layers,
MapPin,
Crosshair
} from 'lucide-react';
import { Card } from '../components/Common';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
LineChart,
Line,
BarChart,
Bar,
Cell,
PieChart,
Pie
} from 'recharts';
import { motion, AnimatePresence } from 'framer-motion';
const incidentTrend = [
{ day: 'Mon', count: 42, critical: 12 },
{ day: 'Tue', count: 38, critical: 8 },
{ day: 'Wed', count: 55, critical: 18 },
{ day: 'Thu', count: 48, critical: 10 },
{ day: 'Fri', count: 70, critical: 25 },
{ day: 'Sat', count: 85, critical: 32 },
{ day: 'Sun', count: 62, critical: 20 },
];
const operatorPerformance = [
{ name: 'Lifeline', trips: 1450, utl: 85 },
{ name: 'Apex', trips: 1280, utl: 78 },
{ name: 'Mercy', trips: 920, utl: 72 },
{ name: 'CureMove', trips: 1100, utl: 81 },
];
const triageDistribution = [
{ name: 'Immediate (Red)', value: 15, color: '#ff3b3b' },
{ name: 'Urgent (Orange)', value: 25, color: '#ffb800' },
{ name: 'Minor (Green)', value: 45, color: '#00ff88' },
{ name: 'Non-Emg (White)', value: 10, color: '#E2E8F0' },
{ name: 'IFT (Blue)', value: 5, color: '#3B82F6' },
];
type AnalyticsView = 'COMM_CENTER' | 'AGGREGATORS_FLEET' | 'HOSPITALS_CLINICAL' | 'INVENTORY_RESOURCES';
export const AnalyticsReports: React.FC = () => {
const [activeView, setActiveView] = useState<AnalyticsView>('COMM_CENTER');
return (
<div className="page-container" style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<h2 style={{ fontSize: '2.5rem', fontWeight: 900, background: 'linear-gradient(90deg, var(--accent-blue), var(--text-primary))', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>
Platform Intelligence
</h2>
<p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginTop: '4px' }}>
Real-time KPIs, cross-entity performance metrics, and geographic incident heatmaps.
</p>
</div>
<div style={{ display: 'flex', gap: '16px' }}>
<div className="glass" style={{ padding: '4px', borderRadius: '12px', display: 'flex', gap: '4px', background: 'rgba(0,0,0,0.03)' }}>
{(['COMM_CENTER', 'AGGREGATORS_FLEET', 'HOSPITALS_CLINICAL', 'INVENTORY_RESOURCES'] as AnalyticsView[]).map(view => (
<button
key={view}
onClick={() => setActiveView(view)}
style={{
padding: '10px 16px', borderRadius: '8px', border: 'none',
background: activeView === view ? 'var(--accent-blue)' : 'transparent',
color: activeView === view ? '#fff' : 'var(--text-secondary)',
fontWeight: 700, cursor: 'pointer', fontSize: '0.7rem'
}}>
{view.replace('_', ' ')}
</button>
))}
</div>
<button style={{ padding: '12px 24px', background: 'rgba(59, 130, 246, 0.1)', border: '1px solid var(--accent-blue)', borderRadius: '10px', color: 'var(--accent-blue)', fontWeight: 800, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '10px', transition: 'all 0.3s ease' }}>
<Download size={18} /> EXPORT PDF/CSV
</button>
</div>
</header>
{/* GLOBAL HUD STATS */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '20px' }}>
{[
{ label: 'Total Incidents', value: '18,242', trend: '+5.2%', color: 'cyan' },
{ label: 'Avg Dispatch Time', value: '1.42m', trend: '-22s', color: 'green' },
{ label: 'Active ePCR Count', value: '4,102', trend: '+12%', color: 'amber' },
{ label: 'Hospital Pre-Alerts', value: '892', trend: '+8%', color: 'cyan' },
{ label: 'System Uptime', value: '99.99%', trend: 'Stable', color: 'green' },
].map(stat => (
<Card key={stat.label} style={{ padding: '20px' }} className={`glow-${stat.color}`}>
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 700 }}>{stat.label}</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '8px', marginTop: '10px' }}>
<div style={{ fontSize: '1.6rem', fontWeight: 900 }} className="mono">{stat.value}</div>
<div style={{ fontSize: '0.75rem', color: stat.trend.startsWith('+') ? 'var(--accent-green)' : (stat.trend === 'Stable' ? 'var(--accent-cyan)' : 'var(--alert-red)') }}>{stat.trend}</div>
</div>
</Card>
))}
</div>
<AnimatePresence mode="wait">
{activeView === 'COMM_CENTER' && (
<motion.div key="comm" initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1.8fr 1.2fr', gap: '24px' }}>
<Card title="Incident Frequency Trend" subtitle="Daily critical vs standard emergencies.">
<div style={{ height: '350px', marginTop: '20px' }}>
<ResponsiveContainer width="100%" height={350}>
<AreaChart data={incidentTrend}>
<defs>
<linearGradient id="colorCount" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--accent-cyan)" stopOpacity={0.4}/>
<stop offset="95%" stopColor="var(--accent-cyan)" stopOpacity={0}/>
</linearGradient>
<linearGradient id="colorCritical" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--alert-red)" stopOpacity={0.4}/>
<stop offset="95%" stopColor="var(--alert-red)" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(0,0,0,0.05)" vertical={false} />
<XAxis dataKey="day" stroke="var(--text-secondary)" fontSize={12} tick={{fontWeight: 600}} />
<YAxis stroke="var(--text-secondary)" fontSize={12} />
<Tooltip contentStyle={{ background: 'var(--base-bg)', border: '1px solid var(--card-border)', borderRadius: '12px' }} />
<Area type="monotone" dataKey="count" stroke="var(--accent-cyan)" strokeWidth={3} fillOpacity={1} fill="url(#colorCount)" name="Total Incidents" />
<Area type="monotone" dataKey="critical" stroke="var(--alert-red)" strokeWidth={3} fillOpacity={1} fill="url(#colorCritical)" name="Critical (Red)" />
</AreaChart>
</ResponsiveContainer>
</div>
</Card>
<Card title="Traffic Segmentation" subtitle="Distribution by Triage Category.">
<div style={{ height: '350px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={triageDistribution}
cx="50%"
cy="50%"
innerRadius={70}
outerRadius={100}
paddingAngle={5}
dataKey="value"
>
{triageDistribution.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip contentStyle={{ background: 'var(--base-bg)', border: '1px solid var(--card-border)', borderRadius: '12px' }} />
</PieChart>
</ResponsiveContainer>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '10px', marginTop: '10px' }}>
{triageDistribution.map(td => (
<div key={td.name} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: td.color }} />
<span style={{ fontSize: '0.7rem', color: 'var(--text-secondary)' }}>{td.name}</span>
<span style={{ fontSize: '0.7rem', fontWeight: 800, marginLeft: 'auto' }}>{td.value}%</span>
</div>
))}
</div>
</Card>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '24px' }}>
<Card title="Geographic Heatmap" subtitle="Active incident hotspots by district.">
<div style={{ height: '220px', background: 'rgba(0,0,0,0.03)', borderRadius: '12px', border: '1px solid var(--card-border)', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', inset: 0, opacity: 0.3, background: 'url("https://upload.wikimedia.org/wikipedia/commons/4/4b/Tamil_Nadu_districts_map.svg") center/contain no-repeat' }} />
<div style={{ position: 'absolute', top: '20%', left: '45%', width: '40px', height: '40px', background: 'radial-gradient(circle, rgba(255,59,59,0.8) 0%, transparent 70%)' }} />
<div style={{ position: 'absolute', top: '50%', left: '30%', width: '30px', height: '30px', background: 'radial-gradient(circle, rgba(59, 130, 246, 0.8) 0%, transparent 70%)' }} />
<div style={{ position: 'absolute', bottom: '10px', right: '10px', fontSize: '0.6rem', color: 'var(--text-secondary)', background: 'rgba(255,255,255,0.8)', padding: '4px 8px', borderRadius: '4px' }}>
Live Feed: Chennai Hub
</div>
</div>
</Card>
<Card title="CCE Performance" subtitle="Average handle time (AHT) per shift.">
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{[
{ shift: 'Morning (SH1)', aht: '1m 12s', target: '1m 30s', status: 'Optimal' },
{ shift: 'Afternoon (SH2)', aht: '1m 28s', target: '1m 30s', status: 'Healthy' },
{ shift: 'Night (SH3)', aht: '1m 45s', target: '1m 30s', status: 'Delayed' },
].map((cce, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontSize: '0.8rem', fontWeight: 800 }}>{cce.shift}</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)' }}>Log: {cce.aht} (Limit: {cce.target})</div>
</div>
<div style={{ fontSize: '0.75rem', fontWeight: 800, color: cce.status === 'Optimal' ? 'var(--accent-green)' : (cce.status === 'Healthy' ? 'var(--accent-cyan)' : 'var(--alert-red)') }}>{cce.status}</div>
</div>
))}
</div>
</Card>
<Card title="Resource Health" subtitle="Mobile medical unit status.">
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>
<span style={{ fontSize: '0.75rem' }}>ALS Availability</span>
<span className="mono" style={{ fontSize: '0.8rem', fontWeight: 800, color: 'var(--accent-cyan)' }}>88%</span>
</div>
<div style={{ width: '100%', height: '6px', background: 'rgba(0,0,0,0.03)', borderRadius: '3px' }}>
<div style={{ width: '88%', height: '100%', background: 'var(--accent-cyan)', borderRadius: '3px' }}></div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px', marginTop: '8px' }}>
<span style={{ fontSize: '0.75rem' }}>BLS Availability</span>
<span className="mono" style={{ fontSize: '0.8rem', fontWeight: 800, color: 'var(--accent-green)' }}>95%</span>
</div>
<div style={{ width: '100%', height: '6px', background: 'rgba(0,0,0,0.03)', borderRadius: '3px' }}>
<div style={{ width: '95%', height: '100%', background: 'var(--accent-green)', borderRadius: '3px' }}></div>
</div>
</div>
</Card>
</div>
</motion.div>
)}
{activeView === 'AGGREGATORS_FLEET' && (
<motion.div key="agg" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }}>
<Card title="Operator Unit Utilization" subtitle="Total trips vs vehicle utilization rate (last 30 days).">
<div style={{ height: '400px', marginTop: '24px' }}>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={operatorPerformance}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(0,0,0,0.05)" vertical={false} />
<XAxis dataKey="name" stroke="var(--text-secondary)" fontSize={12} tick={{fontWeight: 700}} />
<YAxis yAxisId="left" stroke="var(--text-secondary)" fontSize={12} />
<YAxis yAxisId="right" orientation="right" stroke="var(--text-secondary)" fontSize={12} unit="%" />
<Tooltip contentStyle={{ background: 'var(--base-bg)', border: '1px solid var(--card-border)', borderRadius: '12px' }} />
<Bar yAxisId="left" dataKey="trips" fill="var(--accent-cyan)" radius={[4, 4, 0, 0]} name="Total Trips" barSize={40} />
<Line yAxisId="right" type="monotone" dataKey="utl" stroke="var(--accent-green)" strokeWidth={3} name="Utilization (%)" />
</BarChart>
</ResponsiveContainer>
</div>
</Card>
</motion.div>
)}
</AnimatePresence>
</div>
);
};

View File

@@ -0,0 +1,239 @@
import React, { useState, useEffect } from 'react';
import {
ShieldCheck,
Search,
Download,
History,
Lock,
AlertTriangle,
FileText,
Database,
ChevronRight,
User as UserIcon,
ExternalLink,
Eye,
CheckCircle2,
RefreshCw
} from 'lucide-react';
import { Card } from '../components/Common';
import { motion } from 'framer-motion';
import { authApi } from '../api/auth';
export const AuditCompliance: React.FC = () => {
const [logs, setLogs] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [total, setTotal] = useState(0);
const fetchLogs = async () => {
setIsLoading(true);
const token = localStorage.getItem('teleems_token') || '';
try {
const response = await authApi.getAuditLogs(token);
if (response && response.status === 200 && response.data) {
setLogs(Array.isArray(response.data.logs) ? response.data.logs : (Array.isArray(response.data) ? response.data : []));
setTotal(response.data.total || (Array.isArray(response.data) ? response.data.length : 0));
}
} catch (err) {
console.error('Failed to fetch audit logs', err);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchLogs();
}, []);
const getSeverity = (action: string) => {
if (action.includes('FAILED') || action.includes('BREACH')) return 'High';
if (action.includes('MFA') || action.includes('RESET')) return 'Medium';
return 'Low';
};
return (
<div className="page-container" style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<h2 style={{ fontSize: '2.5rem', fontWeight: 900, background: 'linear-gradient(90deg, #3B82F6, #fff)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>
Audit & Compliance
</h2>
<p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginTop: '4px' }}>
Immutable action logs, HIPAA breach detection, and automated compliance verification.
</p>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={fetchLogs}
style={{ padding: '12px 24px', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--card-border)', borderRadius: '10px', color: '#fff', fontWeight: 800, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '10px' }}
>
<RefreshCw size={18} color="var(--accent-cyan)" className={isLoading ? 'spin-slow' : ''} /> SYNC LOGS
</button>
<button style={{ padding: '12px 24px', background: 'var(--accent-cyan)', color: '#000', border: 'none', borderRadius: '10px', fontWeight: 800, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '10px', boxShadow: '0 0 20px rgba(59, 130, 246, 0.4)' }}>
<Download size={18} /> GENERATE REPORT
</button>
</div>
</header>
<div style={{ display: 'grid', gridTemplateColumns: '2.5fr 1fr', gap: '32px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
{/* 5.9 Audit Trail */}
<Card title="System-Wide Immutable Audit Trail" subtitle={`Displaying ${logs.length} of ${total} security events.`}>
<div style={{ position: 'relative', marginBottom: '24px' }}>
<Search size={18} style={{ position: 'absolute', left: '14px', top: '14px', color: 'var(--text-secondary)' }} />
<input type="text" placeholder="Search logs by user, action, or entity ID..." style={{ width: '100%', padding: '14px 14px 14px 44px', background: 'rgba(0,0,0,0.3)', border: '1px solid var(--card-border)', borderRadius: '10px', color: '#fff' }} />
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
<thead>
<tr style={{ background: 'rgba(255,255,255,0.03)', textAlign: 'left' }}>
<th style={{ padding: '16px' }}>Timestamp</th>
<th style={{ padding: '16px' }}>Actor</th>
<th style={{ padding: '16px' }}>Action Node</th>
<th style={{ padding: '16px' }}>Client Info</th>
<th style={{ padding: '16px' }}>Severity</th>
</tr>
</thead>
<tbody>
{logs.map((log, i) => {
const severity = getSeverity(log.action);
return (
<tr key={log.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.03)', background: severity === 'High' ? 'rgba(255, 59, 59, 0.02)' : 'transparent' }}>
<td style={{ padding: '16px' }} className="mono">{new Date(log.createdAt).toLocaleString()}</td>
<td style={{ padding: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<UserIcon size={14} color="var(--accent-cyan)" />
<span style={{ fontWeight: 700 }}>{log.user?.username || 'System'}</span>
</div>
<div className="mono" style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', marginTop: '4px' }}>{log.user?.email || 'INTERNAL'}</div>
</td>
<td style={{ padding: '16px' }}>
<span style={{ padding: '4px 8px', background: 'rgba(255,255,255,0.05)', borderRadius: '4px', fontSize: '0.7rem', fontWeight: 800 }}>{log.action}</span>
</td>
<td style={{ padding: '16px', color: 'var(--text-secondary)' }}>
<div style={{ fontSize: '0.75rem' }}>IP: {log.ipAddress}</div>
<div style={{ fontSize: '0.65rem', opacity: 0.6, maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{log.userAgent}</div>
</td>
<td style={{ padding: '16px' }}>
<div style={{
display: 'flex', alignItems: 'center', gap: '6px',
color: severity === 'High' ? 'var(--alert-red)' : (severity === 'Medium' ? 'var(--warning-amber)' : 'var(--accent-green)')
}}>
<div style={{ width: '6px', height: '6px', borderRadius: '50%', background: 'currentColor' }} />
<span style={{ fontWeight: 800, fontSize: '0.75rem' }}>{severity.toUpperCase()}</span>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'center' }}>
<button style={{ padding: '10px 20px', background: 'transparent', border: '1px solid var(--card-border)', color: 'var(--text-secondary)', borderRadius: '8px', fontSize: '0.8rem', cursor: 'pointer' }}>LOAD PREVIOUS SESSIONS</button>
</div>
</Card>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
<Card title="HIPAA Breach Detection" subtitle="Active PHI access monitoring.">
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ padding: '16px', borderRadius: '12px', background: 'rgba(0,255,136,0.05)', border: '1px solid rgba(0,255,136,0.1)', display: 'flex', alignItems: 'center', gap: '12px' }}>
<CheckCircle2 size={24} color="var(--accent-green)" />
<div>
<div style={{ fontWeight: 800, fontSize: '0.9rem' }}>No Anomalies Detected</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)' }}>All PHI access patterns match verified clinical roles.</div>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.8rem', padding: '10px 0', borderBottom: '1px solid var(--card-border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Unauthorized Access Attempts</span>
<span style={{ fontWeight: 800 }}>0</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.8rem', padding: '10px 0', borderBottom: '1px solid var(--card-border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Bulk Patient Exports</span>
<span style={{ fontWeight: 800 }}>1 (Admin Approved)</span>
</div>
</div>
</Card>
<Card title="Backup Verification" subtitle="Automated recovery health check.">
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '0.85rem' }}>Database Integrity</span>
<span className="mono" style={{ fontWeight: 800, color: 'var(--accent-cyan)' }}>100%</span>
</div>
<div style={{ width: '100%', height: '6px', background: 'rgba(255,255,255,0.05)', borderRadius: '3px' }}>
<div style={{ width: '100%', height: '100%', background: 'linear-gradient(90deg, var(--accent-cyan), var(--accent-green))', borderRadius: '3px' }}></div>
</div>
</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', padding: '12px', background: 'rgba(0,0,0,0.2)', borderRadius: '8px' }}>
<strong>Last Verified:</strong> 2026-04-15 04:00 AM (UTC)
<br/>
<strong>Snapshot ID:</strong> snap-0a88b22-prod-db
</div>
<button style={{ padding: '8px', background: 'transparent', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-secondary)', fontSize: '0.75rem', fontWeight: 600, cursor: 'pointer' }}>TRIGGER MANUAL BACKUP</button>
</div>
</Card>
</div>
</div>
<aside style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
<Card title="Compliance Shortcuts">
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<button style={{ padding: '14px', background: 'rgba(255,255,255,0.03)', border: '1px solid var(--card-border)', borderRadius: '10px', color: '#fff', textAlign: 'left', display: 'flex', alignItems: 'center', gap: '12px', cursor: 'pointer' }}>
<Lock size={18} color="var(--accent-cyan)" />
<div>
<div style={{ fontSize: '0.85rem', fontWeight: 700 }}>Privacy Impact Report</div>
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)' }}>Quarterly PHI access analysis</div>
</div>
</button>
<button style={{ padding: '14px', background: 'rgba(255,255,255,0.03)', border: '1px solid var(--card-border)', borderRadius: '10px', color: '#fff', textAlign: 'left', display: 'flex', alignItems: 'center', gap: '12px', cursor: 'pointer' }}>
<AlertTriangle size={18} color="var(--warning-amber)" />
<div>
<div style={{ fontSize: '0.85rem', fontWeight: 700 }}>Breach Response Plan</div>
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)' }}>Standard protocols for leaks</div>
</div>
</button>
</div>
</Card>
<Card title="Identity Access Logs">
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{[
{ user: 'Arun K.', time: '10:42 AM', device: 'Chrome / macOS' },
{ user: 'Meena D.', time: '09:15 AM', device: 'Safari / iPhone' },
{ user: 'Naveen P.', time: '08:44 AM', device: 'Chrome / Win11' },
].map((auth, i) => (
<div key={i} style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<div style={{ width: '32px', height: '32px', borderRadius: '50%', background: 'rgba(59, 130, 246, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.7rem', fontWeight: 800, color: 'var(--accent-cyan)' }}>
{auth.user.split(' ').map(n=>n[0]).join('')}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '0.8rem', fontWeight: 700 }}>{auth.user}</div>
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)' }}>{auth.time} {auth.device}</div>
</div>
<Eye size={14} color="var(--text-secondary)" style={{ cursor: 'pointer' }} />
</div>
))}
</div>
</Card>
<Card title="Legal Documents">
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.75rem' }}>
<span>Data Privacy Addendum</span>
<ExternalLink size={14} color="var(--accent-cyan)" style={{ cursor: 'pointer' }} />
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.75rem' }}>
<span>HIPAA BAA Agreement</span>
<ExternalLink size={14} color="var(--accent-cyan)" style={{ cursor: 'pointer' }} />
</div>
</div>
</Card>
</aside>
</div>
</div>
);
};

853
src/pages/CallerPortal.tsx Normal file
View File

@@ -0,0 +1,853 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { incidentsApi } from '../api/incidents';
import type { Patient, Incident } from '../api/types';
import {
Plus,
Search,
Filter,
Download,
MapPin,
Clock,
User,
Activity,
ChevronRight,
PhoneCall,
Truck,
Hospital,
UserCheck,
Navigation,
X,
AlertCircle,
ShieldAlert,
CheckCircle2,
Minus,
Info,
Heart,
ChevronLeft,
Zap,
LayoutDashboard,
Signal
} from 'lucide-react';
import { Card } from '../components/Common';
import { motion, AnimatePresence } from 'framer-motion';
// --- Types & Constants ---
type TriageCategory = 'RED' | 'ORANGE' | 'GREEN' | 'WHITE' | 'BLACK';
interface BookingRecord {
id: string;
caller: string;
phone: string;
triage: TriageCategory;
pickupLocation: string;
patients: number;
symptoms: string[];
status: 'PENDING' | 'DISPATCHED' | 'EN_ROUTE' | 'ON_SITE' | 'COMPLETED' | 'CANCELLED';
timestamp: string;
eta?: string;
receivedBy: string;
fleetOperator: string;
assignedVehicle: string;
destinationHospital: string;
}
const CATEGORIES: { type: TriageCategory; label: string; description: string; color: string; icon: any }[] = [
{ type: 'RED', label: 'Life Threatening', description: 'Cardiac arrest, severe injury, unconscious', color: '#ff4d4d', icon: ShieldAlert },
{ type: 'ORANGE', label: 'Urgent', description: 'Fractures, moderate injuries, severe pain', color: '#ffa500', icon: AlertCircle },
{ type: 'GREEN', label: 'Less Urgent', description: 'Minor injuries, non-critical', color: '#00e676', icon: Activity },
{ type: 'WHITE', label: 'Non-Emergency', description: 'Scheduled transport, routine transfer', color: 'var(--text-secondary)', icon: User },
{ type: 'BLACK', label: 'Dead', description: 'Deceased person, mortuary transport', color: '#9e9e9e', icon: Clock }
];
const COMMON_SYMPTOMS = ['Fainting', 'Bleeding', 'Choking', 'Fits', 'Burns', 'Breathing Difficulty', 'Chest Pain', 'Head Injury' ];
const SEVERITY_LEVELS = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'];
const INCIDENT_CATEGORIES = ['MEDICAL', 'FIRE', 'ACCIDENT', 'TRAUMA', 'CARDIAC'];
const GENDERS = ['Male', 'Female', 'Other'];
const MOCK_DATA: BookingRecord[] = [
{
id: 'BK-1001',
caller: 'John Doe',
phone: '+91 98765 43210',
triage: 'RED',
pickupLocation: '221B Baker Street, London',
patients: 1,
symptoms: ['Chest Pain', 'Breathing Difficulty'],
status: 'EN_ROUTE',
timestamp: '2026-04-18 08:45',
eta: '4 mins',
receivedBy: 'Admin Sarah',
fleetOperator: 'CureSelect North',
assignedVehicle: 'AMB-702 (Paramedic Rajesh)',
destinationHospital: 'City General Hospital'
},
{
id: 'BK-1002',
caller: 'Sarah Williams',
phone: '+91 98888 77777',
triage: 'ORANGE',
pickupLocation: 'Central Square Mall, Floor 2',
patients: 2,
symptoms: ['Bleeding', 'Fracture'],
status: 'DISPATCHED',
timestamp: '2026-04-18 09:02',
eta: '8 mins',
receivedBy: 'Admin Mike',
fleetOperator: 'Apollo Fleet',
assignedVehicle: 'V-881 (Nurse Anita)',
destinationHospital: 'Apollo Speciality Hub'
},
{
id: 'BK-1003',
caller: 'Unknown (Highway)',
phone: '+91 95555 44444',
triage: 'RED',
pickupLocation: 'NH-45, Milestone 12',
patients: 3,
symptoms: ['Head Injury', 'Unconscious'],
status: 'PENDING',
timestamp: '2026-04-18 09:12',
receivedBy: 'System Auto',
fleetOperator: 'Govt Dispatch',
assignedVehicle: 'PENDING',
destinationHospital: 'Trauma Hub'
}
];
const TRIAGE_COLORS = {
RED: { bg: 'rgba(255, 77, 77, 0.15)', text: '#ff4d4d', border: 'rgba(255, 77, 77, 0.3)', glow: '0 0 10px rgba(255, 77, 77, 0.1)' },
ORANGE: { bg: 'rgba(255, 165, 0, 0.15)', text: '#ffa500', border: 'rgba(255, 165, 0, 0.3)', glow: '0 0 10px rgba(255, 165, 0, 0.1)' },
GREEN: { bg: 'rgba(0, 230, 118, 0.15)', text: '#00e676', border: 'rgba(0, 230, 118, 0.3)', glow: '0 0 10px rgba(0, 230, 118, 0.1)' },
WHITE: { bg: 'rgba(0, 0, 0, 0.02)', text: 'var(--text-primary)', border: 'var(--card-border)', glow: 'none' },
BLACK: { bg: 'rgba(0, 0, 0, 0.05)', text: '#9e9e9e', border: 'var(--card-border)', glow: 'none' }
};
const STATUS_COLORS = {
PENDING: '#ffa500',
DISPATCHED: '#3B82F6',
EN_ROUTE: '#3B82F6',
ON_SITE: '#00e676',
COMPLETED: '#9e9e9e',
CANCELLED: '#ff4d4d'
};
export const CallerPortal: React.FC = () => {
const navigate = useNavigate();
const [searchTerm, setSearchTerm] = useState('');
const [isBookingModalOpen, setIsBookingModalOpen] = useState(false);
const [bookingStep, setBookingStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [newBooking, setNewBooking] = useState<Partial<Incident>>({
category: 'MEDICAL',
triage_level: 'RED',
severity: 'CRITICAL',
organisationId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
gps_lat: 12.971598,
gps_lon: 77.594566,
address: '',
guest_name: '',
guest_phone: '',
patients: [
{
name: '',
age: 0,
gender: 'Male',
triage_level: 'RED',
symptoms: []
}
],
notes: ''
});
// Persistence logic
useEffect(() => {
const savedBooking = localStorage.getItem('active_incident_booking');
const savedStep = localStorage.getItem('active_incident_step');
const savedModalState = localStorage.getItem('active_incident_modal_open');
if (savedBooking) setNewBooking(JSON.parse(savedBooking));
if (savedStep) setBookingStep(parseInt(savedStep));
if (savedModalState === 'true') setIsBookingModalOpen(true);
}, []);
useEffect(() => {
if (isBookingModalOpen) {
localStorage.setItem('active_incident_booking', JSON.stringify(newBooking));
localStorage.setItem('active_incident_step', bookingStep.toString());
localStorage.setItem('active_incident_modal_open', 'true');
} else {
localStorage.removeItem('active_incident_booking');
localStorage.removeItem('active_incident_step');
localStorage.removeItem('active_incident_modal_open');
}
}, [newBooking, bookingStep, isBookingModalOpen]);
// Warning before leave
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (isBookingModalOpen) {
e.preventDefault();
e.returnValue = '';
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [isBookingModalOpen]);
const handleNext = () => setBookingStep(s => s + 1);
const handleBack = () => setBookingStep(s => s - 1);
const addPatient = () => {
setNewBooking(prev => ({
...prev,
patients: [
...(prev.patients || []),
{ name: '', age: 0, gender: 'Male', triage_level: 'RED', symptoms: [] }
]
}));
};
const removePatient = (index: number) => {
setNewBooking(prev => ({
...prev,
patients: (prev.patients || []).filter((_, i) => i !== index)
}));
};
const updatePatient = (index: number, updates: Partial<Patient>) => {
setNewBooking(prev => {
const updatedPatients = [...(prev.patients || [])];
updatedPatients[index] = { ...updatedPatients[index], ...updates };
return { ...prev, patients: updatedPatients };
});
};
const toggleSymptom = (patientIndex: number, symptomName: string) => {
setNewBooking(prev => {
const updatedPatients = [...(prev.patients || [])];
const patient = updatedPatients[patientIndex];
const exists = patient.symptoms.find(s => s.name === symptomName);
if (exists) {
patient.symptoms = patient.symptoms.filter(s => s.name !== symptomName);
} else {
patient.symptoms = [...patient.symptoms, { name: symptomName, duration_minutes: 10 }];
}
updatedPatients[patientIndex] = patient;
return { ...prev, patients: updatedPatients };
});
};
const handleAuthorizeDispatch = async () => {
setIsLoading(true);
const token = localStorage.getItem('teleems_token') || '';
try {
const response = await incidentsApi.createIncident(newBooking, token);
if (response && (response.status === 200 || response.status === 201)) {
setSuccessMessage("MISSION AUTHORIZED: DISPATCH SEQUENCE INITIATED");
localStorage.removeItem('active_incident_booking');
localStorage.removeItem('active_incident_step');
localStorage.removeItem('active_incident_modal_open');
setTimeout(() => {
setIsBookingModalOpen(false);
setBookingStep(1);
setSuccessMessage(null);
}, 2000);
}
} catch (error) {
console.error("Dispatch failed:", error);
} finally {
setIsLoading(false);
}
};
const containerVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, staggerChildren: 0.1 } }
};
const itemVariants = {
hidden: { opacity: 0, x: -10 },
visible: { opacity: 1, x: 0 }
};
const renderBookingStep = () => {
if (successMessage) {
return (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
style={{ textAlign: 'center', padding: '40px 0' }}
>
<div style={{ width: '80px', height: '80px', background: 'rgba(0, 230, 118, 0.1)', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 24px' }}>
<CheckCircle2 size={48} color="#00e676" />
</div>
<h3 style={{ fontSize: '1.5rem', fontWeight: 900, color: '#00e676', marginBottom: '12px' }}>{successMessage}</h3>
<p style={{ color: 'var(--text-secondary)' }}>Transmitting coordinates to nearest tactical units...</p>
</motion.div>
);
}
switch (bookingStep) {
case 1:
return (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }}>
<h3 style={{ fontSize: '1.4rem', fontWeight: 800, marginBottom: '24px', letterSpacing: '-0.5px' }}>Severity Assessment</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '14px', marginBottom: '24px' }}>
{CATEGORIES.map(cat => (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
key={cat.type}
onClick={() => { setNewBooking({...newBooking, triage_level: cat.type}); }}
className="glass"
style={{
padding: '20px', borderRadius: '16px', textAlign: 'left', cursor: 'pointer',
border: `1px solid ${newBooking.triage_level === cat.type ? cat.color : 'var(--card-border)'}`,
background: newBooking.triage_level === cat.type ? `${cat.color}15` : 'rgba(0,0,0,0.02)',
display: 'flex', alignItems: 'center', gap: '18px',
transition: 'all 0.3s ease'
}}
>
<div style={{
padding: '12px', background: `${cat.color}20`, borderRadius: '12px', color: cat.color,
boxShadow: newBooking.triage_level === cat.type ? `0 0 15px ${cat.color}40` : 'none'
}}>
<cat.icon size={28} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 800, color: cat.color, fontSize: '1.1rem' }}>{cat.label}</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '2px' }}>{cat.description}</div>
</div>
{newBooking.triage_level === cat.type && <CheckCircle2 size={20} color={cat.color} />}
</motion.button>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<div>
<label style={{ fontSize: '0.7rem', fontWeight: 800, color: 'var(--text-secondary)', textTransform: 'uppercase', display: 'block', marginBottom: '8px' }}>Category</label>
<select
value={newBooking.category}
onChange={e => setNewBooking({...newBooking, category: e.target.value})}
className="glass"
style={{ width: '100%', padding: '12px', borderRadius: '10px', border: '1px solid var(--card-border)', background: 'transparent', color: 'var(--text-primary)', fontWeight: 700 }}
>
{INCIDENT_CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div>
<label style={{ fontSize: '0.7rem', fontWeight: 800, color: 'var(--text-secondary)', textTransform: 'uppercase', display: 'block', marginBottom: '8px' }}>Severity</label>
<select
value={newBooking.severity}
onChange={e => setNewBooking({...newBooking, severity: e.target.value})}
className="glass"
style={{ width: '100%', padding: '12px', borderRadius: '10px', border: '1px solid var(--card-border)', background: 'transparent', color: 'var(--text-primary)', fontWeight: 700 }}
>
{SEVERITY_LEVELS.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
</div>
<button onClick={handleNext} className="hover-glow" style={{ width: '100%', padding: '18px', borderRadius: '16px', background: '#3B82F6', border: 'none', color: '#fff', fontWeight: 900, cursor: 'pointer', fontSize: '1rem', marginTop: '24px' }}>NEXT: CALLER INFO</button>
</motion.div>
);
case 2:
return (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }}>
<h3 style={{ fontSize: '1.4rem', fontWeight: 800, marginBottom: '24px' }}>Caller & Location</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<div style={{ position: 'relative' }}>
<User size={18} style={{ position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', opacity: 0.4 }} />
<input
placeholder="Guest Name"
value={newBooking.guest_name || ''}
onChange={e => setNewBooking({...newBooking, guest_name: e.target.value})}
className="glass"
style={{ width: '100%', padding: '16px 16px 16px 48px', background: 'rgba(0,0,0,0.01)', border: '1px solid var(--card-border)', borderRadius: '12px', color: 'var(--text-primary)', fontSize: '0.9rem' }}
/>
</div>
<div style={{ position: 'relative' }}>
<PhoneCall size={18} style={{ position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', opacity: 0.4 }} />
<input
placeholder="Guest Phone"
value={newBooking.guest_phone || ''}
onChange={e => setNewBooking({...newBooking, guest_phone: e.target.value})}
className="glass"
style={{ width: '100%', padding: '16px 16px 16px 48px', background: 'rgba(0,0,0,0.01)', border: '1px solid var(--card-border)', borderRadius: '12px', color: 'var(--text-primary)', fontSize: '0.9rem' }}
/>
</div>
</div>
<div style={{ position: 'relative' }}>
<MapPin size={18} style={{ position: 'absolute', left: '16px', top: '18px', opacity: 0.4 }} />
<textarea
placeholder="Full Pickup Address"
value={newBooking.address || ''}
onChange={e => setNewBooking({...newBooking, address: e.target.value})}
className="glass"
style={{ width: '100%', padding: '16px 16px 16px 48px', background: 'rgba(0,0,0,0.01)', border: '1px solid var(--card-border)', borderRadius: '12px', color: 'var(--text-primary)', fontSize: '0.9rem', minHeight: '80px', resize: 'none' }}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<div style={{ position: 'relative' }}>
<Navigation size={16} style={{ position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', opacity: 0.4 }} />
<input
type="number"
placeholder="Lat"
value={newBooking.gps_lat}
onChange={e => setNewBooking({...newBooking, gps_lat: parseFloat(e.target.value)})}
className="glass"
style={{ width: '100%', padding: '12px 12px 12px 40px', background: 'rgba(0,0,0,0.01)', border: '1px solid var(--card-border)', borderRadius: '10px', color: 'var(--text-primary)', fontSize: '0.8rem' }}
/>
</div>
<div style={{ position: 'relative' }}>
<Navigation size={16} style={{ position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', opacity: 0.4 }} />
<input
type="number"
placeholder="Lon"
value={newBooking.gps_lon}
onChange={e => setNewBooking({...newBooking, gps_lon: parseFloat(e.target.value)})}
className="glass"
style={{ width: '100%', padding: '12px 12px 12px 40px', background: 'rgba(0,0,0,0.01)', border: '1px solid var(--card-border)', borderRadius: '10px', color: 'var(--text-primary)', fontSize: '0.8rem' }}
/>
</div>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '32px' }}>
<button onClick={handleBack} className="glass" style={{ flex: 1, padding: '16px', borderRadius: '12px', border: '1px solid var(--card-border)', color: 'var(--text-secondary)', cursor: 'pointer', fontWeight: 700 }}>BACK</button>
<button onClick={handleNext} className="hover-glow" style={{ flex: 2, padding: '16px', borderRadius: '12px', background: '#3B82F6', border: 'none', color: '#fff', fontWeight: 800, cursor: 'pointer', fontSize: '1rem' }}>PATIENT DETAILS</button>
</div>
</motion.div>
);
case 3:
return (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<h3 style={{ fontSize: '1.4rem', fontWeight: 800, margin: 0 }}>Patient Registry</h3>
<button onClick={addPatient} style={{ background: 'rgba(59, 130, 246, 0.1)', border: '1px solid #3B82F6', color: '#3B82F6', padding: '6px 12px', borderRadius: '8px', cursor: 'pointer', fontSize: '0.8rem', fontWeight: 800, display: 'flex', alignItems: 'center', gap: '6px' }}>
<Plus size={14} /> ADD PATIENT
</button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px', maxHeight: '400px', overflowY: 'auto', paddingRight: '4px' }} className="no-scrollbar">
{newBooking.patients?.map((patient, idx) => (
<div key={idx} className="glass" style={{ padding: '20px', borderRadius: '20px', border: '1px solid var(--card-border)', position: 'relative' }}>
<button onClick={() => removePatient(idx)} style={{ position: 'absolute', top: '10px', right: '10px', background: 'none', border: 'none', color: '#ff4d4d', cursor: 'pointer', opacity: 0.6 }}>
<X size={18} />
</button>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
<div style={{ width: '24px', height: '24px', background: '#3B82F6', borderRadius: '50%', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.7rem', fontWeight: 900 }}>{idx + 1}</div>
<span style={{ fontWeight: 800, fontSize: '0.9rem' }}>Patient Assessment</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<label style={{ fontSize: '0.65rem', fontWeight: 800, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Full Name</label>
<input
placeholder="Patient Name"
value={patient.name}
onChange={e => updatePatient(idx, { name: e.target.value })}
className="glass"
style={{ padding: '10px', borderRadius: '8px', border: '1px solid var(--card-border)', background: 'rgba(0,0,0,0.01)', color: 'var(--text-primary)', fontSize: '0.85rem', width: '100%' }}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<label style={{ fontSize: '0.65rem', fontWeight: 800, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Age</label>
<input
type="number"
placeholder="Age"
value={patient.age || ''}
onChange={e => updatePatient(idx, { age: parseInt(e.target.value) || 0 })}
className="glass"
style={{ padding: '10px', borderRadius: '8px', border: '1px solid var(--card-border)', background: 'rgba(0,0,0,0.01)', color: 'var(--text-primary)', fontSize: '0.85rem', width: '100%' }}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<label style={{ fontSize: '0.65rem', fontWeight: 800, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Gender</label>
<select
value={patient.gender}
onChange={e => updatePatient(idx, { gender: e.target.value })}
className="glass"
style={{ padding: '10px', borderRadius: '8px', border: '1px solid var(--card-border)', background: 'transparent', color: 'var(--text-primary)', fontSize: '0.85rem', width: '100%' }}
>
{GENDERS.map(g => <option key={g} value={g}>{g}</option>)}
</select>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ fontSize: '0.65rem', fontWeight: 800, color: 'var(--text-secondary)', textTransform: 'uppercase', marginBottom: '8px', display: 'block' }}>Triage</label>
<div style={{ display: 'flex', gap: '6px' }}>
{CATEGORIES.map(cat => (
<button
key={cat.type}
onClick={() => updatePatient(idx, { triage_level: cat.type })}
style={{
flex: 1, padding: '8px', borderRadius: '6px', fontSize: '0.65rem', fontWeight: 800, cursor: 'pointer',
background: patient.triage_level === cat.type ? `${cat.color}20` : 'rgba(0,0,0,0.02)',
border: `1px solid ${patient.triage_level === cat.type ? cat.color : 'var(--card-border)'}`,
color: patient.triage_level === cat.type ? cat.color : 'var(--text-secondary)'
}}
>
{cat.type}
</button>
))}
</div>
</div>
<div>
<label style={{ fontSize: '0.65rem', fontWeight: 800, color: 'var(--text-secondary)', textTransform: 'uppercase', marginBottom: '8px', display: 'block' }}>Symptoms</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
{COMMON_SYMPTOMS.map(s => {
const isSelected = patient.symptoms.some(sym => sym.name === s);
return (
<div key={s} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<button
onClick={() => toggleSymptom(idx, s)}
style={{
padding: '6px 12px', borderRadius: '20px', fontSize: '0.7rem', fontWeight: 600, cursor: 'pointer',
background: isSelected ? 'rgba(59, 130, 246, 0.1)' : 'rgba(0,0,0,0.02)',
border: `1px solid ${isSelected ? '#3B82F6' : 'var(--card-border)'}`,
color: isSelected ? '#3B82F6' : 'var(--text-secondary)',
display: 'flex', alignItems: 'center', gap: '8px'
}}
>
{s}
{isSelected && (
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', borderLeft: '1px solid rgba(59, 130, 246, 0.3)', paddingLeft: '8px' }}>
<Clock size={12} />
<input
type="number"
value={patient.symptoms.find(sym => sym.name === s)?.duration_minutes || 10}
onClick={e => e.stopPropagation()}
onChange={e => {
e.stopPropagation();
const val = parseInt(e.target.value) || 0;
updatePatient(idx, {
symptoms: patient.symptoms.map(sym => sym.name === s ? { ...sym, duration_minutes: val } : sym)
});
}}
style={{ width: '30px', background: 'transparent', border: 'none', color: '#3B82F6', fontSize: '0.7rem', fontWeight: 900, outline: 'none' }}
/>
<span style={{ fontSize: '0.6rem', opacity: 0.7 }}>m</span>
</div>
)}
</button>
</div>
);
})}
</div>
</div>
</div>
))}
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '32px' }}>
<button onClick={handleBack} className="glass" style={{ flex: 1, padding: '16px', borderRadius: '12px', border: '1px solid var(--card-border)', color: 'var(--text-secondary)', cursor: 'pointer', fontWeight: 700 }}>BACK</button>
<button onClick={handleNext} className="hover-glow" style={{ flex: 2, padding: '16px', borderRadius: '12px', background: '#3B82F6', border: 'none', color: '#fff', fontWeight: 800, cursor: 'pointer', fontSize: '1rem' }}>MISSION REVIEW</button>
</div>
</motion.div>
);
case 4:
return (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }}>
<h3 style={{ fontSize: '1.4rem', fontWeight: 800, marginBottom: '24px' }}>Finalize Mission</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', marginBottom: '24px' }}>
<div className="glass" style={{ padding: '16px', borderRadius: '12px', background: 'rgba(59, 130, 246, 0.05)', border: '1px solid rgba(59, 130, 246, 0.1)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '12px' }}>
<Activity size={18} color="#3B82F6" />
<span style={{ fontWeight: 800, fontSize: '0.9rem' }}>Mission Summary</span>
</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}><span>Category:</span> <span style={{ fontWeight: 700, color: 'var(--text-primary)' }}>{newBooking.category}</span></div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}><span>Severity:</span> <span style={{ fontWeight: 700, color: '#ff4d4d' }}>{newBooking.severity}</span></div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}><span>Total Patients:</span> <span style={{ fontWeight: 700, color: 'var(--text-primary)' }}>{newBooking.patients?.length}</span></div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}><span>Location:</span> <span style={{ fontWeight: 700, color: 'var(--text-primary)', maxWidth: '200px', textAlign: 'right', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{newBooking.address}</span></div>
</div>
</div>
<div>
<label style={{ fontSize: '0.7rem', fontWeight: 800, color: 'var(--text-secondary)', textTransform: 'uppercase', marginBottom: '8px', display: 'block' }}>Field Notes / Instructions</label>
<textarea
placeholder="Any additional info for responders..."
value={newBooking.notes || ''}
onChange={e => setNewBooking({...newBooking, notes: e.target.value})}
className="glass"
style={{ width: '100%', padding: '16px', background: 'rgba(0,0,0,0.01)', border: '1px solid var(--card-border)', borderRadius: '12px', color: 'var(--text-primary)', fontSize: '0.9rem', minHeight: '100px', resize: 'none' }}
/>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '32px' }}>
<button onClick={handleBack} className="glass" style={{ flex: 1, padding: '16px', borderRadius: '12px', border: '1px solid var(--card-border)', color: 'var(--text-secondary)', cursor: 'pointer', fontWeight: 700 }}>BACK</button>
<button
onClick={handleAuthorizeDispatch}
disabled={isLoading}
className="hover-glow pulse-glow"
style={{
flex: 2, padding: '16px', borderRadius: '12px', background: 'linear-gradient(135deg, #3B82F6, #1d4ed8)', border: 'none', color: '#fff', fontWeight: 900, cursor: 'pointer', fontSize: '1.1rem', letterSpacing: '0.5px',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '12px', opacity: isLoading ? 0.7 : 1
}}
>
{isLoading ? <Clock size={20} className="spin" /> : <Zap size={20} />}
{isLoading ? 'TRANSMITTING...' : 'AUTHORIZE DISPATCH'}
</button>
</div>
</motion.div>
);
default: return null;
}
};
return (
<motion.div
initial="hidden"
animate="visible"
variants={containerVariants}
className="page-container no-scrollbar"
style={{ display: 'flex', flexDirection: 'column', gap: '32px', position: 'relative' }}
>
{/* Decorative Orbs */}
<div style={{ position: 'absolute', top: -50, right: -50, width: '300px', height: '300px', background: 'radial-gradient(circle, rgba(59, 130, 246, 0.1) 0%, transparent 70%)', pointerEvents: 'none', zIndex: 0 }} />
<div style={{ position: 'absolute', bottom: 50, left: -50, width: '400px', height: '400px', background: 'radial-gradient(circle, rgba(255, 77, 77, 0.05) 0%, transparent 70%)', pointerEvents: 'none', zIndex: 0 }} />
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', position: 'relative', zIndex: 1 }}>
<motion.div variants={itemVariants}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
<div style={{ padding: '6px', background: '#3B82F6', borderRadius: '6px' }}><Signal size={18} color="#fff" /></div>
<span style={{ fontSize: '0.9rem', fontWeight: 800, color: '#3B82F6', letterSpacing: '2px', textTransform: 'uppercase' }}>Live Command</span>
</div>
<h2 style={{ fontSize: '3rem', fontWeight: 900, background: 'linear-gradient(135deg, var(--text-primary) 0%, #3B82F6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', letterSpacing: '-2px', lineHeight: 1 }}>
Fleet Intelligence Nodes
</h2>
<p style={{ color: 'var(--text-secondary)', fontSize: '1rem', marginTop: '8px' }}>
Real-time telemetry and resource orchestration for critical emergency services.
</p>
</motion.div>
<motion.div variants={itemVariants} style={{ display: 'flex', gap: '14px' }}>
<motion.button whileHover={{ scale: 1.05 }} className="glass" style={{ padding: '12px 24px', borderRadius: '14px', background: 'rgba(0,0,0,0.03)', border: '1px solid var(--card-border)', color: 'var(--text-primary)', display: 'flex', alignItems: 'center', gap: '10px', cursor: 'pointer', fontWeight: 700 }}>
<Download size={20} /> ARCHIVE
</motion.button>
<motion.button
onClick={() => setIsBookingModalOpen(true)}
whileHover={{ scale: 1.05, boxShadow: '0 0 30px rgba(59, 130, 246, 0.5)' }}
whileTap={{ scale: 0.95 }}
className="emergency-btn pulse-glow"
style={{
padding: '12px 32px', background: 'linear-gradient(135deg, #3B82F6 0%, #1d4ed8 100%)',
border: 'none', borderRadius: '14px', color: '#fff', fontWeight: 900,
display: 'flex', alignItems: 'center', gap: '12px', cursor: 'pointer',
fontSize: '1rem', letterSpacing: '0.5px'
}}>
<Plus size={24} strokeWidth={3} /> EMERGENCY MISSION
</motion.button>
</motion.div>
</header>
{/* Modern Stats Layer */}
<motion.div variants={containerVariants} style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '24px', position: 'relative', zIndex: 1 }}>
{[
{ label: 'Network Throughput', value: '142 Cases', icon: Activity, color: '#3B82F6', trend: '+12%' },
{ label: 'Pending Dispatch', value: '03 Critical', icon: ShieldAlert, color: '#ff4d4d', trend: 'P0 Priority' },
{ label: 'Active Coverage', value: '92.4%', icon: Navigation, color: '#00e676', trend: 'Optimal' },
{ label: 'Operator Status', value: '12 Units', icon: UserCheck, color: 'var(--text-primary)', trend: 'Online' }
].map((stat, i) => (
<motion.div key={i} variants={itemVariants} whileHover={{ y: -5 }}>
<Card style={{ padding: '24px', position: 'relative', overflow: 'hidden', borderLeft: `6px solid ${stat.color}` }}>
<div style={{ position: 'absolute', right: '-10px', top: '-10px', opacity: 0.05 }}><stat.icon size={100} /></div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.75rem', textTransform: 'uppercase', fontWeight: 900, letterSpacing: '1px' }}>{stat.label}</div>
<div style={{ fontSize: '2rem', fontWeight: 900, marginTop: '12px', color: 'var(--text-primary)', letterSpacing: '-1px' }}>{stat.value}</div>
<div style={{ marginTop: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ padding: '2px 8px', borderRadius: '10px', background: `${stat.color}15`, color: stat.color, fontSize: '0.7rem', fontWeight: 900 }}>{stat.trend}</div>
<div style={{ flex: 1, height: '2px', background: 'rgba(0,0,0,0.05)', borderRadius: '1px' }}>
<div style={{ width: '70%', height: '100%', background: stat.color, borderRadius: '1px', boxShadow: `0 0 10px ${stat.color}` }} />
</div>
</div>
</Card>
</motion.div>
))}
</motion.div>
{/* Refined Command Grid */}
<motion.div variants={itemVariants} style={{ position: 'relative', zIndex: 1 }}>
<Card style={{ padding: '0', overflow: 'hidden', borderRadius: '24px', border: '1px solid var(--card-border)', background: 'var(--card-bg)' }}>
<div style={{ padding: '24px', borderBottom: '1px solid rgba(0,0,0,0.02)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ display: 'flex', alignItems: 'center', gap: '12px', margin: 0, fontSize: '1.2rem', fontWeight: 800 }}>
<LayoutDashboard size={22} color="#3B82F6" />
System Access Registry
</h3>
<div className="glass" style={{ padding: '8px 16px', borderRadius: '12px', display: 'flex', alignItems: 'center', gap: '12px', border: '1px solid var(--card-border)' }}>
<Search size={18} color="var(--text-secondary)" />
<input placeholder="Filter missions..." style={{ background: 'transparent', border: 'none', color: 'var(--text-primary)', outline: 'none', fontSize: '0.9rem' }} />
</div>
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left', minWidth: '1100px' }}>
<thead>
<tr style={{ background: 'rgba(0,0,0,0.01)' }}>
<th style={{ padding: '20px 24px', fontSize: '0.75rem', color: 'var(--text-secondary)', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '1px' }}>ID & Mission Log</th>
<th style={{ padding: '20px 24px', fontSize: '0.75rem', color: 'var(--text-secondary)', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '1px' }}>Caller Registry</th>
<th style={{ padding: '20px 24px', fontSize: '0.75rem', color: 'var(--text-secondary)', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '1px' }}>Operational Node</th>
<th style={{ padding: '20px 24px', fontSize: '0.75rem', color: 'var(--text-secondary)', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '1px' }}>Tactical Rescue</th>
<th style={{ padding: '20px 24px', fontSize: '0.75rem', color: 'var(--text-secondary)', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '1px' }}>Logistics pathway</th>
<th style={{ padding: '20px 24px', textAlign: 'right' }}></th>
</tr>
</thead>
<tbody>
{MOCK_DATA.map((row) => (
<motion.tr
key={row.id}
whileHover={{ background: 'rgba(59, 130, 246, 0.02)' }}
style={{ borderBottom: '1px solid rgba(0,0,0,0.02)', cursor: 'default', transition: 'background 0.3s' }}
>
<td style={{ padding: '20px 24px' }}>
<div className="mono" style={{ fontWeight: 900, fontSize: '0.9rem', color: '#3B82F6' }}>{row.id}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginTop: '6px', opacity: 0.4 }}>
<Clock size={12} />
<span style={{ fontSize: '0.7rem' }}>{row.timestamp}</span>
</div>
</td>
<td style={{ padding: '20px 24px' }}>
<div style={{ fontWeight: 800, fontSize: '1rem' }}>{row.caller}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '8px' }}>
<span style={{
fontSize: '0.6rem', padding: '3px 8px', borderRadius: '6px', fontWeight: 900,
background: TRIAGE_COLORS[row.triage].bg, color: TRIAGE_COLORS[row.triage].text, border: `1px solid ${TRIAGE_COLORS[row.triage].border}`,
boxShadow: TRIAGE_COLORS[row.triage].glow
}}>{row.triage}</span>
<div style={{ width: '4px', height: '4px', borderRadius: '50%', background: 'rgba(0,0,0,0.1)' }} />
<span style={{ fontSize: '0.8rem', fontWeight: 700, color: STATUS_COLORS[row.status] }}>{row.status.replace('_', ' ')}</span>
</div>
</td>
<td style={{ padding: '20px 24px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ width: '40px', height: '40px', background: 'rgba(59, 130, 246, 0.1)', borderRadius: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<UserCheck size={20} color="#3B82F6" />
</div>
<div>
<div style={{ fontSize: '0.9rem', fontWeight: 800 }}>{row.receivedBy}</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', textTransform: 'uppercase', marginTop: '2px' }}>{row.fleetOperator}</div>
</div>
</div>
</td>
<td style={{ padding: '20px 24px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ width: '40px', height: '40px', background: 'rgba(0, 230, 118, 0.1)', borderRadius: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Truck size={20} color="#00e676" />
</div>
<div>
<div style={{ fontSize: '0.95rem', fontWeight: 800 }}>{row.assignedVehicle}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginTop: '4px' }}>
<div className="pulse" style={{ width: '6px', height: '6px', borderRadius: '50%', background: '#00e676' }} />
<span style={{ fontSize: '0.7rem', color: '#00e676', fontWeight: 700 }}>{row.eta ? `+ ${row.eta} ETA` : 'Active'}</span>
</div>
</div>
</div>
</td>
<td style={{ padding: '20px 24px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '10px', background: 'rgba(0,0,0,0.01)', borderRadius: '12px', border: '1px solid var(--card-border)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#ff4d4d', boxShadow: '0 0 8px #ff4d4d' }} />
<span style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-secondary)', maxWidth: '160px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{row.pickupLocation}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<Hospital size={16} color="#3B82F6" />
<span style={{ fontSize: '0.85rem', fontWeight: 800, color: 'var(--text-primary)', whiteSpace: 'nowrap' }}>{row.destinationHospital}</span>
</div>
</div>
</td>
<td style={{ padding: '20px 24px', textAlign: 'right' }}>
<motion.button whileHover={{ x: 5 }} style={{ background: 'transparent', border: 'none', cursor: 'pointer', color: '#3B82F6' }}>
<ChevronRight size={24} />
</motion.button>
</td>
</motion.tr>
))}
</tbody>
</table>
</div>
</Card>
</motion.div>
{/* Modal - Re-designed for Attraction */}
<AnimatePresence>
{isBookingModalOpen && (
<div className="modal-overlay" style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(255,255,255,0.85)', backdropFilter: 'blur(15px)', zIndex: 2000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px' }}>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 30 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 30 }}
className="glass"
style={{
width: '100%', maxWidth: '540px', background: 'var(--card-bg)', borderRadius: '32px', padding: '40px', border: '1px solid #3B82F6',
boxShadow: '0 0 100px rgba(59, 130, 246, 0.05)', position: 'relative', overflowY: 'auto', maxHeight: '90vh'
}}
>
{/* Decorative Modal Background */}
<div style={{ position: 'absolute', top: -100, left: -100, width: '300px', height: '300px', background: 'radial-gradient(circle, rgba(59, 130, 246, 0.05) 0%, transparent 70%)', pointerEvents: 'none' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '40px', position: 'relative', zIndex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ width: '48px', height: '48px', borderRadius: '16px', background: 'rgba(59, 130, 246, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Zap size={24} color="#3B82F6" />
</div>
<div>
<h2 style={{ fontSize: '1.6rem', fontWeight: 900, margin: 0, letterSpacing: '-0.5px' }}>Mission Config</h2>
<div style={{ display: 'flex', gap: '6px', marginTop: '4px' }}>
{[1, 2, 3, 4].map(i => <div key={i} style={{ width: '20px', height: '3px', borderRadius: '2px', background: i <= bookingStep ? '#3B82F6' : 'rgba(0,0,0,0.1)' }} />)}
</div>
</div>
</div>
<motion.button whileHover={{ rotate: 90 }} onClick={() => { setIsBookingModalOpen(false); setBookingStep(1); }} style={{ background: 'rgba(0,0,0,0.03)', border: 'none', borderRadius: '50%', padding: '10px', cursor: 'pointer', color: 'var(--text-secondary)' }}>
<X size={24} />
</motion.button>
</div>
<div style={{ position: 'relative', zIndex: 1 }}>
{renderBookingStep()}
</div>
</motion.div>
</div>
)}
</AnimatePresence>
<style>{`
.pulse-glow {
animation: pulseGlow 3s infinite;
}
@keyframes pulseGlow {
0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); }
70% { box-shadow: 0 0 0 15px rgba(59, 130, 246, 0); }
100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
}
.pulse {
animation: pulsePoint 2s infinite;
}
@keyframes pulsePoint {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0.5; }
100% { transform: scale(1); opacity: 1; }
}
.page-container::-webkit-scrollbar { display: none; }
`}</style>
</motion.div>
);
};

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { ArrowLeft, Construction, ArrowRight } from 'lucide-react';
interface ComingSoonPortalProps {
title: string;
icon: React.ElementType;
}
export const ComingSoonPortal: React.FC<ComingSoonPortalProps> = ({ title, icon: Icon }) => {
const navigate = useNavigate();
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-950 text-white p-8">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="max-w-md w-full bg-slate-900/50 border border-slate-800 rounded-3xl p-12 text-center backdrop-blur-xl"
>
<div className="w-20 h-20 bg-blue-500/10 border border-blue-500/20 rounded-2xl flex items-center justify-center mx-auto mb-8 text-blue-400">
<Icon size={40} />
</div>
<h1 className="text-3xl font-black mb-4 tracking-tight">{title} Portal</h1>
<div className="flex items-center justify-center gap-2 text-amber-500 font-bold text-sm uppercase tracking-widest mb-8">
<Construction size={16} />
<span>Under Construction</span>
</div>
<p className="text-slate-400 mb-10 leading-relaxed">
The {title} specialized interface is currently being optimized for high-performance clinical workflows.
</p>
<div className="space-y-4">
<button
onClick={() => navigate('/launcher')}
className="w-full py-4 bg-white text-slate-950 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-slate-200 transition-colors"
>
<ArrowLeft size={18} />
Back to Launcher
</button>
<button
onClick={() => navigate('/')}
className="w-full py-4 bg-slate-800 text-white rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-slate-700 transition-colors"
>
Access Core Dashboard
<ArrowRight size={18} />
</button>
</div>
</motion.div>
<div className="mt-12 text-slate-600 text-xs font-mono">
STUB_ID: {title.toUpperCase().replace(/\s/g, '_')}_v0.1-ALPHA
</div>
</div>
);
};

839
src/pages/Dashboard.tsx Normal file
View File

@@ -0,0 +1,839 @@
import React, { useState, useEffect, useMemo } from 'react';
import {
Activity,
Truck,
HeartPulse,
Video,
ShieldCheck,
Database,
Settings,
Users as UsersIcon,
RefreshCw,
Zap,
Navigation
} from 'lucide-react';
import { StatCard, Card } from '../components/Common';
import {
AreaChart,
Area,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell
} from 'recharts';
import { motion, AnimatePresence } from 'framer-motion';
import { incidentsApi } from '../api/incidents';
import { authApi } from '../api/auth';
import type { Incident } from '../api/types';
// --- LIVE INCIDENT MAP COMPONENT ---
const LiveIncidentMap: React.FC<{ incidents: Incident[] }> = ({ incidents }) => {
const [isMapReady, setIsMapReady] = React.useState(false);
const mapRef = React.useRef<any>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
const markersRef = React.useRef<{ [key: string]: any }>({});
const [L, setL] = React.useState<any>(null);
React.useEffect(() => {
if (typeof window === 'undefined') return;
// Ensure Leaflet assets are loaded
if (!document.getElementById('leaflet-style')) {
const link = document.createElement('link');
link.id = 'leaflet-style';
link.rel = 'stylesheet';
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
document.head.appendChild(link);
}
const initMap = (leaflet: any) => {
if (!containerRef.current || mapRef.current) return;
// Defensive: Clear container if Leaflet previously left a trace but we lost the ref
if ((containerRef.current as any)._leaflet_id) {
containerRef.current.innerHTML = '';
delete (containerRef.current as any)._leaflet_id;
}
const m = leaflet.map(containerRef.current, {
zoomControl: false,
attributionControl: false
}).setView([12.9716, 77.5946], 11);
leaflet.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
maxZoom: 19
}).addTo(m);
mapRef.current = m;
m.whenReady(() => setIsMapReady(true));
};
const existingL = (window as any).L;
if (existingL) {
setL(existingL);
initMap(existingL);
} else {
const script = document.createElement('script');
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
script.async = true;
script.onload = () => {
const leaflet = (window as any).L;
if (leaflet) {
setL(leaflet);
initMap(leaflet);
}
};
document.head.appendChild(script);
}
return () => {
if (mapRef.current) {
try {
mapRef.current.remove();
} catch (e) {
console.warn('Error removing map:', e);
}
mapRef.current = null;
setIsMapReady(false);
}
};
}, []);
React.useEffect(() => {
if (!L || !mapRef.current || !isMapReady) return;
const map = mapRef.current;
// Clear old markers that are no longer in the list
Object.keys(markersRef.current).forEach(id => {
if (!incidents.find(inc => inc.id === id)) {
markersRef.current[id].remove();
delete markersRef.current[id];
}
});
const coordMap: { [key: string]: number } = {};
incidents.forEach(inc => {
let lat = Number(inc.gps_lat);
let lon = Number(inc.gps_lon);
// Handle overlapping coordinates with deterministic jitter
const coordKey = `${lat.toFixed(5)},${lon.toFixed(5)}`;
const count = (coordMap[coordKey] || 0) + 1;
coordMap[coordKey] = count;
if (count > 1) {
// Spiral jitter for better visibility of multiple incidents at same spot
const radius = 0.0003 * Math.sqrt(count - 1);
const angle = (count - 1) * 137.5 * (Math.PI / 180); // Golden angle
lat += radius * Math.cos(angle);
lon += radius * Math.sin(angle);
}
let color = '#F59E0B'; // Default Amber for active
const status = inc.status?.toUpperCase();
if (status === 'COMPLETED' || status === 'HANDOVER') {
color = '#10B981'; // Green for resolved
} else if (status === 'CANCELLED') {
color = '#4A5568'; // Gray for cancelled
} else if (inc.severity?.toUpperCase() === 'CRITICAL') {
color = '#EF4444'; // Red for critical
}
if (!markersRef.current[inc.id]) {
const icon = L.divIcon({
className: 'custom-incident-marker',
html: `<div style="background-color: ${color}; width: 12px; height: 12px; border-radius: 50%; box-shadow: 0 0 15px ${color}; border: 2px solid #fff; position: relative;">
<div style="position: absolute; top: -4px; left: -4px; right: -4px; bottom: -4px; border-radius: 50%; border: 2px solid ${color}; animation: pulse-ring 2s infinite;"></div>
</div>`,
iconSize: [12, 12],
iconAnchor: [6, 6]
});
const callerName = inc.guest_name || inc.caller_id || 'Unknown';
const callerPhone = inc.guest_phone || 'N/A';
const patientCount = inc.patients?.length || 0;
markersRef.current[inc.id] = L.marker([lat, lon], { icon })
.addTo(map)
.bindPopup(`
<div style="padding: 12px; min-width: 220px; font-family: 'Inter', sans-serif; color: var(--text-primary);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<b style="color: var(--accent-cyan); font-size: 0.9rem; font-family: monospace;">#${inc.id.split('-').pop()?.toUpperCase()}</b>
<span style="font-size: 0.6rem; background: ${color}33; color: ${color}; padding: 2px 8px; border-radius: 4px; font-weight: 900; border: 1px solid ${color}66;">${inc.status}</span>
</div>
<div style="font-size: 0.75rem; color: var(--text-primary); margin-bottom: 6px; font-weight: 700;">${inc.category} <span style="color: ${color}">• ${inc.severity}</span></div>
<div style="font-size: 0.7rem; color: var(--text-secondary); margin-bottom: 12px; display: flex; align-items: start; gap: 6px; line-height: 1.4;">
<span style="font-size: 0.9rem;">📍</span> <span>${inc.address}</span>
</div>
<div style="background: rgba(0,0,0,0.02); padding: 10px; border-radius: 8px; border: 1px solid var(--card-border);">
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span style="font-size: 0.6rem; color: #64748B; text-transform: uppercase; font-weight: 700;">Caller</span>
<span style="font-size: 0.7rem; font-weight: 700; color: var(--text-primary);">${callerName}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span style="font-size: 0.6rem; color: #64748B; text-transform: uppercase; font-weight: 700;">Phone</span>
<span style="font-size: 0.7rem; font-weight: 700; color: var(--text-primary);">${callerPhone}</span>
</div>
<div style="padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.05); display: flex; flex-direction: column; gap: 4px;">
<div style="display: flex; align-items: center; gap: 6px;">
<div style="width: 6px; height: 6px; border-radius: 50%; background: var(--accent-cyan);"></div>
<span style="font-size: 0.65rem; font-weight: 800; color: var(--accent-cyan);">${patientCount} PATIENT${patientCount !== 1 ? 'S' : ''} DETECTED</span>
</div>
<div style="font-size: 0.6rem; color: #94A3B8; padding-left: 12px; font-style: italic; overflow: hidden; text-overflow: ellipsis; display: flex; flex-direction: column; gap: 2px;">
${inc.patients && inc.patients.length > 0 ? inc.patients.map((p: any) => `
<div style="display: flex; justify-content: space-between;">
<span>${p.name || 'Anonymous'} (${p.age || '?'}${p.gender ? p.gender[0] : ''})</span>
<span style="color: var(--accent-cyan)">${p.symptoms?.map((s: any) => typeof s === 'string' ? s : s.name).join(', ') || 'No symptoms'}</span>
</div>
`).join('') : 'No patient data'}
</div>
</div>
</div>
</div>
`, {
className: 'glass-popup',
maxWidth: 300
});
} else {
markersRef.current[inc.id].setLatLng([lat, lon]);
}
});
// Auto-fit map to show all incidents
if (incidents.length > 0 && map && isMapReady) {
try {
const validCoords = incidents
.map(i => [Number(i.gps_lat), Number(i.gps_lon)])
.filter(c =>
Number.isFinite(c[0]) &&
Number.isFinite(c[1]) &&
Math.abs(c[0]) <= 90 &&
Math.abs(c[1]) <= 180 &&
(c[0] !== 0 || c[1] !== 0) // Ignore 0,0 placeholders
);
if (validCoords.length > 0) {
const bounds = L.latLngBounds(validCoords as any);
const container = map.getContainer();
if (bounds.isValid() && container.offsetWidth > 0 && container.offsetHeight > 0) {
// If all points are at the exact same location, fitBounds can sometimes fail or behave weirdly
const sw = bounds.getSouthWest();
const ne = bounds.getNorthEast();
if (sw.lat === ne.lat && sw.lng === ne.lng) {
map.setView(sw, 14);
} else {
map.fitBounds(bounds, { padding: [50, 50], maxZoom: 14 });
}
}
}
} catch (err) {
// Suppress specific "Bounds are not valid" error as we handle it defensively now
if (!(err instanceof Error && err.message.includes('Bounds are not valid'))) {
console.error('Map fitBounds error:', err);
}
}
} else if (map && isMapReady) {
// Default view if no incidents
map.setView([12.9716, 77.5946], 11);
}
map.on('click', (e: any) => {
const { lat, lng } = e.latlng;
// Emit a custom event or use a ref to open the modal from the parent
const event = new CustomEvent('map-click-add', { detail: { lat, lng } });
window.dispatchEvent(event);
});
}, [incidents, L, isMapReady]);
return (
<div ref={containerRef} id="dashboard-heatmap-container" style={{ height: '450px', borderRadius: '12px', overflow: 'hidden', position: 'relative', border: '1px solid var(--card-border)' }}>
{!L && <div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-secondary)', background: 'rgba(0,0,0,0.02)' }}>INITIALIZING GLOBAL HEATMAP...</div>}
</div>
);
};
// --- CUSTOM CHART COMPONENTS ---
const CustomChartTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
return (
<div className="glass" style={{
padding: '12px',
background: 'rgba(255, 255, 255, 0.95)',
border: `1px solid ${payload[0].payload.color}`,
boxShadow: `0 8px 32px rgba(0,0,0,0.05), 0 0 20px ${payload[0].payload.color}11`,
borderRadius: '12px',
backdropFilter: 'blur(10px)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '2px', background: payload[0].payload.color, boxShadow: `0 0 8px ${payload[0].payload.color}` }}></div>
<span style={{ fontSize: '0.7rem', fontWeight: 800, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>{payload[0].name} Status</span>
</div>
<div style={{ fontSize: '1.2rem', fontWeight: 900, color: 'var(--text-primary)', display: 'flex', alignItems: 'baseline', gap: '4px' }}>
{payload[0].value}
<span style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', fontWeight: 600 }}>ASSETS</span>
</div>
</div>
);
}
return null;
};
export const Dashboard: React.FC = () => {
const [incidents, setIncidents] = useState<Incident[]>([]);
const [users, setUsers] = useState<any[]>([]);
const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [lastUpdated, setLastUpdated] = useState(new Date());
const user = useMemo(() => {
try {
const stored = localStorage.getItem('teleems_user');
if (!stored || stored === 'undefined' || stored === 'null') return {};
const parsed = JSON.parse(stored);
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}, []);
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 [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [clickCoords, setClickCoords] = useState<{ lat: number, lng: number } | null>(null);
const [newIncident, setNewIncident] = useState({
category: 'MEDICAL',
severity: 'HIGH',
address: '',
notes: ''
});
useEffect(() => {
const handleMapClick = (e: any) => {
setClickCoords(e.detail);
setNewIncident(prev => ({ ...prev, address: `Lat: ${e.detail.lat.toFixed(4)}, Lon: ${e.detail.lng.toFixed(4)}` }));
setIsAddModalOpen(true);
};
window.addEventListener('map-click-add', handleMapClick);
return () => window.removeEventListener('map-click-add', handleMapClick);
}, []);
const handleQuickAdd = async () => {
if (!token || !clickCoords) return;
try {
const payload = {
...newIncident,
gps_lat: clickCoords.lat,
gps_lon: clickCoords.lng,
patients: [{ name: 'Pending', age: 0, gender: 'Unknown', symptoms: [], triage_code: 'GREEN' }]
};
await incidentsApi.createIncident(payload, token);
setIsAddModalOpen(false);
fetchData(); // Refresh map
} catch (error) {
console.error('Failed to quick-add incident:', error);
}
};
const fetchData = async () => {
if (!token) return;
setIsLoading(true);
try {
// Use individual try-catches or settle all to ensure one failure doesn't block the rest
const [incRes, userRes, auditRes] = await Promise.allSettled([
incidentsApi.getIncidents({}, token),
authApi.getUsers(token),
authApi.getAuditLogs(token)
]);
if (incRes.status === 'fulfilled' && incRes.value && incRes.value.data) {
const processed = (incRes.value.data || []).map((inc: any) => ({
...inc,
gps_lat: Number(inc.gps_lat),
gps_lon: Number(inc.gps_lon)
}));
setIncidents(processed);
} else if (incRes.status === 'rejected') {
console.error('Failed to fetch incidents:', incRes.reason);
}
if (userRes.status === 'fulfilled' && userRes.value && userRes.value.data) {
setUsers(Array.isArray(userRes.value.data) ? userRes.value.data : []);
}
if (auditRes.status === 'fulfilled' && auditRes.value && !auditRes.value.status) {
setAuditLogs(Array.isArray(auditRes.value.data) ? auditRes.value.data : (Array.isArray(auditRes.value.logs) ? auditRes.value.logs : []));
} else {
setAuditLogs([]);
if (auditRes.status === 'rejected') {
console.warn('Audit logs unavailable');
}
}
setLastUpdated(new Date());
} catch (error) {
console.error('Failed to fetch dashboard data:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 30000); // Poll every 30s
return () => clearInterval(interval);
}, [token]);
// Derived Metrics
const activeIncidents = incidents.filter(i =>
!['COMPLETED', 'CANCELLED', 'HANDOVER'].includes(i.status?.toUpperCase())
);
const fleetOperators = users.filter((u: any) => {
const r = Array.isArray(u.roles) ? u.roles.map((sr: any) => String(sr).toUpperCase()) : [];
return r.includes('FLEET_OPERATOR') || r.includes('FLEET OPERATOR');
});
const criticalIssues = incidents.filter(i => i.severity?.toUpperCase() === 'CRITICAL');
const fleetStatusData = [
{ name: 'IDLE', value: users.filter(u => u.status === 'ACTIVE').length, color: '#3B82F6' },
{ name: 'ACTIVE', value: activeIncidents.length, color: '#10B981' },
{ name: 'ALERT', value: criticalIssues.length, color: '#EF4444' },
{ name: 'OFFLINE', value: users.filter(u => u.status === 'INACTIVE').length || 2, color: '#4A5568' },
];
const activityData = Array.isArray(auditLogs) ? auditLogs.slice(0, 7).reverse().map((log) => ({
time: log.timestamp ? new Date(log.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '--:--',
count: Math.floor(Math.random() * 20) + 10 // Mocking activity intensity from audit density
})) : [];
return (
<div className="page-container" style={{ display: 'flex', flexDirection: 'column', gap: '24px', paddingBottom: '40px' }}>
{/* Header Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
<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>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '4px' }}>
<span className="status-pulse" style={{ background: 'var(--accent-green)' }}></span>
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
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>
<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>
</div>
</div>
{/* Primary Stats Bar */}
<div className="stats-bar" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', marginBottom: 0 }}>
<StatCard
label="Active Incidents"
value={activeIncidents.length}
icon={Activity}
glowColor="red"
pulse={activeIncidents.length > 0}
trend={{ value: '14%', isUp: true }}
/>
<StatCard
label="Operational Fleet"
value={fleetOperators.length}
subValue={users.length.toString()}
icon={Truck}
glowColor="cyan"
/>
<StatCard
label="Dispatch SLA"
value="1.4s"
icon={Zap}
glowColor="green"
trend={{ value: '0.2s', isUp: false }}
/>
<StatCard
label="Critical Cases"
value={criticalIssues.length}
icon={HeartPulse}
glowColor="amber"
/>
<StatCard
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' }}>
{/* Real-time Heatmap */}
<Card title="Global Incident Explorer" subtitle="System-wide incident history and live telemetry">
<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={{ width: '10px', height: '10px', borderRadius: '50%', background: '#EF4444', boxShadow: '0 0 10px #EF4444' }}></div>
<span style={{ fontWeight: 700, letterSpacing: '0.05em' }}>CRITICAL</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '8px' }}>
<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>
</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>
</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">
<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">
<PieChart>
<Pie
data={fleetStatusData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={8}
dataKey="value"
stroke="none"
animationBegin={0}
animationDuration={1500}
>
{fleetStatusData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.color}
style={{ filter: `drop-shadow(0 0 8px ${entry.color}44)` }}
/>
))}
</Pie>
<Tooltip content={<CustomChartTooltip />} cursor={{ fill: 'transparent' }} />
</PieChart>
</ResponsiveContainer>
{/* Central Stat */}
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
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 }}>
{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>
</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)' }}>
{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 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>
<span className="mono" style={{ fontSize: '0.7rem', fontWeight: 800, color: 'var(--text-primary)' }}>{percentage}%</span>
</div>
);
})}
</div>
</div>
</Card>
<Card title="System Performance" subtitle="Transaction density (30s avg)">
<div style={{ height: '140px', minWidth: 0, position: 'relative' }}>
<ResponsiveContainer width="100%" height={140}>
<AreaChart data={activityData}>
<defs>
<linearGradient id="colorSessions" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--accent-cyan)" stopOpacity={0.4}/>
<stop offset="95%" stopColor="var(--accent-cyan)" stopOpacity={0}/>
</linearGradient>
</defs>
<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)" />
</AreaChart>
</ResponsiveContainer>
</div>
</Card>
</div>
</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)' }}>
<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>
</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>
<div className="glass hover-glow" style={{ padding: '20px', borderRadius: '12px', border: '1px solid var(--card-border)' }}>
<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>
</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>
<div className="glass hover-glow" style={{ padding: '20px', borderRadius: '12px', border: '1px solid var(--card-border)' }}>
<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>
</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>
<div className="glass hover-glow" style={{ padding: '20px', borderRadius: '12px', border: '1px solid var(--card-border)' }}>
<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>
</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>
</div>
</Card>
<Card title="Critical Task Cluster" style={{ background: 'rgba(255, 59, 59, 0.03)' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '14px' }}>
{[
{ 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>
<div style={{ fontSize: '0.8rem', fontWeight: 700 }}>{r.label}</div>
<div style={{ fontSize: '0.6rem', color: r.color }}>{r.status}</div>
</div>
<div className="status-pulse" style={{ background: r.color }}></div>
</div>
))}
</div>
</Card>
</section>
{/* 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))' }}>
{[
{ label: 'Foundation', progress: 100 },
{ label: 'MVP Core', progress: 100 },
{ label: 'Clinical AI', progress: 85 },
{ label: 'Fleet Intel', progress: 42 },
{ 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>
<div style={{ height: '4px', background: 'rgba(0,0,0,0.02)', borderRadius: '2px', overflow: 'hidden' }}>
<motion.div
initial={{ width: 0 }}
animate={{ width: `${phase.progress}%` }}
transition={{ duration: 1, delay: i * 0.1 }}
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)'}`
}}
></motion.div>
</div>
</div>
))}
</div>
</Card>
<div className="glass" style={{
minWidth: '300px',
flex: '1 1 320px',
padding: '16px',
border: '1px solid var(--card-border)',
display: 'flex',
alignItems: 'center',
gap: '12px',
overflow: 'hidden',
whiteSpace: 'nowrap'
}}>
<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' }}>
<motion.div
animate={{ x: [400, -800] }}
transition={{ duration: 25, repeat: Infinity, ease: "linear" }}
style={{ display: 'inline-block', whiteSpace: 'nowrap', fontWeight: 700 }}
>
HIPAA COMPLIANT | ABDM SYNCED | ISO 27001 AUDIT PASSED | PHI ENCRYPTED | DPDP ACT ALIGNED | END-TO-END TLS 1.3 ACTIVE
</motion.div>
</div>
</div>
</div>
{/* Quick Add Modal */}
<AnimatePresence>
{isAddModalOpen && (
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(255,255,255,0.8)', backdropFilter: 'blur(10px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10000 }}>
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.9 }} className="glass glow-cyan" style={{ width: '400px', padding: '24px', background: 'var(--base-bg)', borderRadius: '20px', border: '1px solid var(--card-border)' }}>
<h3 style={{ fontSize: '1.2rem', fontWeight: 800, marginBottom: '20px', color: 'var(--accent-cyan)' }}>QUICK LOG INCIDENT</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div>
<label style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', fontWeight: 800, textTransform: 'uppercase' }}>Coordinates</label>
<div className="glass mono" style={{ padding: '10px', marginTop: '4px', fontSize: '0.8rem', background: 'rgba(0,0,0,0.01)' }}>
{clickCoords?.lat.toFixed(6)}, {clickCoords?.lng.toFixed(6)}
</div>
</div>
<div>
<label style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', fontWeight: 800, textTransform: 'uppercase' }}>Category</label>
<select
value={newIncident.category}
onChange={(e) => setNewIncident({ ...newIncident, category: e.target.value })}
className="glass"
style={{ width: '100%', padding: '10px', marginTop: '4px', background: 'rgba(0,0,0,0.03)', color: 'var(--text-primary)', border: '1px solid var(--card-border)', borderRadius: '8px' }}
>
<option value="MEDICAL">MEDICAL</option>
<option value="TRAUMA">TRAUMA</option>
<option value="CARDIAC">CARDIAC</option>
<option value="ACCIDENT">ACCIDENT</option>
</select>
</div>
<div>
<label style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', fontWeight: 800, textTransform: 'uppercase' }}>Severity</label>
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
{['LOW', 'HIGH', 'CRITICAL'].map(s => (
<button
key={s}
onClick={() => setNewIncident({ ...newIncident, severity: s })}
style={{
flex: 1,
padding: '8px',
borderRadius: '6px',
fontSize: '0.7rem',
fontWeight: 800,
cursor: 'pointer',
background: newIncident.severity === s ? (s === 'CRITICAL' ? 'var(--alert-red)' : 'var(--accent-cyan)') : 'rgba(0,0,0,0.03)',
color: newIncident.severity === s ? '#fff' : 'var(--text-secondary)',
border: 'none',
transition: 'all 0.2s'
}}
>
{s}
</button>
))}
</div>
</div>
<div>
<label style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', fontWeight: 800, textTransform: 'uppercase' }}>Notes / Address Info</label>
<textarea
value={newIncident.notes}
onChange={(e) => setNewIncident({ ...newIncident, notes: e.target.value, address: e.target.value || `Lat: ${clickCoords?.lat.toFixed(4)}` })}
placeholder="Enter scene details..."
className="glass"
style={{ width: '100%', padding: '10px', marginTop: '4px', height: '80px', background: 'rgba(0,0,0,0.03)', color: 'var(--text-primary)', border: '1px solid var(--card-border)', borderRadius: '8px', resize: 'none' }}
/>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '24px' }}>
<button onClick={() => setIsAddModalOpen(false)} className="glass" style={{ flex: 1, padding: '12px', borderRadius: '10px', cursor: 'pointer', border: '1px solid var(--card-border)', color: 'var(--text-secondary)', fontWeight: 700 }}>CANCEL</button>
<button onClick={handleQuickAdd} className="glow-cyan" style={{ flex: 1, padding: '12px', borderRadius: '10px', cursor: 'pointer', background: 'var(--accent-cyan)', color: '#000', border: 'none', fontWeight: 800 }}>LOG MISSION</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};

1063
src/pages/FleetDispatch.css Normal file

File diff suppressed because it is too large Load Diff

3290
src/pages/FleetDispatch.tsx Normal file

File diff suppressed because it is too large Load Diff

296
src/pages/FleetLogin.tsx Normal file
View File

@@ -0,0 +1,296 @@
import React, { useState } from 'react';
import { useNavigate, NavLink } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import {
Truck,
Zap,
ShieldCheck,
Lock,
User,
ArrowRight,
Cpu,
Radio,
Activity,
KeyRound,
ShieldAlert,
Eye,
EyeOff,
Crosshair,
Signal
} from 'lucide-react';
import { authApi } from '../api/auth';
import './Login.css'; // Reuse core login styles but we'll override some for the tactical look
export const FleetLogin = () => {
const [username, setUsername] = useState('fleet_operator');
const [password, setPassword] = useState('Fleet@123');
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 navigate = useNavigate();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setShowError('');
// --- MOCK LOGIN FOR FLEET OPERATOR ---
if (username === 'fleet_operator' && password === 'Fleet@123') {
setTimeout(() => {
localStorage.setItem('teleems_auth', 'true');
localStorage.setItem('teleems_token', 'mock-fleet-token-2026');
localStorage.setItem('teleems_user', JSON.stringify({
id: 'fleet-op-001',
username: 'fleet_operator',
roles: ['FLEET_OPERATOR'],
metadata: {
organization: { company_name: 'TeleEMS Fleet Services' }
}
}));
setIsLoading(false);
navigate('/fleet-operator');
}, 1000);
return;
}
try {
const response = await authApi.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 {
localStorage.setItem('teleems_auth', 'true');
localStorage.setItem('teleems_token', response.data.access_token || '');
localStorage.setItem('teleems_user', JSON.stringify(response.data.user || {}));
navigate('/fleet-operator');
}
} else {
setShowError(response.message || 'Access Denied: Invalid Credentials');
}
} catch (err) {
setShowError('Tactical Network Unavailable: Check Connection');
} finally {
setIsLoading(false);
}
};
const handleMfaVerify = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setShowError('');
try {
const response = await authApi.verifyMfa(mfaSessionToken, mfaCode);
if (response.status === 201 || response.status === 200) {
localStorage.setItem('teleems_auth', 'true');
localStorage.setItem('teleems_token', response.data.access_token || '');
const userToStore = response.data.user || tempUser || {};
userToStore.mfa_enabled = true;
localStorage.setItem('teleems_user', JSON.stringify(userToStore));
navigate('/fleet-operator');
} else {
setShowError('Invalid Security Token');
}
} catch (err) {
setShowError('Token Verification Failed');
} finally {
setIsLoading(false);
}
};
return (
<div className="login-page fleet-login-theme" style={{ background: '#020617' }}>
{/* Tactical Background Elements */}
<div className="login-grid-decor" style={{ opacity: 0.1, backgroundImage: 'linear-gradient(rgba(59, 130, 246, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(59, 130, 246, 0.1) 1px, transparent 1px)', backgroundSize: '40px 40px' }} />
<div className="scanline" style={{ background: 'linear-gradient(to bottom, transparent 0%, rgba(59, 130, 246, 0.05) 50%, transparent 100%)' }} />
<div className="login-overlay" style={{ background: 'radial-gradient(circle at center, transparent 0%, rgba(2, 6, 23, 0.8) 100%)' }} />
{/* Decorative Radar/Circle */}
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: '600px', height: '600px', border: '1px solid rgba(59, 130, 246, 0.05)', borderRadius: '50%', pointerEvents: 'none' }}
>
<div style={{ position: 'absolute', top: '0', left: '50%', width: '2px', height: '100%', background: 'linear-gradient(to bottom, rgba(59, 130, 246, 0.2), transparent)' }} />
</motion.div>
<motion.div
key={loginStep}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.5, ease: "circOut" }}
className="login-card glass"
style={{
background: 'rgba(15, 23, 42, 0.8)',
border: '1px solid rgba(59, 130, 246, 0.3)',
boxShadow: '0 0 50px rgba(0, 0, 0, 0.5), inset 0 0 20px rgba(59, 130, 246, 0.1)'
}}
>
<div className="login-header">
<motion.div
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="login-logo"
style={{ background: 'rgba(59, 130, 246, 0.1)', border: '1px solid var(--accent-cyan)' }}
>
<Truck className="text-cyan-400" size={28} style={{ color: 'var(--accent-cyan)' }} />
</motion.div>
<h1 className="login-title" style={{ letterSpacing: '0.1em', fontWeight: 900 }}>
{loginStep === 'login' ? 'FLEET TERMINAL' : 'SECURE TOKEN'}
</h1>
<p className="login-subtitle" style={{ color: 'var(--accent-cyan)', opacity: 0.8, fontSize: '0.7rem', fontWeight: 800, textTransform: 'uppercase' }}>
{loginStep === 'login' ? 'Sector: Dispatch • Active Node: CS-88' : 'Identity Verification Required'}
</p>
</div>
{loginStep === 'login' ? (
<form onSubmit={handleLogin} className="login-form">
<div className="input-group">
<label className="input-label" style={{ color: 'var(--accent-cyan)', opacity: 0.6 }}>Operator ID</label>
<div className="input-wrapper" style={{ background: 'rgba(0, 0, 0, 0.3)', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
<User className="input-icon" size={18} style={{ color: 'var(--accent-cyan)' }} />
<input
type="text"
className="login-input mono"
placeholder="ID_ENTRY"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={{ color: '#fff' }}
required
/>
</div>
</div>
<div className="input-group">
<label className="input-label" style={{ color: 'var(--accent-cyan)', opacity: 0.6 }}>Command Key</label>
<div className="input-wrapper" style={{ background: 'rgba(0, 0, 0, 0.3)', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
<Lock className="input-icon" size={18} style={{ color: 'var(--accent-cyan)' }} />
<input
type={showPassword ? "text" : "password"}
className="login-input mono"
placeholder="KEY_REQUIRED"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ color: '#fff' }}
required
/>
<button type="button" onClick={() => setShowPassword(!showPassword)} style={{ background: 'none', border: 'none', color: 'var(--accent-cyan)', cursor: 'pointer', paddingRight: '12px' }}>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
<button
id="fleet-login-submit"
type="submit"
className="login-button"
disabled={isLoading}
style={{
background: 'var(--accent-cyan)',
color: '#000',
fontWeight: 900,
boxShadow: '0 0 20px rgba(59, 130, 246, 0.4)'
}}
>
{isLoading ? (
<Cpu className="spin" size={20} />
) : (
<>
INITIALIZE SESSION
<ArrowRight size={20} />
</>
)}
</button>
</form>
) : (
<form onSubmit={handleMfaVerify} className="login-form">
<div className="input-group">
<label className="input-label" style={{ color: 'var(--accent-cyan)', opacity: 0.6 }}>TOTP Authorization</label>
<div className="input-wrapper" style={{ background: 'rgba(0, 0, 0, 0.3)', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
<KeyRound className="input-icon" size={18} style={{ color: 'var(--accent-cyan)' }} />
<input
type="text"
className="login-input mono"
placeholder="000 000"
maxLength={6}
value={mfaCode}
onChange={(e) => setMfaCode(e.target.value.replace(/\D/g, ''))}
style={{ color: '#fff' }}
required
/>
</div>
</div>
<button
type="submit"
className="login-button"
disabled={isLoading}
style={{ background: 'var(--accent-cyan)', color: '#000', fontWeight: 900 }}
>
{isLoading ? (
<Cpu className="spin" size={20} />
) : (
<>
VERIFY IDENTITY
<ShieldCheck size={20} />
</>
)}
</button>
</form>
)}
<AnimatePresence>
{showError && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="security-badge" style={{ color: '#ef4444', border: '1px solid rgba(239, 68, 68, 0.2)', background: 'rgba(239, 68, 68, 0.05)' }}>
<ShieldAlert size={14} />
<span>{showError}</span>
</motion.div>
)}
</AnimatePresence>
<div className="security-badge" style={{ borderColor: 'rgba(59, 130, 246, 0.2)', background: 'rgba(59, 130, 246, 0.05)' }}>
<Signal size={14} color="var(--accent-cyan)" />
<span style={{ color: 'var(--accent-cyan)', fontWeight: 700 }}>SECURE UPLINK ESTABLISHED</span>
</div>
<div className="login-footer" style={{ marginTop: '24px', borderTop: '1px solid rgba(59, 130, 246, 0.1)', paddingTop: '16px', textAlign: 'center' }}>
<NavLink to="/login" style={{ color: 'var(--accent-cyan)', textDecoration: 'none', fontSize: '0.8rem', fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', opacity: 0.7 }}>
<ArrowRight size={14} style={{ transform: 'rotate(180deg)' }} /> BACK TO STANDARD LOGIN
</NavLink>
</div>
</motion.div>
{/* Page-level status indicators */}
<div className="login-status-indicators" style={{ bottom: '40px', right: '40px' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', background: 'rgba(15, 23, 42, 0.8)', padding: '8px 16px', borderRadius: '8px', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
<span style={{ fontSize: '0.65rem', fontWeight: 800, color: 'var(--accent-cyan)' }}>COMMS_STRENGTH</span>
<div style={{ display: 'flex', gap: '2px' }}>
{[1, 2, 3, 4].map(i => <div key={i} style={{ width: '3px', height: i * 3, background: 'var(--accent-cyan)' }} />)}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--accent-green)', fontSize: '0.7rem', fontWeight: 800 }}>
<Radio size={14} className="pulse" /> LIVE TELEMETRY SYNC
</div>
</div>
</div>
<div className="login-sys-log" style={{ bottom: '40px', left: '40px', opacity: 0.3 }}>
<p>TERMINAL_ID: DISPATCH-X7</p>
<p>PROTOCOL: CS-SECURE-v4</p>
<p>ENCRYPTION: QUANTUM-SAFE</p>
</div>
</div>
);
};

View File

@@ -0,0 +1,395 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
Activity,
Truck,
Zap,
ShieldCheck,
MapPin,
Clock,
Navigation,
AlertTriangle,
Fuel,
Gauge,
Thermometer,
Wind,
Bell,
Settings,
ChevronRight,
LayoutGrid,
Route as RouteIcon,
Users,
Search,
CheckCircle2,
ShoppingCart
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card, StatCard } from '../components/Common';
import { fleetApi } from '../api/fleet';
import { incidentsApi } from '../api/incidents';
import type { Incident } from '../api/types';
import {
AreaChart,
Area,
Tooltip,
ResponsiveContainer,
BarChart,
Bar,
XAxis,
YAxis
} from 'recharts';
// --- NEW FLEET MODULES ---
import { FleetAssets } from './fleet/FleetAssets';
import { FleetPersonnel } from './fleet/FleetPersonnel';
import { FleetInventory } from './fleet/FleetInventory';
import { FleetScheduling } from './fleet/FleetScheduling';
// --- MOCK DATA FOR THE ENHANCED FEEL ---
const MOCK_VEHICLES = [
{ id: 'V001', number: 'TN-01-AM-1024', status: 'EN_ROUTE', speed: 45, fuel: 82, lat: 13.0827, lng: 80.2707, type: 'ALS' },
{ id: 'V002', number: 'TN-05-AM-5521', status: 'IDLE', speed: 0, fuel: 65, lat: 13.0067, lng: 80.2575, type: 'BLS' },
{ id: 'V003', number: 'TN-07-AM-1122', status: 'TRANSPORTING', speed: 52, fuel: 45, lat: 12.9667, lng: 80.2475, type: 'TRANSFER' },
{ id: 'V004', number: 'TN-09-AM-9988', status: 'AT_SCENE', speed: 0, fuel: 78, lat: 12.9941, lng: 80.1709, type: 'AIR' },
];
const PERFORMANCE_DATA = [
{ time: '08:00', trips: 12, response: 14 },
{ time: '10:00', trips: 18, response: 12 },
{ time: '12:00', trips: 25, response: 18 },
{ time: '14:00', trips: 22, response: 15 },
{ time: '16:00', trips: 30, response: 22 },
{ time: '18:00', trips: 28, response: 19 },
];
// --- LIVE MAP COMPONENT ---
const CommandMap: React.FC<{ vehicles: any[] }> = ({ vehicles }) => {
const [L, setL] = useState<any>(null);
const mapRef = React.useRef<any>(null);
useEffect(() => {
if (typeof window === 'undefined') return;
const loadLeaflet = () => {
const leaflet = (window as any).L;
if (leaflet) {
setL(leaflet);
if (!mapRef.current) {
const m = leaflet.map('fleet-command-map', {
zoomControl: false,
attributionControl: false
}).setView([13.0827, 80.2707], 12);
leaflet.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
maxZoom: 20
}).addTo(m);
mapRef.current = m;
}
}
};
if (!(window as any).L) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
document.head.appendChild(link);
const script = document.createElement('script');
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
script.async = true;
script.onload = loadLeaflet;
document.head.appendChild(script);
} else {
loadLeaflet();
}
return () => {
if (mapRef.current) {
mapRef.current.remove();
mapRef.current = null;
}
};
}, []);
useEffect(() => {
if (!L || !mapRef.current) return;
const map = mapRef.current;
vehicles.forEach(v => {
const getStatusColor = (status: string) => {
switch (status) {
case 'IDLE': return '#94A3B8';
case 'EN_ROUTE': return '#3B82F6';
case 'AT_SCENE': return '#F59E0B';
case 'TRANSPORTING': return '#EF4444';
case 'AT_HOSPITAL': return '#A855F7';
case 'BREAKDOWN': return '#000000';
case 'OFF_DUTY': return '#FFFFFF';
default: return '#3B82F6';
}
};
const color = getStatusColor(v.status);
const icon = L.divIcon({
className: 'custom-marker',
html: `<div style="background: ${color}; width: 24px; height: 24px; border-radius: 50% 50% 50% 0; transform: rotate(-45deg); display: flex; align-items: center; justify-content: center; box-shadow: 0 0 15px ${color}; border: 2px solid #fff;">
<div style="transform: rotate(45deg); color: #fff;"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-1.1 0-2 .9-2 2v9c0 .6.4 1 1 1h2"/><circle cx="7" cy="17" r="2"/><path d="M9 17h6"/><circle cx="17" cy="17" r="2"/></svg></div>
</div>`,
iconSize: [24, 24],
iconAnchor: [12, 24]
});
L.marker([v.lat, v.lng], { icon }).addTo(map).bindPopup(`<b>${v.number}</b><br/>Status: ${v.status}`);
});
}, [L, vehicles]);
return <div id="fleet-command-map" style={{ height: '100%', minHeight: '400px', borderRadius: '16px' }} />;
};
export const FleetOperatorDashboard: React.FC = () => {
const [searchParams] = useSearchParams();
const activeTab = searchParams.get('tab') || 'overview';
const [isLoading, setIsLoading] = useState(true);
const [incidents, setIncidents] = useState<Incident[]>([]);
const [vehicles, setVehicles] = useState(MOCK_VEHICLES);
useEffect(() => {
const fetchData = async () => {
try {
const token = localStorage.getItem('teleems_token') || '';
const res = await incidentsApi.getIncidents({}, token);
if (res && res.data) setIncidents(res.data.slice(0, 5));
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
const menuItems = [
{ id: 'overview', label: 'Command Center', icon: LayoutGrid },
{ id: 'assets', label: 'Fleet Assets', icon: Truck },
{ id: 'personnel', label: 'Personnel Hub', icon: Users },
{ id: 'scheduling', label: 'Mission Control', icon: Navigation },
{ id: 'inventory', label: 'Supply Chain', icon: ShoppingCart },
{ id: 'analytics', label: 'Fleet Intel', icon: Activity },
];
const renderContent = () => {
switch (activeTab) {
case 'overview':
return (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Stats Grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: '24px', marginBottom: '32px' }}>
<StatCard label="Live Fleet" value="48" icon={Truck} glowColor="cyan" trend={{ value: '12%', isUp: true }} />
<StatCard label="Active Trips" value="12" icon={Activity} glowColor="green" pulse />
<StatCard label="Avg Response" value="8.4m" icon={Zap} glowColor="amber" trend={{ value: '0.5m', isUp: false }} />
<StatCard label="Compliance" value="99.2%" icon={ShieldCheck} glowColor="cyan" />
</div>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '24px' }}>
{/* Left Column: Map and Fleet List */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<Card style={{ height: '500px', padding: 0, overflow: 'hidden', border: '1px solid rgba(59, 130, 246, 0.3)', position: 'relative' }}>
<CommandMap vehicles={vehicles} />
<div style={{ position: 'absolute', top: '20px', left: '20px', zIndex: 1000 }}>
<div className="glass" style={{ padding: '12px', borderRadius: '12px', display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px', border: '1px solid rgba(255,255,255,0.1)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#3B82F6' }}></div>
<span style={{ fontSize: '0.65rem', fontWeight: 600 }}>EN ROUTE</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#94A3B8' }}></div>
<span style={{ fontSize: '0.65rem', fontWeight: 600 }}>IDLE</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#F59E0B' }}></div>
<span style={{ fontSize: '0.65rem', fontWeight: 600 }}>AT SCENE</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#EF4444' }}></div>
<span style={{ fontSize: '0.65rem', fontWeight: 600 }}>TRANSPORTING</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#A855F7' }}></div>
<span style={{ fontSize: '0.65rem', fontWeight: 600 }}>HOSPITAL</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#000000' }}></div>
<span style={{ fontSize: '0.65rem', fontWeight: 600 }}>BREAKDOWN</span>
</div>
</div>
</div>
</Card>
<Card title="Live Fleet Telemetry">
<div className="table-container no-scrollbar" style={{ maxHeight: '300px', overflowY: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.1)', textAlign: 'left' }}>
<th style={{ padding: '12px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Vehicle</th>
<th style={{ padding: '12px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Status</th>
<th style={{ padding: '12px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Speed</th>
<th style={{ padding: '12px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Fuel</th>
<th style={{ padding: '12px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Actions</th>
</tr>
</thead>
<tbody>
{vehicles.map(v => (
<tr key={v.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
<td style={{ padding: '16px 12px' }}>
<div style={{ fontWeight: 700 }}>{v.number}</div>
<div style={{ fontSize: '0.7rem', opacity: 0.5 }}>{v.type} UNIT</div>
</td>
<td style={{ padding: '16px 12px' }}>
<span style={{
fontSize: '0.65rem',
fontWeight: 900,
padding: '4px 8px',
borderRadius: '4px',
background: v.status === 'IDLE' ? 'rgba(148, 163, 184, 0.1)' : 'rgba(59, 130, 246, 0.1)',
color: v.status === 'IDLE' ? '#94A3B8' : 'var(--accent-cyan)',
border: `1px solid ${v.status === 'IDLE' ? 'rgba(148, 163, 184, 0.2)' : 'rgba(59, 130, 246, 0.2)'}`
}}>{v.status}</span>
</td>
<td style={{ padding: '16px 12px' }} className="mono">{v.speed} km/h</td>
<td style={{ padding: '16px 12px' }}>
<div style={{ width: '100px', height: '6px', background: 'rgba(255,255,255,0.1)', borderRadius: '3px', overflow: 'hidden' }}>
<div style={{ width: `${v.fuel}%`, height: '100%', background: v.fuel < 30 ? 'var(--alert-red)' : 'var(--accent-green)' }}></div>
</div>
</td>
<td style={{ padding: '16px 12px' }}>
<button className="btn-ghost-sm"><Navigation size={14} /></button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
{/* Right Column: Performance and Incidents */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<Card title="Operational Load">
<div style={{ height: '200px', width: '100%', position: 'relative' }}>
<ResponsiveContainer width="100%" height="100%" minWidth={0} minHeight={0}>
<AreaChart data={PERFORMANCE_DATA}>
<defs>
<linearGradient id="colorTrips" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--accent-cyan)" stopOpacity={0.3}/>
<stop offset="95%" stopColor="var(--accent-cyan)" stopOpacity={0}/>
</linearGradient>
</defs>
<Tooltip
contentStyle={{ background: '#1E293B', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px' }}
itemStyle={{ color: 'var(--accent-cyan)' }}
/>
<Area type="monotone" dataKey="trips" stroke="var(--accent-cyan)" fillOpacity={1} fill="url(#colorTrips)" />
</AreaChart>
</ResponsiveContainer>
</div>
</Card>
<Card title="Active Incident Feed">
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{incidents.length > 0 ? incidents.map(inc => (
<div key={inc.id} className="glass hover-glow" style={{ padding: '16px', borderRadius: '12px', borderLeft: `4px solid ${inc.severity === 'CRITICAL' ? 'var(--alert-red)' : 'var(--accent-cyan)'}` }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span className="mono" style={{ fontSize: '0.8rem', fontWeight: 800 }}>#{inc.id.split('-').pop()?.toUpperCase()}</span>
<span style={{ fontSize: '0.6rem', color: 'var(--alert-red)', fontWeight: 900 }}>{inc.severity}</span>
</div>
<div style={{ fontSize: '0.875rem', fontWeight: 600, marginBottom: '4px' }}>{inc.category}</div>
<div style={{ fontSize: '0.75rem', opacity: 0.6, display: 'flex', alignItems: 'center', gap: '4px' }}>
<MapPin size={12} /> {inc.address}
</div>
</div>
)) : (
<div style={{ textAlign: 'center', padding: '20px', opacity: 0.5 }}>No active incidents</div>
)}
</div>
<button style={{ width: '100%', marginTop: '16px', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: 'var(--accent-cyan)', fontWeight: 700, cursor: 'pointer' }}>
VIEW ALL INCIDENTS
</button>
</Card>
<Card title="Fleet Health Indicators">
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div style={{ padding: '16px', background: 'rgba(255,255,255,0.02)', borderRadius: '12px', textAlign: 'center' }}>
<Gauge size={24} color="var(--accent-cyan)" style={{ marginBottom: '8px' }} />
<div style={{ fontSize: '1.25rem', fontWeight: 800 }}>82%</div>
<div style={{ fontSize: '0.65rem', opacity: 0.5 }}>AVAILABILITY</div>
</div>
<div style={{ padding: '16px', background: 'rgba(255,255,255,0.02)', borderRadius: '12px', textAlign: 'center' }}>
<Fuel size={24} color="var(--accent-green)" style={{ marginBottom: '8px' }} />
<div style={{ fontSize: '1.25rem', fontWeight: 800 }}>94%</div>
<div style={{ fontSize: '0.65rem', opacity: 0.5 }}>FUEL READINESS</div>
</div>
</div>
</Card>
</div>
</div>
</div>
);
case 'assets':
return <FleetAssets />;
case 'personnel':
return <FleetPersonnel />;
case 'scheduling':
return <FleetScheduling />;
case 'inventory':
return <FleetInventory />;
case 'analytics':
return <div className="glass" style={{ padding: '40px', textAlign: 'center', color: 'var(--text-secondary)' }}>Fleet Intelligence Reports Loading...</div>;
default:
return null;
}
};
return (
<div className="fleet-operator-dashboard" style={{
background: '#020617',
color: '#F8FAFC',
minHeight: '100vh',
fontFamily: "'Inter', sans-serif",
display: 'flex',
flexDirection: 'column'
}}>
{/* Main Content Area */}
<div style={{
flex: 1,
padding: '32px',
maxWidth: '100%'
}}>
{/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '32px' }}>
<div>
<h1 style={{ fontSize: '2.5rem', fontWeight: 900, letterSpacing: '-0.05em', color: 'var(--accent-cyan)', textTransform: 'uppercase' }}>
{menuItems.find(m => m.id === activeTab)?.label || 'Fleet Command'}
</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', opacity: 0.7 }}>
<div className="status-pulse" style={{ background: 'var(--accent-green)' }}></div>
<span style={{ fontSize: '0.875rem' }}>Strategic Operations Live Platform Telemetry</span>
</div>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<div style={{ background: 'rgba(255,255,255,0.05)', padding: '8px 16px', borderRadius: '12px', border: '1px solid rgba(255,255,255,0.1)', display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: '0.65rem', textTransform: 'uppercase', opacity: 0.5 }}>Tactical Time</div>
<div style={{ fontWeight: 700 }}>{new Date().toLocaleTimeString()}</div>
</div>
<Clock size={20} color="var(--accent-cyan)" />
</div>
<button className="btn-icon glass"><Bell size={20} /></button>
<button className="btn-icon glass"><Settings size={20} /></button>
</div>
</div>
{renderContent()}
</div>
</div>
);
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,893 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Hospital,
CheckCircle,
AlertCircle,
Plus,
Phone,
Settings,
CheckCircle2,
MoreVertical,
Shield,
User as UserIcon,
Navigation2,
XCircle,
Stethoscope,
Lock,
Edit2,
Trash2,
Eye,
EyeOff
} from 'lucide-react';
import { Card } from '../components/Common';
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Tooltip } from 'recharts';
import { motion, AnimatePresence } from 'framer-motion';
import { authApi } from '../api/auth';
import { incidentsApi } from '../api/incidents';
type ViewMode = 'NETWORK_OVERVIEW' | 'HOSPITAL_MGMT' | 'APPROVAL_QUEUE' | 'ANALYTICS';
export const HospitalsNetwork: React.FC = () => {
const [viewMode, setViewMode] = useState<ViewMode>('NETWORK_OVERVIEW');
const [isModalOpen, setIsModalOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [realHospitals, setRealHospitals] = useState<any[]>([]);
const [incidents, setIncidents] = useState<any[]>([]);
const [issues, setIssues] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [editingHospital, setEditingHospital] = useState<any | null>(null);
const navigate = useNavigate();
const loadIncidents = async () => {
try {
const token = localStorage.getItem('teleems_token') || '';
if (!token) return;
const res = await incidentsApi.getIncidents({ limit: 10 }, token);
if (res && res.data) {
setIncidents(res.data);
}
} catch (err) {
console.error('Failed to load incidents:', err);
}
};
const loadHospitals = async () => {
try {
const token = localStorage.getItem('teleems_token') || '';
if (!token) return;
const response = await authApi.getUsers(token);
if (response && (response.status === 401 || response.message?.toLowerCase().includes('expired'))) {
localStorage.removeItem('teleems_auth');
localStorage.removeItem('teleems_token');
localStorage.removeItem('teleems_user');
navigate('/login');
return;
}
if (response && response.data) {
// Robust Hospital Node extraction
const filteredAdmins = response.data.filter((u: any) => {
const roles = Array.isArray(u.roles) ? u.roles.map((r: any) => String(r).toUpperCase()) : [];
return roles.includes('HOSPITAL ADMIN') || roles.includes('HOSPITAL_ADMIN');
});
const hospitalNodes = filteredAdmins.map((u: any) => {
const metaHosp = u.metadata?.hospital || {};
const metaOrg = u.metadata?.organization || {};
// Determine activity status
const activeInc = incidents.filter(i => i.hospital_id === u.id && i.status !== 'RESOLVED').length;
const [available] = (metaHosp.beds || '0/0').split('/').map((n: string) => parseInt(n) || 0);
let activityStatus = 'IDLE';
if (activeInc > 5) activityStatus = 'CRITICAL LOAD';
else if (activeInc > 0) activityStatus = `HANDLING ${activeInc} INCIDENTS`;
else if (available < 5) activityStatus = 'NEAR CAPACITY';
return {
id: u.id,
name: metaHosp.name || metaOrg.company_name || u.name || u.username || 'Unknown Hospital',
type: metaHosp.type || metaHosp.specialization || 'Multi-Specialty',
beds: metaHosp.beds || '15/60',
status: u.status || 'ACTIVE',
activity: activityStatus,
accreditation: metaHosp.accreditation || 'NABH',
admin: u.name || u.username,
phone: u.phone || 'Contact Support',
email: u.email,
city: metaHosp.city || metaOrg.city || 'Chennai',
radius: metaHosp.radius || '15km',
zones: [metaHosp.city || metaOrg.city || 'Chennai'],
rawMetadata: u.metadata,
roles: u.roles || []
};
});
setRealHospitals(hospitalNodes);
// Derive current issues
const newIssues = hospitalNodes.map(h => {
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 };
return null;
}).filter(Boolean);
setIssues(newIssues as any[]);
}
} catch (error) {
console.error('Failed to fetch hospitals:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadHospitals();
loadIncidents();
const interval = setInterval(() => {
loadHospitals();
loadIncidents();
}, 30000);
return () => clearInterval(interval);
}, []);
const hospitalStats = useMemo(() => {
return realHospitals.map(h => {
const [available, total] = (h.beds || '0/0').split('/').map((n: string) => parseInt(n) || 0);
return {
name: h.name,
total: total || 100,
available: available || 0
};
});
}, [realHospitals]);
const handleHospitalSubmit = async (data: any) => {
setIsSubmitting(true);
try {
const token = localStorage.getItem('teleems_token') || '';
if (!token) {
throw new Error('No authentication token found. Please login again.');
}
const result = await authApi.registerUser(data, token);
if (result.error || result.status === 401) {
throw new Error(result.error?.message || result.message || 'Unauthorized');
}
console.log('Hospital Registration Success:', result);
alert('Hospital registered successfully!');
setIsModalOpen(false);
loadHospitals();
} catch (error: any) {
console.error('Registration failed:', error);
const isExpired = error.message.includes('expired');
alert(`Registration failed: ${error.message}${isExpired ? '. Your session has expired, redirecting to login...' : ''}`);
if (isExpired) {
localStorage.removeItem('teleems_auth');
localStorage.removeItem('teleems_token');
localStorage.removeItem('teleems_user');
navigate('/login');
}
} finally {
setIsSubmitting(false);
}
};
const handleStatusToggle = async (hospital: any) => {
try {
const newStatus = hospital.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE';
const token = localStorage.getItem('teleems_token') || '';
const payload = {
name: hospital.admin,
email: hospital.email,
phone: hospital.phone || '',
status: newStatus,
role: 'HOSPITAL_ADMIN',
metadata: hospital.rawMetadata
};
const res = await authApi.updateUser(hospital.id, payload, token);
if (res.status === 401) {
navigate('/login');
return;
}
loadHospitals();
} catch (error) {
console.error('Failed to toggle status:', error);
}
};
const handleEditHospitalSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!editingHospital) return;
setIsSubmitting(true);
try {
const token = localStorage.getItem('teleems_token') || '';
const payload = {
name: editingHospital.admin,
email: editingHospital.email,
phone: editingHospital.phone || '',
status: editingHospital.status,
role: 'HOSPITAL_ADMIN',
metadata: {
...editingHospital.rawMetadata,
hospital: {
...editingHospital.rawMetadata?.hospital,
name: editingHospital.name,
city: editingHospital.city
}
}
};
const res = await authApi.updateUser(editingHospital.id, payload, token);
if (res.status === 401) {
navigate('/login');
return;
}
setEditingHospital(null);
loadHospitals();
} catch (error) {
console.error('Update failed:', error);
} finally {
setIsSubmitting(false);
}
};
const triggerSubmit = () => {
const form = document.getElementById('hospital-reg-form') as HTMLFormElement;
if (form) form.requestSubmit();
};
return (
<div className="page-container" style={{ padding: '0 40px 40px 40px' }}>
<header className="network-header-premium" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<h2 style={{ fontSize: '2.5rem', fontWeight: 900, background: 'linear-gradient(90deg, var(--accent-blue), var(--text-primary))', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', letterSpacing: '-1.5px' }}>
Hospital Governance
</h2>
<p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginTop: '4px', fontWeight: 700, letterSpacing: '0.5px' }}>
NETWORK OPERATIONAL CONTROL {realHospitals.length} ACTIVE NODES
</p>
</div>
<div className="glass" style={{ padding: '6px', borderRadius: '12px', display: 'flex', gap: '4px', background: 'rgba(0,0,0,0.03)' }}>
<button
onClick={() => setViewMode('NETWORK_OVERVIEW')}
style={{
padding: '10px 20px', borderRadius: '8px', border: 'none',
background: viewMode === 'NETWORK_OVERVIEW' ? 'var(--accent-cyan)' : 'transparent',
color: viewMode === 'NETWORK_OVERVIEW' ? '#fff' : 'var(--text-secondary)',
fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.8rem'
}}>
<Shield size={16} /> NETWORK OVERVIEW
</button>
<button
onClick={() => setViewMode('HOSPITAL_MGMT')}
style={{
padding: '10px 20px', borderRadius: '8px', border: 'none',
background: viewMode === 'HOSPITAL_MGMT' ? 'var(--accent-cyan)' : 'transparent',
color: viewMode === 'HOSPITAL_MGMT' ? '#fff' : 'var(--text-secondary)',
fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.8rem'
}}>
<Hospital size={16} /> ACCOUNTS
</button>
<button
onClick={() => setViewMode('APPROVAL_QUEUE')}
style={{
padding: '10px 20px', borderRadius: '8px', border: 'none',
background: viewMode === 'APPROVAL_QUEUE' ? 'var(--accent-cyan)' : 'transparent',
color: viewMode === 'APPROVAL_QUEUE' ? '#fff' : 'var(--text-secondary)',
fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.8rem'
}}>
<CheckCircle2 size={16} /> APPROVALS <span style={{ background: 'var(--alert-red)', color: '#fff', fontSize: '0.6rem', padding: '2px 6px', borderRadius: '10px' }}>2</span>
</button>
<button
onClick={() => setViewMode('ANALYTICS')}
style={{
padding: '10px 20px', borderRadius: '8px', border: 'none',
background: viewMode === 'ANALYTICS' ? 'var(--accent-cyan)' : 'transparent',
color: viewMode === 'ANALYTICS' ? '#fff' : 'var(--text-secondary)',
fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.8rem'
}}>
<Stethoscope size={16} /> REPORTS
</button>
</div>
</header>
<AnimatePresence mode="wait">
{viewMode === 'NETWORK_OVERVIEW' && (
<motion.div key="overview" initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
{/* NETWORK PULSE STRIP */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '20px' }}>
<Card style={{ padding: '20px', background: 'rgba(59, 130, 246, 0.03)', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', textTransform: 'uppercase', fontWeight: 800 }}>Live Incidents</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '10px', marginTop: '8px' }}>
<div style={{ fontSize: '2.5rem', fontWeight: 900, color: 'var(--accent-cyan)' }}>{incidents.filter(i => i.status !== 'RESOLVED').length}</div>
<div style={{ fontSize: '0.8rem', color: 'var(--accent-green)', fontWeight: 700 }}>ACTIVE</div>
</div>
</Card>
<Card style={{ padding: '20px', background: 'rgba(255, 82, 82, 0.03)', border: '1px solid rgba(255, 82, 82, 0.2)' }}>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', textTransform: 'uppercase', fontWeight: 800 }}>Critical Issues</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '10px', marginTop: '8px' }}>
<div style={{ fontSize: '2.5rem', fontWeight: 900, color: 'var(--alert-red)' }}>{issues.filter(i => i.type === 'CRITICAL').length}</div>
<div style={{ fontSize: '0.8rem', color: 'var(--alert-red)', fontWeight: 700 }}>ALERTS</div>
</div>
</Card>
<Card style={{ padding: '20px' }}>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', textTransform: 'uppercase', fontWeight: 800 }}>Network Sync</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '10px', marginTop: '8px' }}>
<div style={{ fontSize: '2.5rem', fontWeight: 900, color: 'var(--text-primary)' }}>98%</div>
<div style={{ fontSize: '0.8rem', color: 'var(--accent-cyan)', fontWeight: 700 }}>STABLE</div>
</div>
</Card>
<Card style={{ padding: '20px' }}>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', textTransform: 'uppercase', fontWeight: 800 }}>System Health</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginTop: '12px' }}>
<div style={{ width: '12px', height: '12px', borderRadius: '50%', background: 'var(--accent-green)', boxShadow: '0 0 10px var(--accent-green)' }}></div>
<div style={{ fontSize: '1.2rem', fontWeight: 800 }}>OPTIMAL</div>
</div>
</Card>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 2fr) 1fr', gap: '24px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ fontSize: '1.2rem', fontWeight: 800, display: 'flex', alignItems: 'center', gap: '10px' }}>
<Hospital size={20} color="var(--accent-cyan)" /> HOSPITAL NODES
</h3>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '20px' }}>
{realHospitals.length === 0 && !isLoading && (
<div style={{ gridColumn: '1/-1', textAlign: 'center', padding: '40px', color: 'var(--text-secondary)' }}>
No hospital nodes registered in network.
</div>
)}
{realHospitals.map((h) => (
<Card key={h.id} className="hover-glow" style={{ padding: '20px', border: issues.some(i => i.hospital === h.name && i.type === 'CRITICAL') ? '1px solid var(--alert-red)' : '1px solid var(--card-border)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ width: '44px', height: '44px', background: 'rgba(59, 130, 246, 0.1)', borderRadius: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Hospital size={22} color="var(--accent-cyan)" />
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.65rem', fontWeight: 800, color: h.status === 'ACTIVE' ? 'var(--accent-green)' : 'var(--alert-red)', textTransform: 'uppercase' }}>
<div style={{ width: '6px', height: '6px', borderRadius: '50%', background: h.status === 'ACTIVE' ? 'var(--accent-green)' : 'var(--alert-red)' }} />
{h.status}
</div>
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', fontWeight: 700 }}>{h.activity}</div>
</div>
</div>
<div style={{ marginTop: '20px' }}>
<div style={{ fontSize: '1.2rem', fontWeight: 800 }}>{h.name}</div>
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
<span style={{ fontSize: '0.65rem', color: 'var(--accent-cyan)', fontWeight: 700 }}>{h.type}</span>
<span style={{ fontSize: '0.65rem', color: 'var(--text-secondary)' }}> {h.city}</span>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '24px', borderTop: '1px solid var(--card-border)', paddingTop: '16px' }}>
<div>
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Available Beds</div>
<div className="mono" style={{ fontSize: '1rem', fontWeight: 800, color: h.beds.startsWith('0') ? 'var(--alert-red)' : 'var(--accent-green)' }}>{h.beds}</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Active Cases</div>
<div className="mono" style={{ fontSize: '1rem', fontWeight: 800, color: 'var(--text-primary)' }}>{incidents.filter(i => i.hospital_id === h.id && i.status !== 'RESOLVED').length}</div>
</div>
</div>
</Card>
))}
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<Card title="CRITICAL ISSUES" subtitle="Real-time network alerts" style={{ border: '1px solid rgba(255, 82, 82, 0.3)' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginTop: '15px' }}>
{issues.length === 0 ? (
<div style={{ padding: '20px', textAlign: 'center', color: 'var(--text-secondary)', fontSize: '0.8rem' }}>No critical issues detected.</div>
) : (
issues.map((issue, idx) => (
<div key={idx} style={{ padding: '12px', background: issue.type === 'CRITICAL' ? 'rgba(255, 82, 82, 0.1)' : 'rgba(255, 183, 77, 0.1)', borderLeft: `4px solid ${issue.type === 'CRITICAL' ? 'var(--alert-red)' : 'var(--warning-amber)'}`, borderRadius: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
<AlertCircle size={14} color={issue.type === 'CRITICAL' ? 'var(--alert-red)' : 'var(--warning-amber)'} />
<span style={{ fontSize: '0.75rem', fontWeight: 800, color: issue.type === 'CRITICAL' ? 'var(--alert-red)' : 'var(--warning-amber)' }}>{issue.type}</span>
</div>
<div style={{ fontSize: '0.85rem', fontWeight: 600 }}>{issue.msg}</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', marginTop: '4px' }}>Identified 2m ago</div>
</div>
))
)}
</div>
</Card>
<Card title="LIVE ACTIVITY" subtitle="Latest incidents across network">
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px', marginTop: '15px' }}>
{incidents.length === 0 ? (
<div style={{ padding: '20px', textAlign: 'center', color: 'var(--text-secondary)', fontSize: '0.8rem' }}>Monitoring network traffic...</div>
) : (
incidents.slice(0, 5).map((inc, i) => (
<div key={i} style={{ paddingBottom: '12px', borderBottom: '1px solid var(--card-border)', display: 'flex', gap: '12px' }}>
<div style={{ width: '32px', height: '32px', background: 'rgba(0,0,0,0.03)', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<Navigation2 size={16} color="var(--accent-cyan)" />
</div>
<div>
<div style={{ fontSize: '0.85rem', fontWeight: 700 }}>{inc.patient_name || 'Emergency Call'}</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)' }}>{inc.status.toUpperCase()} {inc.priority || 'P1'}</div>
<div style={{ fontSize: '0.65rem', color: 'var(--accent-cyan)', marginTop: '4px' }}>{new Date(inc.created_at).toLocaleTimeString()}</div>
</div>
</div>
))
)}
</div>
{incidents.length > 5 && (
<button style={{ width: '100%', padding: '10px', background: 'transparent', border: 'none', color: 'var(--accent-cyan)', fontSize: '0.75rem', fontWeight: 700, cursor: 'pointer', marginTop: '10px' }}>VIEW ALL ACTIVITY</button>
)}
</Card>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
<Card title="Regional Bed Capacity" subtitle="Real-time availability by hospital node">
<div style={{ height: '300px', marginTop: '20px' }}>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={hospitalStats} layout="vertical">
<XAxis type="number" hide />
<YAxis dataKey="name" type="category" width={100} stroke="var(--text-secondary)" fontSize={11} tick={{fontWeight: 600}} />
<Tooltip cursor={{fill: 'rgba(0,0,0,0.02)'}} contentStyle={{ background: 'var(--base-bg)', border: '1px solid var(--card-border)', borderRadius: '8px' }} />
<Bar dataKey="total" fill="rgba(0,0,0,0.03)" radius={[0, 4, 4, 0]} barSize={20} />
<Bar dataKey="available" fill="var(--accent-cyan)" radius={[0, 4, 4, 0]} barSize={20} />
</BarChart>
</ResponsiveContainer>
</div>
</Card>
<Card title="HMIS Data Exchange Health" subtitle="Integration status of hospital information systems">
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{[
{ name: 'Apollo Main', api: 'REST/FHIR', status: 'Healthy', latency: '45ms', lastSync: '12s ago' },
{ name: 'MGM Healthcare', api: 'REST/FHIR', status: 'Healthy', latency: '38ms', lastSync: '5s ago' },
{ name: 'MIOT Int.', api: 'REST/FHIR', status: 'Healthy', latency: '42ms', lastSync: '8s ago' },
{ name: 'Global Health', api: 'HL7v2', status: 'Syncing', latency: '120ms', lastSync: '1m ago' },
{ name: 'Stanley Medical', api: 'Direct SQL', status: 'Critical', latency: '--', lastSync: '14h ago' },
].map((item, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '14px', background: 'rgba(0,0,0,0.02)', borderRadius: '10px', border: '1px solid var(--card-border)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: item.status === 'Healthy' ? 'var(--accent-green)' : (item.status === 'Syncing' ? 'var(--warning-amber)' : 'var(--alert-red)') }} />
<div>
<div style={{ fontSize: '0.85rem', fontWeight: 700 }}>{item.name}</div>
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)' }}>{item.api} Protocol</div>
</div>
</div>
<div style={{ textAlign: 'right' }}>
<div className="mono" style={{ fontSize: '0.75rem', color: 'var(--accent-cyan)' }}>{item.latency}</div>
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)' }}>{item.lastSync}</div>
</div>
</div>
))}
</div>
</Card>
</div>
</motion.div>
)}
{viewMode === 'HOSPITAL_MGMT' && (
<HospitalManagement
key="management"
hospitals={realHospitals}
onRegister={() => setIsModalOpen(true)}
onEdit={(h) => setEditingHospital(h)}
onToggleStatus={handleStatusToggle}
/>
)}
{viewMode === 'APPROVAL_QUEUE' && (
<motion.div key="approvals" initial={{ opacity: 0 }} animate={{ opacity: 1 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<Card title="Pending Hospital Registrations" subtitle="Review and approve new hospital node requests.">
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '20px' }}>
<thead>
<tr style={{ textAlign: 'left', borderBottom: '1px solid var(--card-border)' }}>
<th style={{ padding: '16px' }}>Request Details</th>
<th style={{ padding: '16px' }}>Admin Info</th>
<th style={{ padding: '16px' }}>Submitted</th>
<th style={{ padding: '16px', textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody>
{[
{ id: 'REQ-9901', name: 'Global Health City', city: 'Chennai', admin: 'Dr. S. Karthik', email: 'sk@globalhealth.com', date: '2026-04-16' },
{ id: 'REQ-9905', name: 'Fortis Malar', city: 'Chennai', admin: 'Pavitra M.', email: 'p.malar@fortis.com', date: '2026-04-15' }
].map(req => (
<tr key={req.id} style={{ borderBottom: '1px solid rgba(0,0,0,0.02)' }}>
<td style={{ padding: '16px' }}>
<div style={{ fontWeight: 700 }}>{req.name}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{req.city} Node</div>
</td>
<td style={{ padding: '16px' }}>
<div style={{ fontSize: '0.85rem' }}>{req.admin}</div>
<div style={{ fontSize: '0.7rem', color: 'var(--accent-cyan)' }}>{req.email}</div>
</td>
<td style={{ padding: '16px', fontSize: '0.85rem' }}>{req.date}</td>
<td style={{ padding: '16px', textAlign: 'right' }}>
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
<button style={{ padding: '8px 16px', background: 'var(--accent-green)', color: '#fff', border: 'none', borderRadius: '4px', fontWeight: 700, cursor: 'pointer' }}>APPROVE</button>
<button style={{ padding: '8px 16px', background: 'rgba(0,0,0,0.02)', color: 'var(--alert-red)', border: '1px solid var(--alert-red)', borderRadius: '4px', fontWeight: 700, cursor: 'pointer' }}>REJECT</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</Card>
</motion.div>
)}
{viewMode === 'ANALYTICS' && (
<motion.div key="analytics" initial={{ opacity: 0 }} animate={{ opacity: 1 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '24px' }}>
<Card title="Total Incidents Handled" subtitle="Last 30 days total volume">
<div style={{ fontSize: '3rem', fontWeight: 900, color: 'var(--accent-cyan)', margin: '20px 0' }}>1,248</div>
<div style={{ color: 'var(--accent-green)', fontWeight: 700 }}> 14% vs last month</div>
</Card>
<Card title="Avg Handover Time" subtitle="Ambulance arrival to ED handoff">
<div style={{ fontSize: '3rem', fontWeight: 900, color: 'var(--text-primary)', margin: '20px 0' }}>12.4m</div>
<div style={{ color: 'var(--accent-green)', fontWeight: 700 }}>-1.2m improvement</div>
</Card>
<Card title="Clinical Escalation Rate" subtitle="TeleLink sessions requiring specialist">
<div style={{ fontSize: '3rem', fontWeight: 900, color: 'var(--alert-red)', margin: '20px 0' }}>8.2%</div>
<div style={{ color: 'var(--text-secondary)', fontWeight: 700 }}>Target: &lt; 10%</div>
</Card>
</div>
<Card title="Network Volume Trends">
<div style={{ height: '300px', display: 'flex', alignItems: 'flex-end', gap: '10px', padding: '40px 0' }}>
{[40, 65, 52, 88, 70, 95, 82, 60, 75, 90, 110, 85].map((h, i) => (
<div key={i} style={{ flex: 1, background: 'var(--accent-cyan)', height: `${h}%`, borderRadius: '4px 4px 0 0', opacity: 0.6 + (h/200), position: 'relative' }}>
<div style={{ position: 'absolute', top: '-25px', left: '50%', transform: 'translateX(-50%)', fontSize: '0.6rem', fontWeight: 900 }}>{h}</div>
</div>
))}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.7rem', color: 'var(--text-secondary)', marginTop: '10px' }}>
<span>APR 01</span>
<span>APR 07</span>
<span>APR 14</span>
<span>APR 21</span>
<span>APR 28</span>
</div>
</Card>
</motion.div>
)}
</AnimatePresence>
<Modal
isOpen={isModalOpen}
title="Register New Hospital"
onClose={() => setIsModalOpen(false)}
onSubmit={triggerSubmit}
loading={isSubmitting}
>
<HospitalRegistrationForm onSubmit={handleHospitalSubmit} loading={isSubmitting} />
</Modal>
{/* EDIT HOSPITAL MODAL */}
<Modal
isOpen={!!editingHospital}
title={`Edit ${editingHospital?.name}`}
onClose={() => setEditingHospital(null)}
onSubmit={() => {
const form = document.getElementById('hospital-edit-form') as HTMLFormElement;
if (form) form.requestSubmit();
}}
loading={isSubmitting}
>
{editingHospital && (
<form id="hospital-edit-form" onSubmit={handleEditHospitalSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
<div>
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--accent-cyan)', marginBottom: '8px', fontWeight: 700 }}>HOSPITAL FULL NAME</label>
<input name="name" type="text" required value={editingHospital.name} onChange={(e) => setEditingHospital({...editingHospital, name: e.target.value})} style={{ width: '100%', padding: '12px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }} />
</div>
<div>
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--accent-cyan)', marginBottom: '8px', fontWeight: 700 }}>PRIMARY CITY</label>
<input name="city" type="text" required value={editingHospital.city} onChange={(e) => setEditingHospital({...editingHospital, city: e.target.value})} style={{ width: '100%', padding: '12px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }} />
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1fr', gap: '20px' }}>
<div>
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--accent-cyan)', marginBottom: '8px', fontWeight: 700 }}>ADMINISTRATOR NAME</label>
<input name="admin" type="text" required value={editingHospital.admin} onChange={(e) => setEditingHospital({...editingHospital, admin: e.target.value})} style={{ width: '100%', padding: '12px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }} />
</div>
<div>
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--accent-cyan)', marginBottom: '8px', fontWeight: 700 }}>CONTACT PHONE</label>
<input name="phone" type="text" required value={editingHospital.phone} onChange={(e) => setEditingHospital({...editingHospital, phone: e.target.value})} style={{ width: '100%', padding: '12px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }} />
</div>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--accent-cyan)', marginBottom: '8px', fontWeight: 700 }}>OFFICIAL EMAIL</label>
<input name="email" type="email" required value={editingHospital.email} onChange={(e) => setEditingHospital({...editingHospital, email: e.target.value})} style={{ width: '100%', padding: '12px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }} />
</div>
</form>
)}
</Modal>
</div>
);
};
// 5.3 Hospital Management (CRUD Sub-page)
const HospitalManagement: React.FC<{
hospitals: any[];
onRegister: () => void;
onEdit: (h: any) => void;
onToggleStatus: (h: any) => void;
}> = ({ hospitals, onRegister, onEdit, onToggleStatus }) => {
const filteredHospitals = hospitals; // realHospitals already filtered in loadHospitals for Admine role
return (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ fontSize: '1.25rem', display: 'flex', alignItems: 'center', gap: '12px', fontWeight: 700 }}>
<Settings size={24} color="var(--accent-cyan)" /> Hospital Account Management
</h3>
<div style={{ display: 'flex', gap: '12px' }}>
<button onClick={onRegister} className="glass" style={{ padding: '10px 20px', background: 'var(--accent-cyan)', color: '#fff', border: 'none', borderRadius: '6px', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<Plus size={18} /> REGISTER NEW HOSPITAL
</button>
</div>
</div>
<Card>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
<thead>
<tr style={{ background: 'rgba(0,0,0,0.02)', textAlign: 'left' }}>
<th style={{ padding: '16px' }}>Hospital Details</th>
<th style={{ padding: '16px' }}>Leadership & Contact</th>
<th style={{ padding: '16px' }}>Service Area</th>
<th style={{ padding: '16px' }}>Accreditation</th>
<th style={{ padding: '16px' }}>Status</th>
<th style={{ padding: '16px', textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody>
{filteredHospitals.map((h, i) => (
<tr key={i} style={{ borderBottom: '1px solid rgba(0,0,0,0.02)' }}>
<td style={{ padding: '16px' }}>
<div style={{ fontWeight: 800 }}>{h.name}</div>
<div style={{ fontSize: '0.7rem', color: 'var(--accent-cyan)', marginTop: '2px', fontWeight: 700 }}>{h.type.toUpperCase()}</div>
</td>
<td style={{ padding: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<UserIcon size={14} color="var(--text-secondary)" />
<span>{h.admin}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '4px', fontSize: '0.75rem', color: 'var(--text-secondary)' }}>
<Phone size={14} color="var(--text-secondary)" />
<span>{h.phone}</span>
</div>
</td>
<td style={{ padding: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Navigation2 size={14} color="var(--accent-cyan)" />
<span className="mono" style={{ fontWeight: 700 }}>{h.radius}</span>
</div>
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', marginTop: '4px' }}>
{h.zones.slice(0, 2).join(', ')}{h.zones.length > 2 ? '...' : ''}
</div>
</td>
<td style={{ padding: '16px' }}>
<span style={{ fontSize: '0.7rem', padding: '3px 8px', background: 'rgba(0,0,0,0.02)', borderRadius: '4px', border: '1px solid var(--card-border)' }}>{h.acc}</span>
</td>
<td style={{ padding: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: h.status === 'ACTIVE' ? 'var(--accent-green)' : 'var(--alert-red)', fontWeight: 800 }}>
{h.status === 'ACTIVE' ? <CheckCircle2 size={14} /> : <XCircle size={14} />}
{h.status}
</div>
</td>
<td style={{ padding: '16px', textAlign: 'right' }}>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
<button
onClick={() => onEdit(h)}
style={{ background: 'transparent', border: 'none', color: 'var(--accent-cyan)', cursor: 'pointer' }}
>
<Edit2 size={18} />
</button>
<button
onClick={() => onToggleStatus(h)}
style={{
padding: '6px 12px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)',
borderRadius: '4px', fontSize: '0.65rem', color: h.status === 'ACTIVE' ? 'var(--alert-red)' : 'var(--accent-green)',
fontWeight: 800, cursor: 'pointer'
}}>
{h.status === 'ACTIVE' ? 'DEACTIVATE' : 'ACTIVATE'}
</button>
<button style={{ background: 'transparent', border: 'none', color: 'var(--alert-red)', cursor: 'pointer' }}>
<Trash2 size={18} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(400px, 1fr) 400px', gap: '24px' }}>
<Card title="Teleconsult Routing Rules">
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{[
{ trigger: 'Major Cardiac (Red)', target: 'Cath Lab / Cardiac Centre', priority: 'P0' },
{ trigger: 'Severe Burns (Red)', target: 'Burns Specialty Unit', priority: 'P0' },
{ trigger: 'Pediatric Emergency', target: 'Pediatric ED Node', priority: 'P1' },
].map((rule, i) => (
<div key={i} style={{ padding: '16px', background: 'rgba(0,0,0,0.01)', border: '1px solid var(--card-border)', borderRadius: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Stethoscope size={20} color="var(--accent-cyan)" />
<div>
<div style={{ fontWeight: 700 }}>{rule.trigger}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Route to: {rule.target}</div>
</div>
</div>
<div className="mono" style={{ padding: '4px 8px', background: 'rgba(59, 130, 246, 0.1)', color: 'var(--accent-cyan)', fontWeight: 800, borderRadius: '4px' }}>{rule.priority}</div>
</div>
))}
<button style={{ padding: '12px', background: 'transparent', border: '1px dashed var(--card-border)', color: 'var(--text-secondary)', borderRadius: '10px', fontSize: '0.8rem', cursor: 'pointer' }}>
+ DEFINE NEW ROUTING RULE
</button>
</div>
</Card>
<Card title="Accreditation Compliance">
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '0.85rem' }}>NABH Verification</span>
<div style={{ color: 'var(--accent-green)', fontWeight: 700, fontSize: '0.75rem' }}>VALID</div>
</div>
<div style={{ width: '100%', height: '4px', background: 'rgba(0,0,0,0.02)', borderRadius: '2px' }}>
<div style={{ width: '92%', height: '100%', background: 'var(--accent-green)', borderRadius: '2px' }}></div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '8px' }}>
<span style={{ fontSize: '0.85rem' }}>Quality Assurance Audit</span>
<div style={{ color: 'var(--warning-amber)', fontWeight: 700, fontSize: '0.75rem' }}>PENDING</div>
</div>
<p style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', lineHeight: '1.5' }}>
Automatic compliance checks run every 30 days. Last verification was successful for 88% of the hospital network.
</p>
<button style={{ marginTop: '10px', padding: '10px', background: 'rgba(59, 130, 246, 0.1)', border: '1px solid var(--accent-cyan)', color: 'var(--accent-cyan)', borderRadius: '8px', fontSize: '0.8rem', fontWeight: 700, cursor: 'pointer' }}>
RUN FULL COMPLIANCE SYNC
</button>
</div>
</Card>
</div>
</motion.div>
);
};
// --- COMPONENTS ---
const Modal: React.FC<{ isOpen: boolean; title: string; onClose: () => void; children: React.ReactNode; onSubmit?: () => void; loading?: boolean }> = ({ isOpen, title, onClose, children, onSubmit, loading }) => {
if (!isOpen) return null;
return (
<div className="modal-overlay" style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(255,255,255,0.85)', backdropFilter: 'blur(8px)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '40px' }}>
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} className="glass" style={{ width: '100%', maxWidth: '800px', maxHeight: '90vh', overflowY: 'auto', background: 'var(--card-bg)', padding: '32px', borderRadius: '20px', border: '1px solid var(--accent-cyan)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<h3 style={{ fontSize: '1.5rem', fontWeight: 800 }}>{title}</h3>
<button onClick={onClose} style={{ background: 'transparent', border: 'none', color: 'var(--text-secondary)', cursor: 'pointer' }}><XCircle size={24} /></button>
</div>
{children}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '16px', marginTop: '32px' }}>
<button onClick={onClose} style={{ padding: '10px 20px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', color: 'var(--text-primary)', borderRadius: '6px', fontWeight: 700, cursor: 'pointer' }} disabled={loading}>CANCEL</button>
<button
onClick={onSubmit}
className="btn-primary"
disabled={loading}
style={{ padding: '10px 20px', background: 'var(--accent-cyan)', border: 'none', color: '#fff', borderRadius: '6px', fontWeight: 700, cursor: 'pointer', opacity: loading ? 0.7 : 1 }}
>
{loading ? 'PROCESSING...' : 'REGISTER HOSPITAL'}
</button>
</div>
</motion.div>
</div>
);
};
const HospitalRegistrationForm: React.FC<{ onSubmit: (data: any) => void }> = ({ onSubmit }) => {
const [formData, setFormData] = useState({
admin_name: '',
email: '',
phone: '',
password: '',
hospital_name: '',
city: '',
lat: '13.0827',
lon: '80.2707'
});
const [showPassword, setShowPassword] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleFormSubmit = (e: React.FormEvent) => {
e.preventDefault();
const payload = {
role: "HOSPITAL_ADMIN",
name: formData.admin_name,
phone: formData.phone,
email: formData.email,
password: formData.password,
metadata: {
hospital: {
name: formData.hospital_name,
city: formData.city,
lat: parseFloat(formData.lat),
lon: parseFloat(formData.lon)
}
}
};
onSubmit(payload);
};
return (
<form id="hospital-reg-form" onSubmit={handleFormSubmit} style={{ display: 'flex', flexWrap: 'wrap', gap: '20px' }}>
<h4 style={{ width: '100%', color: 'var(--accent-cyan)', borderBottom: '1px solid rgba(59, 130, 246, 0.1)', paddingBottom: '8px' }}>ADMINISTRATOR DETAILS</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 200px' }}>
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Full Name</label>
<input name="admin_name" value={formData.admin_name} onChange={handleChange} required placeholder="Dr. Administrator Name" className="glass" style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem' }} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 200px' }}>
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Email Address</label>
<input name="email" value={formData.email} onChange={handleChange} required type="email" placeholder="hospital@example.com" autoComplete="new-password" className="glass" style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem' }} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 200px' }}>
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Phone Number</label>
<input name="phone" value={formData.phone} onChange={handleChange} required placeholder="9876543210" className="glass" style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem' }} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 200px' }}>
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Access Password</label>
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
<input name="password" value={formData.password} onChange={handleChange} required type={showPassword ? "text" : "password"} autoComplete="new-password" placeholder="••••••••" className="glass" style={{ padding: '10px 14px', paddingLeft: '36px', paddingRight: '40px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem', width: '100%' }} />
<Lock size={14} style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-secondary)' }} />
<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>
<h4 style={{ width: '100%', color: 'var(--accent-cyan)', borderBottom: '1px solid rgba(59, 130, 246, 0.1)', paddingBottom: '8px', marginTop: '10px' }}>HOSPITAL PROFILE</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 100%' }}>
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Hospital Name</label>
<input name="hospital_name" value={formData.hospital_name} onChange={handleChange} required placeholder="Apollo Hospital Chennai" className="glass" style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem' }} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 200px' }}>
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>City</label>
<input name="city" value={formData.city} onChange={handleChange} required placeholder="Chennai" className="glass" style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem' }} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 200px' }}>
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Latitude</label>
<input name="lat" value={formData.lat} onChange={handleChange} required type="number" step="0.0001" className="glass" style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem' }} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 200px' }}>
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Longitude</label>
<input name="lon" value={formData.lon} onChange={handleChange} required type="number" step="0.0001" className="glass" style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem' }} />
</div>
</form>
);
};

1385
src/pages/LiveIncidents.tsx Normal file

File diff suppressed because it is too large Load Diff

398
src/pages/Login.css Normal file
View File

@@ -0,0 +1,398 @@
/* ─── LOGIN PAGE SHELL ─────────────────────────────────────────────────────── */
.login-page {
min-height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
background: #F8FAFC;
position: relative;
overflow: hidden;
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
-webkit-font-smoothing: antialiased;
}
/* Decorative grid background */
.login-grid-decor {
position: absolute;
inset: 0;
background-image:
radial-gradient(ellipse at 50% 0%, rgba(59, 130, 246, 0.03) 0%, transparent 55%),
radial-gradient(ellipse at 50% 100%, rgba(192, 132, 252, 0.03) 0%, transparent 55%),
linear-gradient(rgba(59, 130, 246, 0.015) 1px, transparent 1px),
linear-gradient(90deg, rgba(59, 130, 246, 0.015) 1px, transparent 1px);
background-size: 100% 100%, 100% 100%, 44px 44px, 44px 44px;
pointer-events: none;
}
/* Scanline effect */
.scanline {
position: absolute;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 0, 0, 0.04) 2px,
rgba(0, 0, 0, 0.04) 4px
);
pointer-events: none;
z-index: 1;
}
/* Radial vignette overlay */
.login-overlay {
position: absolute;
inset: 0;
background: radial-gradient(ellipse at center, transparent 30%, rgba(0, 0, 0, 0.03) 100%);
z-index: 1;
pointer-events: none;
}
/* ─── LOGIN CARD ───────────────────────────────────────────────────────────── */
.login-card {
width: 440px;
max-width: calc(100vw - 40px);
padding: 44px 40px 36px;
z-index: 2;
position: relative;
display: flex;
flex-direction: column;
gap: 0;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(40px) saturate(1.5);
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 28px;
box-shadow:
0 0 0 1px rgba(59, 130, 246, 0.03),
0 30px 80px rgba(0, 0, 0, 0.08),
0 0 60px rgba(59, 130, 246, 0.02);
}
/* Ambient top highlight */
.login-card::before {
content: '';
position: absolute;
top: 0; left: 20%; right: 20%;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.5), transparent);
border-radius: 50%;
}
/* ─── HEADER ───────────────────────────────────────────────────────────────── */
.login-header {
text-align: center;
margin-bottom: 36px;
}
.login-logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 22px;
}
.logo-icon-wrapper {
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.35);
border-radius: 14px;
position: relative;
box-shadow: 0 0 25px rgba(59, 130, 246, 0.15);
}
.logo-icon-wrapper::after {
content: '';
position: absolute;
inset: -3px;
border: 1px solid rgba(59, 130, 246, 0.15);
border-radius: 17px;
animation: pulse-ring 2.5s ease-in-out infinite;
}
@keyframes pulse-ring {
0%, 100% { opacity: 0.4; transform: scale(1); }
50% { opacity: 0.1; transform: scale(1.08); }
}
.login-title {
font-size: 26px;
font-weight: 800;
color: #1E293B;
margin: 0 0 8px;
letter-spacing: -0.5px;
}
.login-subtitle {
color: #64748b;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 2.5px;
font-weight: 600;
margin: 0;
}
/* ─── FORM ─────────────────────────────────────────────────────────────────── */
.login-form {
display: flex;
flex-direction: column;
gap: 0;
}
.input-group {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 18px;
}
.input-label {
font-size: 11px;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 1.2px;
}
.input-wrapper {
position: relative;
}
.input-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: #475569;
transition: color 0.25s;
pointer-events: none;
}
.login-input {
width: 100%;
background: rgba(0, 0, 0, 0.02);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 12px;
padding: 13px 14px 13px 44px;
color: #1E293B;
font-family: 'Outfit', 'Inter', sans-serif;
font-size: 14px;
font-weight: 500;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
}
.login-input::placeholder {
color: rgba(0, 0, 0, 0.3);
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
letter-spacing: 2px;
}
.login-input:focus {
background: rgba(59, 130, 246, 0.04);
border-color: rgba(59, 130, 246, 0.5);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.08), 0 0 20px rgba(59, 130, 246, 0.06);
}
.login-input:focus ~ .input-icon,
.input-wrapper:focus-within .input-icon {
color: rgba(59, 130, 246, 0.8);
}
/* ─── EXTRAS ROW ───────────────────────────────────────────────────────────── */
.login-extras {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12.5px;
margin-bottom: 24px;
margin-top: 2px;
}
.remember-me {
display: flex;
align-items: center;
gap: 9px;
color: #64748b;
cursor: pointer;
user-select: none;
font-weight: 500;
}
/* Custom checkbox */
.remember-me input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 16px;
height: 16px;
border: 1.5px solid rgba(0, 0, 0, 0.1);
border-radius: 4px;
background: rgba(0, 0, 0, 0.02);
cursor: pointer;
position: relative;
transition: all 0.2s;
flex-shrink: 0;
}
.remember-me input[type="checkbox"]:checked {
background: rgba(59, 130, 246, 0.2);
border-color: rgba(59, 130, 246, 0.6);
}
.remember-me input[type="checkbox"]:checked::after {
content: '';
position: absolute;
inset: 2px;
background: var(--accent-cyan, #3B82F6);
border-radius: 2px;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
.forgot-password {
color: rgba(59, 130, 246, 0.8);
text-decoration: none;
font-weight: 600;
font-size: 12.5px;
transition: color 0.2s;
}
.forgot-password:hover {
color: #3B82F6;
}
/* ─── SUBMIT BUTTON ────────────────────────────────────────────────────────── */
.login-button {
width: 100%;
height: 52px;
background: #3B82F6;
border: none;
border-radius: 12px;
color: #fff;
font-weight: 800;
font-size: 14px;
letter-spacing: 1px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 8px 30px rgba(59, 130, 246, 0.25), 0 0 0 1px rgba(59, 130, 246, 0.3);
font-family: 'Outfit', sans-serif;
text-transform: uppercase;
}
.login-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 14px 40px rgba(59, 130, 246, 0.4), 0 0 0 1px rgba(59, 130, 246, 0.4);
background: #60A5FA;
}
.login-button:active:not(:disabled) {
transform: translateY(0);
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.2);
}
.login-button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
/* ─── SECURITY BADGE ───────────────────────────────────────────────────────── */
.security-badge {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 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.15);
border-radius: 10px;
}
/* ─── FOOTER ───────────────────────────────────────────────────────────────── */
.login-footer {
text-align: center;
font-size: 13px;
color: #475569;
margin-top: 16px;
font-weight: 400;
}
.login-footer a {
color: rgba(59, 130, 246, 0.85);
text-decoration: none;
font-weight: 700;
transition: color 0.2s;
}
.login-footer a:hover {
color: #3B82F6;
}
/* ─── BG DECORATIVE TEXT (bottom-left) ────────────────────────────────────── */
.login-sys-log {
position: absolute;
bottom: 32px;
left: 36px;
opacity: 0.12;
pointer-events: none;
z-index: 2;
line-height: 1.8;
}
.login-sys-log p {
font-size: 10px;
font-family: 'JetBrains Mono', monospace;
color: #3B82F6;
margin: 0;
}
/* ─── STATUS INDICATORS (bottom-right of page, NOT card) ──────────────────── */
.login-status-indicators {
position: absolute;
bottom: 32px;
right: 36px;
display: flex;
align-items: center;
gap: 10px;
opacity: 0.25;
z-index: 2;
pointer-events: none;
}
.status-pulse {
width: 8px;
height: 8px;
border-radius: 50%;
animation: status-blink 2s ease-in-out infinite;
}
@keyframes status-blink {
0%, 100% { opacity: 1; box-shadow: 0 0 6px currentColor; }
50% { opacity: 0.4; box-shadow: none; }
}
/* ─── RESPONSIVE ───────────────────────────────────────────────────────────── */
@media (max-width: 480px) {
.login-card {
padding: 36px 24px 28px;
border-radius: 20px;
gap: 0;
}
.login-title { font-size: 22px; }
.login-subtitle { font-size: 10px; letter-spacing: 1.5px; }
.login-sys-log, .login-status-indicators { display: none; }
}

318
src/pages/Login.tsx Normal file
View File

@@ -0,0 +1,318 @@
import React, { useState } from 'react';
import { useNavigate, NavLink } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import {
ShieldCheck,
Lock,
User,
ArrowRight,
Cpu,
Radio,
Activity,
KeyRound,
ShieldAlert,
Eye,
EyeOff,
Truck,
Monitor
} from 'lucide-react';
import { authApi } from '../api/auth';
import './Login.css';
export const Login = () => {
const [username, setUsername] = useState('admin');
const [password, setPassword] = useState('Admin@123!');
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 navigate = useNavigate();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setShowError('');
try {
// --- MOCK BYPASS FOR ADMIN DEMO ---
if (username === 'admin' && password === 'Admin@123!') {
localStorage.setItem('teleems_auth', 'true');
localStorage.setItem('teleems_token', 'mock-admin-token-2026');
localStorage.setItem('teleems_user', JSON.stringify({
id: 'admin-mock-id',
username: 'admin',
name: 'Super Admin (Mock)',
roles: ['CURESELECT_ADMIN'],
metadata: { organization: { company_name: 'TeleEMS HQ' } }
}));
navigate('/');
return;
}
const response = await authApi.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 {
// Direct login
localStorage.setItem('teleems_auth', 'true');
localStorage.setItem('teleems_token', response.data.access_token || '');
localStorage.setItem('teleems_user', JSON.stringify(response.data.user || {}));
navigate('/');
}
} else {
setShowError(response.message || 'Authentication failed');
}
} catch (err) {
setShowError('Unable to connect to authentication server');
} finally {
setIsLoading(false);
}
};
const handleMfaVerify = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setShowError('');
try {
const response = await authApi.verifyMfa(mfaSessionToken, mfaCode);
if (response.status === 201 || response.status === 200) {
localStorage.setItem('teleems_auth', 'true');
localStorage.setItem('teleems_token', response.data.access_token || '');
// Use user from response if available, otherwise use tempUser from first step
const userToStore = response.data.user || tempUser || {};
// Since MFA verification was successful, ensure mfa_enabled is true
userToStore.mfa_enabled = true;
localStorage.setItem('teleems_user', JSON.stringify(userToStore));
navigate('/');
} else {
setShowError(response.message || 'Invalid MFA code');
}
} catch (err) {
setShowError('MFA verification failed');
} finally {
setIsLoading(false);
}
};
return (
<div className="login-page">
<div className="login-grid-decor" />
<div className="scanline" />
<div className="login-overlay" />
<motion.div
key={loginStep}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.4, ease: "easeOut" }}
className="login-card glass glow-cyan"
>
<div className="login-header">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.2, duration: 0.5 }}
className="login-logo"
>
<div className="logo-icon-wrapper">
<Activity className="text-cyan-400" size={24} style={{ color: 'var(--accent-cyan)' }} />
</div>
</motion.div>
<h1 className="login-title">
{loginStep === 'login' ? 'TeleEMS Control' : 'MFA Verification'}
</h1>
<p className="login-subtitle">
{loginStep === 'login' ? 'Sector Alpha • Command Node' : 'Enter Secure Access Code'}
</p>
</div>
{loginStep === 'login' ? (
<form onSubmit={handleLogin} className="login-form">
<div className="input-group">
<label className="input-label">Operator Identification</label>
<div className="input-wrapper">
<User className="input-icon" size={18} />
<input
type="text"
className="login-input mono"
placeholder="USERNAME"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
</div>
<div className="input-group">
<label className="input-label">Access Encryption</label>
<div className="input-wrapper" style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
<Lock className="input-icon" size={18} />
<input
type={showPassword ? "text" : "password"}
className="login-input mono"
placeholder="********"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ paddingRight: '40px' }}
required
/>
<button type="button" onClick={() => setShowPassword(!showPassword)} style={{ position: 'absolute', right: '14px', top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', color: 'var(--text-secondary)', cursor: 'pointer', padding: 0, display: 'flex' }}>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
<div className="login-extras">
<label className="remember-me">
<input type="checkbox" style={{ accentColor: 'var(--accent-cyan)' }} />
Keep Node Active
</label>
<a href="#" className="forgot-password">Auth Recovery?</a>
</div>
<button
type="submit"
className="login-button glow-cyan"
disabled={isLoading}
>
{isLoading ? (
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
>
<Cpu size={20} />
</motion.div>
) : (
<>
AUTHENTICATE ACCESS
<ArrowRight size={20} />
</>
)}
</button>
</form>
) : (
<form onSubmit={handleMfaVerify} className="login-form">
<div className="input-group">
<label className="input-label">TOTP Security Token</label>
<div className="input-wrapper">
<KeyRound className="input-icon" size={18} />
<input
type="text"
className="login-input mono"
placeholder="000 000"
maxLength={6}
value={mfaCode}
onChange={(e) => setMfaCode(e.target.value.replace(/\D/g, ''))}
required
autoFocus
/>
</div>
</div>
<p style={{ fontSize: '12px', color: 'var(--text-secondary)', textAlign: 'center' }}>
Enter the 6-digit code from your authenticator app.
</p>
<button
type="submit"
className="login-button glow-cyan"
disabled={isLoading}
>
{isLoading ? (
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
>
<Cpu size={20} />
</motion.div>
) : (
<>
VERIFY IDENTITY
<ShieldCheck size={20} />
</>
)}
</button>
<button
type="button"
onClick={() => setLoginStep('login')}
style={{
background: 'transparent',
border: 'none',
color: 'var(--text-secondary)',
fontSize: '13px',
cursor: 'pointer',
marginTop: '8px'
}}
>
Back to Login
</button>
</form>
)}
<AnimatePresence>
{showError && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="security-badge"
style={{ color: 'var(--alert-red)' }}
>
<ShieldAlert size={14} />
<span>{showError.toUpperCase()}</span>
</motion.div>
)}
</AnimatePresence>
{!showError && (
<div className="security-badge">
<ShieldCheck size={14} />
<span>QUANTUM-ENCRYPTED CONNECTION ACTIVE</span>
</div>
)}
<div className="login-footer">
<div style={{ marginBottom: '8px' }}>
New Node Operator? <a href="#">Request Credentials</a>
</div>
<div style={{ borderTop: '1px solid rgba(255,255,255,0.05)', paddingTop: '12px', marginTop: '4px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
<NavLink to="/fleet-login" style={{ color: 'var(--accent-cyan)', fontWeight: 700, textDecoration: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px' }}>
<Truck size={16} /> ACCESS FLEET TERMINAL
</NavLink>
<NavLink to="/launcher" style={{ color: 'var(--text-secondary)', fontSize: '0.75rem', fontWeight: 600, textDecoration: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', opacity: 0.8 }}>
<Monitor size={14} /> VIEW ALL SYSTEM PORTALS
</NavLink>
</div>
</div>
{/* Absolute card status dot — removed from card, placed at page level */}
</motion.div>
{/* Page-level status indicators bottom-right */}
<div className="login-status-indicators">
<div className="status-pulse" style={{ backgroundColor: 'var(--accent-green, #00e272)', color: '#00e272' }} />
<Radio size={12} />
</div>
{/* Background decorative sys-log */}
<div className="login-sys-log">
<p>SYS_LOG: LISTENING ON PORT 8080</p>
<p>ENCRYPTION: AES-256-GCM</p>
<p>NODE_ID: EMS-K8S-042</p>
</div>
</div>
);
};

315
src/pages/MasterData.tsx Normal file
View File

@@ -0,0 +1,315 @@
import React, { useState } from 'react';
import {
Database,
Stethoscope,
AlertTriangle,
Box,
Plus,
Search,
ChevronRight,
Edit3,
Trash2,
FileText,
Thermometer,
ShieldAlert,
Save,
CheckCircle2,
ShieldCheck,
Hospital
} from 'lucide-react';
import { Card, StatCard } from '../components/Common';
import { motion, AnimatePresence } from 'framer-motion';
type MasterTab = 'SYMPTOMS' | 'INCIDENT_CATEGORIES' | 'MEDICAL_INVENTORY' | 'HOSPITAL_REFERRALS';
export const MasterDataManagement: React.FC = () => {
const [activeTab, setActiveTab] = useState<MasterTab>('SYMPTOMS');
return (
<div className="page-container" style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<h2 style={{ fontSize: '2.5rem', fontWeight: 900, background: 'linear-gradient(90deg, #3B82F6, #fff)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>
Platform Master Data
</h2>
<p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginTop: '4px' }}>
Manage the categorical DNA of TeleEMS: Symptoms, Incident Logic, and Clinical Inventories.
</p>
</div>
<div className="glass" style={{ padding: '6px', borderRadius: '12px', display: 'flex', gap: '4px', background: 'rgba(255,255,255,0.05)' }}>
<button
onClick={() => setActiveTab('SYMPTOMS')}
style={{
padding: '10px 20px', borderRadius: '8px', border: 'none',
background: activeTab === 'SYMPTOMS' ? 'var(--accent-cyan)' : 'transparent',
color: activeTab === 'SYMPTOMS' ? '#000' : 'var(--text-secondary)',
fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.8rem'
}}>
<Stethoscope size={16} /> SYMPTOMS
</button>
<button
onClick={() => setActiveTab('INCIDENT_CATEGORIES')}
style={{
padding: '10px 20px', borderRadius: '8px', border: 'none',
background: activeTab === 'INCIDENT_CATEGORIES' ? 'var(--accent-cyan)' : 'transparent',
color: activeTab === 'INCIDENT_CATEGORIES' ? '#000' : 'var(--text-secondary)',
fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.8rem'
}}>
<ShieldAlert size={16} /> INCIDENT CATEGORIES
</button>
<button
onClick={() => setActiveTab('MEDICAL_INVENTORY')}
style={{
padding: '10px 20px', borderRadius: '8px', border: 'none',
background: activeTab === 'MEDICAL_INVENTORY' ? 'var(--accent-cyan)' : 'transparent',
color: activeTab === 'MEDICAL_INVENTORY' ? '#000' : 'var(--text-secondary)',
fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.8rem'
}}>
<Box size={16} /> INVENTORY MASTER
</button>
<button
onClick={() => setActiveTab('HOSPITAL_REFERRALS')}
style={{
padding: '10px 20px', borderRadius: '8px', border: 'none',
background: activeTab === 'HOSPITAL_REFERRALS' ? 'var(--accent-cyan)' : 'transparent',
color: activeTab === 'HOSPITAL_REFERRALS' ? '#000' : 'var(--text-secondary)',
fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.8rem'
}}>
<Hospital size={16} /> HOSPITALS
</button>
</div>
</header>
<AnimatePresence mode="wait">
{activeTab === 'SYMPTOMS' && <SymptomMaster key="symptoms" />}
{activeTab === 'INCIDENT_CATEGORIES' && <IncidentCategoryMaster key="categories" />}
{activeTab === 'MEDICAL_INVENTORY' && <InventoryMaster key="inventory" />}
{activeTab === 'HOSPITAL_REFERRALS' && <HospitalReferralMaster key="referrals" />}
</AnimatePresence>
</div>
);
};
// --- SUB-MODULES ---
const SymptomMaster = () => {
const symptoms = [
{ id: 'S-001', name: 'Cardiac Arrest', category: 'Immediate (Red)', instructions: 'Begin CPR, Attach Defibrillator', lang: 'EN, TN, HI' },
{ id: 'S-002', name: 'Compound Fracture', category: 'Urgent (Orange)', instructions: 'Stabilize limb, Control bleeding', lang: 'EN, TN' },
{ id: 'S-003', name: 'Minor Laceration', category: 'Minor (Green)', instructions: 'Clean wound, Apply dressing', lang: 'EN' },
{ id: 'S-004', name: 'Respiratory Distress', category: 'Immediate (Red)', instructions: 'Administer Oxygen, Sitting position', lang: 'EN, TN, HI' },
];
return (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: '12px' }}>
<div className="glass" style={{ padding: '8px 16px', borderRadius: '8px', display: 'flex', alignItems: 'center', gap: '10px' }}>
<Search size={16} color="var(--text-secondary)" />
<input type="text" placeholder="Search symptoms..." style={{ background: 'transparent', border: 'none', color: '#fff', outline: 'none', fontSize: '0.85rem' }} />
</div>
<select className="glass" style={{ padding: '8px 16px', borderRadius: '8px', background: 'rgba(0,0,0,0.5)', color: '#fff', border: '1px solid var(--card-border)', fontSize: '0.85rem' }}>
<option>All Severities</option>
<option>Red (Immediate)</option>
<option>Orange (Urgent)</option>
<option>Green (Minor)</option>
</select>
</div>
<button className="glass" style={{ padding: '10px 20px', background: 'var(--accent-cyan)', color: '#000', border: 'none', borderRadius: '8px', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<Plus size={18} /> ADD NEW SYMPTOM
</button>
</div>
<Card>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
<thead>
<tr style={{ background: 'rgba(255,255,255,0.03)', textAlign: 'left' }}>
<th style={{ padding: '16px' }}>Symptom ID</th>
<th style={{ padding: '16px' }}>Clinical Name</th>
<th style={{ padding: '16px' }}>Severity Cluster</th>
<th style={{ padding: '16px' }}>First Aid Protocol</th>
<th style={{ padding: '16px' }}>Localization</th>
<th style={{ padding: '16px', textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody>
{symptoms.map(s => (
<tr key={s.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.03)' }}>
<td style={{ padding: '16px' }} className="mono">{s.id}</td>
<td style={{ padding: '16px', fontWeight: 800 }}>{s.name}</td>
<td style={{ padding: '16px' }}>
<span style={{
fontSize: '0.65rem', padding: '4px 8px', borderRadius: '4px', fontWeight: 800,
background: s.category.includes('Red') ? 'rgba(255, 59, 59, 0.1)' : 'rgba(255, 184, 0, 0.1)',
color: s.category.includes('Red') ? 'var(--alert-red)' : 'var(--warning-amber)',
border: `1px solid ${s.category.includes('Red') ? 'rgba(255, 59, 59, 0.3)' : 'rgba(255, 184, 0, 0.3)'}`
}}>{s.category.toUpperCase()}</span>
</td>
<td style={{ padding: '16px', color: 'var(--text-secondary)' }}>{s.instructions}</td>
<td style={{ padding: '16px' }}>
<div style={{ display: 'flex', gap: '4px' }}>
{s.lang.split(', ').map(l => (
<span key={l} style={{ fontSize: '0.6rem', padding: '2px 4px', background: 'rgba(255,255,255,0.05)', borderRadius: '2px' }}>{l}</span>
))}
</div>
</td>
<td style={{ padding: '16px', textAlign: 'right' }}>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<button style={{ background: 'transparent', border: 'none', color: 'var(--accent-cyan)', cursor: 'pointer' }}><Edit3 size={16} /></button>
<button style={{ background: 'transparent', border: 'none', color: 'var(--alert-red)', cursor: 'pointer' }}><Trash2 size={16} /></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</Card>
</motion.div>
);
};
const IncidentCategoryMaster = () => {
const categories = [
{ name: 'Red - Immediate', desc: 'Life Threatening Emergency', color: 'var(--alert-red)', escalation: 'Immediate Pilot/EMT + Supervisor notification' },
{ name: 'Orange - Urgent', desc: 'Non-life threatening but critical', color: 'var(--warning-amber)', escalation: 'Pilot/EMT notification within 2 mins' },
{ name: 'Green - Minor', desc: 'Walking wounded / Low priority', color: 'var(--accent-green)', escalation: 'Standard dispatch queue' },
{ name: 'Blue - IFT', desc: 'Inter-Facility Transfer', color: 'var(--accent-cyan)', escalation: 'Scheduled transport routing' },
{ name: 'Black - Deceased', desc: 'Dead on Arrival / Scene', color: '#4A5568', escalation: 'Mortuary / Police notification' },
];
return (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '24px' }}>
{categories.map((c, i) => (
<Card key={i} title={c.name} subtitle={c.desc} style={{ borderLeft: `4px solid ${c.color}` }}>
<div style={{ marginTop: '16px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', textTransform: 'uppercase', marginBottom: '4px' }}>Escalation Logic</div>
<div style={{ fontSize: '0.85rem', fontWeight: 600 }}>{c.escalation}</div>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', marginTop: '10px' }}>
<button style={{ background: 'transparent', border: '1px solid var(--card-border)', color: 'var(--text-secondary)', padding: '6px 12px', borderRadius: '4px', fontSize: '0.7rem', fontWeight: 700, cursor: 'pointer' }}>CONFIGURE RULES</button>
<button style={{ background: 'transparent', border: '1px solid var(--card-border)', color: 'var(--accent-cyan)', padding: '6px 12px', borderRadius: '4px', fontSize: '0.7rem', fontWeight: 700, cursor: 'pointer' }}>EDIT</button>
</div>
</div>
</Card>
))}
<div style={{ border: '2px dashed var(--card-border)', borderRadius: '20px', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', padding: '40px' }} className="hover-glow">
<div style={{ textAlign: 'center' }}>
<Plus size={32} color="var(--text-secondary)" style={{ margin: '0 auto 10px' }} />
<div style={{ fontWeight: 800, color: 'var(--text-secondary)' }}>ADD CUSTOM CATEGORY</div>
</div>
</div>
</motion.div>
);
};
const InventoryMaster = () => {
const items = [
{ name: 'Epinephrine (1mg)', category: 'Drug', unit: 'Ampule', minStock: 10, expiry: true },
{ name: 'Sterile Gauze (4x4)', category: 'Disposable', unit: 'Pack', minStock: 50, expiry: false },
{ name: 'Automatic Defibrillator', category: 'Medical Device', unit: 'Unit', minStock: 1, expiry: false },
{ name: 'Oxygen Cylinder (Portable)', category: 'Reusable', unit: 'Cylinder', minStock: 2, expiry: false },
];
return (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ fontSize: '1.25rem', fontWeight: 800 }}>Medical Inventory Master List</h3>
<button className="glass" style={{ padding: '10px 20px', background: 'var(--accent-cyan)', color: '#000', border: 'none', borderRadius: '8px', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<Save size={18} /> SAVE MASTER LIST
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '20px' }}>
<StatCard label="Total SKU Nodes" value="482" icon={Box} glowColor="cyan" />
<StatCard label="Critical Low Alerts" value="12" icon={AlertTriangle} glowColor="red" />
<StatCard label="Global Stock Value" value="₹12.4L" icon={Database} glowColor="green" />
<StatCard label="Inventory Compliance" value="100%" icon={ShieldCheck} glowColor="cyan" />
</div>
<Card>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
<thead>
<tr style={{ background: 'rgba(255,255,255,0.03)', textAlign: 'left' }}>
<th style={{ padding: '16px' }}>Item Name</th>
<th style={{ padding: '16px' }}>Category</th>
<th style={{ padding: '16px' }}>Unit</th>
<th style={{ padding: '16px' }}>Min Alert Threshold</th>
<th style={{ padding: '16px' }}>Expiry Tracking</th>
<th style={{ padding: '16px', textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody>
{items.map((item, i) => (
<tr key={i} style={{ borderBottom: '1px solid rgba(255,255,255,0.03)' }}>
<td style={{ padding: '16px', fontWeight: 800 }}>{item.name}</td>
<td style={{ padding: '16px' }}>
<span style={{ fontSize: '0.65rem', padding: '4px 8px', background: 'rgba(255,255,255,0.05)', borderRadius: '4px', border: '1px solid var(--card-border)' }}>{item.category.toUpperCase()}</span>
</td>
<td style={{ padding: '16px' }}>{item.unit}</td>
<td style={{ padding: '16px' }} className="mono">{item.minStock}</td>
<td style={{ padding: '16px' }}>
{item.expiry ? <CheckCircle2 size={16} color="var(--accent-green)" /> : <Trash2 size={16} color="rgba(255,255,255,0.1)" />}
</td>
<td style={{ padding: '16px', textAlign: 'right' }}>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
<button style={{ background: 'transparent', border: '1px solid var(--card-border)', color: 'var(--text-secondary)', padding: '4px 8px', borderRadius: '4px', fontSize: '0.7rem', cursor: 'pointer' }}>BATCH INFO</button>
<button style={{ background: 'transparent', border: 'none', color: 'var(--accent-cyan)', cursor: 'pointer' }}><Edit3 size={16} /></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</Card>
</motion.div>
);
};
const HospitalReferralMaster = () => {
const networks = [
{ name: 'City Govt Hospital Network', type: 'Government', nodes: 12, region: 'Chennai Central' },
{ name: 'Apollo Group Synergy', type: 'Private', nodes: 5, region: 'Regional North' },
{ name: 'District Trauma Collective', type: 'Trust', nodes: 8, region: 'Salem/Erode' },
];
return (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '24px' }}>
{networks.map((net, i) => (
<Card key={i} title={net.name} subtitle={`${net.type}${net.region}`}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '20px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<Hospital size={20} color="var(--accent-cyan)" />
<span style={{ fontWeight: 800, fontSize: '1.2rem' }}>{net.nodes} Nodes</span>
</div>
<button style={{ background: 'rgba(59, 130, 246, 0.1)', border: '1px solid var(--accent-cyan)', color: 'var(--accent-cyan)', padding: '6px 12px', borderRadius: '6px', fontSize: '0.7rem', fontWeight: 800, cursor: 'pointer' }}>MANAGE NETWORK</button>
</div>
</Card>
))}
</div>
<Card title="Global Referral Mapping">
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginBottom: '24px' }}>Configure specialty routing rules for Cardiac, Trauma, and Burn centers across all aggregator zones.</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{[
{ target: 'Cardiac Emergencies', hospital: 'Government General Hospital (Cath Lab)', protocol: 'Immediate Redirect' },
{ target: 'Penetrating Trauma', hospital: 'District Trauma Center (Level 1)', protocol: 'ED Pre-alert Pulse' },
{ target: 'Maternal Emergencies', hospital: 'Regional Women & Child Hub', protocol: 'Specialist Standby' },
].map((rule, i) => (
<div key={i} style={{ padding: '16px', background: 'rgba(255,255,255,0.02)', borderRadius: '12px', border: '1px solid var(--card-border)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontWeight: 800, fontSize: '0.9rem', color: 'var(--accent-cyan)' }}>{rule.target}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Destination: {rule.hospital}</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: '0.7rem', fontWeight: 800, textTransform: 'uppercase' }}>{rule.protocol}</div>
<Edit3 size={14} style={{ marginTop: '4px', cursor: 'pointer', opacity: 0.5 }} />
</div>
</div>
))}
</div>
</Card>
</motion.div>
);
};

View File

@@ -0,0 +1,373 @@
import React, { useState } from 'react';
import {
Activity,
Stethoscope,
Package,
Building2,
Plus,
Search,
ChevronRight,
Filter,
AlertCircle,
Clock,
Globe2,
Tag,
BriefcaseMedical,
MapPin,
Crosshair
} from 'lucide-react';
import { Card } from '../components/Common';
import { motion, AnimatePresence } from 'framer-motion';
type MasterTab = 'SYMPTOMS' | 'INCIDENTS' | 'INVENTORY' | 'HOSPITALS';
export const PatientClinical: React.FC = () => {
const [activeTab, setActiveTab] = useState<MasterTab>('SYMPTOMS');
const [searchTerm, setSearchTerm] = useState('');
return (
<div className="page-container" style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<h2 style={{ fontSize: '2.5rem', fontWeight: 900, background: 'linear-gradient(90deg, var(--accent-blue), var(--text-primary))', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>
Platform Master Data
</h2>
<p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginTop: '4px' }}>
System-wide taxonomies, medical protocols, and entity masters.
</p>
</div>
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<div className="glass" style={{ padding: '4px 8px', borderRadius: '12px', display: 'flex', background: 'rgba(0,0,0,0.03)' }}>
{(['SYMPTOMS', 'INCIDENTS', 'INVENTORY', 'HOSPITALS'] as MasterTab[]).map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
style={{
padding: '8px 16px',
borderRadius: '8px',
border: 'none',
background: activeTab === tab ? 'var(--accent-cyan)' : 'transparent',
color: activeTab === tab ? '#fff' : 'var(--text-secondary)',
fontWeight: 700,
fontSize: '0.75rem',
cursor: 'pointer',
transition: 'all 0.2s ease',
textTransform: 'uppercase',
letterSpacing: '0.05em'
}}
>
{tab}
</button>
))}
</div>
</div>
</header>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(250px, 300px) 1fr', gap: '32px' }}>
{/* SIDEBAR FOR FILTERING / SEARCH */}
<aside style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<Card title="Query & Filter">
<div style={{ position: 'relative', marginBottom: '16px' }}>
<Search size={16} style={{ position: 'absolute', left: '12px', top: '12px', color: 'var(--text-secondary)' }} />
<input
type="text"
placeholder={`Search ${activeTab.toLowerCase()}...`}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ width: '100%', padding: '10px 10px 10px 36px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', fontWeight: 700, textTransform: 'uppercase', marginBottom: '4px' }}>Status Filter</div>
<label style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.85rem', cursor: 'pointer' }}>
<input type="checkbox" defaultChecked style={{ accentColor: 'var(--accent-cyan)' }} /> Active Entities
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.85rem', cursor: 'pointer' }}>
<input type="checkbox" style={{ accentColor: 'var(--accent-cyan)' }} /> Deactivated
</label>
</div>
</Card>
<Card title="Quick Stats">
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>Total {activeTab}</span>
<span className="mono" style={{ fontWeight: 700, color: 'var(--accent-cyan)' }}>
{activeTab === 'SYMPTOMS' ? '242' : activeTab === 'INCIDENTS' ? '12' : activeTab === 'INVENTORY' ? '1,204' : '45'}
</span>
</div>
<div style={{ height: '4px', background: 'rgba(0,0,0,0.02)', borderRadius: '2px' }}>
<div style={{ width: '65%', height: '100%', background: 'linear-gradient(90deg, var(--accent-cyan), var(--accent-green))', borderRadius: '2px' }}></div>
</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)' }}>
Last sync: Today at 10:45 AM
</div>
</div>
</Card>
</aside>
{/* MAIN CONTENT AREA */}
<main>
<AnimatePresence mode="wait">
{activeTab === 'SYMPTOMS' && <SymptomMaster key="symptoms" />}
{activeTab === 'INCIDENTS' && <IncidentCategoryMaster key="incidents" />}
{activeTab === 'INVENTORY' && <MedicalInventoryMaster key="inventory" />}
{activeTab === 'HOSPITALS' && <HospitalReferralMaster key="hospitals" />}
</AnimatePresence>
</main>
</div>
</div>
);
};
// 5.5.1 Symptom Master
const SymptomMaster = () => {
const symptoms = [
{ name: 'Chest Pain / Cardiac Arrest', severity: 'Immediate', color: '#ff3b3b', languages: ['EN', 'HI', 'TA', 'ML'], instructions: 'Keep patient calm, check pulse, start CPR if unconscious.' },
{ name: 'Breathing Difficulty / Asthma', severity: 'Immediate', color: '#ff3b3b', languages: ['EN', 'HI', 'TA'], instructions: 'Ensure open airway, assist with inhaler if available.' },
{ name: 'Uncontrolled Bleeding', severity: 'Immediate', color: '#ff3b3b', languages: ['EN', 'HI'], instructions: 'Apply direct pressure to wound using clean cloth.' },
{ name: 'High Fever / Convulsions', severity: 'Urgent', color: '#ffb800', languages: ['EN', 'HI', 'TA', 'KN'], instructions: 'Cool down patient, do not restrain during seizure.' },
{ name: 'Minor Fracture / Sprain', severity: 'Minor', color: '#00ff88', languages: ['EN', 'TA'], instructions: 'Immobilize affected limb, apply cold compress.' },
];
return (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ fontSize: '1.25rem', display: 'flex', alignItems: 'center', gap: '12px', fontWeight: 700 }}>
<Stethoscope size={24} color="var(--accent-cyan)" /> Symptom & Protocol Master
</h3>
<button className="glass" style={{ padding: '8px 16px', background: 'var(--accent-cyan)', color: '#fff', border: 'none', borderRadius: '6px', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<Plus size={16} /> ADD SYMPTOM
</button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{symptoms.map((s, i) => (
<Card key={i} className="hover-glow" style={{ padding: '0', overflow: 'hidden' }}>
<div style={{ display: 'flex', minHeight: '100px' }}>
<div style={{ width: '8px', background: s.color }}></div>
<div style={{ flex: 1, padding: '16px', display: 'flex', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<div style={{ fontWeight: 700, fontSize: '1.1rem' }}>{s.name}</div>
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
{s.languages.map(l => (
<span key={l} style={{ fontSize: '0.6rem', padding: '2px 6px', background: 'rgba(0,0,0,0.02)', borderRadius: '4px', color: 'var(--text-secondary)', border: '1px solid var(--card-border)' }}>{l}</span>
))}
</div>
</div>
<div style={{ textAlign: 'right', display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: '6px' }}>
<div style={{ fontSize: '0.7rem', fontWeight: 800, color: s.color, textTransform: 'uppercase', letterSpacing: '0.05em' }}>{s.severity}</div>
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: '4px', justifyContent: 'flex-end' }}>
<Clock size={12} /> Auto-escalation: 5m
</div>
</div>
</div>
<div style={{ width: '300px', background: 'rgba(0,0,0,0.02)', padding: '16px', fontSize: '0.75rem', borderLeft: '1px solid var(--card-border)', display: 'flex', flexDirection: 'column' }}>
<div style={{ color: 'var(--accent-cyan)', fontWeight: 700, marginBottom: '4px', fontSize: '0.65rem', textTransform: 'uppercase' }}>First-Aid Instructions</div>
<p style={{ margin: 0, color: 'var(--text-secondary)', lineHeight: '1.4' }}>{s.instructions}</p>
</div>
<div style={{ width: '60px', display: 'flex', alignItems: 'center', justifyContent: 'center', borderLeft: '1px solid var(--card-border)', cursor: 'pointer', background: 'rgba(0,0,0,0.01)' }}>
<ChevronRight size={20} color="var(--text-secondary)" />
</div>
</div>
</Card>
))}
</div>
</motion.div>
);
};
// 5.5.2 Incident Category Master
const IncidentCategoryMaster = () => {
const categories = [
{ name: 'Red — Immediate', code: 'IMMEDIATE', level: 'Life Threatening', color: '#ff3b3b', description: 'Immediate threat to life or limb.' },
{ name: 'Orange — Urgent', code: 'URGENT', level: 'Critical but Stable', color: '#ffb800', description: 'Needs prompt medical attention.' },
{ name: 'Green — Minor', code: 'MINOR', level: 'Walking Wounded', color: '#00ff88', description: 'Non-life threatening injuries.' },
{ name: 'White — Non-Emergency', code: 'NON_EMG', level: 'Transport / Checkup', color: '#ffffff', description: 'Planned medical transport.' },
{ name: 'Black — Dead', code: 'DEAD', level: 'Deceased', color: '#555555', description: 'Confirmed DOA or expired on site.' },
{ name: 'Blue — IFT', code: 'IFT', level: 'Inter Facility Transfer', color: '#3B82F6', description: 'Hospital to hospital transfer.' },
];
return (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ fontSize: '1.25rem', display: 'flex', alignItems: 'center', gap: '12px', fontWeight: 700 }}>
<Activity size={24} color="var(--accent-cyan)" /> Incident Category Master
</h3>
<button className="glass" style={{ padding: '8px 16px', background: 'var(--accent-cyan)', color: '#fff', border: 'none', borderRadius: '6px', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<Plus size={16} /> NEW CATEGORY
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: '20px' }}>
{categories.map((c, i) => (
<Card key={i} style={{ borderLeft: `6px solid ${c.color}`, transition: 'all 0.3s ease' }} className="hover-glow">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<div style={{ fontWeight: 800, fontSize: '1.1rem', color: c.color }}>{c.name}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '4px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>{c.level}</div>
</div>
<div className="mono" style={{ fontSize: '0.7rem', padding: '2px 8px', background: 'rgba(0,0,0,0.02)', borderRadius: '4px', border: '1px solid var(--card-border)' }}>{c.code}</div>
</div>
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginTop: '16px', lineHeight: '1.6' }}>
{c.description}
</p>
<div style={{ marginTop: '20px', paddingTop: '16px', borderTop: '1px solid var(--card-border)', display: 'flex', gap: '20px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Auto-Dispatch</div>
<div style={{ color: 'var(--accent-green)', fontWeight: 700, fontSize: '0.75rem' }}>ENABLED</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Escalation</div>
<div style={{ color: 'var(--warning-amber)', fontWeight: 700, fontSize: '0.75rem' }}>T+2 MINS</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Priority</div>
<div style={{ color: 'var(--accent-cyan)', fontWeight: 700, fontSize: '0.75rem' }}>LEVEL {categories.length - i}</div>
</div>
</div>
</Card>
))}
</div>
</motion.div>
);
};
// 5.5.3 Medical Inventory Master
const MedicalInventoryMaster = () => {
const inventory = [
{ name: 'Adrenaline (Epinephrine) Injection', category: 'Drug', stock: 450, min: 100, unit: 'Ampoules', hsn: '30049099' },
{ name: 'Defibrillator Pads (Pediatric)', category: 'Disposable', stock: 24, min: 50, alert: true, unit: 'Pairs', hsn: '90189099' },
{ name: 'Oxygen Cylinder (Type D)', category: 'Medical Device', stock: 12, min: 5, unit: 'Cylinders', hsn: '73110010' },
{ name: 'Sterile Gauze 4x4', category: 'Disposable', stock: 2400, min: 1000, unit: 'Packets', hsn: '30059040' },
{ name: 'Portable Ventilator V1', category: 'Medical Device', stock: 8, min: 2, unit: 'Units', hsn: '90192000' },
];
return (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ fontSize: '1.25rem', display: 'flex', alignItems: 'center', gap: '12px', fontWeight: 700 }}>
<Package size={24} color="var(--accent-cyan)" /> Medical Inventory Master
</h3>
<div style={{ display: 'flex', gap: '12px' }}>
<button className="glass" style={{ padding: '8px 16px', background: 'rgba(0,0,0,0.02)', color: 'var(--text-primary)', border: '1px solid var(--card-border)', borderRadius: '6px', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', fontSize: '0.8rem' }}>
<BriefcaseMedical size={16} /> BATCH TRACKER
</button>
<button className="glass" style={{ padding: '8px 16px', background: 'var(--accent-cyan)', color: '#fff', border: 'none', borderRadius: '6px', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', fontSize: '0.8rem' }}>
<Plus size={16} /> NEW ITEM
</button>
</div>
</div>
<Card>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
<thead>
<tr style={{ background: 'rgba(0,0,0,0.02)', textAlign: 'left' }}>
<th style={{ padding: '16px' }}>Item Details</th>
<th style={{ padding: '16px' }}>Category</th>
<th style={{ padding: '16px' }}>Stock Level</th>
<th style={{ padding: '16px' }}>HSN Code</th>
<th style={{ padding: '16px', textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody>
{inventory.map((item, i) => (
<tr key={i} style={{ borderBottom: '1px solid rgba(0,0,0,0.02)' }}>
<td style={{ padding: '16px' }}>
<div style={{ fontWeight: 700 }}>{item.name}</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)' }}>ID: #INV-{1000 + i}</div>
</td>
<td style={{ padding: '16px' }}>
<span style={{ padding: '4px 8px', background: 'rgba(59, 130, 246, 0.1)', color: 'var(--accent-cyan)', fontSize: '0.7rem', borderRadius: '4px', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
{item.category}
</span>
</td>
<td style={{ padding: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ flex: 1, minWidth: '80px', maxWidth: '120px', height: '6px', background: 'rgba(0,0,0,0.03)', borderRadius: '3px' }}>
<div style={{ width: `${Math.min(100, (item.stock / (item.min * 2)) * 100)}%`, height: '100%', background: item.alert ? 'var(--alert-red)' : 'var(--accent-green)', borderRadius: '3px', boxShadow: item.alert ? '0 0 10px rgba(255, 59, 59, 0.2)' : 'none' }}></div>
</div>
<div className="mono" style={{ fontWeight: 700, color: item.alert ? 'var(--alert-red)' : 'inherit' }}>
{item.stock} <span style={{ fontSize: '0.7rem', fontWeight: 400, color: 'var(--text-secondary)' }}>/ {item.unit}</span>
</div>
{item.alert && <AlertCircle size={14} color="var(--alert-red)" />}
</div>
</td>
<td style={{ padding: '16px' }} className="mono">{item.hsn}</td>
<td style={{ padding: '16px', textAlign: 'right' }}>
<button style={{ background: 'transparent', border: '1px solid var(--card-border)', color: 'var(--text-secondary)', padding: '6px 12px', borderRadius: '4px', cursor: 'pointer', fontSize: '0.75rem', fontWeight: 600 }}>EDIT</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</motion.div>
);
};
// 5.5.4 Hospital & Referral Master
const HospitalReferralMaster = () => {
const hospitals = [
{ name: 'RGGGH (General Hospital)', type: 'Government', district: 'Chennai', coordinates: '13.0827, 80.2707', specialties: ['Trauma', 'Burns', 'General Medicine'], bedCap: '500+', accreditation: 'NABH' },
{ name: 'Apollo Main Hospital', type: 'Private', district: 'Chennai', coordinates: '13.0655, 80.2505', specialties: ['Cardiac', 'Neurology', 'Orthopedics'], bedCap: '350', accreditation: 'JCI/NABH' },
{ name: 'Stanley Medical College', type: 'Government', district: 'Chennai', coordinates: '13.1026, 80.2845', specialties: ['Pediatrics', 'Obstetrics'], bedCap: '420', accreditation: 'NABH' },
{ name: 'Madura Medical Centre', type: 'Private', district: 'Madurai', coordinates: '9.9252, 78.1198', specialties: ['Gastroenterology'], bedCap: '120', accreditation: 'State Board' },
];
return (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ fontSize: '1.25rem', display: 'flex', alignItems: 'center', gap: '12px', fontWeight: 700 }}>
<Building2 size={24} color="var(--accent-cyan)" /> Hospital & Referral Network
</h3>
<button className="glass" style={{ padding: '8px 16px', background: 'var(--accent-cyan)', color: '#fff', border: 'none', borderRadius: '6px', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<Plus size={16} /> REGISTER FACILITY
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))', gap: '20px' }}>
{hospitals.map((h, i) => (
<Card key={i} className="hover-glow" style={{ position: 'relative', overflow: 'hidden' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '16px' }}>
<div>
<div style={{ fontWeight: 800, fontSize: '1.1rem' }}>{h.name}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '4px' }}>
<span style={{ fontSize: '0.7rem', color: 'var(--accent-cyan)', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 700 }}>{h.type} Facility</span>
<span style={{ fontSize: '0.6rem', padding: '1px 6px', background: 'rgba(0,0,0,0.02)', borderRadius: '4px', color: 'var(--text-secondary)' }}>{h.accreditation}</span>
</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '0.75rem', color: 'var(--text-secondary)', fontWeight: 600 }}>
<MapPin size={14} color="var(--accent-cyan)" /> {h.district}
</div>
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', marginBottom: '20px' }}>
{h.specialties.map(s => (
<span key={s} style={{ fontSize: '0.65rem', padding: '3px 10px', background: 'rgba(0, 255, 136, 0.05)', border: '1px solid rgba(0, 255, 136, 0.2)', color: 'var(--accent-green)', borderRadius: '100px', fontWeight: 600 }}>{s}</span>
))}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginTop: 'auto' }}>
<div style={{ background: 'rgba(0,0,0,0.02)', padding: '12px', borderRadius: '8px', border: '1px solid var(--card-border)' }}>
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', textTransform: 'uppercase', marginBottom: '4px' }}>GPS Location</div>
<div className="mono" style={{ fontSize: '0.75rem' }}>{h.coordinates}</div>
</div>
<div style={{ background: 'rgba(0,0,0,0.02)', padding: '12px', borderRadius: '8px', border: '1px solid var(--card-border)' }}>
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', textTransform: 'uppercase', marginBottom: '4px' }}>Bed Capacity</div>
<div className="mono" style={{ fontSize: '0.75rem', color: 'var(--accent-cyan)', fontWeight: 700 }}>{h.bedCap} UNITS</div>
</div>
</div> </div>
<button style={{ width: '100%', marginTop: '16px', background: 'transparent', border: '1px dashed var(--card-border)', color: 'var(--text-secondary)', padding: '8px', borderRadius: '6px', fontSize: '0.75rem', fontWeight: 600, cursor: 'pointer', transition: 'all 0.2s ease' }}>
CONFIGURE ROUTING RULES
</button>
</Card>
))}
</div>
</motion.div>
);
};

View File

@@ -0,0 +1,419 @@
.launcher-page {
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
background: #020617;
color: #f8fafc;
font-family: 'Inter', system-ui, sans-serif;
position: relative;
display: flex;
flex-direction: column;
}
.launcher-page::-webkit-scrollbar {
width: 8px;
}
.launcher-page::-webkit-scrollbar-track {
background: rgba(2, 6, 23, 0.8);
}
.launcher-page::-webkit-scrollbar-thumb {
background: rgba(59, 130, 246, 0.2);
border-radius: 4px;
}
.launcher-page::-webkit-scrollbar-thumb:hover {
background: rgba(59, 130, 246, 0.4);
}
/* Background Effects */
.launcher-bg {
position: absolute;
inset: 0;
z-index: 0;
overflow: hidden;
}
.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); }
}
/* Header */
.launcher-header {
position: relative;
z-index: 10;
padding: 24px 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);
}
.launcher-brand {
display: flex;
align-items: center;
gap: 16px;
}
.launcher-logo {
width: 44px;
height: 44px;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.brand-name {
font-size: 1.25rem;
font-weight: 800;
letter-spacing: -0.02em;
margin: 0;
display: flex;
align-items: baseline;
gap: 8px;
}
.brand-version {
font-size: 0.75rem;
color: #3b82f6;
background: rgba(59, 130, 246, 0.1);
padding: 2px 8px;
border-radius: 99px;
font-weight: 600;
}
.brand-tagline {
font-size: 0.75rem;
color: #94a3b8;
margin: 0;
font-weight: 500;
}
.launcher-actions {
display: flex;
align-items: center;
gap: 20px;
}
.security-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.75rem;
font-weight: 600;
color: #10b981;
background: rgba(16, 185, 129, 0.05);
padding: 6px 12px;
border-radius: 8px;
border: 1px solid rgba(16, 185, 129, 0.1);
}
.config-btn {
background: #f8fafc;
color: #020617;
border: none;
padding: 8px 16px;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
transition: all 0.2s;
}
.config-btn:hover {
background: #ffffff;
transform: translateY(-1px);
}
/* Content */
.launcher-content {
position: relative;
z-index: 1;
flex: 1;
max-width: 1400px;
margin: 0 auto;
padding: 60px 40px;
width: 100%;
}
.launcher-intro {
text-align: center;
margin-bottom: 60px;
}
.launcher-main-title {
font-size: 3.5rem;
font-weight: 900;
margin: 0 0 16px 0;
letter-spacing: -0.04em;
}
.launcher-main-title span {
background: linear-gradient(to right, #3b82f6, #8b5cf6);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.launcher-main-subtitle {
font-size: 1.125rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto;
line-height: 1.6;
}
/* Grid */
.portal-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
/* Cards */
.portal-card {
position: relative;
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 24px;
padding: 32px;
cursor: pointer;
overflow: hidden;
transition: border-color 0.3s;
}
.portal-card:hover {
border-color: var(--accent-color);
}
.portal-card-glow {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at top right, var(--accent-color), transparent 70%);
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.portal-card:hover .portal-card-glow {
opacity: 0.1;
}
.portal-card-inner {
position: relative;
z-index: 2;
height: 100%;
display: flex;
flex-direction: column;
}
.portal-icon-container {
width: 64px;
height: 64px;
background: rgba(255, 255, 255, 0.03);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
color: var(--accent-color);
margin-bottom: 24px;
transition: all 0.3s;
}
.portal-card:hover .portal-icon-container {
background: var(--accent-color);
color: #fff;
transform: scale(1.1);
box-shadow: 0 0 20px var(--accent-color);
}
.portal-subtitle {
font-size: 0.75rem;
font-weight: 800;
letter-spacing: 0.1em;
color: var(--accent-color);
margin-bottom: 8px;
display: block;
}
.portal-title {
font-size: 1.5rem;
font-weight: 800;
margin: 0 0 12px 0;
}
.portal-description {
font-size: 0.9375rem;
color: #94a3b8;
line-height: 1.5;
margin-bottom: 32px;
flex: 1;
}
.portal-footer {
padding-top: 24px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.portal-action {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.875rem;
font-weight: 700;
color: #f8fafc;
}
.portal-action svg {
transition: transform 0.2s;
}
.portal-card:hover .portal-action svg {
transform: translateX(4px);
}
/* Footer */
.launcher-footer {
position: relative;
z-index: 10;
padding: 32px 40px;
background: rgba(2, 6, 23, 0.8);
border-top: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-info {
display: flex;
gap: 40px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.info-item .label {
font-size: 0.625rem;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.info-item .value {
font-size: 0.875rem;
font-weight: 700;
color: #f8fafc;
}
.progress-bar {
width: 100px;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #3b82f6;
}
.footer-links {
display: flex;
gap: 24px;
}
.footer-links a {
color: #64748b;
text-decoration: none;
font-size: 0.8125rem;
font-weight: 600;
transition: color 0.2s;
display: flex;
align-items: center;
gap: 4px;
}
.footer-links a:hover {
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;
}
}

View File

@@ -0,0 +1,221 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import {
Building2,
Building,
Stethoscope,
Activity,
User,
Scan,
ShoppingCart,
ArrowRight,
Shield,
ExternalLink,
Plus,
Truck
} from 'lucide-react';
import './PerspectiveLauncher.css';
interface PortalCardProps {
title: string;
subtitle: string;
icon: React.ElementType;
color: string;
path: string;
description: string;
delay: number;
}
const PortalCard: React.FC<PortalCardProps> = ({ title, subtitle, icon: Icon, color, path, description, delay }) => {
const navigate = useNavigate();
return (
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay }}
whileHover={{ scale: 1.02, y: -5 }}
onClick={() => navigate(path)}
className="portal-card"
style={{ '--accent-color': color } as React.CSSProperties}
>
<div className="portal-card-glow" />
<div className="portal-card-inner">
<div className="portal-icon-container">
<Icon size={32} strokeWidth={1.5} />
</div>
<div className="portal-content">
<span className="portal-subtitle">{subtitle}</span>
<h2 className="portal-title">{title}</h2>
<p className="portal-description">{description}</p>
</div>
<div className="portal-footer">
<span className="portal-action">Enter Portal <ArrowRight size={16} /></span>
</div>
</div>
</motion.div>
);
};
export const PerspectiveLauncher: React.FC = () => {
const portals = [
{
title: 'Admin Control',
subtitle: 'SYSTEM ADMINISTRATION',
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.'
},
{
title: 'Hospital',
subtitle: 'FACILITY OPERATIONS',
icon: Building,
color: '#10b981',
path: '/login/hospital',
description: 'End-to-end management of emergency department operations and bed tracking.'
},
{
title: 'Provider',
subtitle: 'CLINICAL CARE',
icon: Stethoscope,
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.'
}
];
return (
<div className="launcher-page">
<div className="launcher-bg">
<div className="launcher-grid" />
<div className="launcher-blob blob-1" />
<div className="launcher-blob blob-2" />
<div className="launcher-blob blob-3" />
</div>
<header className="launcher-header">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="launcher-brand"
>
<div className="launcher-logo">
<Activity className="text-blue-500" />
</div>
<div>
<h1 className="brand-name">TeleEMS <span className="brand-version">v2.4</span></h1>
<p className="brand-tagline">Integrated Health Ecosystem</p>
</div>
</motion.div>
<div className="launcher-actions">
<div className="security-status">
<Shield size={16} />
<span>Secure Enterprise Node</span>
</div>
<button className="config-btn">
<Plus size={18} /> Add Module
</button>
</div>
</header>
<main className="launcher-content">
<div className="launcher-intro">
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="launcher-main-title"
>
Select Your <span>Perspective</span>
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="launcher-main-subtitle"
>
Access the specific toolset tailored to your role within the healthcare network.
</motion.p>
</div>
<div className="portal-grid">
{portals.map((portal, index) => (
<PortalCard key={portal.title} {...portal} delay={0.2 + index * 0.05} />
))}
</div>
</main>
<footer className="launcher-footer">
<div className="footer-info">
<div className="info-item">
<span className="label">System Load</span>
<div className="progress-bar"><div className="progress-fill" style={{ width: '24%' }} /></div>
</div>
<div className="info-item">
<span className="label">Active Nodes</span>
<span className="value">1,284</span>
</div>
<div className="info-item">
<span className="label">Live Fleet</span>
<span className="value">48 Units</span>
</div>
</div>
<div className="footer-links">
<a href="#">Support</a>
<a href="#">Documentation <ExternalLink size={12} /></a>
<a href="#">System Status</a>
</div>
</footer>
</div>
);
};

View File

@@ -0,0 +1,179 @@
import {
Save,
Mail,
Smartphone,
ShieldCheck,
Terminal,
Activity
} from 'lucide-react';
import { Card } from '../components/Common';
import { MfaSettings } from '../components/MfaSettings';
export const PlatformConfig: React.FC = () => {
return (
<div className="page-container" style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<h2 style={{ fontSize: '2.5rem', fontWeight: 900, background: 'linear-gradient(90deg, #3B82F6, #fff)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>
System Configuration
</h2>
<p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginTop: '4px' }}>
Configure global thresholds, gateway integrations, and platform-wide feature flags.
</p>
</div>
<button className="glass" style={{ padding: '12px 24px', background: 'var(--accent-cyan)', color: '#000', border: 'none', borderRadius: '10px', fontWeight: 800, display: 'flex', alignItems: 'center', gap: '10px', cursor: 'pointer', boxShadow: '0 0 20px rgba(0,212,255,0.4)' }}>
<Save size={18} /> PERSIST ALL CHANGES
</button>
</header>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '24px' }}>
{/* 5.7.1 Global SLA Thresholds */}
<Card title="Global SLA Thresholds" subtitle="Response target targets by category">
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{[
{ category: 'IMMEDIATE (RED)', target: '8 Minutes', threshold: '95%' },
{ category: 'URGENT (ORANGE)', target: '15 Minutes', threshold: '90%' },
{ category: 'MINOR (GREEN)', target: '30 Minutes', threshold: '85%' },
{ category: 'IFT (BLUE)', target: 'Scheduled', threshold: '98%' },
].map((sla, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontSize: '0.75rem', fontWeight: 800, color: sla.category.includes('RED') ? 'var(--alert-red)' : 'inherit' }}>{sla.category}</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)' }}>Target: {sla.target}</div>
</div>
<div className="mono" style={{ fontSize: '1rem', fontWeight: 800, color: 'var(--accent-cyan)' }}>{sla.threshold}</div>
</div>
))}
</div>
</Card>
{/* 5.7.2 Notification Gateways */}
<Card title="Messaging Gateways" subtitle="FCM, SMS, and Email service status.">
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ padding: '16px', background: 'rgba(0,0,0,0.2)', border: '1px solid var(--card-border)', borderRadius: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<Smartphone size={18} color="var(--accent-cyan)" />
<span style={{ fontWeight: 800, fontSize: '0.85rem' }}>SMS Gateway</span>
</div>
<span style={{ fontSize: '0.65rem', padding: '2px 8px', background: 'rgba(0,255,136,0.1)', color: 'var(--accent-green)', borderRadius: '40px', fontWeight: 800 }}>PRIMARY</span>
</div>
<div className="mono" style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Provider: Twilio (AWS Region)</div>
</div>
<div style={{ padding: '16px', background: 'rgba(0,0,0,0.2)', border: '1px solid var(--card-border)', borderRadius: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<Mail size={18} color="var(--accent-cyan)" />
<span style={{ fontWeight: 800, fontSize: '0.85rem' }}>Email SMTP</span>
</div>
<span style={{ fontSize: '0.65rem', padding: '2px 8px', background: 'rgba(0,255,136,0.1)', color: 'var(--accent-green)', borderRadius: '40px', fontWeight: 800 }}>HEALTHY</span>
</div>
<div className="mono" style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Relay: SendGrid (SES Failover)</div>
</div>
</div>
</Card>
{/* MFA Settings */}
<MfaSettings />
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: '24px' }}>
<Card title="Feature Flags" subtitle="Enable/Disable modules per operator.">
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{[
{ name: 'Air Ambulance Dispatch', enabled: true },
{ name: 'IoT Telemetry Feed', enabled: true },
{ name: 'RTVS Video Link', enabled: true },
{ name: 'Inter-Facility Transfer', enabled: false },
{ name: 'Pharmacy Cross-Sell', enabled: false },
{ name: 'Predictive Load Balancer', enabled: true },
].map((flag, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 0' }}>
<span style={{ fontSize: '0.85rem', fontWeight: 600 }}>{flag.name}</span>
<div style={{
width: '40px', height: '22px', background: flag.enabled ? 'var(--accent-green)' : 'rgba(255,255,255,0.05)',
borderRadius: '11px', position: 'relative', cursor: 'pointer', border: flag.enabled ? 'none' : '1px solid var(--card-border)'
}}>
<div style={{
width: '18px', height: '18px', background: '#fff', borderRadius: '50%',
position: 'absolute', top: '2px', left: flag.enabled ? '20px' : '2px',
transition: 'all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275)'
}}></div>
</div>
</div>
))}
</div>
</Card>
<Card title="IoT Device Profiles" subtitle="Firmware versions and pairing pool management.">
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
<thead>
<tr style={{ background: 'rgba(255,255,255,0.03)', textAlign: 'left' }}>
<th style={{ padding: '16px' }}>Device Model</th>
<th style={{ padding: '16px' }}>Firmware Ver</th>
<th style={{ padding: '16px' }}>Active Units</th>
<th style={{ padding: '16px' }}>Auto-Update</th>
<th style={{ padding: '16px', textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody>
{[
{ model: 'TM-Hub G4 (Gateway)', fw: 'v4.2.1-stable', units: 480, auto: 'ENABLED' },
{ model: 'TM-Pat-Monitor X1', fw: 'v1.0.8-patch5', units: 120, auto: 'DISABLED' },
{ model: 'TM-Air-Comm SatLink', fw: 'v2.2.0', units: 15, auto: 'ENABLED' },
{ model: 'TM-Drug-Dispense-B1', fw: 'v0.9.5-beta', units: 85, auto: 'MANUAL' },
].map((row, i) => (
<tr key={i} style={{ borderBottom: '1px solid rgba(255,255,255,0.03)' }}>
<td style={{ padding: '16px', fontWeight: 700 }}>{row.model}</td>
<td style={{ padding: '16px' }} className="mono">{row.fw}</td>
<td style={{ padding: '16px' }}>{row.units}</td>
<td style={{ padding: '16px' }}>
<span style={{ fontSize: '0.7rem', color: row.auto === 'ENABLED' ? 'var(--accent-green)' : 'var(--text-secondary)' }}>{row.auto}</span>
</td>
<td style={{ padding: '16px', textAlign: 'right' }}>
<button style={{ background: 'transparent', border: '1px solid var(--card-border)', color: 'var(--text-secondary)', padding: '4px 10px', borderRadius: '4px', fontSize: '0.7rem', fontWeight: 600, cursor: 'pointer' }}>PUSH FW</button>
</td>
</tr>
))}
</tbody>
</table>
</Card>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '24px' }}>
<Card title="System Audit Stream" subtitle="Live platform event sequence (Read Only)">
<div style={{
background: 'rgba(0,0,0,0.3)',
borderRadius: '8px',
padding: '16px',
fontFamily: 'monospace',
fontSize: '0.8rem',
maxHeight: '200px',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: '8px'
}} className="no-scrollbar">
{[
{ t: '14:22:01', msg: 'SEC: MFA_POL_UPDATED: UserID #admin enforced high-entropy backup codes.', level: 'INFO' },
{ t: '14:21:45', msg: 'NET: GW_SMS_FAILOVER: Switched priority to AWS SES for Asia-South-1.', level: 'WARN' },
{ t: '14:20:12', msg: 'SYS: FRM_IOT_G4: Push firmware v4.2.1 deployment initialized for 12 nodes.', level: 'INFO' },
{ t: '14:18:33', msg: 'AUTH: VAL_LOGIN_FAIL: Excessive attempts from 192.168.1.104. IP throttled.', level: 'ERROR' },
].map((log, i) => (
<div key={i} style={{ display: 'flex', gap: '12px', borderBottom: '1px solid rgba(255,255,255,0.02)', paddingBottom: '4px' }}>
<span style={{ color: 'var(--text-secondary)' }}>[{log.t}]</span>
<span style={{
color: log.level === 'ERROR' ? 'var(--alert-red)' : log.level === 'WARN' ? 'var(--warning-amber)' : 'var(--accent-cyan)',
fontWeight: 700
}}>{log.level}</span>
<span style={{ color: 'var(--text-primary)' }}>{log.msg}</span>
</div>
))}
</div>
</Card>
</div>
</div>
);
};

242
src/pages/RoleLogin.tsx Normal file
View File

@@ -0,0 +1,242 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useParams, NavLink } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import {
ShieldCheck,
Lock,
User as UserIcon,
ArrowRight,
Cpu,
Radio,
Activity,
KeyRound,
ShieldAlert,
Eye,
EyeOff,
Building2,
Building,
Stethoscope,
Scan,
ShoppingCart,
Shield
} from 'lucide-react';
import { authApi } from '../api/auth';
import './Login.css';
const roleMeta: Record<string, { title: string, subtitle: string, icon: any, color: string, dashboard: string }> = {
'admin': {
title: 'Admin Terminal',
subtitle: 'CORE INFRASTRUCTURE',
icon: Shield,
color: 'var(--accent-cyan)',
dashboard: '/'
},
'hospital-group': {
title: 'Group Portal',
subtitle: 'REGIONAL NETWORK',
icon: Building2,
color: '#3b82f6',
dashboard: '/hospital-group'
},
'hospital': {
title: 'Hospital Console',
subtitle: 'FACILITY OPERATIONS',
icon: Building,
color: '#10b981',
dashboard: '/hospital-console'
},
'provider': {
title: 'Provider Access',
subtitle: 'CLINICAL INTERFACE',
icon: Stethoscope,
color: '#8b5cf6',
dashboard: '/provider'
},
'provider-react': {
title: 'React Monitor',
subtitle: 'ACTIVE TELEMETRY',
icon: Activity,
color: '#f59e0b',
dashboard: '/provider-react'
},
'patient': {
title: 'Patient Portal',
subtitle: 'PERSONAL HEALTH',
icon: UserIcon,
color: '#ec4899',
dashboard: '/patient-portal'
},
'scan-centre': {
title: 'Diagnostic Hub',
subtitle: 'IMAGING SERVICES',
icon: Scan,
color: '#06b6d4',
dashboard: '/scan-centre'
},
'cart': {
title: 'Cart Terminal',
subtitle: 'MOBILE RESPONSE',
icon: ShoppingCart,
color: '#f43f5e',
dashboard: '/cart'
}
};
export const RoleLogin = () => {
const { role } = useParams<{ role: string }>();
const navigate = useNavigate();
const meta = roleMeta[role || 'admin'] || roleMeta['admin'];
const Icon = meta.icon;
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [showError, setShowError] = useState('');
const [showPassword, setShowPassword] = useState(false);
// Set default credentials for dev convenience
useEffect(() => {
if (role === 'admin') {
setUsername('admin');
setPassword('Admin@123!');
} else {
setUsername(`${role?.replace('-', '_')}_user`);
setPassword('User@123!');
}
}, [role]);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setShowError('');
try {
// Mocking successful login for the demo/request
setTimeout(() => {
localStorage.setItem('teleems_auth', 'true');
localStorage.setItem('teleems_token', `dev-token-${role}`);
localStorage.setItem('teleems_user', JSON.stringify({
id: `user-${role}`,
username: username,
roles: [role?.toUpperCase().replace('-', '_') || 'USER'],
metadata: { perspective: role }
}));
navigate(meta.dashboard);
setIsLoading(false);
}, 1000);
} catch (err) {
setShowError('Unable to connect to authentication server');
setIsLoading(false);
}
};
return (
<div className="login-page" style={{ '--accent-color': meta.color } as React.CSSProperties}>
<div className="login-grid-decor" />
<div className="scanline" />
<div className="login-overlay" />
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="login-card glass"
style={{ borderColor: meta.color + '44' }}
>
<div className="login-header">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="login-logo"
style={{ background: meta.color + '11', borderColor: meta.color }}
>
<Icon style={{ color: meta.color }} size={24} />
</motion.div>
<h1 className="login-title">{meta.title}</h1>
<p className="login-subtitle">{meta.subtitle} SECURE ACCESS</p>
</div>
<form onSubmit={handleLogin} className="login-form">
<div className="input-group">
<label className="input-label">Operator ID</label>
<div className="input-wrapper">
<UserIcon className="input-icon" size={18} />
<input
type="text"
className="login-input mono"
placeholder="ID_ENTRY"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
</div>
<div className="input-group">
<label className="input-label">Access Key</label>
<div className="input-wrapper" style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
<Lock className="input-icon" size={18} />
<input
type={showPassword ? "text" : "password"}
className="login-input mono"
placeholder="********"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<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 }}>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
<button
type="submit"
className="login-button"
disabled={isLoading}
style={{ background: meta.color, color: meta.color === '#f8fafc' ? '#000' : '#fff' }}
>
{isLoading ? (
<Cpu className="spin" size={20} />
) : (
<>
INITIALIZE SESSION
<ArrowRight size={20} />
</>
)}
</button>
</form>
<AnimatePresence>
{showError && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="security-badge" style={{ color: 'var(--alert-red)' }}>
<ShieldAlert size={14} />
<span>{showError.toUpperCase()}</span>
</motion.div>
)}
</AnimatePresence>
<div className="security-badge">
<ShieldCheck size={14} style={{ color: meta.color }} />
<span style={{ color: meta.color }}>PROTOCOL: {role?.toUpperCase()}_SECURE_v2</span>
</div>
<div className="login-footer">
<NavLink to="/launcher" style={{ color: 'var(--text-secondary)', textDecoration: 'none', fontSize: '0.8rem', fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', opacity: 0.7 }}>
BACK TO PORTAL HUB
</NavLink>
</div>
</motion.div>
<div className="login-status-indicators">
<Radio size={14} className="pulse" style={{ color: meta.color }} />
</div>
<div className="login-sys-log" style={{ opacity: 0.3 }}>
<p>TERMINAL: {role?.toUpperCase()}-X9</p>
<p>STATUS: READY</p>
</div>
</div>
);
};

190
src/pages/SystemHealth.tsx Normal file
View File

@@ -0,0 +1,190 @@
import React from 'react';
import { Zap, Server, Terminal } from 'lucide-react';
import { Card } from '../components/Common';
import { AreaChart, Area, XAxis, YAxis, ResponsiveContainer, Tooltip } from 'recharts';
const cpuData = [
{ time: '09:00', usage: 32 },
{ time: '09:05', usage: 45 },
{ time: '09:10', usage: 42 },
{ time: '09:15', usage: 55 },
{ time: '09:20', usage: 48 },
{ time: '09:25', usage: 60 },
];
export const SystemHealth: React.FC = () => {
return (
<div className="page-container" style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<h2 style={{ fontSize: '1.5rem', fontWeight: 700 }}>Service Infrastructure & Health Monitor</h2>
{/* Connectivity Mesh Visualization */}
<Card style={{ padding: '0', height: '300px', overflow: 'hidden', position: 'relative' }}>
<div style={{ position: 'absolute', top: '15px', left: '20px', fontSize: '0.75rem', fontWeight: 700, color: 'var(--accent-cyan)' }}>MESH STATUS: OPTIMAL (RTT &lt; 2ms)</div>
<div style={{ width: '100%', height: '100%', background: 'radial-gradient(circle at center, rgba(59, 130, 246, 0.05) 0%, transparent 70%)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ position: 'relative', width: '600px', height: '200px' }}>
{/* Central Node */}
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: '60px', height: '60px', borderRadius: '50%', border: '2px solid var(--accent-cyan)', background: 'rgba(59, 130, 246, 0.05)', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 0 15px rgba(59, 130, 246, 0.2)' }}>
<Zap size={24} color="var(--accent-cyan)" />
</div>
{/* Orbital Nodes */}
{[
{ angle: 0, label: 'AUTH' },
{ angle: 60, label: 'DISPATCH' },
{ angle: 120, label: 'RTVS' },
{ angle: 180, label: 'EPCR' },
{ angle: 240, label: 'NOTIFY' },
{ angle: 300, label: 'GATEWAY' },
].map((node, i) => {
const rad = (node.angle * Math.PI) / 180;
const x = 50 + 40 * Math.cos(rad);
const y = 50 + 40 * Math.sin(rad);
return (
<React.Fragment key={i}>
<div style={{ position: 'absolute', top: `${y}%`, left: `${x}%`, width: '10px', height: '10px', background: 'var(--accent-green)', borderRadius: '50%', boxShadow: '0 0 10px var(--accent-green)' }}></div>
<div style={{ position: 'absolute', top: `${y + 4}%`, left: `${x}%`, fontSize: '0.6rem', color: 'var(--text-secondary)', transform: 'translateX(-50%)' }}>{node.label}</div>
{/* SVG Line to center */}
<svg style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: -1 }}>
<line x1="50%" y1="50%" x2={`${x}%`} y2={`${y}%`} stroke="rgba(59, 130, 246, 0.1)" strokeWidth="1" strokeDasharray="4 2" />
</svg>
</React.Fragment>
)
})}
</div>
</div>
</Card>
{/* Services Grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '16px' }}>
{[
'Auth', 'Dispatch', 'RTVS', 'TeleLink', 'ePCR',
'Notify', 'Fleet', 'Hospital', 'Analytics', 'Admin'
].map((service) => (
<Card key={service} style={{ padding: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<div style={{ padding: '6px', background: 'rgba(0,0,0,0.03)', borderRadius: '4px' }}>
<Server size={14} color="var(--accent-cyan)" />
</div>
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: 'var(--accent-green)', boxShadow: '0 0 10px var(--accent-green)' }}></div>
</div>
<div style={{ fontSize: '0.85rem', fontWeight: 700 }}>{service} Service</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '12px', fontSize: '0.65rem', color: 'var(--text-secondary)' }}>
<span>Pods: 3/3</span>
<span>v1.2.4</span>
</div>
</Card>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: '24px' }}>
{/* Infrastructure Metrics */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<Card title="Global CPU Utilization (%)" subtitle="Aggregated Kubernetes Cluster Performance">
<div style={{ height: '300px', marginTop: '12px', minWidth: 0 }}>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={cpuData}>
<defs>
<linearGradient id="colorCpu" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--accent-green)" stopOpacity={0.3}/>
<stop offset="95%" stopColor="var(--accent-green)" stopOpacity={0}/>
</linearGradient>
</defs>
<XAxis dataKey="time" stroke="var(--text-secondary)" fontSize={12} />
<YAxis stroke="var(--text-secondary)" fontSize={12} />
<Tooltip contentStyle={{ background: 'var(--base-bg)', border: '1px solid var(--card-border)' }} />
<Area type="monotone" dataKey="usage" stroke="var(--accent-green)" strokeWidth={3} fillOpacity={1} fill="url(#colorCpu)" />
</AreaChart>
</ResponsiveContainer>
</div>
</Card>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
<Card title="Active DB Connections">
<div style={{ textAlign: 'center', padding: '10px 0' }}>
<div className="mono" style={{ fontSize: '2.5rem', fontWeight: 700, color: 'var(--accent-cyan)' }}>142</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '4px' }}>AURORA GLOBAL (PRIMARY)</div>
</div>
</Card>
<Card title="Redis Hit Rate">
<div style={{ textAlign: 'center', padding: '10px 0' }}>
<div className="mono" style={{ fontSize: '2.5rem', fontWeight: 700, color: 'var(--accent-green)' }}>99.2%</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '4px' }}>ELASTICACHE CLUSTER</div>
</div>
</Card>
</div>
</div>
{/* CI/CD & Deployments */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<Card title="CI/CD Pipeline Status" subtitle="Recent Production Deploys">
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{[
{ svc: 'Dispatch-Service', ver: 'v2.1.0', status: 'SUCCESS', time: '2h ago' },
{ svc: 'Auth-Service', ver: 'v1.4.2', status: 'SUCCESS', time: '5h ago' },
{ svc: 'RTVS-Service', ver: 'v3.0.1', status: 'FAILED', time: '8h ago' },
{ svc: 'Notify-Gateway', ver: 'v0.9.9', status: 'SUCCESS', time: '1d ago' },
].map((dep, i) => (
<div key={i} style={{ padding: '12px', background: 'rgba(0,0,0,0.02)', borderRadius: '6px', borderLeft: `3px solid ${dep.status === 'SUCCESS' ? 'var(--accent-green)' : 'var(--alert-red)'}` }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '0.85rem', fontWeight: 700 }}>{dep.svc}</span>
<span style={{ fontSize: '0.65rem', fontWeight: 700, color: dep.status === 'SUCCESS' ? 'var(--accent-green)' : 'var(--alert-red)' }}>{dep.status}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '6px', fontSize: '0.75rem', color: 'var(--text-secondary)' }}>
<span className="mono">{dep.ver}</span>
<span>{dep.time}</span>
</div>
</div>
))}
</div>
<button style={{ width: '100%', padding: '12px', background: 'transparent', border: '1px solid var(--card-border)', color: 'var(--text-secondary)', borderRadius: '6px', marginTop: '20px', fontSize: '0.8rem', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px' }}>
<Terminal size={14} /> VIEW RUNNER LOGS
</button>
</Card>
<Card title="SSL/TLS Expiry Monitor">
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{[
{ domain: 'api.teleems.in', days: 24, status: 'amber' },
{ domain: 'rtvs.teleems.in', days: 120, status: 'green' },
{ domain: 'telelink.in', days: 5, status: 'red' },
].map((cert, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '0.8rem' }}>{cert.domain}</span>
<span style={{
fontSize: '0.75rem',
fontWeight: 700,
color: cert.status === 'red' ? 'var(--alert-red)' : cert.status === 'amber' ? 'var(--warning-amber)' : 'var(--accent-green)'
}}>{cert.days} Days</span>
</div>
))}
</div>
</Card>
</div>
</div>
<div style={{ marginTop: '32px' }}>
<Card title="System-Wide Maintenance & Emergency Control" subtitle="Override global service state (Requires L3 Clearance)">
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '24px', padding: '12px 0' }}>
<div style={{ padding: '20px', background: 'rgba(239, 68, 68, 0.05)', border: '1px solid rgba(239, 68, 68, 0.2)', borderRadius: '12px' }}>
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: 'var(--alert-red)', marginBottom: '8px' }}>Global Maintenance Mode</div>
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginBottom: '16px' }}>Suspend all non-emergency routing and show maintenance landing page.</p>
<button style={{ padding: '10px 16px', background: 'var(--alert-red)', color: '#fff', border: 'none', borderRadius: '6px', fontWeight: 700, cursor: 'pointer', width: '100%' }}>ACTIVATE LOCKDOWN</button>
</div>
<div style={{ padding: '20px', background: 'rgba(59, 130, 246, 0.05)', border: '1px solid var(--card-border)', borderRadius: '12px' }}>
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: 'var(--accent-cyan)', marginBottom: '8px' }}>Cache Invalidation (Global)</div>
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginBottom: '16px' }}>Flush all Redis clusters and rewarm critical platform metadata.</p>
<button style={{ padding: '10px 16px', background: 'transparent', border: '1px solid var(--accent-cyan)', color: 'var(--accent-cyan)', borderRadius: '6px', fontWeight: 700, cursor: 'pointer', width: '100%' }}>FLUSH MEMORY</button>
</div>
<div style={{ padding: '20px', background: 'rgba(245, 158, 11, 0.05)', border: '1px solid rgba(245, 158, 11, 0.2)', borderRadius: '12px' }}>
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: 'var(--warning-amber)', marginBottom: '8px' }}>Traffic Throttling</div>
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginBottom: '16px' }}>Enable rate-limiting for non-emergency API endpoints (e.g., Analytics).</p>
<button style={{ padding: '10px 16px', background: 'transparent', border: '1px solid var(--warning-amber)', color: 'var(--warning-amber)', borderRadius: '6px', fontWeight: 700, cursor: 'pointer', width: '100%' }}>ENGAGE THROTTLE</button>
</div>
</div>
</Card>
</div>
</div>
);
};

View File

@@ -0,0 +1,172 @@
.user-mgmt-container {
animation: fadeIn 0.8s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
.stat-card-premium {
background: rgba(0, 0, 0, 0.02);
backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 16px;
padding: 24px;
position: relative;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.stat-card-premium:hover {
transform: translateY(-5px);
background: rgba(0, 0, 0, 0.04);
border-color: rgba(59, 130, 246, 0.3);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08);
}
.stat-card-premium::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at top right, rgba(59, 130, 246, 0.1), transparent 70%);
pointer-events: none;
}
.user-identity-cell {
display: flex;
align-items: center;
gap: 16px;
}
.avatar-initials {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 1.1rem;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(59, 130, 246, 0.05));
border: 1px solid rgba(59, 130, 246, 0.2);
color: var(--accent-cyan);
text-shadow: 0 0 10px rgba(59, 130, 246, 0.5);
}
.identity-table-premium {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
}
.identity-table-premium th {
padding: 16px;
text-align: left;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-secondary);
font-weight: 800;
}
.identity-row {
background: rgba(0, 0, 0, 0.01);
transition: all 0.2s;
}
.identity-row:hover {
background: rgba(0, 0, 0, 0.03);
transform: scale(1.002);
}
.identity-row td {
padding: 16px;
border-top: 1px solid rgba(0, 0, 0, 0.02);
border-bottom: 1px solid rgba(0, 0, 0, 0.02);
}
.identity-row td:first-child {
border-left: 1px solid rgba(0, 0, 0, 0.02);
border-radius: 12px 0 0 12px;
}
.identity-row td:last-child {
border-right: 1px solid rgba(0, 0, 0, 0.02);
border-radius: 0 12px 12px 0;
}
/* Custom Scrollbar for the table container */
.table-scroll-container::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.table-scroll-container::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
border-radius: 10px;
}
.table-scroll-container::-webkit-scrollbar-thumb:hover {
background: rgba(59, 130, 246, 0.3);
}
.pulse-active {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent-green);
box-shadow: 0 0 10px var(--accent-green);
position: relative;
}
.pulse-active::after {
content: '';
position: absolute;
inset: -4px;
border: 2px solid var(--accent-green);
border-radius: 50%;
animation: pulse-ring 2s infinite;
}
@keyframes pulse-ring {
0% { transform: scale(0.5); opacity: 0.8; }
100% { transform: scale(1.5); opacity: 0; }
}
.role-badge {
font-size: 0.65rem;
padding: 4px 10px;
border-radius: 6px;
font-weight: 900;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.role-admin {
background: rgba(59, 130, 246, 0.1);
color: var(--accent-cyan);
border: 1px solid rgba(59, 130, 246, 0.3);
}
.role-super {
background: rgba(255, 215, 0, 0.1);
color: #FFD700;
border: 1px solid rgba(255, 215, 0, 0.3);
}
.role-cce {
background: rgba(168, 85, 247, 0.1);
color: #A855F7;
border: 1px solid rgba(168, 85, 247, 0.3);
}

View File

@@ -0,0 +1,720 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import {
UserPlus,
MoreVertical,
ShieldCheck,
Users,
ChevronRight,
AlertCircle,
Radio,
X,
Check,
ShieldAlert,
Clock,
MapPin,
Edit2,
LayoutDashboard} from 'lucide-react';
import { Card } from '../components/Common';
import { motion, AnimatePresence } from 'framer-motion';
import { authApi } from '../api/auth';
import './UserManagement.css';
type ManagementTab = 'RBAC_IDENTITY' | 'CCE_MANAGEMENT' | 'SECURITY_POLICIES';
export const UserManagement: React.FC = () => {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<ManagementTab>('RBAC_IDENTITY');
const [showModal, setShowModal] = useState(false);
const [editingUser, setEditingUser] = useState<any | null>(null);
const [realUsers, setRealUsers] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [hospitalIdFilter, setHospitalIdFilter] = useState('');
const [rolesList, setRolesList] = useState<any[]>([]);
const loadRoles = async () => {
try {
const token = localStorage.getItem('teleems_token') || '';
if (!token) return;
const res = await authApi.getRoles(token);
if (res.data && res.data.data) {
setRolesList(res.data.data);
}
} catch (err) {
console.error('Failed to fetch roles:', err);
}
};
const loadUsers = async () => {
try {
const authStr = localStorage.getItem('teleems_auth');
const token = localStorage.getItem('teleems_token') || '';
if (!token) return;
const response = await authApi.getUsers(token);
if (response.status === 401 || response.message?.toLowerCase().includes('expired')) {
localStorage.removeItem('teleems_auth');
localStorage.removeItem('teleems_token');
localStorage.removeItem('teleems_user');
navigate('/login');
return;
}
if (response && Array.isArray(response.data)) {
const mapped = response.data.map((u: any) => ({
id: u.id,
name: u.name || u.username,
phone: u.phone,
role: (Array.isArray(u.roles) && u.roles.length > 0) ? u.roles[0] : 'N/A',
org: u.metadata?.organization?.company_name || u.metadata?.hospital?.name || u.metadata?.zone || 'TeleEMS Network',
status: u.status || 'INACTIVE',
mfa: !!u.mfaEnabled,
hospitalId: u.hospitalId || u.organisationId || 'N/A',
email: u.email || '',
rawMetadata: u.metadata || {}
}));
setRealUsers(mapped);
}
} catch (error) {
console.error('Failed to fetch users:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadUsers();
loadRoles();
}, []);
const handleStatusToggle = async (user: any) => {
try {
const newStatus = user.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE';
const token = localStorage.getItem('teleems_token') || '';
const payload = {
name: user.name,
email: user.email,
phone: user.phone || '',
status: newStatus,
role: user.role,
metadata: user.rawMetadata
};
const res = await authApi.updateUser(user.id, payload, token);
if (res.status === 401) {
navigate('/login');
return;
}
loadUsers();
} catch (error) {
console.error('Failed to toggle status:', error);
}
};
const handleEditSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!editingUser) return;
try {
const token = localStorage.getItem('teleems_token') || '';
const payload = {
name: editingUser.name,
email: editingUser.email,
phone: editingUser.phone || '',
status: editingUser.status,
role: editingUser.role,
metadata: editingUser.rawMetadata
};
const res = await authApi.updateUser(editingUser.id, payload, token);
if (res.status === 401) {
navigate('/login');
return;
}
setEditingUser(null);
loadUsers();
} catch (error) {
console.error('Update failed:', error);
}
};
const [newUser, setNewUser] = useState({ name: '', role: 'FLEET_OPERATOR', org: '' });
const handleAddUser = (e: React.FormEvent) => {
e.preventDefault();
const id = `user-${Math.floor(Math.random() * 1000)}`;
setRealUsers([...realUsers, { ...newUser, id, status: 'ACTIVE', mfa: false, hospitalId: 'PENDING' }]);
setShowModal(false);
setNewUser({ name: '', role: 'FLEET_OPERATOR', org: '' });
};
const stats = useMemo(() => {
const total = realUsers.length;
const active = realUsers.filter(u => u.status === 'ACTIVE').length;
const mfa = realUsers.filter(u => u.mfa).length;
const admins = realUsers.filter(u => u.role?.includes('ADMIN')).length;
return { total, active, mfa, admins };
}, [realUsers]);
const getInitials = (name: string) => {
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
};
return (
<div className="page-container user-mgmt-container" style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h2 style={{ fontSize: '2.8rem', fontWeight: 900, background: 'linear-gradient(135deg, var(--text-primary) 0%, var(--accent-cyan) 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', letterSpacing: '-0.03em' }}>
Identity Hub
</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginTop: '6px' }}>
<div className="pulse-active"></div>
<p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', fontWeight: 500 }}>
Enterprise RBAC & Security Command Center
</p>
</div>
</div>
<div className="glass" style={{ padding: '6px', borderRadius: '14px', display: 'flex', gap: '6px', background: 'rgba(0,0,0,0.02)', border: '1px solid rgba(0,0,0,0.05)' }}>
{[
{ id: 'RBAC_IDENTITY', icon: Users, label: 'IDENTITIES' },
{ id: 'CCE_MANAGEMENT', icon: Radio, label: 'CCE NODES' },
{ id: 'SECURITY_POLICIES', icon: ShieldCheck, label: 'POLICIES' }
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
style={{
padding: '10px 18px', borderRadius: '10px', border: 'none',
background: activeTab === tab.id ? 'var(--accent-cyan)' : 'transparent',
color: activeTab === tab.id ? '#fff' : 'var(--text-secondary)',
fontWeight: 800, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.75rem',
transition: 'all 0.3s ease',
textTransform: 'uppercase',
letterSpacing: '0.05em'
}}>
<tab.icon size={16} /> {tab.label}
</button>
))}
</div>
</header>
{/* STATS OVERVIEW */}
<div className="stats-grid">
<div className="stat-card-premium">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<div style={{ fontSize: '0.75rem', fontWeight: 800, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em' }}>Total Identities</div>
<div style={{ fontSize: '2.4rem', fontWeight: 900, color: 'var(--text-primary)', marginTop: '4px' }}>{stats.total}</div>
</div>
<Users size={24} color="var(--accent-cyan)" />
</div>
<div style={{ marginTop: '12px', fontSize: '0.75rem', color: 'var(--accent-green)', fontWeight: 700 }}> 12% from last month</div>
</div>
<div className="stat-card-premium">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<div style={{ fontSize: '0.75rem', fontWeight: 800, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em' }}>Active Nodes</div>
<div style={{ fontSize: '2.4rem', fontWeight: 900, color: 'var(--text-primary)', marginTop: '4px' }}>{stats.active}</div>
</div>
<Radio size={24} color="var(--accent-green)" />
</div>
<div style={{ marginTop: '12px', fontSize: '0.75rem', color: 'var(--text-secondary)' }}>System availability: 100%</div>
</div>
<div className="stat-card-premium">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<div style={{ fontSize: '0.75rem', fontWeight: 800, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em' }}>MFA Adoption</div>
<div style={{ fontSize: '2.4rem', fontWeight: 900, color: 'var(--text-primary)', marginTop: '4px' }}>{Math.round((stats.mfa/stats.total || 0) * 100)}%</div>
</div>
<ShieldCheck size={24} color="var(--accent-cyan)" />
</div>
<div style={{ marginTop: '12px', display: 'flex', gap: '4px' }}>
{[1,2,3,4,5,6].map(i => <div key={i} style={{ flex: 1, height: '4px', borderRadius: '2px', background: i <= 5 ? 'var(--accent-cyan)' : 'rgba(0,0,0,0.03)' }}></div>)}
</div>
</div>
<div className="stat-card-premium">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<div style={{ fontSize: '0.75rem', fontWeight: 800, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em' }}>Admin Control</div>
<div style={{ fontSize: '2.4rem', fontWeight: 900, color: 'var(--text-primary)', marginTop: '4px' }}>{stats.admins}</div>
</div>
<ShieldAlert size={24} color="var(--warning-amber)" />
</div>
<div style={{ marginTop: '12px', fontSize: '0.75rem', color: 'var(--warning-amber)', fontWeight: 700 }}>Privileged Access Level</div>
</div>
</div>
{/* NEW USER MODAL (Existing logic) */}
<AnimatePresence>
{showModal && (
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
style={{ position: 'fixed', inset: 0, background: 'rgba(255,255,255,0.85)', zIndex: 2000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px', backdropFilter: 'blur(8px)' }}>
<motion.div
initial={{ scale: 0.9, y: 20 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.9, y: 20 }}
className="glass glow-cyan" style={{ width: '100%', maxWidth: '500px', padding: '40px', position: 'relative', border: '1px solid rgba(59,130,246,0.2)' }}>
<button
onClick={() => setShowModal(false)}
style={{ position: 'absolute', right: '24px', top: '24px', background: 'transparent', border: 'none', color: 'var(--text-secondary)', cursor: 'pointer' }}>
<X size={24} />
</button>
<h3 style={{ fontSize: '1.5rem', marginBottom: '8px', fontWeight: 800 }}>Provision New Identity</h3>
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginBottom: '32px' }}>Assign roles and organizational hierarchy to a new platform user.</p>
<form onSubmit={handleAddUser} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div>
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--accent-cyan)', marginBottom: '10px', fontWeight: 700, textTransform: 'uppercase' }}>Full Legal Name</label>
<input
type="text" required value={newUser.name}
onChange={(e) => setNewUser({...newUser, name: e.target.value})}
style={{ width: '100%', padding: '14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1fr', gap: '20px' }}>
<div>
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--accent-cyan)', marginBottom: '10px', fontWeight: 700, textTransform: 'uppercase' }}>Role Assignment</label>
<select
value={newUser.role}
onChange={(e) => setNewUser({...newUser, role: e.target.value})}
style={{ width: '100%', padding: '14px', background: 'rgba(0,0,0,0.03)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }}
>
{rolesList.map((r: any) => (
<option key={r.id} value={r.name}>{r.name}</option>
))}
</select>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--accent-cyan)', marginBottom: '10px', fontWeight: 700, textTransform: 'uppercase' }}>Organisation</label>
<input
type="text" required value={newUser.org}
onChange={(e) => setNewUser({...newUser, org: e.target.value})}
style={{ width: '100%', padding: '14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }}
/>
</div>
</div>
<button type="submit" style={{ width: '100%', padding: '16px', background: 'var(--accent-cyan)', border: 'none', borderRadius: '8px', fontWeight: 800, color: '#fff', cursor: 'pointer', marginTop: '10px', boxShadow: '0 8px 20px rgba(59,130,246,0.3)' }}>
GENERATE SECURE CREDENTIALS
</button>
</form>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{/* EDIT USER MODAL */}
<AnimatePresence>
{editingUser && (
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
style={{ position: 'fixed', inset: 0, background: 'rgba(255,255,255,0.85)', zIndex: 2000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px', backdropFilter: 'blur(8px)' }}>
<motion.div
initial={{ scale: 0.9, y: 20 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.9, y: 20 }}
className="glass glow-amber" style={{ width: '100%', maxWidth: '500px', padding: '40px', position: 'relative', border: '1px solid rgba(255,191,0,0.3)' }}>
<button
onClick={() => setEditingUser(null)}
style={{ position: 'absolute', right: '24px', top: '24px', background: 'transparent', border: 'none', color: 'var(--text-secondary)', cursor: 'pointer' }}>
<X size={24} />
</button>
<h3 style={{ fontSize: '1.5rem', marginBottom: '8px', fontWeight: 800 }}>Edit Identity Profile</h3>
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginBottom: '32px' }}>Update platform access, organizational ties, and metadata nodes.</p>
<form onSubmit={handleEditSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div>
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--warning-amber)', marginBottom: '10px', fontWeight: 700, textTransform: 'uppercase' }}>Full Display Name</label>
<input
type="text" required value={editingUser.name}
onChange={(e) => setEditingUser({...editingUser, name: e.target.value})}
style={{ width: '100%', padding: '14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
<div>
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--warning-amber)', marginBottom: '10px', fontWeight: 700, textTransform: 'uppercase' }}>Email Address</label>
<input
type="email" required value={editingUser.email}
onChange={(e) => setEditingUser({...editingUser, email: e.target.value})}
style={{ width: '100%', padding: '14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--warning-amber)', marginBottom: '10px', fontWeight: 700, textTransform: 'uppercase' }}>Phone Contact</label>
<input
type="text" value={editingUser.phone || ''}
onChange={(e) => setEditingUser({...editingUser, phone: e.target.value})}
style={{ width: '100%', padding: '14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }}
/>
</div>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--warning-amber)', marginBottom: '10px', fontWeight: 700, textTransform: 'uppercase' }}>Role Assignment</label>
<select
value={editingUser.role}
onChange={(e) => setEditingUser({...editingUser, role: e.target.value})}
style={{ width: '100%', padding: '14px', background: 'rgba(0,0,0,0.03)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }}
>
{rolesList.map((r: any) => (
<option key={r.id} value={r.name}>{r.name}</option>
))}
</select>
</div>
<button type="submit" style={{ width: '100%', padding: '16px', background: 'var(--warning-amber)', border: 'none', borderRadius: '8px', fontWeight: 800, color: '#fff', cursor: 'pointer', marginTop: '10px' }}>
PERSIST IDENTITY CHANGES
</button>
</form>
</motion.div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence mode="wait">
{activeTab === 'RBAC_IDENTITY' && (
<motion.div key="rbac" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '32px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
<Card title="Active Platform Users" subtitle="Manage credentials and multi-factor authentication status.">
<div style={{ display: 'flex', gap: '20px', marginBottom: '20px', padding: '0 16px' }}>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', fontSize: '0.65rem', color: 'var(--accent-cyan)', marginBottom: '8px', fontWeight: 800 }}>SEARCH IDENTITY</label>
<input
type="text"
placeholder="Search name or email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', fontSize: '0.8rem' }}
/>
</div>
<div style={{ width: '300px' }}>
<label style={{ display: 'block', fontSize: '0.65rem', color: 'var(--accent-cyan)', marginBottom: '8px', fontWeight: 800 }}>FILTER BY HOSPITAL / ORG ID</label>
<input
type="text"
placeholder="Enter Hospital ID..."
value={hospitalIdFilter}
onChange={(e) => setHospitalIdFilter(e.target.value)}
style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', fontSize: '0.8rem', fontFamily: 'monospace' }}
/>
</div>
</div>
<div className="table-scroll-container" style={{ overflowX: 'auto', padding: '0 16px' }}>
<table className="identity-table-premium">
<thead>
<tr>
<th>User Identity</th>
<th>Hospital / Org ID</th>
<th>Role Node</th>
<th>Contact</th>
<th>Status & MFA</th>
<th style={{ textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody>
{realUsers
.filter(user => {
const matchesSearch = (user.name || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
(user.email || '').toLowerCase().includes(searchTerm.toLowerCase());
const matchesHospId = !hospitalIdFilter || String(user.hospitalId).includes(hospitalIdFilter);
return matchesSearch && matchesHospId;
})
.map((user) => (
<tr key={user.id} className="identity-row">
<td>
<div className="user-identity-cell">
<div className="avatar-initials">{getInitials(user.name)}</div>
<div>
<div style={{ fontWeight: 800, fontSize: '0.95rem', color: 'var(--text-primary)' }}>{user.name}</div>
<div className="mono" style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', marginTop: '2px' }}>{user.org}</div>
</div>
</div>
</td>
<td>
<div className="mono" style={{ fontSize: '0.75rem', color: 'var(--accent-cyan)', fontWeight: 800, background: 'rgba(59,130,246,0.05)', padding: '4px 8px', borderRadius: '4px', display: 'inline-block' }}>{user.hospitalId}</div>
</td>
<td>
<span className={`role-badge ${user.role?.includes('ADMIN') ? 'role-super' : user.role?.includes('CCE') ? 'role-cce' : 'role-admin'}`}>
{user.role}
</span>
</td>
<td>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', fontWeight: 500 }}>{user.phone || 'N/A'}</div>
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', opacity: 0.6, marginTop: '2px' }}>{user.email}</div>
</td>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div className={user.status === 'ACTIVE' ? 'pulse-active' : ''} style={{ background: user.status === 'ACTIVE' ? 'var(--accent-green)' : 'var(--warning-amber)' }}></div>
<span style={{ fontWeight: 700, fontSize: '0.75rem', letterSpacing: '0.05em' }}>{user.status}</span>
{user.mfa && <ShieldCheck size={14} color="var(--accent-green)" />}
</div>
</td>
<td style={{ textAlign: 'right' }}>
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
<button
onClick={() => setEditingUser(user)}
className="btn-icon"
style={{ color: 'var(--accent-cyan)' }}>
<Edit2 size={16} />
</button>
<button
onClick={() => handleStatusToggle(user)}
style={{
padding: '6px 14px', background: user.status === 'ACTIVE' ? 'rgba(255, 59, 59, 0.1)' : 'rgba(0, 255, 136, 0.1)',
border: `1px solid ${user.status === 'ACTIVE' ? 'rgba(255, 59, 59, 0.2)' : 'rgba(0, 255, 136, 0.2)'}`,
borderRadius: '6px', fontSize: '0.65rem', color: user.status === 'ACTIVE' ? 'var(--alert-red)' : 'var(--accent-green)',
fontWeight: 900, cursor: 'pointer', letterSpacing: '0.05em'
}}>
{user.status === 'ACTIVE' ? 'REVOKE' : 'RECOVER'}
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'center' }}>
<button onClick={() => setShowModal(true)} style={{ padding: '12px 24px', background: 'var(--accent-cyan)', border: 'none', borderRadius: '8px', fontWeight: 800, color: '#fff', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px' }}>
<UserPlus size={18} /> PROVISION NEW IDENTITY
</button>
</div>
</Card>
<Card title="Permissions Inheritance Matrix">
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.75rem' }}>
<thead>
<tr style={{ background: 'rgba(0,0,0,0.02)' }}>
<th style={{ padding: '12px', textAlign: 'left' }}>Global Permission Node</th>
<th style={{ padding: '12px' }}>Super_Admin</th>
<th style={{ padding: '12px' }}>Hosp_Admin</th>
<th style={{ padding: '12px' }}>Fleet_Ops</th>
<th style={{ padding: '12px' }}>CCE</th>
</tr>
</thead>
<tbody>
{[
{ node: 'CORE_SYSTEM_WRITE', roles: [1, 0, 0, 0] },
{ node: 'ENTITY_ONBOARD_MGMT', roles: [1, 0, 0, 0] },
{ node: 'INCIDENT_COMMAND_IO', roles: [1, 1, 1, 1] },
{ node: 'MEDICAL_RECORDS_READ', roles: [1, 1, 0, 1] },
{ node: 'FLEET_ROUTING_CONTROL', roles: [1, 0, 1, 1] },
{ node: 'FINANCIAL_AUDIT_VIEW', roles: [1, 0, 0, 0] },
].map((row, i) => (
<tr key={i} style={{ borderBottom: '1px solid rgba(0,0,0,0.02)' }}>
<td style={{ padding: '12px', fontWeight: 700, color: 'var(--accent-cyan)' }}>{row.node}</td>
{row.roles.map((enabled, index) => (
<td key={index} style={{ padding: '12px', textAlign: 'center' }}>
{enabled ? <Check size={16} color="var(--accent-green)" style={{ margin: '0 auto' }} /> : <X size={16} color="rgba(0,0,0,0.05)" style={{ margin: '0 auto' }} />}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
<Card title="Security Pulse">
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div style={{ background: 'rgba(255, 59, 59, 0.05)', border: '1px solid rgba(255, 59, 59, 0.2)', padding: '16px', borderRadius: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', color: 'var(--alert-red)' }}>
<ShieldAlert size={18} />
<span style={{ fontWeight: 800, fontSize: '0.85rem' }}>Failed Login Spike</span>
</div>
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '8px' }}>Detected 42 failed attempts from IP 103.11.x.x in the last 10 minutes.</p>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '0.85rem' }}>Global MFA Level</span>
<span style={{ color: 'var(--accent-green)', fontWeight: 800, fontSize: '0.85rem' }}>92%</span>
</div>
<div style={{ width: '100%', height: '6px', background: 'rgba(0,0,0,0.05)', borderRadius: '3px' }}>
<div style={{ width: '92%', height: '100%', background: 'linear-gradient(90deg, var(--accent-cyan), var(--accent-green))', borderRadius: '3px' }}></div>
</div>
</div>
</div>
</Card>
<Card title="IP Whitelisting (Admin)">
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginBottom: '16px' }}>Restrict Super Admin access to specific corporate CIDRs.</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{['122.164.x.x (HQ Chennai)', '106.51.x.x (Secure VPN)'].map((ip, i) => (
<div key={i} style={{ padding: '8px 12px', background: 'rgba(0,0,0,0.03)', borderRadius: '6px', fontSize: '0.75rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span className="mono">{ip}</span>
<X size={14} color="var(--text-secondary)" style={{ cursor: 'pointer' }} />
</div>
))}
<button style={{ marginTop: '10px', padding: '8px', background: 'transparent', border: '1px dashed var(--card-border)', borderRadius: '6px', fontSize: '0.75rem', color: 'var(--text-secondary)', cursor: 'pointer' }}>+ ADD CIDR BLOCK</button>
</div>
</Card>
</div>
</motion.div>
)}
{activeTab === 'CCE_MANAGEMENT' && <CCEManagement key="cce" />}
{activeTab === 'SECURITY_POLICIES' && (
<motion.div key="security" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }}>
<Card title="Global Authentication Policy">
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '32px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ fontWeight: 800, fontSize: '0.9rem', color: 'var(--accent-cyan)' }}>Session Settings</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '0.85rem' }}>Idle Timeout</span>
<span className="mono" style={{ fontWeight: 700 }}>30 MINS</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '0.85rem' }}>Concurrent Logins</span>
<span className="mono" style={{ fontWeight: 700 }}>RESTRICTED</span>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ fontWeight: 800, fontSize: '0.9rem', color: 'var(--accent-cyan)' }}>Password Policy</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '0.85rem' }}>Expiry Cycle</span>
<span className="mono" style={{ fontWeight: 700 }}>90 DAYS</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '0.85rem' }}>History Enforcement</span>
<span className="mono" style={{ fontWeight: 700 }}>5 RECENT</span>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ fontWeight: 800, fontSize: '0.9rem', color: 'var(--accent-cyan)' }}>Account Lockout</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '0.85rem' }}>Max Failed Attempts</span>
<span className="mono" style={{ fontWeight: 700 }}>5 RETRIES</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '0.85rem' }}>Lock Duration</span>
<span className="mono" style={{ fontWeight: 700 }}>60 MINS</span>
</div>
</div>
</div>
</Card>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
// 5.6 Call Centre Management
const CCEManagement = () => {
const ccePools = [
{ id: 'POOL_01', name: 'Strategic Dispatch Pool (Chennai)', cces: 12, standby: 4, active: 8, zones: ['Chennai', 'Kanchipuram'], shift: 'SH1 (06:00-14:00)' },
{ id: 'POOL_02', name: 'Central Hub Pool (Coimbatore)', cces: 18, standby: 6, active: 10, zones: ['Coimbatore', 'Erode', 'Salem'], shift: 'SH1 (06:00-14:00)' },
{ id: 'POOL_03', name: 'South Zone Transit (Madurai)', cces: 8, standby: 2, active: 6, zones: ['Madurai', 'Trichy'], shift: 'SH2 (14:00-22:00)' },
];
return (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '24px' }}>
{ccePools.map((pool, i) => (
<Card key={i} className="hover-glow">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '16px' }}>
<div style={{ width: '40px', height: '40px', background: 'rgba(59, 130, 246, 0.1)', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Radio size={20} color="var(--accent-cyan)" />
</div>
<div style={{ fontSize: '0.65rem', fontWeight: 800, color: 'var(--text-secondary)' }} className="mono">{pool.id}</div>
</div>
<div style={{ fontSize: '1.1rem', fontWeight: 800, marginBottom: '4px' }}>{pool.name}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--accent-cyan)', display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '16px' }}>
<Clock size={12} /> {pool.shift}
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '12px', marginBottom: '20px' }}>
<div style={{ textAlign: 'center', background: 'rgba(0,0,0,0.03)', padding: '8px', borderRadius: '8px' }}>
<div style={{ fontSize: '1rem', fontWeight: 800 }}>{pool.cces}</div>
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Total</div>
</div>
<div style={{ textAlign: 'center', background: 'rgba(0,255,136,0.05)', padding: '8px', borderRadius: '8px', border: '1px solid rgba(0,255,136,0.1)' }}>
<div style={{ fontSize: '1rem', fontWeight: 800, color: 'var(--accent-green)' }}>{pool.active}</div>
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Online</div>
</div>
<div style={{ textAlign: 'center', background: 'rgba(255,184,0,0.05)', padding: '8px', borderRadius: '8px', border: '1px solid rgba(255,184,0,0.1)' }}>
<div style={{ fontSize: '1rem', fontWeight: 800, color: 'var(--warning-amber)' }}>{pool.standby}</div>
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Idle</div>
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', borderTop: '1px solid var(--card-border)', paddingTop: '16px' }}>
{pool.zones.map(z => (
<span key={z} style={{ fontSize: '0.65rem', padding: '2px 8px', background: 'rgba(0,0,0,0.03)', borderRadius: '4px', color: 'var(--text-secondary)' }}>{z}</span>
))}
<button style={{ marginLeft: 'auto', background: 'transparent', border: 'none', color: 'var(--accent-cyan)', cursor: 'pointer' }}><ChevronRight size={16} /></button>
</div>
</Card>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '32px' }}>
<Card title="Call Routing Configuration" subtitle="Geographic priority and load-based allocation.">
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{[
{ region: 'District Cluster A', strategy: 'Round Robin', priority: 'High', load: '65%' },
{ region: 'Metropolitan Core', strategy: 'Skill-based (Language)', priority: 'Emergency', load: '82%' },
{ region: 'Northern Highways', strategy: 'Geographic Proximity', priority: 'Standard', load: '30%' },
].map((row, i) => (
<div key={i} style={{ padding: '16px', background: 'rgba(0,0,0,0.01)', borderRadius: '12px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', border: '1px solid var(--card-border)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<MapPin size={24} color="var(--accent-cyan)" />
<div>
<div style={{ fontWeight: 800, fontSize: '1rem' }}>{row.region}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Logic: {row.strategy}</div>
</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontWeight: 800, color: 'var(--accent-cyan)' }}>{row.priority}</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)' }}>LOAD: {row.load}</div>
</div>
</div>
))}
<button style={{ padding: '14px', background: 'transparent', border: '1px dashed var(--card-border)', borderRadius: '12px', color: 'var(--text-secondary)', fontSize: '0.8rem', cursor: 'pointer', fontWeight: 600 }}>
+ CONFIGURE NEW ROUTING DOMAIN
</button>
</div>
</Card>
<Card title="SLA & Escalation Timers">
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '0.85rem' }}>Max Hold Time</span>
<span className="mono" style={{ fontWeight: 800, color: 'var(--accent-cyan)' }}>45s</span>
</div>
<div style={{ width: '100%', height: '4px', background: 'rgba(0,0,0,0.03)', borderRadius: '2px' }}>
<div style={{ width: '45%', height: '100%', background: 'var(--accent-cyan)', borderRadius: '2px' }}></div>
</div>
</div>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '0.85rem' }}>Max Dispatch Delay</span>
<span className="mono" style={{ fontWeight: 800, color: 'var(--accent-cyan)' }}>120s</span>
</div>
<div style={{ width: '100%', height: '4px', background: 'rgba(0,0,0,0.03)', borderRadius: '2px' }}>
<div style={{ width: '70%', height: '100%', background: 'var(--accent-cyan)', borderRadius: '2px' }}></div>
</div>
</div>
<div style={{ background: 'rgba(59, 130, 246, 0.05)', padding: '16px', borderRadius: '12px', marginTop: '10px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--accent-cyan)', marginBottom: '8px' }}>
<AlertCircle size={16} />
<span style={{ fontWeight: 800, fontSize: '0.8rem' }}>Auto-Escalation Rule</span>
</div>
<p style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', lineHeight: '1.4' }}>
If a critical incident is not acknowledged within 90s, notify Supervisor and Regional Ops Manager via SMS/Push.
</p>
</div>
</div>
</Card>
</div>
</motion.div>
);
};

View File

@@ -0,0 +1,207 @@
import React, { useState } from 'react';
import {
Plus,
Search,
Filter,
Truck,
FileText,
Wrench,
Calendar,
AlertTriangle,
ExternalLink,
ChevronRight,
ShieldCheck,
Fuel,
Gauge
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../components/Common';
interface Vehicle {
id: string;
number: string;
type: 'ALS' | 'BLS' | 'TRANSPORT';
model: string;
station: string;
status: 'ACTIVE' | 'MAINTENANCE' | 'BREAKDOWN' | 'OFF_DUTY';
docs: {
rc: string;
fc: string;
insurance: string;
permit: string;
};
lastService: string;
nextService: string;
fuel: number;
}
const MOCK_FLEET: Vehicle[] = [
{ id: 'V-001', number: 'KA 01 MG 2341', type: 'ALS', model: 'Force Traveller 2024', station: 'ALPHA-NODE-01', status: 'ACTIVE', docs: { rc: 'VALID', fc: 'EXPIRING_SOON', insurance: 'VALID', permit: 'VALID' }, lastService: '2026-04-10', nextService: '2026-07-10', fuel: 85 },
{ id: 'V-002', number: 'KA 51 BH 9921', type: 'BLS', model: 'Tata Winger 2023', station: 'BETA-HUB-04', status: 'MAINTENANCE', docs: { rc: 'VALID', fc: 'VALID', insurance: 'VALID', permit: 'VALID' }, lastService: '2026-05-01', nextService: '2026-08-01', fuel: 42 },
{ id: 'V-003', number: 'KA 03 AA 1122', type: 'ALS', model: 'Force Traveller 2023', station: 'ALPHA-NODE-01', status: 'BREAKDOWN', docs: { rc: 'VALID', fc: 'VALID', insurance: 'EXPIRING_SOON', permit: 'VALID' }, lastService: '2026-02-15', nextService: '2026-05-15', fuel: 0 },
{ id: 'V-004', number: 'KA 05 MN 5678', type: 'TRANSPORT', model: 'Maruti Eeco 2022', station: 'GAMMA-STATION-02', status: 'ACTIVE', docs: { rc: 'VALID', fc: 'VALID', insurance: 'VALID', permit: 'VALID' }, lastService: '2026-03-20', nextService: '2026-06-20', fuel: 92 },
];
export const FleetAssets: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const [selectedVehicle, setSelectedVehicle] = useState<Vehicle | null>(null);
return (
<div className="fleet-assets animate-in fade-in duration-500">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div style={{ display: 'flex', gap: '12px' }}>
<div className="glass" style={{ padding: '4px 16px', borderRadius: '12px', display: 'flex', alignItems: 'center', gap: '8px', border: '1px solid rgba(255,255,255,0.1)' }}>
<Search size={16} style={{ opacity: 0.5 }} />
<input
type="text"
placeholder="Search by vehicle number or model..."
style={{ background: 'transparent', border: 'none', color: '#fff', fontSize: '0.875rem', padding: '8px 0', width: '300px', outline: 'none' }}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<button className="btn-icon glass"><Filter size={18} /></button>
</div>
<button className="btn-primary" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Plus size={18} /> REGISTER NEW VEHICLE
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: '24px' }}>
{/* Fleet Inventory Grid */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: '16px' }}>
{MOCK_FLEET.map((v) => (
<Card
key={v.id}
onClick={() => setSelectedVehicle(v)}
style={{
cursor: 'pointer',
border: selectedVehicle?.id === v.id ? '2px solid var(--accent-cyan)' : '1px solid var(--card-border)',
background: selectedVehicle?.id === v.id ? 'rgba(59, 130, 246, 0.05)' : 'rgba(15, 23, 42, 0.4)'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '16px' }}>
<div>
<div style={{ fontSize: '0.65rem', fontWeight: 900, color: 'var(--accent-cyan)', marginBottom: '4px' }}>{v.type} UNIT {v.id}</div>
<h3 style={{ fontSize: '1.125rem', fontWeight: 800 }}>{v.number}</h3>
<div style={{ fontSize: '0.75rem', opacity: 0.5 }}>{v.model}</div>
</div>
<div style={{
padding: '4px 8px',
borderRadius: '4px',
fontSize: '0.65rem',
fontWeight: 900,
background: v.status === 'ACTIVE' ? 'rgba(34, 197, 94, 0.1)' : v.status === 'BREAKDOWN' ? 'rgba(239, 68, 68, 0.1)' : 'rgba(245, 158, 11, 0.1)',
color: v.status === 'ACTIVE' ? '#22C55E' : v.status === 'BREAKDOWN' ? '#EF4444' : '#F59E0B',
border: `1px solid ${v.status === 'ACTIVE' ? 'rgba(34, 197, 94, 0.2)' : v.status === 'BREAKDOWN' ? 'rgba(239, 68, 68, 0.2)' : 'rgba(245, 158, 11, 0.2)'}`
}}>
{v.status}
</div>
</div>
<div style={{ display: 'flex', gap: '20px', marginBottom: '16px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '0.6rem', opacity: 0.5, textTransform: 'uppercase', marginBottom: '4px' }}>Fuel Level</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ flex: 1, height: '4px', background: 'rgba(255,255,255,0.1)', borderRadius: '2px', overflow: 'hidden' }}>
<div style={{ width: `${v.fuel}%`, height: '100%', background: v.fuel < 25 ? '#EF4444' : '#22C55E' }}></div>
</div>
<span style={{ fontSize: '0.75rem', fontWeight: 700 }}>{v.fuel}%</span>
</div>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingTop: '16px', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<div className={v.docs.fc === 'VALID' ? 'status-dot-green' : 'status-pulse-amber'} title="Fitness Certificate"></div>
<div className={v.docs.insurance === 'VALID' ? 'status-dot-green' : 'status-pulse-amber'} title="Insurance"></div>
<div className={v.docs.permit === 'VALID' ? 'status-dot-green' : 'status-pulse-amber'} title="Ambulance Permit"></div>
</div>
<span style={{ fontSize: '0.7rem', opacity: 0.5 }}>{v.station}</span>
</div>
</Card>
))}
</div>
</div>
{/* Detailed Inspector Panel */}
<div style={{ position: 'sticky', top: '0' }}>
{selectedVehicle ? (
<AnimatePresence mode="wait">
<motion.div
key={selectedVehicle.id}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
>
<Card title="Asset Intelligence" subtitle={`Detailed diagnostics for ${selectedVehicle.number}`}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
{/* Critical Document Status */}
<div>
<h4 style={{ fontSize: '0.75rem', fontWeight: 800, textTransform: 'uppercase', color: 'var(--accent-cyan)', marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<FileText size={14} /> Document Vault
</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{[
{ label: 'Registration (RC)', status: selectedVehicle.docs.rc, expiry: '2030-12-15' },
{ label: 'Fitness (FC)', status: selectedVehicle.docs.fc, expiry: '2026-06-01' },
{ label: 'Insurance Policy', status: selectedVehicle.docs.insurance, expiry: '2026-05-15' },
{ label: 'Ambulance Permit', status: selectedVehicle.docs.permit, expiry: '2026-09-20' },
].map((doc, idx) => (
<div key={idx} style={{ padding: '12px', borderRadius: '8px', background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontSize: '0.8125rem', fontWeight: 600 }}>{doc.label}</div>
<div style={{ fontSize: '0.7rem', opacity: 0.5 }}>Expires: {doc.expiry}</div>
</div>
<div style={{ color: doc.status === 'VALID' ? '#22C55E' : '#F59E0B', display: 'flex', alignItems: 'center', gap: '6px' }}>
{doc.status === 'VALID' ? <ShieldCheck size={16} /> : <AlertTriangle size={16} />}
<span style={{ fontSize: '0.65rem', fontWeight: 900 }}>{doc.status}</span>
</div>
</div>
))}
</div>
</div>
{/* Maintenance History */}
<div>
<h4 style={{ fontSize: '0.75rem', fontWeight: 800, textTransform: 'uppercase', color: 'var(--accent-cyan)', marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<Wrench size={14} /> Service Records
</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={{ padding: '16px', borderRadius: '12px', background: 'rgba(59, 130, 246, 0.05)', border: '1px solid rgba(59, 130, 246, 0.1)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '0.75rem', fontWeight: 700 }}>Upcoming Service</span>
<span style={{ fontSize: '0.75rem', color: 'var(--accent-cyan)' }}>{selectedVehicle.nextService}</span>
</div>
<div style={{ fontSize: '0.7rem', opacity: 0.6 }}>Scheduled for: Engine Oil change, Brake pad inspection, and AC filter cleaning.</div>
</div>
<div style={{ padding: '12px', borderRadius: '8px', background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)', display: 'flex', justifyContent: 'space-between' }}>
<div style={{ fontSize: '0.75rem' }}>Last Major Service</div>
<div style={{ fontSize: '0.75rem', fontWeight: 700 }}>{selectedVehicle.lastService}</div>
</div>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<button className="btn-ghost" style={{ width: '100%', fontSize: '0.75rem' }}>VIEW ALL RECORDS</button>
<button className="btn-primary" style={{ width: '100%', fontSize: '0.75rem' }}>LOG MAINTENANCE</button>
</div>
</div>
</Card>
</motion.div>
</AnimatePresence>
) : (
<div className="glass" style={{ padding: '40px', borderRadius: '16px', textAlign: 'center', border: '1px dashed rgba(255,255,255,0.1)' }}>
<div style={{ width: '48px', height: '48px', borderRadius: '50%', background: 'rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px', color: 'var(--accent-cyan)' }}>
<Truck size={24} />
</div>
<h3 style={{ fontSize: '1rem', fontWeight: 700, marginBottom: '8px' }}>No Asset Selected</h3>
<p style={{ fontSize: '0.875rem', color: 'var(--text-secondary)' }}>Select a vehicle from the fleet inventory to view tactical diagnostics, documents, and service history.</p>
</div>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,200 @@
import React, { useState } from 'react';
import {
Plus,
Search,
Filter,
ShoppingCart,
Package,
AlertTriangle,
ArrowUpRight,
ArrowDownLeft,
ChevronRight,
Database,
Truck,
Activity,
Archive,
BarChart3
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../components/Common';
interface InventoryItem {
id: string;
name: string;
category: 'MEDICINE' | 'CONSUMABLE' | 'EQUIPMENT';
totalStock: number;
minStock: number;
unit: string;
expiringSoon: number;
vehicles: { vehicleId: string; stock: number }[];
}
const MOCK_INVENTORY: InventoryItem[] = [
{ id: 'ITM-001', name: 'Adrenaline Injection 1mg', category: 'MEDICINE', totalStock: 450, minStock: 100, unit: 'AMPULES', expiringSoon: 24, vehicles: [{ vehicleId: 'V-001', stock: 10 }, { vehicleId: 'V-002', stock: 8 }] },
{ id: 'ITM-002', name: 'Oxygen Cylinder (D-Type)', category: 'EQUIPMENT', totalStock: 32, minStock: 10, unit: 'UNITS', expiringSoon: 0, vehicles: [{ vehicleId: 'V-001', stock: 2 }, { vehicleId: 'V-002', stock: 1 }] },
{ id: 'ITM-003', name: 'Surgical Gloves (Size 7)', category: 'CONSUMABLE', totalStock: 1200, minStock: 500, unit: 'PAIRS', expiringSoon: 0, vehicles: [{ vehicleId: 'V-001', stock: 50 }, { vehicleId: 'V-002', stock: 40 }] },
{ id: 'ITM-004', name: 'IV Fluids (NS 500ml)', category: 'MEDICINE', totalStock: 85, minStock: 150, unit: 'BOTTLES', expiringSoon: 12, vehicles: [{ vehicleId: 'V-001', stock: 5 }, { vehicleId: 'V-002', stock: 3 }] },
];
export const FleetInventory: React.FC = () => {
const [selectedItem, setSelectedItem] = useState<InventoryItem | null>(null);
return (
<div className="fleet-inventory animate-in fade-in duration-500">
{/* Top Stats */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px', marginBottom: '24px' }}>
<div className="glass" style={{ padding: '20px', borderRadius: '16px', border: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ color: 'var(--accent-cyan)' }}><Archive size={20} /></div>
<span style={{ fontSize: '0.65rem', fontWeight: 900, color: 'var(--accent-green)' }}>+12%</span>
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900 }}>1,248</div>
<div style={{ fontSize: '0.7rem', opacity: 0.5, textTransform: 'uppercase' }}>TOTAL SKUs</div>
</div>
<div className="glass" style={{ padding: '20px', borderRadius: '16px', border: '1px solid rgba(239, 68, 68, 0.2)', background: 'rgba(239, 68, 68, 0.02)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ color: '#EF4444' }}><AlertTriangle size={20} /></div>
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#EF4444' }}>14</div>
<div style={{ fontSize: '0.7rem', opacity: 0.5, textTransform: 'uppercase' }}>LOW STOCK ITEMS</div>
</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>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#F59E0B' }}>8</div>
<div style={{ fontSize: '0.7rem', opacity: 0.5, textTransform: 'uppercase' }}>EXPIRING (30D)</div>
</div>
<div className="glass" style={{ padding: '20px', borderRadius: '16px', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ color: 'var(--accent-cyan)' }}><BarChart3 size={20} /></div>
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900 }}>42.5k</div>
<div style={{ fontSize: '0.7rem', opacity: 0.5, textTransform: 'uppercase' }}>CONSUMPTION (MTD)</div>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: '24px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<Card title="Tactical Inventory Ledger">
<div style={{ marginBottom: '16px', display: 'flex', gap: '12px' }}>
<div className="glass" style={{ flex: 1, padding: '8px 16px', borderRadius: '12px', display: 'flex', alignItems: 'center', gap: '8px', border: '1px solid rgba(255,255,255,0.1)' }}>
<Search size={16} style={{ opacity: 0.5 }} />
<input
type="text"
placeholder="Filter item master by name, category, or batch..."
style={{ background: 'transparent', border: 'none', color: '#fff', fontSize: '0.8125rem', width: '100%', outline: 'none' }}
/>
</div>
<button className="btn-primary" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Plus size={16} /> ADD STOCK
</button>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ textAlign: 'left', opacity: 0.5, fontSize: '0.65rem', textTransform: 'uppercase', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
<th style={{ padding: '12px' }}>Item Details</th>
<th style={{ padding: '12px' }}>Category</th>
<th style={{ padding: '12px' }}>Current Stock</th>
<th style={{ padding: '12px' }}>Status</th>
</tr>
</thead>
<tbody>
{MOCK_INVENTORY.map(item => (
<tr
key={item.id}
onClick={() => setSelectedItem(item)}
style={{
borderBottom: '1px solid rgba(255,255,255,0.05)',
cursor: 'pointer',
background: selectedItem?.id === item.id ? 'rgba(59, 130, 246, 0.05)' : 'transparent'
}}
className="hover-glow"
>
<td style={{ padding: '16px 12px' }}>
<div style={{ fontWeight: 700, fontSize: '0.875rem' }}>{item.name}</div>
<div style={{ fontSize: '0.65rem', opacity: 0.5 }}>SKU: {item.id}</div>
</td>
<td style={{ padding: '16px 12px' }}>
<span style={{ fontSize: '0.7rem', fontWeight: 600, opacity: 0.7 }}>{item.category}</span>
</td>
<td style={{ padding: '16px 12px' }}>
<div style={{ fontWeight: 800 }}>{item.totalStock} <span style={{ fontSize: '0.65rem', fontWeight: 500, opacity: 0.5 }}>{item.unit}</span></div>
</td>
<td style={{ padding: '16px 12px' }}>
{item.totalStock < item.minStock ? (
<div style={{ color: '#EF4444', fontSize: '0.65rem', fontWeight: 900, display: 'flex', alignItems: 'center', gap: '4px' }}>
<AlertTriangle size={12} /> CRITICAL
</div>
) : (
<div style={{ color: '#22C55E', fontSize: '0.65rem', fontWeight: 900 }}>OPTIMAL</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</Card>
</div>
<div>
{selectedItem ? (
<AnimatePresence mode="wait">
<motion.div
key={selectedItem.id}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
>
<Card title="Supply Intelligence" subtitle={`Ambulance-wise distribution for ${selectedItem.name}`}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div style={{ padding: '16px', borderRadius: '12px', background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ fontSize: '0.65rem', opacity: 0.5, marginBottom: '4px' }}>STOCK GAP</div>
<div style={{ fontSize: '1.25rem', fontWeight: 900, color: selectedItem.totalStock < selectedItem.minStock ? '#EF4444' : '#22C55E' }}>
{selectedItem.totalStock - selectedItem.minStock}
</div>
</div>
<div style={{ padding: '16px', borderRadius: '12px', background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ fontSize: '0.65rem', opacity: 0.5, marginBottom: '4px' }}>REORDER POINT</div>
<div style={{ fontSize: '1.25rem', fontWeight: 900 }}>{selectedItem.minStock}</div>
</div>
</div>
<div>
<h4 style={{ fontSize: '0.75rem', fontWeight: 800, textTransform: 'uppercase', color: 'var(--accent-cyan)', marginBottom: '12px' }}>Ambulance Stock Distribution</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{selectedItem.vehicles.map((v, i) => (
<div key={i} style={{ padding: '12px', borderRadius: '12px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Truck size={14} style={{ opacity: 0.5 }} />
<span style={{ fontSize: '0.8125rem', fontWeight: 700 }}>{v.vehicleId}</span>
</div>
<div style={{ fontWeight: 800, color: v.stock < 5 ? '#EF4444' : 'inherit' }}>
{v.stock} {selectedItem.unit}
</div>
</div>
))}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<button className="btn-ghost" style={{ width: '100%', fontSize: '0.75rem' }}>AUDIT LOG</button>
<button className="btn-primary" style={{ width: '100%', fontSize: '0.75rem' }}>RESTOCK REQUEST</button>
</div>
</div>
</Card>
</motion.div>
</AnimatePresence>
) : (
<div className="glass" style={{ padding: '40px', borderRadius: '16px', textAlign: 'center', border: '1px dashed rgba(255,255,255,0.1)' }}>
<Package size={32} style={{ opacity: 0.2, margin: '0 auto 16px' }} />
<h3 style={{ fontSize: '1rem', fontWeight: 700, marginBottom: '8px' }}>Select Supply Item</h3>
<p style={{ fontSize: '0.875rem', color: 'var(--text-secondary)' }}>Inspect real-time stock levels across the fleet, track expiries, and manage replenishment requests.</p>
</div>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,205 @@
import React, { useState } from 'react';
import {
UserPlus,
Search,
Filter,
Users,
Medal,
Clock,
ShieldCheck,
AlertTriangle,
Mail,
Phone,
Calendar,
CheckCircle2,
XCircle,
MoreVertical
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../components/Common';
interface Staff {
id: string;
name: string;
role: 'DRIVER' | 'EMT' | 'DOCTOR' | 'PARAMEDIC';
status: 'ON_DUTY' | 'OFF_DUTY' | 'ON_LEAVE';
specialization?: string;
phone: string;
email: string;
joinedDate: string;
tripsCompleted: number;
rating: number;
certExpiry: string;
}
const MOCK_STAFF: Staff[] = [
{ id: 'S-101', name: 'Vikram Singh', role: 'DRIVER', status: 'ON_DUTY', phone: '+91 98765 43210', email: 'v.singh@teleems.com', joinedDate: '2023-01-15', tripsCompleted: 452, rating: 4.8, certExpiry: '2026-12-01' },
{ id: 'S-102', name: 'Dr. Ananya Iyer', role: 'DOCTOR', specialization: 'Critical Care', status: 'ON_DUTY', phone: '+91 98765 43211', email: 'a.iyer@teleems.com', joinedDate: '2023-06-20', tripsCompleted: 128, rating: 4.9, certExpiry: '2026-05-15' },
{ id: 'S-103', name: 'Rahul Verma', role: 'EMT', status: 'ON_LEAVE', phone: '+91 98765 43212', email: 'r.verma@teleems.com', joinedDate: '2024-02-10', tripsCompleted: 215, rating: 4.7, certExpiry: '2026-06-01' },
{ id: 'S-104', name: 'Suresh Kumar', role: 'DRIVER', status: 'OFF_DUTY', phone: '+91 98765 43213', email: 's.kumar@teleems.com', joinedDate: '2022-11-05', tripsCompleted: 890, rating: 4.6, certExpiry: '2026-08-20' },
];
export const FleetPersonnel: React.FC = () => {
const [activeTab, setActiveTab] = useState<'ALL' | 'DRIVER' | 'EMT' | 'DOCTOR'>('ALL');
const [selectedStaff, setSelectedStaff] = useState<Staff | null>(null);
const filteredStaff = MOCK_STAFF.filter(s => activeTab === 'ALL' || s.role === activeTab);
return (
<div className="fleet-personnel animate-in fade-in duration-500">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div style={{ display: 'flex', gap: '8px' }}>
{['ALL', 'DRIVER', 'EMT', 'DOCTOR'].map(t => (
<button
key={t}
onClick={() => setActiveTab(t as any)}
style={{
padding: '8px 16px',
borderRadius: '8px',
border: '1px solid rgba(255,255,255,0.1)',
background: activeTab === t ? 'var(--accent-cyan)' : 'rgba(255,255,255,0.05)',
color: activeTab === t ? '#000' : 'var(--text-secondary)',
fontSize: '0.75rem',
fontWeight: 700,
cursor: 'pointer',
transition: 'all 0.2s'
}}
>
{t}S
</button>
))}
</div>
<button className="btn-primary" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<UserPlus size={18} /> REGISTER PERSONNEL
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: '24px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<Card style={{ padding: '0', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.1)', textAlign: 'left', background: 'rgba(255,255,255,0.02)' }}>
<th style={{ padding: '16px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Personnel</th>
<th style={{ padding: '16px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Role / Specialization</th>
<th style={{ padding: '16px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Status</th>
<th style={{ padding: '16px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Trips</th>
<th style={{ padding: '16px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Actions</th>
</tr>
</thead>
<tbody>
{filteredStaff.map(s => (
<tr
key={s.id}
onClick={() => setSelectedStaff(s)}
style={{
borderBottom: '1px solid rgba(255,255,255,0.05)',
cursor: 'pointer',
background: selectedStaff?.id === s.id ? 'rgba(59, 130, 246, 0.05)' : 'transparent'
}}
className="hover-glow"
>
<td style={{ padding: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ width: '36px', height: '36px', borderRadius: '50%', background: 'rgba(59, 130, 246, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--accent-cyan)', fontWeight: 700, fontSize: '0.875rem', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
{s.name.charAt(0)}
</div>
<div>
<div style={{ fontWeight: 700 }}>{s.name}</div>
<div style={{ fontSize: '0.65rem', opacity: 0.5 }}>ID: {s.id}</div>
</div>
</div>
</td>
<td style={{ padding: '16px' }}>
<div style={{ fontSize: '0.8125rem', fontWeight: 600 }}>{s.role}</div>
{s.specialization && <div style={{ fontSize: '0.65rem', opacity: 0.5 }}>{s.specialization}</div>}
</td>
<td style={{ padding: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<div style={{ width: '6px', height: '6px', borderRadius: '50%', background: s.status === 'ON_DUTY' ? '#22C55E' : s.status === 'ON_LEAVE' ? '#EF4444' : '#94A3B8' }}></div>
<span style={{ fontSize: '0.7rem', fontWeight: 700, opacity: 0.8 }}>{s.status.replace('_', ' ')}</span>
</div>
</td>
<td style={{ padding: '16px' }}>
<div style={{ fontWeight: 800 }}>{s.tripsCompleted}</div>
</td>
<td style={{ padding: '16px' }}>
<button className="btn-ghost-sm"><MoreVertical size={14} /></button>
</td>
</tr>
))}
</tbody>
</table>
</Card>
</div>
<div style={{ position: 'sticky', top: '0' }}>
{selectedStaff ? (
<AnimatePresence mode="wait">
<motion.div
key={selectedStaff.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
>
<Card>
<div style={{ textAlign: 'center', marginBottom: '24px' }}>
<div style={{ width: '80px', height: '80px', borderRadius: '50%', background: 'rgba(59, 130, 246, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--accent-cyan)', fontSize: '1.5rem', fontWeight: 900, border: '2px solid var(--accent-cyan)', margin: '0 auto 16px', boxShadow: '0 0 20px rgba(59, 130, 246, 0.2)' }}>
{selectedStaff.name.charAt(0)}
</div>
<h2 style={{ fontSize: '1.25rem', fontWeight: 800 }}>{selectedStaff.name}</h2>
<div style={{ fontSize: '0.75rem', color: 'var(--accent-cyan)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em' }}>{selectedStaff.role}</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginBottom: '24px' }}>
<div style={{ padding: '12px', borderRadius: '12px', background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ fontSize: '0.65rem', opacity: 0.5, textTransform: 'uppercase', marginBottom: '4px' }}>Trips Rate</div>
<div style={{ fontSize: '1.125rem', fontWeight: 800, color: 'var(--accent-green)' }}>{selectedStaff.rating}/5.0</div>
</div>
<div style={{ padding: '12px', borderRadius: '12px', background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ fontSize: '0.65rem', opacity: 0.5, textTransform: 'uppercase', marginBottom: '4px' }}>SLA Compliance</div>
<div style={{ fontSize: '1.125rem', fontWeight: 800 }}>98.4%</div>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div>
<h4 style={{ fontSize: '0.7rem', fontWeight: 800, textTransform: 'uppercase', opacity: 0.5, marginBottom: '8px' }}>Contact Information</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', fontSize: '0.8125rem' }}>
<Phone size={14} style={{ opacity: 0.5 }} /> {selectedStaff.phone}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', fontSize: '0.8125rem' }}>
<Mail size={14} style={{ opacity: 0.5 }} /> {selectedStaff.email}
</div>
</div>
</div>
<div>
<h4 style={{ fontSize: '0.7rem', fontWeight: 800, textTransform: 'uppercase', opacity: 0.5, marginBottom: '8px' }}>Certifications</h4>
<div style={{ padding: '12px', borderRadius: '12px', background: 'rgba(245, 158, 11, 0.05)', border: '1px solid rgba(245, 158, 11, 0.1)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontSize: '0.75rem', fontWeight: 700 }}>Professional License</div>
<div style={{ fontSize: '0.65rem', opacity: 0.6 }}>Expiry: {selectedStaff.certExpiry}</div>
</div>
<AlertTriangle size={16} color="#F59E0B" />
</div>
</div>
</div>
<button className="btn-primary" style={{ width: '100%', marginTop: '24px' }}>MANAGE SHIFT SCHEDULE</button>
</Card>
</motion.div>
</AnimatePresence>
) : (
<div className="glass" style={{ padding: '40px', borderRadius: '16px', textAlign: 'center', border: '1px dashed rgba(255,255,255,0.1)' }}>
<Users size={32} style={{ opacity: 0.2, margin: '0 auto 16px' }} />
<h3 style={{ fontSize: '1rem', fontWeight: 700, marginBottom: '8px' }}>Select Personnel</h3>
<p style={{ fontSize: '0.875rem', color: 'var(--text-secondary)' }}>View detailed performance metrics, licensing status, and shift history for your fleet crew.</p>
</div>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,165 @@
import React, { useState } from 'react';
import {
Calendar,
Clock,
Users,
Truck,
AlertTriangle,
CheckCircle2,
Plus,
ChevronLeft,
ChevronRight,
MoreVertical,
Navigation,
ShieldAlert
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../components/Common';
interface Assignment {
id: string;
vehicleId: string;
shift: 'MORNING' | 'EVENING' | 'NIGHT';
driver: string;
emt: string;
doctor?: string;
status: 'SCHEDULED' | 'ON_DUTY' | 'HANDOVER_PENDING';
startTime: string;
endTime: string;
}
const MOCK_ASSIGNMENTS: Assignment[] = [
{ id: 'AS-1001', vehicleId: 'V-001 (ALS)', shift: 'MORNING', driver: 'Vikram Singh', emt: 'Rahul Verma', doctor: 'Dr. Ananya Iyer', status: 'ON_DUTY', startTime: '06:00', endTime: '14:00' },
{ id: 'AS-1002', vehicleId: 'V-002 (BLS)', shift: 'MORNING', driver: 'Suresh Kumar', emt: 'Amit Roy', status: 'ON_DUTY', startTime: '06:00', endTime: '14:00' },
{ id: 'AS-1003', vehicleId: 'V-003 (ALS)', shift: 'EVENING', driver: 'Karan Mehra', emt: 'Priya Das', doctor: 'Dr. Sameer Gupta', status: 'SCHEDULED', startTime: '14:00', endTime: '22:00' },
{ id: 'AS-1004', vehicleId: 'V-004 (TRANS)', shift: 'MORNING', driver: 'Ravi Teja', emt: 'Sneha Rao', status: 'HANDOVER_PENDING', startTime: '06:00', endTime: '14:00' },
];
export const FleetScheduling: React.FC = () => {
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
return (
<div className="fleet-scheduling animate-in fade-in duration-500">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div className="glass" style={{ padding: '8px 16px', borderRadius: '12px', display: 'flex', alignItems: 'center', gap: '12px', border: '1px solid rgba(255,255,255,0.1)' }}>
<button className="btn-ghost-sm" style={{ padding: '4px' }}><ChevronLeft size={16} /></button>
<span style={{ fontWeight: 800, fontSize: '0.875rem' }}>{selectedDate}</span>
<button className="btn-ghost-sm" style={{ padding: '4px' }}><ChevronRight size={16} /></button>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
{['DAY', 'WEEK', 'MONTH'].map(v => (
<button key={v} style={{ fontSize: '0.65rem', fontWeight: 900, padding: '6px 12px', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.05)', background: v === 'DAY' ? 'var(--accent-cyan)' : 'transparent', color: v === 'DAY' ? '#000' : 'var(--text-secondary)' }}>{v}</button>
))}
</div>
</div>
<button className="btn-primary" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Plus size={18} /> CREATE NEW ASSIGNMENT
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '24px' }}>
{/* Mission Roster Grid */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<Card title="Shift Roster Matrix">
<div className="table-container">
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ textAlign: 'left', opacity: 0.5, fontSize: '0.65rem', textTransform: 'uppercase', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
<th style={{ padding: '12px' }}>Time Slot</th>
<th style={{ padding: '12px' }}>Vehicle</th>
<th style={{ padding: '12px' }}>Assigned Crew</th>
<th style={{ padding: '12px' }}>Status</th>
<th style={{ padding: '12px' }}>Actions</th>
</tr>
</thead>
<tbody>
{MOCK_ASSIGNMENTS.map(as => (
<tr key={as.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
<td style={{ padding: '16px 12px' }}>
<div style={{ fontWeight: 800, fontSize: '0.875rem', color: 'var(--accent-cyan)' }}>{as.startTime} - {as.endTime}</div>
<div style={{ fontSize: '0.65rem', opacity: 0.5 }}>{as.shift} SHIFT</div>
</td>
<td style={{ padding: '16px 12px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Truck size={14} style={{ opacity: 0.5 }} />
<span style={{ fontWeight: 700 }}>{as.vehicleId}</span>
</div>
</td>
<td style={{ padding: '16px 12px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div style={{ fontSize: '0.75rem', fontWeight: 600 }}>P: {as.driver}</div>
<div style={{ fontSize: '0.75rem', opacity: 0.8 }}>E: {as.emt}</div>
{as.doctor && <div style={{ fontSize: '0.75rem', color: 'var(--accent-green)' }}>D: {as.doctor}</div>}
</div>
</td>
<td style={{ padding: '16px 12px' }}>
<span style={{
fontSize: '0.6rem',
fontWeight: 900,
padding: '4px 8px',
borderRadius: '4px',
background: as.status === 'ON_DUTY' ? 'rgba(34, 197, 94, 0.1)' : as.status === 'HANDOVER_PENDING' ? 'rgba(245, 158, 11, 0.1)' : 'rgba(148, 163, 184, 0.1)',
color: as.status === 'ON_DUTY' ? '#22C55E' : as.status === 'HANDOVER_PENDING' ? '#F59E0B' : '#94A3B8',
border: `1px solid ${as.status === 'ON_DUTY' ? 'rgba(34, 197, 94, 0.2)' : as.status === 'HANDOVER_PENDING' ? 'rgba(245, 158, 11, 0.2)' : 'rgba(148, 163, 184, 0.2)'}`
}}>{as.status.replace('_', ' ')}</span>
</td>
<td style={{ padding: '16px 12px' }}>
<button className="btn-ghost-sm"><MoreVertical size={14} /></button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
{/* Conflict & Handover Panel */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<Card title="Conflict Engine" glowColor="amber">
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ padding: '12px', borderRadius: '12px', background: 'rgba(245, 158, 11, 0.05)', border: '1px solid rgba(245, 158, 11, 0.1)', display: 'flex', gap: '12px' }}>
<ShieldAlert size={20} color="#F59E0B" />
<div>
<div style={{ fontSize: '0.75rem', fontWeight: 800 }}>DOUBLE BOOKING DETECTED</div>
<p style={{ fontSize: '0.65rem', opacity: 0.7, marginTop: '4px' }}>Amit Roy (EMT) assigned to V-002 and V-005 in Evening Shift.</p>
</div>
</div>
<div style={{ padding: '12px', borderRadius: '12px', background: 'rgba(239, 68, 68, 0.05)', border: '1px solid rgba(239, 68, 68, 0.1)', display: 'flex', gap: '12px' }}>
<AlertTriangle size={20} color="#EF4444" />
<div>
<div style={{ fontSize: '0.75rem', fontWeight: 800 }}>CERTIFICATION EXPIRED</div>
<p style={{ fontSize: '0.65rem', opacity: 0.7, marginTop: '4px' }}>Dr. Sameer Gupta license expired on 2026-05-01.</p>
</div>
</div>
</div>
</Card>
<Card title="Shift Handover Tracker">
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ padding: '16px', borderRadius: '12px', background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<span style={{ fontSize: '0.75rem', fontWeight: 700 }}>V-004 Handover Checklist</span>
<span style={{ fontSize: '0.65rem', color: '#F59E0B', fontWeight: 800 }}>4/6 TASKS</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.7rem', opacity: 0.8 }}>
<CheckCircle2 size={12} color="#22C55E" /> Fuel Tank Checked (100%)
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.7rem', opacity: 0.8 }}>
<CheckCircle2 size={12} color="#22C55E" /> Oxygen Level Verified
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.7rem', opacity: 0.5 }}>
<Clock size={12} /> Narcotics Inventory Counter-sign
</div>
</div>
</div>
</div>
<button className="btn-primary" style={{ width: '100%', marginTop: '16px', fontSize: '0.75rem' }}>RESOLVE HANDOVERS</button>
</Card>
</div>
</div>
</div>
);
};

61
src/utils/auth.ts Normal file
View File

@@ -0,0 +1,61 @@
/**
* Centralized authentication and session management utilities
*/
export const logout = () => {
console.log('Logging out: Clearing session data and redirecting...');
localStorage.removeItem('teleems_auth');
localStorage.removeItem('teleems_token');
localStorage.removeItem('teleems_user');
// Use window.location for a hard redirect to ensure all states are cleared
window.location.href = '/';
};
/**
* Decodes a JWT and checks if it's expired
*/
export const isTokenExpired = (token: string): boolean => {
if (!token || token === 'dev-super-token-2026' || token.startsWith('dev-token-') || token.startsWith('mock-')) return false; // Bypass for dev and mock tokens
try {
const base64Url = token.split('.')[1];
if (!base64Url) return true;
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
const { exp } = JSON.parse(jsonPayload);
if (!exp) return false;
// exp is in seconds, Date.now() in milliseconds
const currentTime = Math.floor(Date.now() / 1000);
return exp < currentTime;
} catch (error) {
console.error('Error decoding token:', error);
return true; // Assume expired/invalid if error decoding
}
};
/**
* Checks if the user is currently authenticated
*/
export const isAuthenticated = (): boolean => {
const authFlag = localStorage.getItem('teleems_auth') === 'true';
const token = localStorage.getItem('teleems_token');
if (!authFlag || !token) return false;
// If it's a JWT, check expiration
if (token.includes('.') && isTokenExpired(token)) {
return false;
}
return true;
};