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