complete overall implementation.

This commit is contained in:
2026-05-13 12:54:38 +05:30
parent 64a41be96b
commit 6ab819e74f
19 changed files with 6580 additions and 690 deletions

View File

@@ -17,10 +17,12 @@
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-router-dom": "^7.14.1", "react-router-dom": "^7.14.1",
"recharts": "^3.8.1" "recharts": "^3.8.1",
"socket.io-client": "^4.8.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.4", "@eslint/js": "^9.39.4",
"@types/google.maps": "^3.64.0",
"@types/node": "^24.12.2", "@types/node": "^24.12.2",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",

View File

@@ -21,12 +21,17 @@ export const fleetApi = {
return apiClient.get(url, { token }); return apiClient.get(url, { token });
}, },
getStationVehicles: async (token: string, stationId: string) => {
return apiClient.get(`/v1/fleet/vehicles?station_id=${stationId}`, { token });
},
createVehicle: async (vehicleData: any, token: string) => { createVehicle: async (vehicleData: any, token: string) => {
return apiClient.post('/v1/fleet/vehicles', vehicleData, { token }); return apiClient.post('/v1/fleet/vehicles', vehicleData, { token });
}, },
getVehicles: async (token: string, orgId: string) => { getVehicles: async (token: string, orgId?: string) => {
return apiClient.get(`/v1/fleet/vehicles?org_id=${orgId}`, { token }); const url = orgId ? `/v1/fleet/vehicles?org_id=${orgId}` : `/v1/fleet/vehicles`;
return apiClient.get(url, { token });
}, },
updateVehicleDetails: async (vehicleId: string, vehicleData: any, token: string) => { updateVehicleDetails: async (vehicleId: string, vehicleData: any, token: string) => {
@@ -37,8 +42,13 @@ export const fleetApi = {
return apiClient.post('/v1/fleet/staff', staffData, { token }); return apiClient.post('/v1/fleet/staff', staffData, { token });
}, },
getStaff: async (token: string, orgId: string) => { getStaff: async (token: string, orgId?: string) => {
return apiClient.get(`/v1/fleet/staff?organisationId=${orgId}`, { token }); const url = orgId ? `/v1/fleet/staff?organisationId=${orgId}` : `/v1/fleet/staff`;
return apiClient.get(url, { token });
},
getRoster: async (token: string) => {
return apiClient.get('/v1/fleet/roster', { token });
}, },
createRoster: async (rosterData: any, token: string) => { createRoster: async (rosterData: any, token: string) => {
@@ -51,5 +61,43 @@ export const fleetApi = {
getInventoryMaster: async (token: string) => { getInventoryMaster: async (token: string) => {
return apiClient.get('/v1/fleet/inventory/master', { token }); return apiClient.get('/v1/fleet/inventory/master', { token });
},
createInventoryMaster: async (payload: any[], token: string) => {
return apiClient.post('/v1/fleet/inventory/master', payload, { token });
},
updateInventoryMaster: async (itemId: string, payload: any, token: string) => {
return apiClient.patch(`/v1/fleet/inventory/master/${itemId}`, payload, { token });
},
restockInventory: async (payload: any[], token: string) => {
return apiClient.post('/v1/fleet/inventory/warehouse', payload, { token });
},
getInventoryMetadata: async (token: string, category?: string) => {
const url = category ? `/v1/fleet/inventory/metadata?category=${category}` : '/v1/fleet/inventory/metadata';
return apiClient.get(url, { token });
},
getWarehouseStock: async (token: string) => {
return apiClient.get('/v1/fleet/inventory/warehouse', { token });
},
assignToVehicle: async (vehicleId: string, payload: any, token: string) => {
return apiClient.post(`/v1/fleet/vehicles/${vehicleId}/inventory/bulk`, payload, { token });
},
getPendingRestockRequests: async (token: string) => {
return apiClient.get('/v1/fleet/inventory/restock-requests?status=PENDING', { token });
},
getRestockRequests: async (token: string, status?: string) => {
const url = status ? `/v1/fleet/inventory/restock-requests?status=${status}` : '/v1/fleet/inventory/restock-requests';
return apiClient.get(url, { token });
},
updateRestockRequestStatus: async (requestId: string, status: string, token: string) => {
return apiClient.patch(`/v1/fleet/inventory/restock-requests/${requestId}/status`, { status }, { token });
} }
}; };

View File

@@ -6,7 +6,8 @@ import {
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
AlertCircle, AlertCircle,
MoreVertical MoreVertical,
Monitor
} from 'lucide-react'; } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { PerspectiveSwitcher } from './PerspectiveSwitcher'; import { PerspectiveSwitcher } from './PerspectiveSwitcher';
@@ -18,6 +19,7 @@ export const Sidebar: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const isFleetPage = location.pathname.startsWith('/fleet-operator');
// Safely parse user data // Safely parse user data
const user = useMemo(() => { const user = useMemo(() => {
@@ -77,6 +79,31 @@ export const Sidebar: React.FC = () => {
const initials = (displayName.split(' ').map((n: string) => n[0]).join('').toUpperCase().substring(0, 2)) || 'FO'; const initials = (displayName.split(' ').map((n: string) => n[0]).join('').toUpperCase().substring(0, 2)) || 'FO';
const filteredNavItems = useMemo(() => { const filteredNavItems = useMemo(() => {
if (isFleetPage) {
const fleetItems = NAVIGATION_CONFIG.filter(item =>
item.id.startsWith('fleet-') || item.path.includes('/fleet-operator')
);
const isAdmin = user.roles?.some((r: string) => {
const norm = r.toLowerCase().replace(/\s+/g, '_');
return norm === 'cureselect_admin' || norm === 'admin';
});
if (isAdmin) {
return [
{
id: 'launcher',
label: 'Portal Hub',
icon: Monitor,
path: '/launcher',
roles: ['CURESELECT_ADMIN']
},
...fleetItems
];
}
return fleetItems;
}
// The active perspective role being viewed (e.g. 'hospital_admin', 'fleet_operator', 'cureselect_admin') // The active perspective role being viewed (e.g. 'hospital_admin', 'fleet_operator', 'cureselect_admin')
const activeRole = currentRole.toLowerCase().replace(/\s+/g, '_'); const activeRole = currentRole.toLowerCase().replace(/\s+/g, '_');
@@ -99,7 +126,7 @@ export const Sidebar: React.FC = () => {
}; };
return filterItems(NAVIGATION_CONFIG); return filterItems(NAVIGATION_CONFIG);
}, [currentRole]); }, [currentRole, isFleetPage, user.roles]);
const renderNavItem = (item: NavItem, isSubItem = false) => { const renderNavItem = (item: NavItem, isSubItem = false) => {
const Icon = item.icon || AlertCircle; const Icon = item.icon || AlertCircle;
@@ -127,7 +154,7 @@ export const Sidebar: React.FC = () => {
to={itemTo} to={itemTo}
title={isSidebarCollapsed ? item.label : undefined} title={isSidebarCollapsed ? item.label : undefined}
style={({ isActive: linkActive }) => { style={({ isActive: linkActive }) => {
const active = linkActive || isActive; const active = itemSearch ? isActive : linkActive;
return { return {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@@ -137,8 +164,8 @@ export const Sidebar: React.FC = () => {
margin: isSidebarCollapsed ? '4px 12px' : '2px 12px', margin: isSidebarCollapsed ? '4px 12px' : '2px 12px',
borderRadius: '12px', borderRadius: '12px',
textDecoration: 'none', textDecoration: 'none',
color: active ? '#fff' : '#94A3B8', color: active ? (isFleetPage ? '#06B6D4' : '#fff') : (isFleetPage ? '#475569' : '#94A3B8'),
background: active ? 'linear-gradient(90deg, rgba(6, 182, 212, 0.15), rgba(59, 130, 246, 0.05))' : 'transparent', background: active ? (isFleetPage ? 'linear-gradient(90deg, rgba(6, 182, 212, 0.08), rgba(59, 130, 246, 0.02))' : 'linear-gradient(90deg, rgba(6, 182, 212, 0.15), rgba(59, 130, 246, 0.05))') : 'transparent',
borderLeft: active && !isSubItem && !isSidebarCollapsed ? '3px solid #06B6D4' : '3px solid transparent', borderLeft: active && !isSubItem && !isSidebarCollapsed ? '3px solid #06B6D4' : '3px solid transparent',
boxShadow: active && isSidebarCollapsed ? '0 0 0 1px rgba(6,182,212,0.4)' : 'none', boxShadow: active && isSidebarCollapsed ? '0 0 0 1px rgba(6,182,212,0.4)' : 'none',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
@@ -146,10 +173,10 @@ export const Sidebar: React.FC = () => {
overflow: 'hidden' overflow: 'hidden'
}; };
}} }}
className={(navData) => `nav-item-link ${navData.isActive || isActive ? 'active' : ''}`} className={(navData) => `nav-item-link ${(itemSearch ? isActive : navData.isActive) ? 'active' : ''}`}
> >
{({ isActive: linkActive }) => { {({ isActive: linkActive }) => {
const active = linkActive || isActive; const active = itemSearch ? isActive : linkActive;
return ( return (
<> <>
<Icon size={20} color={active ? '#06B6D4' : 'currentColor'} style={{ flexShrink: 0 }} /> <Icon size={20} color={active ? '#06B6D4' : 'currentColor'} style={{ flexShrink: 0 }} />
@@ -197,7 +224,7 @@ export const Sidebar: React.FC = () => {
style={{ overflow: 'hidden' }} style={{ overflow: 'hidden' }}
> >
<div style={{ <div style={{
borderLeft: '1px solid rgba(255,255,255,0.1)', borderLeft: isFleetPage ? '1px solid rgba(15, 23, 42, 0.08)' : '1px solid rgba(255,255,255,0.1)',
marginLeft: '28px', marginLeft: '28px',
paddingTop: '4px', paddingTop: '4px',
paddingBottom: '8px' paddingBottom: '8px'
@@ -215,8 +242,8 @@ export const Sidebar: React.FC = () => {
<> <>
<style>{` <style>{`
.nav-item-link:not(.active):hover { .nav-item-link:not(.active):hover {
background: rgba(255,255,255,0.03) !important; background: ${isFleetPage ? 'rgba(15, 23, 42, 0.03)' : 'rgba(255,255,255,0.03)'} !important;
color: #fff !important; color: ${isFleetPage ? '#0F172A' : '#fff'} !important;
transform: translateX(6px); transform: translateX(6px);
} }
`}</style> `}</style>
@@ -225,8 +252,8 @@ export const Sidebar: React.FC = () => {
animate={{ width: isSidebarCollapsed ? 80 : 280 }} animate={{ width: isSidebarCollapsed ? 80 : 280 }}
transition={{ duration: 0.3, ease: 'easeInOut' }} transition={{ duration: 0.3, ease: 'easeInOut' }}
style={{ style={{
background: '#040B16', // Deep dark aesthetic background: isFleetPage ? '#FFFFFF' : '#040B16',
borderRight: '1px solid rgba(255,255,255,0.05)', borderRight: isFleetPage ? '1px solid rgba(15, 23, 42, 0.08)' : '1px solid rgba(255,255,255,0.05)',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: '100vh', height: '100vh',
@@ -241,7 +268,7 @@ export const Sidebar: React.FC = () => {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: isSidebarCollapsed ? 'center' : 'space-between', justifyContent: isSidebarCollapsed ? 'center' : 'space-between',
borderBottom: '1px solid rgba(255,255,255,0.05)', borderBottom: isFleetPage ? '1px solid rgba(15, 23, 42, 0.08)' : '1px solid rgba(255,255,255,0.05)',
flexShrink: 0, flexShrink: 0,
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
@@ -266,7 +293,7 @@ export const Sidebar: React.FC = () => {
<AnimatePresence> <AnimatePresence>
{!isSidebarCollapsed && ( {!isSidebarCollapsed && (
<motion.div initial={{ opacity: 0, width: 0 }} animate={{ opacity: 1, width: 'auto' }} exit={{ opacity: 0, width: 0 }} style={{ overflow: 'hidden', whiteSpace: 'nowrap' }}> <motion.div initial={{ opacity: 0, width: 0 }} animate={{ opacity: 1, width: 'auto' }} exit={{ opacity: 0, width: 0 }} style={{ overflow: 'hidden', whiteSpace: 'nowrap' }}>
<h2 style={{ fontSize: '1.25rem', fontWeight: 900, color: '#fff', margin: 0, letterSpacing: '-0.5px', lineHeight: 1.2 }}>CureSelect</h2> <h2 style={{ fontSize: '1.25rem', fontWeight: 900, color: isFleetPage ? '#0F172A' : '#fff', margin: 0, letterSpacing: '-0.5px', lineHeight: 1.2 }}>CureSelect</h2>
<span style={{ fontSize: '0.65rem', fontWeight: 700, color: '#06B6D4', textTransform: 'uppercase', letterSpacing: '2px' }}>Platform</span> <span style={{ fontSize: '0.65rem', fontWeight: 700, color: '#06B6D4', textTransform: 'uppercase', letterSpacing: '2px' }}>Platform</span>
</motion.div> </motion.div>
)} )}
@@ -279,19 +306,19 @@ export const Sidebar: React.FC = () => {
onClick={() => setIsSidebarCollapsed(true)} onClick={() => setIsSidebarCollapsed(true)}
title="Collapse Menu" title="Collapse Menu"
style={{ style={{
background: 'rgba(255,255,255,0.02)', background: isFleetPage ? 'rgba(15, 23, 42, 0.02)' : 'rgba(255,255,255,0.02)',
border: '1px solid rgba(255,255,255,0.06)', border: isFleetPage ? '1px solid rgba(15, 23, 42, 0.06)' : '1px solid rgba(255,255,255,0.06)',
borderRadius: '8px', borderRadius: '8px',
padding: '6px', padding: '6px',
color: '#94A3B8', color: isFleetPage ? '#475569' : '#94A3B8',
cursor: 'pointer', cursor: 'pointer',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
transition: 'all 0.2s' transition: 'all 0.2s'
}} }}
onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'} onMouseEnter={e => e.currentTarget.style.background = isFleetPage ? 'rgba(15, 23, 42, 0.06)' : 'rgba(255,255,255,0.08)'}
onMouseLeave={e => e.currentTarget.style.background = 'rgba(255,255,255,0.02)'} onMouseLeave={e => e.currentTarget.style.background = isFleetPage ? 'rgba(15, 23, 42, 0.02)' : 'rgba(255,255,255,0.02)'}
> >
<ChevronRight size={14} style={{ transform: 'rotate(180deg)' }} /> <ChevronRight size={14} style={{ transform: 'rotate(180deg)' }} />
</button> </button>
@@ -302,8 +329,8 @@ export const Sidebar: React.FC = () => {
<nav style={{ flex: 1, padding: '16px 0', overflowY: 'auto', minHeight: 0 }} className="no-scrollbar"> <nav style={{ flex: 1, padding: '16px 0', overflowY: 'auto', minHeight: 0 }} className="no-scrollbar">
<AnimatePresence> <AnimatePresence>
{!isSidebarCollapsed && ( {!isSidebarCollapsed && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} style={{ padding: '0 24px', marginBottom: '8px', fontSize: '0.65rem', fontWeight: 700, color: '#94A3B8', textTransform: 'uppercase', letterSpacing: '1px' }}> <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} style={{ padding: '0 24px', marginBottom: '8px', fontSize: '0.65rem', fontWeight: 700, color: isFleetPage ? '#475569' : '#94A3B8', textTransform: 'uppercase', letterSpacing: '1px' }}>
Main Menu {isFleetPage ? 'Fleet Command' : 'Main Menu'}
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
@@ -311,7 +338,7 @@ export const Sidebar: React.FC = () => {
</nav> </nav>
{/* User Footer Profile */} {/* User Footer Profile */}
<div style={{ padding: '16px', borderTop: '1px solid rgba(255,255,255,0.05)', display: 'flex', flexDirection: 'column', gap: '12px', flexShrink: 0 }}> <div style={{ padding: '16px', borderTop: isFleetPage ? '1px solid rgba(15, 23, 42, 0.08)' : '1px solid rgba(255,255,255,0.05)', display: 'flex', flexDirection: 'column', gap: '12px', flexShrink: 0 }}>
<AnimatePresence> <AnimatePresence>
{!isSidebarCollapsed && ( {!isSidebarCollapsed && (
@@ -329,26 +356,26 @@ export const Sidebar: React.FC = () => {
{/* Premium User Card */} {/* Premium User Card */}
<div style={{ <div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px',
padding: '12px', background: 'rgba(6, 182, 212, 0.05)', border: '1px solid rgba(6, 182, 212, 0.1)', padding: '12px', background: isFleetPage ? 'rgba(6, 182, 212, 0.04)' : 'rgba(6, 182, 212, 0.05)', border: isFleetPage ? '1px solid rgba(6, 182, 212, 0.08)' : '1px solid rgba(6, 182, 212, 0.1)',
borderRadius: '12px', transition: 'all 0.3s ease', cursor: 'pointer' borderRadius: '12px', transition: 'all 0.3s ease', cursor: 'pointer'
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', minWidth: 0, overflow: 'hidden' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '12px', minWidth: 0, overflow: 'hidden' }}>
<div style={{ <div style={{
width: '32px', height: '32px', borderRadius: '10px', background: '#040B16', width: '32px', height: '32px', borderRadius: '10px', background: isFleetPage ? '#FFFFFF' : '#040B16',
border: '1px solid #06B6D4', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid #06B6D4', display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: '0.75rem', fontWeight: 800, color: '#06B6D4', flexShrink: 0 fontSize: '0.75rem', fontWeight: 800, color: '#06B6D4', flexShrink: 0
}}>{initials}</div> }}>{initials}</div>
<div style={{ minWidth: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}> <div style={{ minWidth: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<div style={{ fontSize: '0.8rem', fontWeight: 700, color: '#fff', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{displayName}</div> <div style={{ fontSize: '0.8rem', fontWeight: 700, color: isFleetPage ? '#0F172A' : '#fff', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{displayName}</div>
<div style={{ fontSize: '0.6rem', fontWeight: 600, color: '#06B6D4', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{currentRole.replace(/_/g, ' ')}</div> <div style={{ fontSize: '0.6rem', fontWeight: 600, color: '#06B6D4', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{currentRole.replace(/_/g, ' ')}</div>
</div> </div>
</div> </div>
<button <button
onClick={(e) => { e.stopPropagation(); handleLogout(); }} onClick={(e) => { e.stopPropagation(); handleLogout(); }}
style={{ background: 'transparent', border: 'none', color: '#94A3B8', cursor: 'pointer', padding: '4px', borderRadius: '6px', transition: 'all 0.2s' }} style={{ background: 'transparent', border: 'none', color: isFleetPage ? '#64748B' : '#94A3B8', cursor: 'pointer', padding: '4px', borderRadius: '6px', transition: 'all 0.2s' }}
onMouseEnter={(e) => { e.currentTarget.style.color = '#EF4444'; e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)'; }} onMouseEnter={(e) => { e.currentTarget.style.color = '#EF4444'; e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = '#94A3B8'; e.currentTarget.style.background = 'transparent'; }} onMouseLeave={(e) => { e.currentTarget.style.color = isFleetPage ? '#64748B' : '#94A3B8'; e.currentTarget.style.background = 'transparent'; }}
title="Sign Out" title="Sign Out"
> >
<LogOut size={16} /> <LogOut size={16} />

View File

@@ -63,8 +63,8 @@ export const NAVIGATION_CONFIG: NavItem[] = [
{ id: 'fleet-mission', label: 'Crew Scheduling', icon: Navigation, path: '/fleet-operator?tab=scheduling', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] }, { id: 'fleet-mission', label: 'Crew Scheduling', icon: Navigation, path: '/fleet-operator?tab=scheduling', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] },
{ id: 'fleet-inventory', label: 'Inventory Management', icon: ShoppingCart, path: '/fleet-operator?tab=inventory', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] }, { id: 'fleet-inventory', label: 'Inventory Management', icon: ShoppingCart, path: '/fleet-operator?tab=inventory', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] },
{ id: 'fleet-trips', label: 'Trip Management', icon: Activity, path: '/fleet-operator?tab=trips', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] }, { id: 'fleet-trips', label: 'Trip Management', icon: Activity, path: '/fleet-operator?tab=trips', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] },
{ id: 'fleet-telematics', label: 'GPS Telematics', icon: Navigation, path: '/fleet-operator?tab=telematics', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] }, // { id: 'fleet-telematics', label: 'GPS Telematics', icon: Navigation, path: '/fleet-operator?tab=telematics', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] },
{ id: 'fleet-referrals', label: 'Referral Network', icon: HeartPulse, path: '/fleet-operator?tab=referrals', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] }, // { id: 'fleet-referrals', label: 'Referral Network', icon: HeartPulse, path: '/fleet-operator?tab=referrals', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] },
{ id: 'fleet-intel', label: 'Fleet Analytics', icon: PieChart, path: '/fleet-operator?tab=analytics', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] }, { id: 'fleet-intel', label: 'Fleet Analytics', icon: PieChart, path: '/fleet-operator?tab=analytics', roles: ['CURESELECT_ADMIN', 'FLEET_OPERATOR'] },
{ {
id: 'clinical', id: 'clinical',

View File

@@ -314,8 +314,9 @@ const LiveLeafletMap: React.FC<{ vehicles: FleetVehicle[], selectedId: string |
const LocationPickerMap: React.FC<{ const LocationPickerMap: React.FC<{
lat: number; lat: number;
lng: number; lng: number;
onLocationSelect: (lat: number, lng: number) => void onLocationSelect: (lat: number, lng: number) => void;
}> = ({ lat, lng, onLocationSelect }) => { notify?: (title: string, message: string, type?: 'success' | 'error' | 'info') => void;
}> = ({ lat, lng, onLocationSelect, notify }) => {
const [isMapReady, setIsMapReady] = useState(false); const [isMapReady, setIsMapReady] = useState(false);
const mapRef = useRef<any>(null); const mapRef = useRef<any>(null);
const markerRef = useRef<any>(null); const markerRef = useRef<any>(null);
@@ -469,7 +470,11 @@ const LocationPickerMap: React.FC<{
setIsLocating(false); setIsLocating(false);
if (accuracy > 100) { if (accuracy > 100) {
if (notify) {
notify('GPS Warning', 'Location accuracy is low. Please manually adjust the pin.', 'info'); notify('GPS Warning', 'Location accuracy is low. Please manually adjust the pin.', 'info');
} else {
console.warn('GPS Warning: Location accuracy is low. Please manually adjust the pin.');
}
} }
}, },
(error) => { (error) => {
@@ -478,7 +483,11 @@ const LocationPickerMap: React.FC<{
if (error.code === 1) msg = 'Location permission denied.'; if (error.code === 1) msg = 'Location permission denied.';
else if (error.code === 3) msg = 'Location request timed out.'; else if (error.code === 3) msg = 'Location request timed out.';
if (notify) {
notify('GPS Error', msg, 'error'); notify('GPS Error', msg, 'error');
} else {
alert(msg);
}
setIsLocating(false); setIsLocating(false);
}, },
{ {
@@ -1843,11 +1852,91 @@ const StationManifest: React.FC<{
station: any; station: any;
vehicles: any[]; vehicles: any[];
staff: any[]; staff: any[];
}> = ({ station, vehicles, staff }) => { }> = ({ station }) => {
const stationVehicles = vehicles.filter(v => v.station_id === station.id || v.station_name === station.name); const [stationVehicles, setStationVehicles] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// For demo/mock purposes, we'll assign some staff to the station useEffect(() => {
const stationStaff = staff.slice(0, 3); const fetchStationVehicles = async () => {
setLoading(true);
setError(null);
try {
const token = localStorage.getItem('teleems_token') || '';
if (!token) throw new Error('No authentication token found.');
console.log(`StationManifest: Fetching vehicles for station: ${station?.id}`);
const response = await fleetApi.getStationVehicles(token, station?.id);
let list = response.data?.data || (Array.isArray(response.data) ? response.data : []);
setStationVehicles(list);
} catch (err: any) {
console.error('Failed to fetch station vehicles:', err);
setError(err.message || 'Failed to fetch station vehicles');
} finally {
setLoading(false);
}
};
if (station?.id) {
fetchStationVehicles();
}
}, [station]);
// Extract unique active crew from fetched station vehicles
const stationStaff = useMemo(() => {
const crewMap = new Map<string, { id: string; name: string; type: string; phone?: string }>();
stationVehicles.forEach(v => {
// Check activeRoster
if (v.activeRoster) {
const roster = v.activeRoster;
if (roster.driver?.user) {
const d = roster.driver;
crewMap.set(d.id, {
id: d.id,
name: d.user.name || 'Unknown Pilot',
type: 'Pilot',
phone: d.user.phone
});
}
if (roster.staff?.user) {
const s = roster.staff;
crewMap.set(s.id, {
id: s.id,
name: s.user.name || 'Unknown EMT',
type: 'EMT',
phone: s.user.phone
});
}
}
// Check activeShift
if (v.activeShift) {
const shift = v.activeShift;
if (shift.driver?.user) {
const d = shift.driver;
crewMap.set(d.id, {
id: d.id,
name: d.user.name || 'Unknown Pilot',
type: 'Pilot',
phone: d.user.phone
});
}
if (shift.staff?.user) {
const s = shift.staff;
crewMap.set(s.id, {
id: s.id,
name: s.user.name || 'Unknown EMT',
type: 'EMT',
phone: s.user.phone
});
}
}
});
return Array.from(crewMap.values());
}, [stationVehicles]);
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
@@ -1863,35 +1952,68 @@ const StationManifest: React.FC<{
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
{/* Deployed Fleet */}
<div className="glass" style={{ padding: '20px', borderRadius: '15px' }}> <div className="glass" style={{ padding: '20px', borderRadius: '15px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
<Truck size={20} color="var(--accent-cyan)" /> <Truck size={20} color="var(--accent-cyan)" />
<h5 style={{ margin: 0, fontSize: '0.9rem', textTransform: 'uppercase', letterSpacing: '0.1em' }}>Deployed Fleet ({stationVehicles.length})</h5> <h5 style={{ margin: 0, fontSize: '0.9rem', textTransform: 'uppercase', letterSpacing: '0.1em' }}>Deployed Fleet ({loading ? '...' : stationVehicles.length})</h5>
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{stationVehicles.length === 0 ? ( {loading ? (
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', fontStyle: 'italic' }}>No vehicles currently docked.</div> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '40px', gap: '12px' }}>
<Loader2 className="spin-slow" color="var(--accent-cyan)" size={24} />
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>DECRYPTING STATION FLEET...</div>
</div>
) : error ? (
<div style={{ fontSize: '0.8rem', color: 'var(--alert-red)', padding: '10px', textAlign: 'center' }}>
{error}
</div>
) : stationVehicles.length === 0 ? (
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', fontStyle: 'italic', textAlign: 'center', padding: '20px' }}>No vehicles currently docked.</div>
) : ( ) : (
stationVehicles.map(v => ( <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div key={v.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px', background: 'rgba(255,255,255,0.02)', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.05)' }}> {stationVehicles.map(v => {
const driverName = v.activeRoster?.driver?.user?.name || v.activeShift?.driver?.user?.name;
const emtName = v.activeRoster?.staff?.user?.name || v.activeShift?.staff?.user?.name;
return (
<div key={v.id} style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '12px', background: 'rgba(255,255,255,0.02)', borderRadius: '10px', border: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div> <div>
<div style={{ fontSize: '0.85rem', fontWeight: 700 }}>{v.registration_number}</div> <div style={{ fontSize: '0.85rem', fontWeight: 700 }}>{v.registration_number}</div>
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)' }}>{v.brand} {v.model}</div> <div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)' }}>{v.brand} {v.model} • {v.vehicle_type}</div>
</div> </div>
<div className="badge" style={{ fontSize: '0.6rem', background: v.status === 'AVAILABLE' ? 'rgba(0, 226, 114, 0.1)' : 'rgba(59, 130, 246, 0.1)', color: v.status === 'AVAILABLE' ? 'var(--accent-green)' : 'var(--accent-cyan)' }}> <div className="badge" style={{ fontSize: '0.6rem', background: v.status === 'AVAILABLE' ? 'rgba(0, 226, 114, 0.1)' : 'rgba(255, 171, 0, 0.1)', color: v.status === 'AVAILABLE' ? 'var(--accent-green)' : 'var(--accent-amber)' }}>
{v.status} {v.status}
</div> </div>
</div> </div>
)) {(driverName || emtName) && (
<div style={{ borderTop: '1px solid rgba(255,255,255,0.04)', paddingTop: '6px', display: 'flex', flexWrap: 'wrap', gap: '8px', fontSize: '0.7rem' }}>
{driverName && <span style={{ color: 'var(--text-secondary)' }}>Pilot: <strong style={{ color: '#fff' }}>{driverName}</strong></span>}
{emtName && <span style={{ color: 'var(--text-secondary)' }}>EMT: <strong style={{ color: '#fff' }}>{emtName}</strong></span>}
</div>
)} )}
</div> </div>
);
})}
</div>
)}
</div> </div>
{/* Active Crew */}
<div className="glass" style={{ padding: '20px', borderRadius: '15px' }}> <div className="glass" style={{ padding: '20px', borderRadius: '15px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
<Users size={20} color="var(--accent-cyan)" /> <Users size={20} color="var(--accent-cyan)" />
<h5 style={{ margin: 0, fontSize: '0.9rem', textTransform: 'uppercase', letterSpacing: '0.1em' }}>Active Crew ({stationStaff.length})</h5> <h5 style={{ margin: 0, fontSize: '0.9rem', textTransform: 'uppercase', letterSpacing: '0.1em' }}>Active Crew ({loading ? '...' : stationStaff.length})</h5>
</div> </div>
{loading ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '40px', gap: '12px' }}>
<Loader2 className="spin-slow" color="var(--accent-cyan)" size={24} />
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>COMPILING CREW LIST...</div>
</div>
) : stationStaff.length === 0 ? (
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', fontStyle: 'italic', textAlign: 'center', padding: '20px' }}>No crew active at this station.</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{stationStaff.map(s => ( {stationStaff.map(s => (
<div key={s.id} style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '10px', background: 'rgba(255,255,255,0.02)', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.05)' }}> <div key={s.id} style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '10px', background: 'rgba(255,255,255,0.02)', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.05)' }}>
@@ -1899,12 +2021,13 @@ const StationManifest: React.FC<{
<User size={16} color="var(--accent-cyan)" /> <User size={16} color="var(--accent-cyan)" />
</div> </div>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div style={{ fontSize: '0.85rem', fontWeight: 700 }}>{s.user?.name || s.name}</div> <div style={{ fontSize: '0.85rem', fontWeight: 700 }}>{s.name}</div>
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)' }}>{s.type} On Call</div> <div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)' }}>{s.type} • On Duty {s.phone ? `(${s.phone})` : ''}</div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
)}
</div> </div>
</div> </div>
@@ -2068,7 +2191,8 @@ const StationRegistrationForm: React.FC<{
onSubmit: (data: any) => void; onSubmit: (data: any) => void;
loading?: boolean; loading?: boolean;
organisationId: string; organisationId: string;
}> = ({ onSubmit, loading, organisationId }) => { notify?: (title: string, message: string, type?: 'success' | 'error' | 'info') => void;
}> = ({ onSubmit, loading, organisationId, notify }) => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
organisationId: organisationId, organisationId: organisationId,
@@ -2206,6 +2330,7 @@ const StationRegistrationForm: React.FC<{
lat={parseFloat(formData.gps_lat) || 13.0827} lat={parseFloat(formData.gps_lat) || 13.0827}
lng={parseFloat(formData.gps_lon) || 80.2707} lng={parseFloat(formData.gps_lon) || 80.2707}
onLocationSelect={handleLocationSelect} onLocationSelect={handleLocationSelect}
notify={notify}
/> />
</div> </div>
</div> </div>
@@ -3242,7 +3367,7 @@ export const FleetDispatch: React.FC = () => {
loading={isSubmitting} loading={isSubmitting}
> >
{modalType === 'BRAND' && <BrandRegistrationForm onSubmit={handleBrandSubmit} loading={isSubmitting} />} {modalType === 'BRAND' && <BrandRegistrationForm onSubmit={handleBrandSubmit} loading={isSubmitting} />}
{modalType === 'STATION' && <StationRegistrationForm organisationId={selectedOwnerId || ''} onSubmit={handleStationSubmit} loading={isSubmitting} />} {modalType === 'STATION' && <StationRegistrationForm organisationId={selectedOwnerId || ''} onSubmit={handleStationSubmit} loading={isSubmitting} notify={notify} />}
{modalType === 'VEHICLE' && <VehicleRegistrationForm organisationId={selectedOwnerId || ''} onSubmit={handleVehicleSubmit} loading={isSubmitting} initialData={editingVehicle} />} {modalType === 'VEHICLE' && <VehicleRegistrationForm organisationId={selectedOwnerId || ''} onSubmit={handleVehicleSubmit} loading={isSubmitting} initialData={editingVehicle} />}
{modalType === 'ROSTER' && <RosterForm vehicles={realVehicles} staff={realStaff} onSubmit={handleRosterSubmit} loading={isSubmitting} />} {modalType === 'ROSTER' && <RosterForm vehicles={realVehicles} staff={realStaff} onSubmit={handleRosterSubmit} loading={isSubmitting} />}
{modalType === 'SHIFT' && <ShiftStartForm vehicles={realVehicles} staff={realStaff} onSubmit={handleShiftStart} loading={isSubmitting} />} {modalType === 'SHIFT' && <ShiftStartForm vehicles={realVehicles} staff={realStaff} onSubmit={handleShiftStart} loading={isSubmitting} />}

View File

@@ -140,7 +140,7 @@ export const FleetLogin = () => {
}; };
return ( return (
<div className="fleet-login-container" style={{ display: 'flex', minHeight: '100vh', width: '100vw', backgroundColor: '#020617', fontFamily: "'Outfit', sans-serif", overflow: 'hidden' }}> <div className="fleet-login-container" style={{ display: 'flex', minHeight: '100vh', width: '100vw', backgroundColor: '#F8FAFC', fontFamily: "'Outfit', sans-serif", overflow: 'hidden' }}>
{/* LEFT SIDE - IMAGE & TACTICAL HUD */} {/* LEFT SIDE - IMAGE & TACTICAL HUD */}
<div className="fleet-login-left" style={{ <div className="fleet-login-left" style={{
@@ -150,7 +150,7 @@ export const FleetLogin = () => {
flexDirection: 'column', flexDirection: 'column',
justifyContent: 'space-between', justifyContent: 'space-between',
padding: '60px', padding: '60px',
borderRight: '1px solid rgba(34, 211, 238, 0.2)' borderRight: '1px solid rgba(6, 182, 212, 0.15)'
}}> }}>
{/* Background Image */} {/* Background Image */}
<div style={{ <div style={{
@@ -159,22 +159,22 @@ export const FleetLogin = () => {
backgroundImage: 'url("https://images.unsplash.com/photo-1587582423116-ec07293f0395?q=80&w=2070&auto=format&fit=crop")', backgroundImage: 'url("https://images.unsplash.com/photo-1587582423116-ec07293f0395?q=80&w=2070&auto=format&fit=crop")',
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center', backgroundPosition: 'center',
filter: 'grayscale(30%) contrast(120%)' filter: 'grayscale(20%) contrast(110%)'
}} /> }} />
{/* Gradients to blend image with the tactical theme */} {/* Gradients to blend image with the tactical theme */}
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
inset: 0, inset: 0,
background: 'linear-gradient(to right, rgba(2, 6, 23, 0.95) 0%, rgba(2, 6, 23, 0.6) 40%, rgba(2, 6, 23, 0.9) 100%)' background: 'linear-gradient(to right, rgba(248, 250, 252, 0.95) 0%, rgba(248, 250, 252, 0.6) 40%, rgba(248, 250, 252, 0.95) 100%)'
}} /> }} />
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
inset: 0, inset: 0,
background: 'radial-gradient(circle at 30% 50%, transparent 0%, rgba(2, 6, 23, 0.8) 100%)' background: 'radial-gradient(circle at 30% 50%, transparent 0%, rgba(248, 250, 252, 0.9) 100%)'
}} /> }} />
{/* Decorative Grid & Radar (HUD elements) */} {/* Decorative Grid & Radar (HUD elements) */}
<div style={{ position: 'absolute', inset: 0, opacity: 0.15, backgroundImage: 'linear-gradient(rgba(34, 211, 238, 0.2) 1px, transparent 1px), linear-gradient(90deg, rgba(34, 211, 238, 0.2) 1px, transparent 1px)', backgroundSize: '40px 40px', pointerEvents: 'none' }} /> <div style={{ position: 'absolute', inset: 0, opacity: 0.12, backgroundImage: 'linear-gradient(rgba(6, 182, 212, 0.2) 1px, transparent 1px), linear-gradient(90deg, rgba(6, 182, 212, 0.2) 1px, transparent 1px)', backgroundSize: '40px 40px', pointerEvents: 'none' }} />
{/* Content Top */} {/* Content Top */}
<div style={{ position: 'relative', zIndex: 10 }}> <div style={{ position: 'relative', zIndex: 10 }}>
@@ -184,12 +184,12 @@ export const FleetLogin = () => {
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
style={{ display: 'flex', alignItems: 'center', gap: '16px' }} style={{ display: 'flex', alignItems: 'center', gap: '16px' }}
> >
<div style={{ padding: '12px', background: 'rgba(34, 211, 238, 0.1)', border: '1px solid #22d3ee', borderRadius: '12px', boxShadow: '0 0 20px rgba(34, 211, 238, 0.2)' }}> <div style={{ padding: '12px', background: 'rgba(6, 182, 212, 0.08)', border: '1px solid #06b6d4', borderRadius: '12px', boxShadow: '0 4px 20px rgba(6, 182, 212, 0.15)' }}>
<Truck size={32} color="#22d3ee" /> <Truck size={32} color="#06b6d4" />
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={{ fontSize: '28px', fontWeight: 900, color: '#fff', letterSpacing: '2px' }}>TELE_EMS</span> <span style={{ fontSize: '28px', fontWeight: 900, color: '#0F172A', letterSpacing: '2px' }}>TELE_EMS</span>
<span style={{ fontSize: '12px', fontWeight: 700, color: '#22d3ee', letterSpacing: '4px' }}>FLEET_OPERATOR</span> <span style={{ fontSize: '12px', fontWeight: 700, color: '#06b6d4', letterSpacing: '4px' }}>FLEET_OPERATOR</span>
</div> </div>
</motion.div> </motion.div>
</div> </div>
@@ -200,16 +200,16 @@ export const FleetLogin = () => {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }} transition={{ duration: 0.8, delay: 0.2 }}
style={{ fontSize: '56px', fontWeight: 900, lineHeight: 1.1, color: '#fff', marginBottom: '24px', letterSpacing: '-1px' }} style={{ fontSize: '56px', fontWeight: 900, lineHeight: 1.1, color: '#0F172A', marginBottom: '24px', letterSpacing: '-1px' }}
> >
Command The Fleet.<br/> Command The Fleet.<br/>
<span style={{ color: '#22d3ee', textShadow: '0 0 30px rgba(34, 211, 238, 0.4)' }}>Save Lives Faster.</span> <span style={{ color: '#06b6d4', textShadow: '0 0 30px rgba(6, 182, 212, 0.2)' }}>Save Lives Faster.</span>
</motion.h1> </motion.h1>
<motion.p <motion.p
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.4 }} transition={{ duration: 0.8, delay: 0.4 }}
style={{ fontSize: '18px', color: '#94a3b8', lineHeight: 1.6, fontWeight: 500 }} style={{ fontSize: '18px', color: '#475569', lineHeight: 1.6, fontWeight: 500 }}
> >
Access real-time telemetry, manage dispatch routes, and monitor critical resources from a single, secure tactical terminal. Access real-time telemetry, manage dispatch routes, and monitor critical resources from a single, secure tactical terminal.
</motion.p> </motion.p>
@@ -220,13 +220,13 @@ export const FleetLogin = () => {
transition={{ duration: 0.8, delay: 0.6 }} transition={{ duration: 0.8, delay: 0.6 }}
style={{ display: 'flex', gap: '24px', marginTop: '40px' }} style={{ display: 'flex', gap: '24px', marginTop: '40px' }}
> >
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#22d3ee', fontSize: '14px', fontWeight: 700 }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#06b6d4', fontSize: '14px', fontWeight: 700 }}>
<Activity size={18} /> REAL-TIME SYNC <Activity size={18} /> REAL-TIME SYNC
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#22d3ee', fontSize: '14px', fontWeight: 700 }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#06b6d4', fontSize: '14px', fontWeight: 700 }}>
<MapPin size={18} /> GPS TRACKING <MapPin size={18} /> GPS TRACKING
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#22d3ee', fontSize: '14px', fontWeight: 700 }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#06b6d4', fontSize: '14px', fontWeight: 700 }}>
<ShieldCheck size={18} /> ENCRYPTED <ShieldCheck size={18} /> ENCRYPTED
</div> </div>
</motion.div> </motion.div>
@@ -242,11 +242,11 @@ export const FleetLogin = () => {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
padding: '40px', padding: '40px',
background: '#0f172a', background: '#FFFFFF',
boxShadow: '-20px 0 50px rgba(0,0,0,0.5)' boxShadow: '-10px 0 50px rgba(15, 23, 42, 0.04), -1px 0 0 rgba(15, 23, 42, 0.05)'
}}> }}>
{/* Subtle background glow */} {/* Subtle background glow */}
<div style={{ position: 'absolute', top: '20%', right: '-10%', width: '300px', height: '300px', background: 'rgba(34, 211, 238, 0.05)', filter: 'blur(100px)', borderRadius: '50%', pointerEvents: 'none' }} /> <div style={{ position: 'absolute', top: '20%', right: '-10%', width: '300px', height: '300px', background: 'rgba(6, 182, 212, 0.03)', filter: 'blur(100px)', borderRadius: '50%', pointerEvents: 'none' }} />
<motion.div <motion.div
key={loginStep} key={loginStep}
@@ -257,10 +257,10 @@ export const FleetLogin = () => {
style={{ width: '100%', maxWidth: '420px', display: 'flex', flexDirection: 'column', gap: '32px', zIndex: 10 }} style={{ width: '100%', maxWidth: '420px', display: 'flex', flexDirection: 'column', gap: '32px', zIndex: 10 }}
> >
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<h2 style={{ fontSize: '28px', fontWeight: 800, color: '#fff', margin: '0 0 8px 0', letterSpacing: '1px' }}> <h2 style={{ fontSize: '28px', fontWeight: 800, color: '#0F172A', margin: '0 0 8px 0', letterSpacing: '1px' }}>
{loginStep === 'login' ? 'TERMINAL ACCESS' : 'MFA REQUIRED'} {loginStep === 'login' ? 'TERMINAL ACCESS' : 'MFA REQUIRED'}
</h2> </h2>
<p style={{ color: '#22d3ee', fontSize: '13px', fontWeight: 600, letterSpacing: '2px', textTransform: 'uppercase', margin: 0, opacity: 0.8 }}> <p style={{ color: '#06b6d4', fontSize: '13px', fontWeight: 600, letterSpacing: '2px', textTransform: 'uppercase', margin: 0, opacity: 0.9 }}>
{loginStep === 'login' ? 'Sector: Dispatch • Active Node: CS-88' : 'Identity Verification'} {loginStep === 'login' ? 'Sector: Dispatch • Active Node: CS-88' : 'Identity Verification'}
</p> </p>
</div> </div>
@@ -268,9 +268,9 @@ export const FleetLogin = () => {
{loginStep === 'login' ? ( {loginStep === 'login' ? (
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}> <form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label style={{ fontSize: '12px', fontWeight: 700, color: '#22d3ee', textTransform: 'uppercase', letterSpacing: '1px', opacity: 0.8 }}>Operator ID</label> <label style={{ fontSize: '12px', fontWeight: 700, color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', opacity: 0.9 }}>Operator ID</label>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<User size={18} color="#22d3ee" style={{ position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', opacity: 0.8 }} /> <User size={18} color="#06b6d4" style={{ position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', opacity: 0.9 }} />
<input <input
type="text" type="text"
placeholder="ID_ENTRY" placeholder="ID_ENTRY"
@@ -279,20 +279,20 @@ export const FleetLogin = () => {
required required
style={{ style={{
width: '100%', padding: '16px 16px 16px 48px', width: '100%', padding: '16px 16px 16px 48px',
background: 'rgba(2, 6, 23, 0.6)', border: '1px solid rgba(34, 211, 238, 0.3)', background: 'rgba(15, 23, 42, 0.03)', border: '1px solid rgba(15, 23, 42, 0.1)',
borderRadius: '12px', color: '#fff', fontSize: '15px', fontWeight: 500, borderRadius: '12px', color: '#0F172A', fontSize: '15px', fontWeight: 500,
outline: 'none', transition: 'all 0.3s', fontFamily: "'Outfit', sans-serif" outline: 'none', transition: 'all 0.3s', fontFamily: "'Outfit', sans-serif"
}} }}
onFocus={(e) => e.target.style.borderColor = '#22d3ee'} onFocus={(e) => e.target.style.borderColor = '#06b6d4'}
onBlur={(e) => e.target.style.borderColor = 'rgba(34, 211, 238, 0.3)'} onBlur={(e) => e.target.style.borderColor = 'rgba(15, 23, 42, 0.1)'}
/> />
</div> </div>
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label style={{ fontSize: '12px', fontWeight: 700, color: '#22d3ee', textTransform: 'uppercase', letterSpacing: '1px', opacity: 0.8 }}>Command Key</label> <label style={{ fontSize: '12px', fontWeight: 700, color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', opacity: 0.9 }}>Command Key</label>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<Lock size={18} color="#22d3ee" style={{ position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', opacity: 0.8 }} /> <Lock size={18} color="#06b6d4" style={{ position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', opacity: 0.9 }} />
<input <input
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
placeholder="KEY_REQUIRED" placeholder="KEY_REQUIRED"
@@ -301,14 +301,14 @@ export const FleetLogin = () => {
required required
style={{ style={{
width: '100%', padding: '16px 48px 16px 48px', width: '100%', padding: '16px 48px 16px 48px',
background: 'rgba(2, 6, 23, 0.6)', border: '1px solid rgba(34, 211, 238, 0.3)', background: 'rgba(15, 23, 42, 0.03)', border: '1px solid rgba(15, 23, 42, 0.1)',
borderRadius: '12px', color: '#fff', fontSize: '15px', fontWeight: 500, borderRadius: '12px', color: '#0F172A', fontSize: '15px', fontWeight: 500,
outline: 'none', transition: 'all 0.3s', fontFamily: "'Outfit', sans-serif", letterSpacing: showPassword ? 'normal' : '3px' outline: 'none', transition: 'all 0.3s', fontFamily: "'Outfit', sans-serif", letterSpacing: showPassword ? 'normal' : '3px'
}} }}
onFocus={(e) => e.target.style.borderColor = '#22d3ee'} onFocus={(e) => e.target.style.borderColor = '#06b6d4'}
onBlur={(e) => e.target.style.borderColor = 'rgba(34, 211, 238, 0.3)'} onBlur={(e) => e.target.style.borderColor = 'rgba(15, 23, 42, 0.1)'}
/> />
<button type="button" onClick={() => setShowPassword(!showPassword)} style={{ position: 'absolute', right: '16px', top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', cursor: 'pointer', color: '#22d3ee', padding: 0 }}> <button type="button" onClick={() => setShowPassword(!showPassword)} style={{ position: 'absolute', right: '16px', top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', cursor: 'pointer', color: '#64748B', padding: 0 }}>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />} {showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button> </button>
</div> </div>
@@ -318,14 +318,14 @@ export const FleetLogin = () => {
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
style={{ style={{
marginTop: '12px', padding: '16px', background: '#22d3ee', color: '#020617', marginTop: '12px', padding: '16px', background: '#06b6d4', color: '#FFFFFF',
border: 'none', borderRadius: '12px', fontSize: '15px', fontWeight: 800, letterSpacing: '1px', border: 'none', borderRadius: '12px', fontSize: '15px', fontWeight: 800, letterSpacing: '1px',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '12px',
cursor: isLoading ? 'not-allowed' : 'pointer', transition: 'all 0.3s', cursor: isLoading ? 'not-allowed' : 'pointer', transition: 'all 0.3s',
boxShadow: '0 10px 25px rgba(34, 211, 238, 0.3)' boxShadow: '0 8px 24px rgba(6, 182, 212, 0.25)'
}} }}
onMouseOver={(e) => { if(!isLoading) { e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 15px 35px rgba(34, 211, 238, 0.4)'; } }} onMouseOver={(e) => { if(!isLoading) { e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 12px 32px rgba(6, 182, 212, 0.35)'; } }}
onMouseOut={(e) => { if(!isLoading) { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = '0 10px 25px rgba(34, 211, 238, 0.3)'; } }} onMouseOut={(e) => { if(!isLoading) { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = '0 8px 24px rgba(6, 182, 212, 0.25)'; } }}
> >
{isLoading ? ( {isLoading ? (
<Cpu className="spin" size={20} /> <Cpu className="spin" size={20} />
@@ -340,9 +340,9 @@ export const FleetLogin = () => {
) : ( ) : (
<form onSubmit={handleMfaVerify} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}> <form onSubmit={handleMfaVerify} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label style={{ fontSize: '12px', fontWeight: 700, color: '#22d3ee', textTransform: 'uppercase', letterSpacing: '1px', opacity: 0.8 }}>TOTP Authorization</label> <label style={{ fontSize: '12px', fontWeight: 700, color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', opacity: 0.9 }}>TOTP Authorization</label>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<KeyRound size={18} color="#22d3ee" style={{ position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', opacity: 0.8 }} /> <KeyRound size={18} color="#06b6d4" style={{ position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', opacity: 0.9 }} />
<input <input
type="text" type="text"
placeholder="000 000" placeholder="000 000"
@@ -352,12 +352,12 @@ export const FleetLogin = () => {
required required
style={{ style={{
width: '100%', padding: '16px 16px 16px 48px', width: '100%', padding: '16px 16px 16px 48px',
background: 'rgba(2, 6, 23, 0.6)', border: '1px solid rgba(34, 211, 238, 0.3)', background: 'rgba(15, 23, 42, 0.03)', border: '1px solid rgba(15, 23, 42, 0.1)',
borderRadius: '12px', color: '#fff', fontSize: '20px', fontWeight: 600, letterSpacing: '4px', textAlign: 'center', borderRadius: '12px', color: '#0F172A', fontSize: '20px', fontWeight: 600, letterSpacing: '4px', textAlign: 'center',
outline: 'none', transition: 'all 0.3s', fontFamily: "'Outfit', sans-serif" outline: 'none', transition: 'all 0.3s', fontFamily: "'Outfit', sans-serif"
}} }}
onFocus={(e) => e.target.style.borderColor = '#22d3ee'} onFocus={(e) => e.target.style.borderColor = '#06b6d4'}
onBlur={(e) => e.target.style.borderColor = 'rgba(34, 211, 238, 0.3)'} onBlur={(e) => e.target.style.borderColor = 'rgba(15, 23, 42, 0.1)'}
/> />
</div> </div>
</div> </div>
@@ -366,11 +366,11 @@ export const FleetLogin = () => {
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
style={{ style={{
marginTop: '12px', padding: '16px', background: '#22d3ee', color: '#020617', marginTop: '12px', padding: '16px', background: '#06b6d4', color: '#FFFFFF',
border: 'none', borderRadius: '12px', fontSize: '15px', fontWeight: 800, letterSpacing: '1px', border: 'none', borderRadius: '12px', fontSize: '15px', fontWeight: 800, letterSpacing: '1px',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '12px',
cursor: isLoading ? 'not-allowed' : 'pointer', transition: 'all 0.3s', cursor: isLoading ? 'not-allowed' : 'pointer', transition: 'all 0.3s',
boxShadow: '0 10px 25px rgba(34, 211, 238, 0.3)' boxShadow: '0 8px 24px rgba(6, 182, 212, 0.25)'
}} }}
> >
{isLoading ? ( {isLoading ? (
@@ -393,7 +393,7 @@ export const FleetLogin = () => {
exit={{ opacity: 0, height: 0 }} exit={{ opacity: 0, height: 0 }}
style={{ overflow: 'hidden' }} style={{ overflow: 'hidden' }}
> >
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '14px', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.3)', borderRadius: '10px', color: '#ef4444', fontSize: '13px', fontWeight: 600, marginTop: '8px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '14px', background: 'rgba(239, 68, 68, 0.06)', border: '1px solid rgba(239, 68, 68, 0.2)', borderRadius: '10px', color: '#ef4444', fontSize: '13px', fontWeight: 600, marginTop: '8px' }}>
<ShieldAlert size={16} /> <ShieldAlert size={16} />
<span>{showError}</span> <span>{showError}</span>
</div> </div>
@@ -401,10 +401,8 @@ export const FleetLogin = () => {
)} )}
</AnimatePresence> </AnimatePresence>
<div style={{ marginTop: '20px', borderTop: '1px solid rgba(6, 182, 212, 0.1)', paddingTop: '24px', textAlign: 'center' }}>
<NavLink to="/login" style={{ color: '#64748B', textDecoration: 'none', fontSize: '13px', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: '8px', transition: 'color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.color = '#06b6d4'} onMouseOut={(e) => e.currentTarget.style.color = '#64748B'}>
<div style={{ marginTop: '20px', borderTop: '1px solid rgba(34, 211, 238, 0.1)', paddingTop: '24px', textAlign: 'center' }}>
<NavLink to="/login" style={{ color: '#94a3b8', textDecoration: 'none', fontSize: '13px', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: '8px', transition: 'color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.color = '#22d3ee'} onMouseOut={(e) => e.currentTarget.style.color = '#94a3b8'}>
<ArrowRight size={14} style={{ transform: 'rotate(180deg)' }} /> RETURN TO STANDARD PORTAL <ArrowRight size={14} style={{ transform: 'rotate(180deg)' }} /> RETURN TO STANDARD PORTAL
</NavLink> </NavLink>
</div> </div>
@@ -424,8 +422,8 @@ export const FleetLogin = () => {
input:-webkit-autofill:hover, input:-webkit-autofill:hover,
input:-webkit-autofill:focus, input:-webkit-autofill:focus,
input:-webkit-autofill:active{ input:-webkit-autofill:active{
-webkit-box-shadow: 0 0 0 30px #020617 inset !important; -webkit-box-shadow: 0 0 0 30px #ffffff inset !important;
-webkit-text-fill-color: white !important; -webkit-text-fill-color: #0f172a !important;
transition: background-color 5000s ease-in-out 0s; transition: background-color 5000s ease-in-out 0s;
} }
@media (max-width: 900px) { @media (max-width: 900px) {

View File

@@ -3,7 +3,7 @@ import { useSearchParams } from 'react-router-dom';
import { import {
Building2, Truck, Users, CalendarDays, ClipboardCheck, Building2, Truck, Users, CalendarDays, ClipboardCheck,
ShoppingCart, Map, MapPin, Navigation, Link2, Activity, ShoppingCart, Map, MapPin, Navigation, Link2, Activity,
Bell, Settings, Search Bell, Settings, Search, Database
} from 'lucide-react'; } from 'lucide-react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
@@ -12,6 +12,13 @@ import { FleetAssets } from './fleet/FleetAssets';
import { FleetPersonnel } from './fleet/FleetPersonnel'; import { FleetPersonnel } from './fleet/FleetPersonnel';
import { FleetScheduling } from './fleet/FleetScheduling'; import { FleetScheduling } from './fleet/FleetScheduling';
import { FleetInventory } from './fleet/FleetInventory'; import { FleetInventory } from './fleet/FleetInventory';
import { FleetWarehouseStock } from './fleet/FleetWarehouseStock';
import { FleetPendingRequests } from './fleet/FleetPendingRequests';
import { LiveDashboard } from './fleet/LiveDashboard';
import { FleetActiveShifts } from './fleet/FleetActiveShifts';
import { FleetTrips } from './fleet/FleetTrips';
import { FleetAnalytics } from './fleet/FleetAnalytics';
import './fleet/FleetOperator.css';
const SCOPE_MODULES = [ const SCOPE_MODULES = [
{ id: 'overview', label: 'Live Dashboard', icon: Map, desc: 'Real-time fleet tracking & telemetry' }, { id: 'overview', label: 'Live Dashboard', icon: Map, desc: 'Real-time fleet tracking & telemetry' },
@@ -24,7 +31,10 @@ const SCOPE_MODULES = [
{ id: 'trips', label: 'Trip Management', icon: MapPin, desc: 'Active, pending, and completed trips' }, { id: 'trips', label: 'Trip Management', icon: MapPin, desc: 'Active, pending, and completed trips' },
{ id: 'telematics', label: 'GPS Telematics', icon: Navigation, desc: 'Geofencing, speed, SOS alerts' }, { id: 'telematics', label: 'GPS Telematics', icon: Navigation, desc: 'Geofencing, speed, SOS alerts' },
{ id: 'referrals', label: 'Referral Network', icon: Link2, desc: 'Hospitals & specialty routing' }, { id: 'referrals', label: 'Referral Network', icon: Link2, desc: 'Hospitals & specialty routing' },
{ id: 'analytics', label: 'Fleet Analytics', icon: Activity, desc: 'KPIs, SLAs, and reports' } { id: 'analytics', label: 'Fleet Analytics', icon: Activity, desc: 'KPIs, SLAs, and reports' },
{ id: 'warehouse', label: 'Warehouse Stock', icon: Database, desc: 'Central warehouse inventory overview' },
{ id: 'pending-requests', label: 'Stock Requests', icon: ShoppingCart, desc: 'View, approve, and complete supply restock requests' },
{ id: 'active-shifts', label: 'Active Shifts', icon: Activity, desc: 'Real-time tracking of active shifts' }
]; ];
const PlaceholderModule: React.FC<{ label: string; icon: React.ElementType }> = ({ label, icon: Icon }) => ( const PlaceholderModule: React.FC<{ label: string; icon: React.ElementType }> = ({ label, icon: Icon }) => (
@@ -53,25 +63,31 @@ export const FleetOperatorDashboard: React.FC = () => {
return <FleetScheduling />; return <FleetScheduling />;
case 'inventory': case 'inventory':
return <FleetInventory />; return <FleetInventory />;
case 'warehouse':
return <FleetWarehouseStock />;
case 'pending-requests':
return <FleetPendingRequests />;
case 'active-shifts':
return <FleetActiveShifts />;
case 'overview': case 'overview':
return <PlaceholderModule label="Live Fleet Dashboard" icon={Map} />; return <LiveDashboard />;
case 'attendance': case 'attendance':
return <PlaceholderModule label="Attendance & Duty" icon={ClipboardCheck} />; return <PlaceholderModule label="Attendance & Duty" icon={ClipboardCheck} />;
case 'trips': case 'trips':
return <PlaceholderModule label="Trip Management" icon={MapPin} />; return <FleetTrips />;
case 'telematics': case 'telematics':
return <PlaceholderModule label="GPS Telematics" icon={Navigation} />; return <PlaceholderModule label="GPS Telematics" icon={Navigation} />;
case 'referrals': case 'referrals':
return <PlaceholderModule label="Referral Network" icon={Link2} />; return <PlaceholderModule label="Referral Network" icon={Link2} />;
case 'analytics': case 'analytics':
return <PlaceholderModule label="Fleet Analytics" icon={Activity} />; return <FleetAnalytics />;
default: default:
return <PlaceholderModule label="Module Loading" icon={Activity} />; return <PlaceholderModule label="Module Loading" icon={Activity} />;
} }
}; };
return ( return (
<div style={{ <div className="fleet-dashboard-container" style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: '100%', height: '100%',
@@ -81,7 +97,7 @@ export const FleetOperatorDashboard: React.FC = () => {
fontFamily: "'Inter', sans-serif" fontFamily: "'Inter', sans-serif"
}}> }}>
{/* Top Header */} {/* Top Header */}
<header style={{ <header className="fleet-dashboard-header" style={{
height: '72px', height: '72px',
padding: '0 32px', padding: '0 32px',
display: 'flex', display: 'flex',
@@ -104,7 +120,7 @@ export const FleetOperatorDashboard: React.FC = () => {
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', background: 'rgba(255,255,255,0.03)', padding: '8px 16px', borderRadius: '20px', border: '1px solid rgba(255,255,255,0.06)' }}> <div className="fleet-search-container" style={{ display: 'flex', alignItems: 'center', gap: '8px', background: 'rgba(255,255,255,0.03)', padding: '8px 16px', borderRadius: '20px', border: '1px solid rgba(255,255,255,0.06)' }}>
<Search size={14} color="#64748B" /> <Search size={14} color="#64748B" />
<input <input
type="text" type="text"
@@ -112,17 +128,17 @@ export const FleetOperatorDashboard: React.FC = () => {
style={{ background: 'transparent', border: 'none', color: '#fff', fontSize: '0.82rem', width: '140px', outline: 'none' }} style={{ background: 'transparent', border: 'none', color: '#fff', fontSize: '0.82rem', width: '140px', outline: 'none' }}
/> />
</div> </div>
<button style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: '10px', padding: '8px', color: '#94A3B8', cursor: 'pointer', position: 'relative', display: 'flex' }}> <button className="fleet-header-btn" style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: '10px', padding: '8px', color: '#94A3B8', cursor: 'pointer', position: 'relative', display: 'flex' }}>
<Bell size={18} /> <Bell size={18} />
<span style={{ position: 'absolute', top: 6, right: 6, width: '7px', height: '7px', background: '#EF4444', borderRadius: '50%', border: '1px solid #040B16' }} /> <span style={{ position: 'absolute', top: 6, right: 6, width: '7px', height: '7px', background: '#EF4444', borderRadius: '50%', border: '1px solid #040B16' }} />
</button> </button>
<button style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: '10px', padding: '8px', color: '#94A3B8', cursor: 'pointer', display: 'flex' }}> <button className="fleet-header-btn" style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: '10px', padding: '8px', color: '#94A3B8', cursor: 'pointer', display: 'flex' }}>
<Settings size={18} /> <Settings size={18} />
</button> </button>
<div style={{ width: '1px', height: '24px', background: 'rgba(255,255,255,0.07)' }} /> <div className="fleet-header-divider" style={{ width: '1px', height: '24px', background: 'rgba(255,255,255,0.07)' }} />
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div style={{ textAlign: 'right' }}> <div className="fleet-profile-name" style={{ textAlign: 'right' }}>
<div style={{ fontSize: '0.8rem', fontWeight: 700, color: '#fff' }}>Station Incharge</div> <div className="fleet-profile-title" style={{ fontSize: '0.8rem', fontWeight: 700, color: '#fff' }}>Station Incharge</div>
<div style={{ fontSize: '0.62rem', color: '#06B6D4' }}>Fleet Operator</div> <div style={{ fontSize: '0.62rem', color: '#06B6D4' }}>Fleet Operator</div>
</div> </div>
<div style={{ width: '34px', height: '34px', borderRadius: '10px', background: 'linear-gradient(135deg, #06B6D4, #3B82F6)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <div style={{ width: '34px', height: '34px', borderRadius: '10px', background: 'linear-gradient(135deg, #06B6D4, #3B82F6)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
@@ -133,7 +149,7 @@ export const FleetOperatorDashboard: React.FC = () => {
</header> </header>
{/* Scrollable Content */} {/* Scrollable Content */}
<div style={{ flex: 1, overflowY: 'auto', padding: '32px' }}> <div className="fleet-dashboard-content" style={{ flex: 1, overflowY: 'auto', padding: '32px' }}>
<motion.div <motion.div
key={activeModule} key={activeModule}
initial={{ opacity: 0, y: 12 }} initial={{ opacity: 0, y: 12 }}

View File

@@ -0,0 +1,168 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Clock, CheckCircle, Car, User, Activity, Loader2, Calendar } from 'lucide-react';
import { motion } from 'framer-motion';
interface ActiveShift {
id: string;
vehicleId: string;
vehicle?: {
registration_number?: string;
vehicle_type?: string;
};
driverId: string;
driver?: {
userId?: string;
type?: string;
user?: { name?: string };
};
staffId: string;
staff?: {
userId?: string;
type?: string;
user?: { name?: string };
};
startTime: string;
endTime: string | null;
status: string;
notes?: string | null;
}
export const FleetActiveShifts: React.FC = () => {
const [shifts, setShifts] = useState<ActiveShift[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const fetchActiveShifts = useCallback(async () => {
setLoading(true);
setError('');
try {
const token = localStorage.getItem('teleems_token') || '';
const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/shifts/active', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const json = await res.json();
if (!res.ok) throw new Error(json.message || 'Failed to fetch active shifts');
const data = json?.data?.data || json?.data || [];
setShifts(Array.isArray(data) ? data : []);
} catch (err: any) {
console.error('Error fetching active shifts:', err);
setError(err.message || 'An error occurred while fetching shifts.');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchActiveShifts();
}, [fetchActiveShifts]);
return (
<div style={{ color: '#F8FAFC', fontFamily: 'Inter, sans-serif' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: 24 }}>
<button
onClick={fetchActiveShifts}
style={{
display: 'flex', alignItems: 'center', gap: 6, padding: '8px 12px',
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 10, color: '#94A3B8', fontSize: '0.75rem', fontWeight: 700, cursor: 'pointer'
}}
>
{loading ? <Loader2 size={13} className="spin" /> : '↺ Refresh'}
</button>
</div>
{error && (
<div style={{ background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.2)', padding: '12px 16px', borderRadius: 12, color: '#EF4444', fontSize: '0.8rem', marginBottom: 20 }}>
{error}
</div>
)}
{loading ? (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 200 }}>
<Loader2 size={32} color="#06B6D4" className="spin" />
</div>
) : shifts.length === 0 ? (
<div style={{ textAlign: 'center', padding: '64px 24px', background: 'rgba(255,255,255,0.01)', border: '1px dashed rgba(255,255,255,0.08)', borderRadius: 16 }}>
<CheckCircle size={36} color="#10B981" style={{ marginBottom: 12 }} />
<p style={{ color: '#64748B', fontSize: '0.88rem', margin: 0 }}>No active shifts at the moment. All shifts have been completed or none are currently scheduled.</p>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{shifts.map((shift, i) => (
<motion.div
key={shift.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.05 }}
style={{
background: 'rgba(255,255,255,0.02)',
border: '1px solid rgba(255,255,255,0.07)',
borderRadius: 16,
padding: '18px 24px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 14, flexWrap: 'wrap', gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
{/* Vehicle identifier */}
<div style={{ padding: '6px 12px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 9, display: 'flex', alignItems: 'center', gap: 7 }}>
<Car size={14} color="#06B6D4" />
<span style={{ fontWeight: 800, fontSize: '0.82rem', color: '#000000' }}>
{shift.vehicle?.registration_number || 'Unknown Vehicle'}
</span>
<span style={{ fontSize: '0.65rem', fontWeight: 700, color: '#000000', background: 'rgba(6,182,212,0.12)', padding: '1px 7px', borderRadius: 4 }}>
{shift.vehicle?.vehicle_type || 'N/A'}
</span>
</div>
{/* Status Badge */}
<div style={{ padding: '5px 10px', background: 'rgba(16,185,129,0.1)', border: `1px solid rgba(16,185,129,0.3)`, borderRadius: 7, display: 'flex', alignItems: 'center', gap: 6 }}>
<Clock size={11} color="#10B981" />
<span style={{ fontSize: '0.7rem', fontWeight: 700, color: '#10B981' }}>{shift.status}</span>
</div>
{/* Time details */}
<div style={{ fontSize: '0.72rem', fontWeight: 600, color: '#64748B', display: 'flex', alignItems: 'center', gap: 4 }}>
<Calendar size={12} /> Started: {new Date(shift.startTime).toLocaleString()}
</div>
</div>
</div>
{/* Crew Members */}
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{/* Driver */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px', background: 'rgba(6,182,212,0.04)', border: '1px solid rgba(6,182,212,0.08)', borderRadius: 12, flex: 1, minWidth: 200 }}>
<User size={15} color="#06B6D4" />
<div>
<div style={{ fontSize: '0.58rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px', fontWeight: 700 }}>Pilot Driver</div>
<div style={{ fontSize: '0.85rem', fontWeight: 700, color: '#000000' }}>
{shift.driver?.user?.name || 'Driver Profile'}
</div>
</div>
</div>
{/* Staff / EMT */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px', background: 'rgba(59,130,246,0.04)', border: '1px solid rgba(59,130,246,0.08)', borderRadius: 12, flex: 1, minWidth: 200 }}>
<Activity size={15} color="#3B82F6" />
<div>
<div style={{ fontSize: '0.58rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px', fontWeight: 700 }}>
{shift.staff?.type === 'DOCTOR' ? 'Medical Doctor' : 'Emergency Medical Technician'}
</div>
<div style={{ fontSize: '0.85rem', fontWeight: 700, color: '#000000' }}>
{shift.staff?.user?.name || 'EMT/Staff Profile'}
</div>
</div>
</div>
</div>
</motion.div>
))}
</div>
)}
<style>{`@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}} .spin{animation:spin 1s linear infinite}`}</style>
</div>
);
};

View File

@@ -0,0 +1,620 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import {
Activity,
Truck,
Users,
TrendingUp,
Calendar,
Clock,
AlertCircle,
Loader2,
RefreshCw,
Award,
Shield,
MapPin,
Car
} from 'lucide-react';
import {
ResponsiveContainer,
PieChart,
Pie,
Cell,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
AreaChart,
Area
} from 'recharts';
import { motion } from 'framer-motion';
import { fleetApi } from '../../api/fleet';
interface APIVehicle {
id: string;
registration_number: string;
chassis_number?: string | null;
brand?: string;
model?: string;
vehicle_type: string;
status?: string;
ownership_type?: string;
gps_lat?: string | number;
gps_lon?: string | number;
activeShift?: any;
activeRoster?: any;
createdAt?: string;
}
export const FleetAnalytics: React.FC = () => {
const [vehicles, setVehicles] = useState<APIVehicle[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [timeRange, setTimeRange] = useState<'7D' | '30D' | 'ALL'>('ALL');
const token = localStorage.getItem('teleems_token') || '';
const fetchVehiclesData = useCallback(async () => {
setLoading(true);
setError('');
try {
const res = await fleetApi.getVehicles(token);
let list: APIVehicle[] = [];
if (res?.data?.data && Array.isArray(res.data.data)) list = res.data.data;
else if (res?.data && Array.isArray(res.data)) list = res.data;
else if (Array.isArray(res)) list = res;
setVehicles(list);
} catch (err: any) {
console.error('Failed to fetch vehicles for analytics:', err);
setError('Failed to load fleet analytics data. Please try again.');
} finally {
setLoading(false);
}
}, [token]);
useEffect(() => {
fetchVehiclesData();
}, [fetchVehiclesData]);
// --- DERIVED ANALYTICS METRICS ---
const stats = useMemo(() => {
const total = vehicles.length;
const available = vehicles.filter(v => v.status === 'AVAILABLE').length;
const busy = vehicles.filter(v => v.status === 'BUSY').length;
const offline = vehicles.filter(v => v.status === 'OFFLINE' || (!v.status)).length;
const withActiveShift = vehicles.filter(v => v.activeShift !== null).length;
const withActiveRoster = vehicles.filter(v => v.activeRoster !== null).length;
const hasGps = vehicles.filter(v => v.gps_lat && parseFloat(v.gps_lat as string) !== 0).length;
const utilizationRate = total > 0 ? Math.round(((busy + withActiveShift) / total) * 100) : 0;
return {
total,
available,
busy,
offline,
withActiveShift,
withActiveRoster,
hasGps,
utilizationRate
};
}, [vehicles]);
// Chart 1: Status Distribution
const statusChartData = useMemo(() => {
const statuses: Record<string, { count: number; color: string }> = {
AVAILABLE: { count: 0, color: '#10B981' }, // Emerald
BUSY: { count: 0, color: '#F59E0B' }, // Amber
OFFLINE: { count: 0, color: '#475569' }, // Slate
MAINTENANCE: { count: 0, color: '#EF4444' } // Red
};
vehicles.forEach(v => {
const status = (v.status || 'OFFLINE').toUpperCase();
if (statuses[status]) {
statuses[status].count += 1;
} else {
statuses['OFFLINE'].count += 1;
}
});
return Object.entries(statuses).map(([name, info]) => ({
name,
value: info.count,
color: info.color
})).filter(d => d.value > 0);
}, [vehicles]);
// Chart 2: Vehicle Type Breakdown
const typeChartData = useMemo(() => {
const types: Record<string, number> = {};
vehicles.forEach(v => {
const type = v.vehicle_type || 'Unknown';
types[type] = (types[type] || 0) + 1;
});
const colors = ['#06B6D4', '#3B82F6', '#8B5CF6', '#EC4899', '#F43F5E'];
return Object.entries(types).map(([name, value], idx) => ({
name,
value,
color: colors[idx % colors.length]
}));
}, [vehicles]);
// Chart 3: Shift / Roster Deployment Metrics
const deploymentChartData = useMemo(() => {
let activeShiftCount = 0;
let scheduledRosterCount = 0;
let unassignedCount = 0;
vehicles.forEach(v => {
if (v.activeShift) {
activeShiftCount++;
} else if (v.activeRoster) {
scheduledRosterCount++;
} else {
unassignedCount++;
}
});
return [
{ name: 'Active Shift (On Duty)', value: activeShiftCount, color: '#10B981' },
{ name: 'Scheduled Roster (Planned)', value: scheduledRosterCount, color: '#8B5CF6' },
{ name: 'Unassigned Units', value: unassignedCount, color: '#F59E0B' }
];
}, [vehicles]);
// Chart 4: Brand Representation
const brandChartData = useMemo(() => {
const brands: Record<string, number> = {};
vehicles.forEach(v => {
const brand = v.brand || 'Unspecified';
brands[brand] = (brands[brand] || 0) + 1;
});
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'];
return Object.entries(brands).map(([name, value], idx) => ({
name,
count: value,
color: colors[idx % colors.length]
}));
}, [vehicles]);
// Chart 5: Mock Fleet Mileage / Performance (Time Series)
const performanceTrendData = useMemo(() => {
return [
{ name: '06:00', trips: 2, responseTime: 8, efficiency: 94 },
{ name: '08:00', trips: 5, responseTime: 12, efficiency: 89 },
{ name: '10:00', trips: 8, responseTime: 14, efficiency: 85 },
{ name: '12:00', trips: 6, responseTime: 10, efficiency: 91 },
{ name: '14:00', trips: 9, responseTime: 15, efficiency: 82 },
{ name: '16:00', trips: 11, responseTime: 13, efficiency: 86 },
{ name: '18:00', trips: 14, responseTime: 16, efficiency: 80 },
{ name: '20:00', trips: 12, responseTime: 11, efficiency: 88 },
{ name: '22:00', trips: 7, responseTime: 9, efficiency: 93 },
{ name: '00:00', trips: 4, responseTime: 7, efficiency: 95 },
];
}, []);
const customTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
return (
<div style={{
background: '#0F172A',
border: '1px solid rgba(255,255,255,0.1)',
padding: '12px 16px',
borderRadius: 12,
boxShadow: '0 10px 25px rgba(0,0,0,0.5)'
}}>
<p style={{ margin: 0, fontSize: '0.75rem', fontWeight: 800, color: '#94A3B8', textTransform: 'uppercase' }}>
{payload[0].name}
</p>
<p style={{ margin: '4px 0 0', fontSize: '1.25rem', fontWeight: 900, color: '#fff' }}>
{payload[0].value} <span style={{ fontSize: '0.75rem', fontWeight: 500, color: '#64748B' }}>Ambulances</span>
</p>
</div>
);
}
return null;
};
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 28, color: '#F8FAFC' }}>
{/* HEADER CONTROLS */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
<div>
<h2 style={{ fontSize: '1.4rem', fontWeight: 900, color: '#fff', margin: 0, letterSpacing: '-0.5px' }}>
Fleet Telemetry & Resource Intelligence
</h2>
<p style={{ margin: '4px 0 0', fontSize: '0.78rem', color: '#64748B' }}>
Real-time analytics harvested from live active-shifts, scheduling rosters and vehicle metadata.
</p>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ display: 'flex', gap: 4, background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 10, padding: 3 }}>
{(['7D', '30D', 'ALL'] as const).map(range => (
<button
key={range}
onClick={() => setTimeRange(range)}
style={{
padding: '6px 12px',
borderRadius: 8,
border: 'none',
fontSize: '0.72rem',
fontWeight: 700,
cursor: 'pointer',
background: timeRange === range ? 'linear-gradient(135deg,#06B6D4,#3B82F6)' : 'transparent',
color: timeRange === range ? '#fff' : '#64748B',
transition: 'all 0.2s'
}}
>
{range === '7D' ? '7 Days' : range === '30D' ? '30 Days' : 'All Time'}
</button>
))}
</div>
<button
onClick={fetchVehiclesData}
disabled={loading}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '8px 16px',
background: 'rgba(255,255,255,0.03)',
border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 10,
color: '#06B6D4',
fontSize: '0.75rem',
fontWeight: 700,
cursor: 'pointer',
transition: 'all 0.2s'
}}
>
<RefreshCw size={12} className={loading ? 'spin' : ''} /> Sync Engine
</button>
</div>
</div>
{loading ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: 350, gap: 16 }}>
<Loader2 size={36} className="spin" color="#06B6D4" />
<p style={{ margin: 0, fontSize: '0.85rem', color: '#64748B', fontWeight: 600 }}>Analyzing fleet database records...</p>
</div>
) : error ? (
<div style={{ textAlign: 'center', padding: '64px 24px', background: 'rgba(239,68,68,0.03)', border: '1px dashed rgba(239,68,68,0.15)', borderRadius: 16 }}>
<AlertCircle size={40} color="#EF4444" style={{ marginBottom: 12 }} />
<p style={{ color: '#EF4444', fontSize: '0.88rem', fontWeight: 600, margin: 0 }}>{error}</p>
<button onClick={fetchVehiclesData} style={{ marginTop: 16, padding: '8px 16px', background: '#EF4444', border: 'none', borderRadius: 8, color: '#fff', fontSize: '0.78rem', fontWeight: 700, cursor: 'pointer' }}>Retry Sync</button>
</div>
) : vehicles.length === 0 ? (
<div style={{ textAlign: 'center', padding: '64px 24px', background: 'rgba(255,255,255,0.01)', border: '1px dashed rgba(255,255,255,0.08)', borderRadius: 16 }}>
<Truck size={40} color="#64748B" style={{ marginBottom: 12 }} />
<p style={{ color: '#64748B', fontSize: '0.88rem', margin: 0 }}>No active fleet assets found in the system registry.</p>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
{/* STAT CARDS SECTION */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16 }}>
{/* Total Fleet Size */}
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 16, padding: 20 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<span style={{ fontSize: '0.75rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase' }}>Fleet Size</span>
<div style={{ background: 'rgba(59,130,246,0.1)', color: '#3B82F6', borderRadius: 8, padding: 6, display: 'flex' }}>
<Truck size={16} />
</div>
</div>
<div style={{ fontSize: '2rem', fontWeight: 900, color: '#fff' }}>{stats.total}</div>
<div style={{ fontSize: '0.68rem', color: '#64748B', marginTop: 4, display: 'flex', alignItems: 'center', gap: 4 }}>
<TrendingUp size={11} color="#10B981" /> Fully Registered Active Units
</div>
</div>
{/* Utilization Rate */}
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 16, padding: 20 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<span style={{ fontSize: '0.75rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase' }}>Utilization Rate</span>
<div style={{ background: 'rgba(16,185,129,0.1)', color: '#10B981', borderRadius: 8, padding: 6, display: 'flex' }}>
<Activity size={16} />
</div>
</div>
<div style={{ fontSize: '2rem', fontWeight: 900, color: '#fff' }}>{stats.utilizationRate}%</div>
<div style={{ fontSize: '0.68rem', color: '#64748B', marginTop: 4, display: 'flex', alignItems: 'center', gap: 4 }}>
<TrendingUp size={11} color="#10B981" /> Ratio of On-Duty Deployments
</div>
</div>
{/* Shift Deployment Status */}
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 16, padding: 20 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<span style={{ fontSize: '0.75rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase' }}>Active Duty Shifts</span>
<div style={{ background: 'rgba(139,92,246,0.1)', color: '#8B5CF6', borderRadius: 8, padding: 6, display: 'flex' }}>
<Clock size={16} />
</div>
</div>
<div style={{ fontSize: '2rem', fontWeight: 900, color: '#fff' }}>{stats.withActiveShift}</div>
<div style={{ fontSize: '0.68rem', color: '#64748B', marginTop: 4, display: 'flex', alignItems: 'center', gap: 4 }}>
<TrendingUp size={11} color="#8B5CF6" /> {stats.withActiveRoster} Planned Rosters Scheduled
</div>
</div>
{/* GPS Telemetry Connectivity */}
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 16, padding: 20 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<span style={{ fontSize: '0.75rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase' }}>GPS Core Status</span>
<div style={{ background: 'rgba(6,182,212,0.1)', color: '#06B6D4', borderRadius: 8, padding: 6, display: 'flex' }}>
<MapPin size={16} />
</div>
</div>
<div style={{ fontSize: '2rem', fontWeight: 900, color: '#fff' }}>{stats.hasGps} / {stats.total}</div>
<div style={{ fontSize: '0.68rem', color: '#10B981', marginTop: 4, display: 'flex', alignItems: 'center', gap: 4 }}>
<Award size={11} /> 100% Secure Telemetry Sync
</div>
</div>
</div>
{/* MAIN CHARTS SECTION */}
<div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1fr', gap: 20, flexWrap: 'wrap' }}>
{/* Chart Block 1: Deployment & Utilization */}
<div style={{ background: 'rgba(255,255,255,0.01)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 20, padding: 24 }}>
<h3 style={{ fontSize: '0.95rem', fontWeight: 800, margin: '0 0 4px', color: '#fff' }}>Operational Deployment Dynamics</h3>
<p style={{ fontSize: '0.72rem', color: '#64748B', margin: '0 0 24px' }}>Real-time balance of actively deployed shifts vs scheduled rosters.</p>
<div style={{ height: 260 }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={deploymentChartData} margin={{ top: 10, right: 10, left: -25, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.03)" />
<XAxis dataKey="name" stroke="#64748B" fontSize={11} tickLine={false} />
<YAxis stroke="#64748B" fontSize={11} tickLine={false} />
<Tooltip content={customTooltip} />
<Bar dataKey="value" radius={[8, 8, 0, 0]} barSize={50}>
{deploymentChartData.map((entry, idx) => (
<Cell key={`cell-${idx}`} fill={entry.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
{/* Chart Block 2: Fleet Status Pie Chart */}
<div style={{ background: 'rgba(255,255,255,0.01)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 20, padding: 24, display: 'flex', flexDirection: 'column' }}>
<h3 style={{ fontSize: '0.95rem', fontWeight: 800, margin: '0 0 4px', color: '#fff' }}>Availability & Health Allocation</h3>
<p style={{ fontSize: '0.72rem', color: '#64748B', margin: '0 0 24px' }}>Current real-time operational state distribution across the entire fleet.</p>
<div style={{ display: 'flex', flex: 1, alignItems: 'center', justifyContent: 'center', gap: 20, flexWrap: 'wrap' }}>
<div style={{ width: 170, height: 170, position: 'relative' }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={statusChartData}
cx="50%"
cy="50%"
innerRadius={55}
outerRadius={75}
paddingAngle={6}
dataKey="value"
>
{statusChartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} stroke="transparent" />
))}
</Pie>
<Tooltip content={customTooltip} />
</PieChart>
</ResponsiveContainer>
{/* Central Text inside Pie */}
<div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ fontSize: '1.5rem', fontWeight: 900, color: '#fff' }}>{stats.total}</span>
<span style={{ fontSize: '0.55rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Active Units</span>
</div>
</div>
{/* Legend list */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, minWidth: 140 }}>
{statusChartData.map((item, idx) => {
const pct = stats.total > 0 ? Math.round((item.value / stats.total) * 100) : 0;
return (
<div key={idx} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: item.color }} />
<span style={{ fontSize: '0.72rem', fontWeight: 700, color: '#94A3B8' }}>{item.name}</span>
</div>
<span style={{ fontSize: '0.72rem', fontWeight: 900, color: '#fff' }}>{item.value} ({pct}%)</span>
</div>
);
})}
</div>
</div>
</div>
</div>
{/* SECOND CHARTS ROW */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.2fr', gap: 20, flexWrap: 'wrap' }}>
{/* Chart Block 3: Brand Representation */}
<div style={{ background: 'rgba(255,255,255,0.01)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 20, padding: 24 }}>
<h3 style={{ fontSize: '0.95rem', fontWeight: 800, margin: '0 0 4px', color: '#fff' }}>Fleet Vehicle Brands</h3>
<p style={{ fontSize: '0.72rem', color: '#64748B', margin: '0 0 24px' }}>Composition of mechanical support brands currently integrated.</p>
<div style={{ height: 240 }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={brandChartData}
dataKey="count"
nameKey="name"
cx="50%"
cy="50%"
outerRadius={80}
label={({ name, percent }) => `${name} (${(percent * 100).toFixed(0)}%)`}
labelLine={{ stroke: 'rgba(255,255,255,0.1)' }}
>
{brandChartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} stroke="transparent" />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</div>
</div>
{/* Chart Block 4: Vehicle Type Breakdown */}
<div style={{ background: 'rgba(255,255,255,0.01)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 20, padding: 24 }}>
<h3 style={{ fontSize: '0.95rem', fontWeight: 800, margin: '0 0 4px', color: '#fff' }}>Vehicle Type Distribution</h3>
<p style={{ fontSize: '0.72rem', color: '#64748B', margin: '0 0 24px' }}>Classification of fleet units (ALS: Advanced Life Support, BLS: Basic Life Support, etc.).</p>
<div style={{ height: 240 }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={typeChartData} layout="vertical" margin={{ top: 10, right: 10, left: -25, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.03)" />
<XAxis type="number" stroke="#64748B" fontSize={11} tickLine={false} />
<YAxis type="category" dataKey="name" stroke="#64748B" fontSize={11} tickLine={false} />
<Tooltip />
<Bar dataKey="value" radius={[0, 6, 6, 0]} barSize={24}>
{typeChartData.map((entry, idx) => (
<Cell key={`cell-${idx}`} fill={entry.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
{/* PERFORMANCE TREND ANALYSIS (AREA CHART) */}
<div style={{ background: 'rgba(255,255,255,0.01)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 20, padding: 24 }}>
<h3 style={{ fontSize: '0.95rem', fontWeight: 800, margin: '0 0 4px', color: '#fff' }}>Performance Trend & Mission Load Density</h3>
<p style={{ fontSize: '0.72rem', color: '#64748B', margin: '0 0 24px' }}>Hourly analysis tracking active emergencies, average dispatch response delays (min), and fleet efficiency (%).</p>
<div style={{ height: 260 }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={performanceTrendData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="colorTrips" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#06B6D4" stopOpacity={0.4}/>
<stop offset="95%" stopColor="#06B6D4" stopOpacity={0}/>
</linearGradient>
<linearGradient id="colorResponse" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#8B5CF6" stopOpacity={0.4}/>
<stop offset="95%" stopColor="#8B5CF6" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.03)" />
<XAxis dataKey="name" stroke="#64748B" fontSize={11} tickLine={false} />
<YAxis stroke="#64748B" fontSize={11} tickLine={false} />
<Tooltip contentStyle={{ background: '#0F172A', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 12 }} />
<Legend verticalAlign="top" height={36} wrapperStyle={{ fontSize: '0.75rem', fontWeight: 700 }} />
<Area type="monotone" name="Active Emergency Dispatches" dataKey="trips" stroke="#06B6D4" strokeWidth={2} fillOpacity={1} fill="url(#colorTrips)" />
<Area type="monotone" name="Response Speed (Minutes)" dataKey="responseTime" stroke="#8B5CF6" strokeWidth={2} fillOpacity={1} fill="url(#colorResponse)" />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
{/* DYNAMIC ACTIVE WORKFORCE INTEGRATION STATUS LIST */}
<div style={{ background: 'rgba(255,255,255,0.01)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 20, padding: 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, flexWrap: 'wrap', gap: 12 }}>
<div>
<h3 style={{ fontSize: '0.95rem', fontWeight: 800, margin: 0, color: '#fff' }}>Operational Dispatch Readiness List</h3>
<p style={{ fontSize: '0.72rem', color: '#64748B', margin: '4px 0 0' }}>Real-time assignment states of active fleet units and current assigned pilots / EMTs.</p>
</div>
<div style={{ background: 'rgba(16,185,129,0.1)', color: '#10B981', borderRadius: 8, padding: '4px 12px', fontSize: '0.7rem', fontWeight: 800, display: 'flex', alignItems: 'center', gap: 6 }}>
<Shield size={12} /> Live Compliance Engine
</div>
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left', fontSize: '0.8rem' }}>
<thead>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
<th style={{ padding: '12px 16px', color: '#64748B', fontWeight: 700 }}>VEHICLE UNIT</th>
<th style={{ padding: '12px 16px', color: '#64748B', fontWeight: 700 }}>TYPE</th>
<th style={{ padding: '12px 16px', color: '#64748B', fontWeight: 700 }}>ASSIGNMENT STATE</th>
<th style={{ padding: '12px 16px', color: '#64748B', fontWeight: 700 }}>ACTIVE PILOT (DRIVER)</th>
<th style={{ padding: '12px 16px', color: '#64748B', fontWeight: 700 }}>ACTIVE STAFF (EMT)</th>
<th style={{ padding: '12px 16px', color: '#64748B', fontWeight: 700 }}>STATUS</th>
</tr>
</thead>
<tbody>
{vehicles.map((v) => {
// Extract active driver/staff from shift or roster
const driverName = v.activeShift?.driver?.user?.name || v.activeRoster?.driver?.user?.name || '—';
const driverPhone = v.activeShift?.driver?.user?.phone || v.activeRoster?.driver?.user?.phone || '';
const staffName = v.activeShift?.staff?.user?.name || v.activeRoster?.staff?.user?.name || '—';
const deploymentState = v.activeShift
? { label: 'ON ACTIVE SHIFT', bg: 'rgba(16,185,129,0.1)', color: '#10B981' }
: v.activeRoster
? { label: 'SCHEDULED ROSTER', bg: 'rgba(139,92,246,0.1)', color: '#8B5CF6' }
: { label: 'UNASSIGNED IDLE', bg: 'rgba(255,255,255,0.04)', color: '#64748B' };
const statusColor = v.status === 'AVAILABLE'
? '#10B981'
: v.status === 'BUSY'
? '#F59E0B'
: '#475569';
return (
<tr key={v.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.04)', transition: 'background 0.2s' }} className="table-row">
<td style={{ padding: '16px', display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 32, height: 32, borderRadius: 8, background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Car size={14} color="#06B6D4" />
</div>
<div>
<div style={{ fontWeight: 800, color: '#fff' }}>{v.registration_number}</div>
<div style={{ fontSize: '0.68rem', color: '#64748B' }}>{v.brand} {v.model}</div>
</div>
</td>
<td style={{ padding: '16px' }}>
<span style={{ fontSize: '0.65rem', fontWeight: 800, color: '#06B6D4', background: 'rgba(6,182,212,0.12)', padding: '2px 8px', borderRadius: 6 }}>
{v.vehicle_type}
</span>
</td>
<td style={{ padding: '16px' }}>
<span style={{ fontSize: '0.65rem', fontWeight: 800, background: deploymentState.bg, color: deploymentState.color, padding: '3px 8px', borderRadius: 6 }}>
{deploymentState.label}
</span>
</td>
<td style={{ padding: '16px' }}>
{driverName !== '—' ? (
<div>
<div style={{ fontWeight: 700, color: '#E2E8F0' }}>{driverName}</div>
{driverPhone && <div style={{ fontSize: '0.68rem', color: '#64748B' }}>{driverPhone}</div>}
</div>
) : '—'}
</td>
<td style={{ padding: '16px' }}>
<span style={{ fontWeight: 700, color: '#E2E8F0' }}>{staffName}</span>
</td>
<td style={{ padding: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: statusColor }} />
<span style={{ fontWeight: 800, color: statusColor }}>{v.status || 'OFFLINE'}</span>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
)}
<style>{`
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.spin { animation: spin 1s linear infinite; }
.table-row:hover { background: rgba(255,255,255,0.015); }
`}</style>
</div>
);
};

View File

@@ -42,15 +42,16 @@ const EMPTY_FORM: VehicleForm = {
}; };
const cardStyle: React.CSSProperties = { const cardStyle: React.CSSProperties = {
background: 'rgba(255,255,255,0.03)', background: '#FFFFFF',
border: '1px solid rgba(255,255,255,0.08)', border: '1px solid rgba(15, 23, 42, 0.08)',
borderRadius: '16px', borderRadius: '16px',
padding: '24px', padding: '24px',
boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)',
}; };
const labelStyle: React.CSSProperties = { const labelStyle: React.CSSProperties = {
fontSize: '0.7rem', fontSize: '0.7rem',
color: '#64748B', color: '#475569',
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: '1px', letterSpacing: '1px',
marginBottom: '6px', marginBottom: '6px',
@@ -58,11 +59,11 @@ const labelStyle: React.CSSProperties = {
}; };
const inputStyle: React.CSSProperties = { const inputStyle: React.CSSProperties = {
background: 'rgba(255,255,255,0.04)', background: '#FFFFFF',
border: '1px solid rgba(255,255,255,0.08)', border: '1px solid #CBD5E1',
padding: '11px 14px', padding: '11px 14px',
borderRadius: '10px', borderRadius: '10px',
color: '#fff', color: '#0F172A',
fontSize: '0.875rem', fontSize: '0.875rem',
outline: 'none', outline: 'none',
width: '100%', width: '100%',
@@ -79,6 +80,8 @@ const selectStyle: React.CSSProperties = {
export const FleetAssets: React.FC = () => { export const FleetAssets: React.FC = () => {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [isSearchFocused, setIsSearchFocused] = useState(false);
const [selectedStationFilter, setSelectedStationFilter] = useState('');
const [vehicles, setVehicles] = useState<Vehicle[]>([]); const [vehicles, setVehicles] = useState<Vehicle[]>([]);
const [stations, setStations] = useState<Station[]>([]); const [stations, setStations] = useState<Station[]>([]);
const [stationsLoading, setStationsLoading] = useState(false); const [stationsLoading, setStationsLoading] = useState(false);
@@ -98,7 +101,6 @@ export const FleetAssets: React.FC = () => {
setTimeout(() => setToast(null), 4000); setTimeout(() => setToast(null), 4000);
}; };
// Fetch stations for the dropdown
const fetchStations = useCallback(async () => { const fetchStations = useCallback(async () => {
setStationsLoading(true); setStationsLoading(true);
try { try {
@@ -118,11 +120,14 @@ export const FleetAssets: React.FC = () => {
} }
}, [token]); }, [token]);
// Fetch registered vehicles — fleet operator endpoint
const fetchVehicles = useCallback(async () => { const fetchVehicles = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/vehicles', { const url = selectedStationFilter
? `https://teleems-api-gateway.onrender.com/v1/fleet/vehicles?station_id=${selectedStationFilter}`
: 'https://teleems-api-gateway.onrender.com/v1/fleet/vehicles';
const res = await fetch(url, {
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
}); });
const json = await res.json(); const json = await res.json();
@@ -136,7 +141,7 @@ export const FleetAssets: React.FC = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [token]); }, [token, selectedStationFilter]);
useEffect(() => { fetchVehicles(); fetchStations(); }, [fetchVehicles, fetchStations]); useEffect(() => { fetchVehicles(); fetchStations(); }, [fetchVehicles, fetchStations]);
@@ -180,7 +185,6 @@ export const FleetAssets: React.FC = () => {
} }
); );
const json = await res.json(); const json = await res.json();
console.log('[Update Vehicle] Response:', json);
if (res.ok || json?.id || json?.data?.id) { if (res.ok || json?.id || json?.data?.id) {
showToast('success', 'Vehicle updated successfully!'); showToast('success', 'Vehicle updated successfully!');
setShowEditModal(false); setShowEditModal(false);
@@ -223,8 +227,6 @@ export const FleetAssets: React.FC = () => {
}); });
const json = await res.json(); const json = await res.json();
console.log('[Register Vehicle] Response:', json);
if (res.ok || res.status === 201 || json?.id || json?.data?.id) { if (res.ok || res.status === 201 || json?.id || json?.data?.id) {
showToast('success', `Vehicle "${form.registration_number}" registered successfully!`); showToast('success', `Vehicle "${form.registration_number}" registered successfully!`);
setShowModal(false); setShowModal(false);
@@ -253,9 +255,8 @@ export const FleetAssets: React.FC = () => {
}; };
return ( return (
<div style={{ color: '#F8FAFC', position: 'relative' }}> <div style={{ color: '#0F172A', position: 'relative' }}>
{/* Toast */}
<AnimatePresence> <AnimatePresence>
{toast && ( {toast && (
<motion.div <motion.div
@@ -265,11 +266,11 @@ export const FleetAssets: React.FC = () => {
style={{ style={{
position: 'fixed', top: '24px', left: '50%', zIndex: 9999, position: 'fixed', top: '24px', left: '50%', zIndex: 9999,
padding: '14px 24px', borderRadius: '12px', display: 'flex', alignItems: 'center', gap: '10px', padding: '14px 24px', borderRadius: '12px', display: 'flex', alignItems: 'center', gap: '10px',
background: toast.type === 'success' ? 'rgba(16,185,129,0.15)' : 'rgba(239,68,68,0.15)', background: toast.type === 'success' ? '#ECFDF5' : '#FEF2F2',
border: `1px solid ${toast.type === 'success' ? 'rgba(16,185,129,0.4)' : 'rgba(239,68,68,0.4)'}`, border: `1px solid ${toast.type === 'success' ? '#10B981' : '#EF4444'}`,
color: toast.type === 'success' ? '#10B981' : '#EF4444', color: toast.type === 'success' ? '#065F46' : '#991B1B',
fontWeight: 600, fontSize: '0.875rem', backdropFilter: 'blur(12px)', fontWeight: 600, fontSize: '0.875rem',
boxShadow: '0 8px 32px rgba(0,0,0,0.4)', boxShadow: '0 8px 32px rgba(0,0,0,0.1)',
}} }}
> >
{toast.type === 'success' ? <CheckCircle size={18} /> : <AlertCircle size={18} />} {toast.type === 'success' ? <CheckCircle size={18} /> : <AlertCircle size={18} />}
@@ -278,142 +279,64 @@ export const FleetAssets: React.FC = () => {
)} )}
</AnimatePresence> </AnimatePresence>
{/* Register Vehicle Modal */}
<AnimatePresence> <AnimatePresence>
{showModal && ( {showModal && (
<motion.div <motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(4px)' }} style={{ position: 'fixed', inset: 0, background: 'rgba(15, 23, 42, 0.5)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(4px)' }}
onClick={(e) => { if (e.target === e.currentTarget) setShowModal(false); }} onClick={(e) => { if (e.target === e.currentTarget) setShowModal(false); }}
> >
<motion.div <motion.div
initial={{ scale: 0.92, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.92, opacity: 0 }} initial={{ scale: 0.92, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.92, opacity: 0 }}
style={{ background: '#0F172A', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '20px', padding: '32px', width: '560px', maxWidth: '95vw', maxHeight: '92vh', overflowY: 'auto' }} style={{ background: '#FFFFFF', borderRadius: '20px', padding: '32px', width: '560px', maxWidth: '95vw', boxShadow: '0 20px 50px rgba(0,0,0,0.1)' }}
> >
{/* Modal Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '28px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '28px' }}>
<div> <div>
<h2 style={{ fontSize: '1.4rem', fontWeight: 900, color: '#fff', margin: 0 }}>Register New Vehicle</h2> <h2 style={{ fontSize: '1.4rem', fontWeight: 900, color: '#0F172A', margin: 0 }}>Register New Vehicle</h2>
<p style={{ fontSize: '0.8rem', color: '#64748B', margin: '4px 0 0' }}>Add an ambulance to your fleet</p> <p style={{ fontSize: '0.8rem', color: '#64748B', margin: '4px 0 0' }}>Add an ambulance to your fleet</p>
</div> </div>
<button onClick={() => setShowModal(false)} style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '8px', padding: '8px', color: '#94A3B8', cursor: 'pointer', display: 'flex' }}> <button onClick={() => setShowModal(false)} style={{ background: '#F1F5F9', border: 'none', borderRadius: '8px', padding: '8px', color: '#475569', cursor: 'pointer' }}>
<X size={18} /> <X size={18} />
</button> </button>
</div> </div>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '18px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '18px' }}>
{/* Registration Number */}
<div> <div>
<label style={labelStyle}>Registration Number *</label> <label style={labelStyle}>Registration Number *</label>
<input <input style={inputStyle} required value={form.registration_number} onChange={e => setForm(f => ({ ...f, registration_number: e.target.value }))} />
style={inputStyle}
placeholder="e.g. KA-01-EMS-1234"
required
value={form.registration_number}
onChange={e => setForm(f => ({ ...f, registration_number: e.target.value }))}
/>
</div> </div>
{/* Vehicle Type */}
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<label style={labelStyle}>Vehicle Type *</label> <label style={labelStyle}>Vehicle Type *</label>
<select <select style={selectStyle} required value={form.vehicle_type} onChange={e => setForm(f => ({ ...f, vehicle_type: e.target.value }))}>
style={selectStyle} {VEHICLE_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
required
value={form.vehicle_type}
onChange={e => setForm(f => ({ ...f, vehicle_type: e.target.value }))}
>
{VEHICLE_TYPES.map(t => (
<option key={t} value={t} style={{ background: '#0F172A' }}>{t}</option>
))}
</select> </select>
<ChevronDown size={14} style={{ position: 'absolute', right: '14px', top: '38px', color: '#64748B', pointerEvents: 'none' }} />
</div> </div>
{/* Assign to Station */}
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<label style={labelStyle}>Assign to Station *</label> <label style={labelStyle}>Assign to Station *</label>
{stationsLoading ? ( <select style={selectStyle} required value={form.station_id} onChange={e => setForm(f => ({ ...f, station_id: e.target.value }))}>
<div style={{ ...inputStyle, display: 'flex', alignItems: 'center', gap: '8px', color: '#64748B' }}> <option value=""> Select a Station </option>
<Loader2 size={14} style={{ animation: 'spin 1s linear infinite' }} /> Loading stations... {stations.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
</div>
) : stations.length === 0 ? (
<div style={{ ...inputStyle, color: '#EF4444', fontSize: '0.8rem' }}>
No stations found. Please create a station first.
</div>
) : (
<>
<select
style={selectStyle}
required
value={form.station_id}
onChange={e => setForm(f => ({ ...f, station_id: e.target.value }))}
>
<option value="" style={{ background: '#0F172A' }}> Select a Station </option>
{stations.map(s => (
<option key={s.id} value={s.id} style={{ background: '#0F172A' }}>{s.name}</option>
))}
</select> </select>
<ChevronDown size={14} style={{ position: 'absolute', right: '14px', top: '38px', color: '#64748B', pointerEvents: 'none' }} />
</>
)}
</div> </div>
{/* Brand & Model */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px' }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px' }}>
<div> <div>
<label style={labelStyle}>Brand *</label> <label style={labelStyle}>Brand *</label>
<input <input style={inputStyle} required value={form.brand} onChange={e => setForm(f => ({ ...f, brand: e.target.value }))} />
style={inputStyle}
placeholder="e.g. Force Motors"
required
value={form.brand}
onChange={e => setForm(f => ({ ...f, brand: e.target.value }))}
/>
</div> </div>
<div> <div>
<label style={labelStyle}>Model *</label> <label style={labelStyle}>Model *</label>
<input <input style={inputStyle} required value={form.model} onChange={e => setForm(f => ({ ...f, model: e.target.value }))} />
style={inputStyle}
placeholder="e.g. Traveller"
required
value={form.model}
onChange={e => setForm(f => ({ ...f, model: e.target.value }))}
/>
</div> </div>
</div> </div>
{/* Chassis Number */}
<div> <div>
<label style={labelStyle}>Chassis Number</label> <label style={labelStyle}>Chassis Number</label>
<input <input style={{ ...inputStyle, fontFamily: 'monospace' }} value={form.chassis_number} onChange={e => setForm(f => ({ ...f, chassis_number: e.target.value }))} />
style={{ ...inputStyle, fontFamily: 'monospace' }}
placeholder="e.g. CH-998877"
value={form.chassis_number}
onChange={e => setForm(f => ({ ...f, chassis_number: e.target.value }))}
/>
</div> </div>
</div> </div>
{/* Actions */}
<div style={{ display: 'flex', gap: '12px', marginTop: '28px' }}> <div style={{ display: 'flex', gap: '12px', marginTop: '28px' }}>
<button <button type="button" onClick={() => setShowModal(false)} style={{ flex: 1, padding: '13px', background: '#F1F5F9', border: 'none', borderRadius: '12px', color: '#475569', cursor: 'pointer', fontWeight: 600 }}>Cancel</button>
type="button" onClick={() => setShowModal(false)} <button type="submit" disabled={submitting} style={{ flex: 2, padding: '13px', background: '#0F172A', border: 'none', borderRadius: '12px', color: '#fff', cursor: 'pointer', fontWeight: 700 }}>{submitting ? 'Registering...' : 'REGISTER VEHICLE'}</button>
style={{ flex: 1, padding: '13px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '12px', color: '#94A3B8', cursor: 'pointer', fontWeight: 600 }}
>
Cancel
</button>
<button
type="submit" disabled={submitting}
style={{ flex: 2, padding: '13px', background: submitting ? 'rgba(6,182,212,0.3)' : 'linear-gradient(135deg, #06B6D4, #3B82F6)', border: 'none', borderRadius: '12px', color: '#fff', cursor: submitting ? 'not-allowed' : 'pointer', fontWeight: 700, fontSize: '0.875rem', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', boxShadow: '0 4px 16px rgba(6,182,212,0.3)' }}
>
{submitting ? <><Loader2 size={18} className="spin" /> Registering...</> : <><Plus size={18} /> REGISTER VEHICLE</>}
</button>
</div> </div>
</form> </form>
</motion.div> </motion.div>
@@ -421,94 +344,64 @@ export const FleetAssets: React.FC = () => {
)} )}
</AnimatePresence> </AnimatePresence>
{/* Edit Vehicle Modal */}
<AnimatePresence> <AnimatePresence>
{showEditModal && editingVehicle && ( {showEditModal && editingVehicle && (
<motion.div <motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(4px)' }} style={{ position: 'fixed', inset: 0, background: 'rgba(15, 23, 42, 0.5)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(4px)' }}
onClick={(e) => { if (e.target === e.currentTarget) setShowEditModal(false); }} onClick={(e) => { if (e.target === e.currentTarget) setShowEditModal(false); }}
> >
<motion.div <motion.div
initial={{ scale: 0.92, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.92, opacity: 0 }} initial={{ scale: 0.92, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.92, opacity: 0 }}
style={{ background: '#0F172A', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '20px', padding: '32px', width: '560px', maxWidth: '95vw', maxHeight: '92vh', overflowY: 'auto' }} style={{ background: '#FFFFFF', borderRadius: '20px', padding: '32px', width: '560px', maxWidth: '95vw', boxShadow: '0 20px 50px rgba(0,0,0,0.1)' }}
> >
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '28px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '28px' }}>
<div> <div>
<h2 style={{ fontSize: '1.4rem', fontWeight: 900, color: '#fff', margin: 0 }}>Edit Vehicle</h2> <h2 style={{ fontSize: '1.4rem', fontWeight: 900, color: '#0F172A', margin: 0 }}>Edit Vehicle</h2>
<p style={{ fontSize: '0.8rem', color: '#64748B', margin: '4px 0 0' }}>Update details for {editingVehicle.registration_number}</p> <p style={{ fontSize: '0.8rem', color: '#64748B', margin: '4px 0 0' }}>Update details for {editingVehicle.registration_number}</p>
</div> </div>
<button onClick={() => setShowEditModal(false)} style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '8px', padding: '8px', color: '#94A3B8', cursor: 'pointer', display: 'flex' }}> <button onClick={() => setShowEditModal(false)} style={{ background: '#F1F5F9', border: 'none', borderRadius: '8px', padding: '8px', color: '#475569', cursor: 'pointer' }}>
<X size={18} /> <X size={18} />
</button> </button>
</div> </div>
<form onSubmit={handleUpdate}> <form onSubmit={handleUpdate}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '18px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '18px' }}>
<div> <div>
<label style={labelStyle}>Registration Number *</label> <label style={labelStyle}>Registration Number *</label>
<input style={inputStyle} required value={editForm.registration_number} <input style={inputStyle} required value={editForm.registration_number} onChange={e => setEditForm(f => ({ ...f, registration_number: e.target.value }))} />
onChange={e => setEditForm(f => ({ ...f, registration_number: e.target.value }))} />
</div> </div>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<label style={labelStyle}>Vehicle Type *</label> <label style={labelStyle}>Vehicle Type *</label>
<select style={selectStyle} required value={editForm.vehicle_type} <select style={selectStyle} required value={editForm.vehicle_type} onChange={e => setEditForm(f => ({ ...f, vehicle_type: e.target.value }))}>
onChange={e => setEditForm(f => ({ ...f, vehicle_type: e.target.value }))}> {VEHICLE_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
{VEHICLE_TYPES.map(t => <option key={t} value={t} style={{ background: '#0F172A' }}>{t}</option>)}
</select> </select>
<ChevronDown size={14} style={{ position: 'absolute', right: '14px', top: '38px', color: '#64748B', pointerEvents: 'none' }} />
</div> </div>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<label style={labelStyle}>Assign to Station</label> <label style={labelStyle}>Assign to Station</label>
{stationsLoading ? ( <select style={selectStyle} value={editForm.station_id} onChange={e => setEditForm(f => ({ ...f, station_id: e.target.value }))}>
<div style={{ ...inputStyle, display: 'flex', alignItems: 'center', gap: '8px', color: '#64748B' }}> <option value=""> Select a Station </option>
<Loader2 size={14} style={{ animation: 'spin 1s linear infinite' }} /> Loading stations... {stations.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
</div>
) : (
<>
<select style={selectStyle} value={editForm.station_id}
onChange={e => setEditForm(f => ({ ...f, station_id: e.target.value }))}>
<option value="" style={{ background: '#0F172A' }}> Select a Station </option>
{stations.map(s => <option key={s.id} value={s.id} style={{ background: '#0F172A' }}>{s.name}</option>)}
</select> </select>
<ChevronDown size={14} style={{ position: 'absolute', right: '14px', top: '38px', color: '#64748B', pointerEvents: 'none' }} />
</>
)}
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px' }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px' }}>
<div> <div>
<label style={labelStyle}>Brand *</label> <label style={labelStyle}>Brand *</label>
<input style={inputStyle} required value={editForm.brand} <input style={inputStyle} required value={editForm.brand} onChange={e => setEditForm(f => ({ ...f, brand: e.target.value }))} />
onChange={e => setEditForm(f => ({ ...f, brand: e.target.value }))} />
</div> </div>
<div> <div>
<label style={labelStyle}>Model *</label> <label style={labelStyle}>Model *</label>
<input style={inputStyle} required value={editForm.model} <input style={inputStyle} required value={editForm.model} onChange={e => setEditForm(f => ({ ...f, model: e.target.value }))} />
onChange={e => setEditForm(f => ({ ...f, model: e.target.value }))} />
</div> </div>
</div> </div>
<div> <div>
<label style={labelStyle}>Chassis Number</label> <label style={labelStyle}>Chassis Number</label>
<input style={{ ...inputStyle, fontFamily: 'monospace' }} value={editForm.chassis_number} <input style={{ ...inputStyle, fontFamily: 'monospace' }} value={editForm.chassis_number} onChange={e => setEditForm(f => ({ ...f, chassis_number: e.target.value }))} />
onChange={e => setEditForm(f => ({ ...f, chassis_number: e.target.value }))} />
</div> </div>
</div> </div>
<div style={{ display: 'flex', gap: '12px', marginTop: '28px' }}> <div style={{ display: 'flex', gap: '12px', marginTop: '28px' }}>
<button type="button" onClick={() => setShowEditModal(false)} <button type="button" onClick={() => setShowEditModal(false)} style={{ flex: 1, padding: '13px', background: '#F1F5F9', border: 'none', borderRadius: '12px', color: '#475569', cursor: 'pointer', fontWeight: 600 }}>Cancel</button>
style={{ flex: 1, padding: '13px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '12px', color: '#94A3B8', cursor: 'pointer', fontWeight: 600 }}> <button type="submit" disabled={submitting} style={{ flex: 2, padding: '13px', background: '#0F172A', border: 'none', borderRadius: '12px', color: '#fff', cursor: 'pointer', fontWeight: 700 }}>{submitting ? 'Updating...' : 'UPDATE VEHICLE'}</button>
Cancel
</button>
<button type="submit" disabled={submitting}
style={{ flex: 2, padding: '13px', background: submitting ? 'rgba(6,182,212,0.3)' : 'linear-gradient(135deg, #06B6D4, #3B82F6)', border: 'none', borderRadius: '12px', color: '#fff', cursor: submitting ? 'not-allowed' : 'pointer', fontWeight: 700, fontSize: '0.875rem', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', boxShadow: '0 4px 16px rgba(6,182,212,0.3)' }}>
{submitting ? <><Loader2 size={18} className="spin" /> Updating...</> : <><Edit2 size={18} /> UPDATE VEHICLE</>}
</button>
</div> </div>
</form> </form>
</motion.div> </motion.div>
@@ -516,44 +409,123 @@ export const FleetAssets: React.FC = () => {
)} )}
</AnimatePresence> </AnimatePresence>
{/* Header */} <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px', flexWrap: 'wrap', gap: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}> <div style={{ display: 'flex', gap: '12px', alignItems: 'center', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', background: 'rgba(255,255,255,0.03)', padding: '8px 16px', borderRadius: '12px', border: '1px solid rgba(255,255,255,0.06)' }}> <div style={{
<Search size={16} color="#64748B" /> display: 'flex',
alignItems: 'center',
gap: '10px',
background: '#FFFFFF',
padding: '10px 16px',
borderRadius: '12px',
border: isSearchFocused ? '1px solid #06B6D4' : '1px solid #CBD5E1',
boxShadow: isSearchFocused ? '0 0 0 3px rgba(6, 182, 212, 0.15)' : '0 2px 4px rgba(15, 23, 42, 0.01)',
transition: 'all 0.2s ease',
width: '280px',
position: 'relative'
}}>
<Search size={16} color={isSearchFocused ? "#06B6D4" : "#64748B"} style={{ transition: 'color 0.2s' }} />
<input <input
type="text" type="text"
placeholder="Search vehicles..." placeholder="Search vehicles..."
style={{ background: 'transparent', border: 'none', color: '#fff', fontSize: '0.875rem', outline: 'none', width: '240px' }}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setIsSearchFocused(true)}
onBlur={() => setIsSearchFocused(false)}
className="stations-search-input"
style={{
background: 'transparent',
border: 'none',
color: '#0F172A',
fontSize: '0.875rem',
outline: 'none',
width: '100%',
paddingRight: searchQuery ? '24px' : '0'
}}
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
style={{
position: 'absolute',
right: '12px',
background: 'transparent',
border: 'none',
cursor: 'pointer',
color: '#94A3B8',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2px',
borderRadius: '50%',
}}
onMouseEnter={e => e.currentTarget.style.color = '#475569'}
onMouseLeave={e => e.currentTarget.style.color = '#94A3B8'}
>
<X size={14} />
</button>
)}
</div>
<div style={{ position: 'relative', display: 'inline-block' }}>
<select
value={selectedStationFilter}
onChange={(e) => setSelectedStationFilter(e.target.value)}
style={{
background: '#FFFFFF',
border: '1px solid #CBD5E1',
padding: '11px 36px 11px 16px',
borderRadius: '12px',
color: '#0F172A',
fontSize: '0.875rem',
outline: 'none',
cursor: 'pointer',
appearance: 'none',
WebkitAppearance: 'none',
minWidth: '200px',
boxShadow: '0 2px 4px rgba(15, 23, 42, 0.01)',
}}
>
<option value="">📁 All Stations</option>
{stations.map(s => (
<option key={s.id} value={s.id}>📍 {s.name}</option>
))}
</select>
<ChevronDown
size={16}
color="#64748B"
style={{
position: 'absolute',
right: '12px',
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'none'
}}
/> />
</div> </div>
</div>
<button <button
onClick={openModal} onClick={openModal}
style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '11px 22px', background: 'linear-gradient(135deg, #06B6D4, #3B82F6)', border: 'none', borderRadius: '12px', color: '#fff', fontWeight: 700, cursor: 'pointer', boxShadow: '0 4px 12px rgba(6,182,212,0.3)', fontSize: '0.875rem' }} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '11px 22px', background: '#0F172A', border: 'none', borderRadius: '12px', color: '#fff', fontWeight: 700, cursor: 'pointer', fontSize: '0.875rem' }}
> >
<Plus size={18} /> REGISTER NEW VEHICLE <Plus size={18} /> REGISTER NEW VEHICLE
</button> </button>
</div> </div>
{/* Loading */}
{loading && ( {loading && (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '200px', gap: '12px', color: '#64748B' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '200px', color: '#64748B' }}>
<Loader2 size={24} style={{ animation: 'spin 1s linear infinite' }} /> <Loader2 size={24} className="spin" />
<span>Loading vehicles...</span>
</div> </div>
)} )}
{/* Empty state */}
{!loading && filtered.length === 0 && ( {!loading && filtered.length === 0 && (
<div style={{ textAlign: 'center', padding: '60px 24px', color: '#64748B' }}> <div style={{ textAlign: 'center', padding: '60px 24px', color: '#64748B' }}>
<Truck size={48} style={{ opacity: 0.2, marginBottom: '16px' }} /> <Truck size={48} style={{ opacity: 0.2, marginBottom: '16px' }} />
<h3 style={{ fontSize: '1.1rem', fontWeight: 700, color: '#94A3B8', marginBottom: '8px' }}>No Vehicles Registered</h3> <h3 style={{ fontSize: '1.1rem', fontWeight: 700, color: '#475569', marginBottom: '8px' }}>No Vehicles Found</h3>
<p style={{ fontSize: '0.875rem' }}>Click "Register New Vehicle" to add your first ambulance.</p>
</div> </div>
)} )}
{/* Vehicle Cards */}
{!loading && filtered.length > 0 && ( {!loading && filtered.length > 0 && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: '20px' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: '20px' }}>
{filtered.map(v => { {filtered.map(v => {
@@ -561,39 +533,22 @@ export const FleetAssets: React.FC = () => {
return ( return (
<motion.div <motion.div
key={v.id} key={v.id}
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }}
animate={{ opacity: 1, y: 0 }} style={{ ...cardStyle, borderLeft: `4px solid ${sc.color}` }}
style={{ ...cardStyle, position: 'relative', overflow: 'hidden', transition: 'transform 0.2s, box-shadow 0.2s', cursor: 'pointer' }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.transform = 'translateY(-4px)'; (e.currentTarget as HTMLElement).style.boxShadow = '0 12px 32px rgba(6,182,212,0.1)'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = 'translateY(0)'; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
> >
<div style={{ position: 'absolute', top: 0, left: 0, width: '4px', height: '100%', background: sc.color, borderRadius: '4px 0 0 4px' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '16px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '16px' }}>
<div> <div>
<div style={{ fontSize: '0.65rem', fontWeight: 900, color: '#06B6D4', marginBottom: '4px' }}>{v.vehicle_type} UNIT</div> <div style={{ fontSize: '0.65rem', fontWeight: 900, color: '#64748B', marginBottom: '4px' }}>{v.vehicle_type} UNIT</div>
<h3 style={{ fontSize: '1rem', fontWeight: 800, color: '#fff', margin: 0 }}>{v.registration_number}</h3> <h3 style={{ fontSize: '1rem', fontWeight: 800, color: '#0F172A', margin: 0 }}>{v.registration_number}</h3>
{(v.brand || v.model) && ( <div style={{ fontSize: '0.78rem', color: '#475569', marginTop: '4px' }}>{v.brand} {v.model}</div>
<div style={{ fontSize: '0.78rem', color: '#94A3B8', marginTop: '4px' }}>{v.brand} {v.model}</div>
)}
</div> </div>
{v.status && ( <div style={{ padding: '4px 10px', borderRadius: '6px', fontSize: '0.65rem', fontWeight: 900, background: sc.bg, color: sc.color }}>{v.status}</div>
<div style={{ padding: '4px 10px', borderRadius: '6px', fontSize: '0.65rem', fontWeight: 900, background: sc.bg, color: sc.color, border: `1px solid ${sc.border}` }}>
{v.status}
</div> </div>
)}
</div>
{v.chassis_number && (
<div style={{ fontSize: '0.72rem', color: '#475569', fontFamily: 'monospace', marginBottom: '12px' }}>
CH: {v.chassis_number}
</div>
)}
<button <button
onClick={e => { e.stopPropagation(); openEditModal(v); }} onClick={() => openEditModal(v)}
style={{ width: '100%', padding: '9px', background: 'rgba(6,182,212,0.08)', border: '1px solid rgba(6,182,212,0.2)', borderRadius: '8px', color: '#06B6D4', fontSize: '0.75rem', fontWeight: 600, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', marginTop: '4px' }} style={{ width: '100%', padding: '9px', background: '#F1F5F9', border: 'none', borderRadius: '8px', color: '#0F172A', fontSize: '0.75rem', fontWeight: 700, cursor: 'pointer' }}
> >
<Edit2 size={13} /> EDIT VEHICLE EDIT DETAILS
</button> </button>
</motion.div> </motion.div>
); );

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,389 @@
/* ─── FLEET OPERATOR LIGHT THEME OVERRIDES ─────────────────────────────────── */
/* 1. Main Dashboard Shell Background & Text Colors */
.fleet-dashboard-container {
background: #F8FAFC !important;
/* Elegant light background */
color: #1E293B !important;
/* Deep slate gray for body text */
}
/* 2. Top Navigation Bar */
.fleet-dashboard-header {
background: rgba(255, 255, 255, 0.85) !important;
border-bottom: 1px solid rgba(15, 23, 42, 0.08) !important;
backdrop-filter: blur(12px) !important;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.02) !important;
}
.fleet-dashboard-header h1 {
color: #0F172A !important;
}
.fleet-dashboard-header .desc,
.fleet-dashboard-header div[style*="color: #94A3B8"],
.fleet-dashboard-header div[style*="color:#94A3B8"] {
color: #475569 !important;
}
/* 3. Search Bar and Wrapper in Header */
.fleet-search-container,
div[style*="background: rgba(255, 255, 255, 0.03)"][style*="border: 1px solid rgba(255, 255, 255, 0.06)"],
div[style*="background:rgba(255,255,255,0.03)"][style*="border:1px solid rgba(255,255,255,0.06)"] {
background: #F1F5F9 !important;
border: 1px solid #CBD5E1 !important;
}
.fleet-search-container input,
input[placeholder*="Search"] {
color: #0F172A !important;
}
.fleet-search-container input::placeholder,
input[placeholder*="Search"]::placeholder {
color: #94A3B8 !important;
}
/* 4. Action Buttons in Header */
.fleet-header-btn,
button[style*="background: rgba(255, 255, 255, 0.03)"][style*="border: 1px solid rgba(255, 255, 255, 0.06)"],
button[style*="background:rgba(255, 255,255,0.03)"][style*="border:1px solid rgba(255,255,255,0.06)"] {
background: #FFFFFF !important;
border: 1px solid #E2E8F0 !important;
color: #475569 !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02) !important;
}
.fleet-header-btn:hover {
background: #F8FAFC !important;
color: #0F172A !important;
}
/* Vertical divider in header */
.fleet-header-divider {
background: #E2E8F0 !important;
}
/* Profile username & user details in header */
.fleet-profile-name,
.fleet-profile-title,
div[style*="color: #fff"][style*="font-size: 0.8rem"],
div[style*="color:#fff"][style*="font-size:0.8rem"],
div[style*="font-size: 0.8rem"][style*="color: #fff"],
div[style*="font-size:0.8rem"][style*="color:#fff"] {
color: #0F172A !important;
}
/* 5. Sub-page Tab Toggle Selection Pill Containers */
div[style*="background: rgba(255, 255, 255, 0.03)"][style*="border: 1px solid rgba(255, 255, 255, 0.06)"][style*="padding: 4px"],
div[style*="background:rgba(255,255,255,0.03)"][style*="border:1px solid rgba(255,255,255,0.06)"][style*="padding:4px"],
div[style*="background: rgba(255, 255, 255, 0.03)"][style*="border: 1px solid rgba(255, 255, 255, 0.06)"][style*="padding: 3px"],
div[style*="background:rgba(255,255,255,0.03)"][style*="border:1px solid rgba(255,255,255,0.06)"][style*="padding:3px"] {
background: #E2E8F0 !important;
border: 1px solid #CBD5E1 !important;
}
/* Unselected buttons in Tab Toggles */
div[style*="background: rgba(255, 255, 255, 0.03)"] button[style*="background: transparent"],
div[style*="background:rgba(255,255,255,0.03)"] button[style*="background:transparent"],
div[style*="background: rgba(255, 255, 255, 0.03)"] button:not([style*="background: linear-gradient"]),
div[style*="background:rgba(255,255,255,0.03)"] button:not([style*="background:linear-gradient"]) {
color: #475569 !important;
}
/* 6. Cards and Main Content Panels (Overriding semi-transparent dark blocks) */
div[style*="background: rgba(255, 255, 255, 0.03)"][style*="border: 1px solid rgba(255, 255, 255, 0.08)"],
div[style*="background:rgba(255,255,255,0.03)"][style*="border:1px solid rgba(255,255,255,0.08)"],
div[style*="background: rgba(255, 255, 255, 0.02)"][style*="border: 1px solid rgba(255, 255, 255, 0.07)"],
div[style*="background:rgba(255,255,255,0.02)"][style*="border:1px solid rgba(255,255,255,0.07)"],
div[style*="background: rgba(255, 255, 255, 0.01)"][style*="border: 1px solid rgba(255, 255, 255, 0.08)"],
div[style*="background:rgba(255,255,255,0.01)"][style*="border:1px solid rgba(255,255,255,0.08)"],
div[style*="background: rgba(255,255,255,0.03)"][style*="border: 1px solid rgba(255,255,255,0.07)"],
div[style*="background:rgba(255,255,255,0.03)"][style*="border:1px solid rgba(255,255,255,0.07)"],
div[style*="background: rgba(255,255,255,0.02)"][style*="border: 1px solid rgba(255,255,255,0.06)"],
div[style*="background:rgba(255,255,255,0.02)"][style*="border:1px solid rgba(255,255,255,0.06)"],
.glass {
background: #FFFFFF !important;
border: 1px solid rgba(15, 23, 42, 0.08) !important;
box-shadow: 0 4px 20px rgba(15, 23, 42, 0.03), 0 2px 8px rgba(15, 23, 42, 0.01) !important;
}
/* Card titles inside components */
.fleet-dashboard-content h1,
.fleet-dashboard-content h2,
.fleet-dashboard-content h3,
.fleet-dashboard-content h4,
.fleet-dashboard-content .card-title {
color: #0F172A !important;
}
/* Overriding white texts to clean slate-dark */
span[style*="color: #fff"],
span[style*="color:#fff"],
div[style*="color: #fff"],
div[style*="color:#fff"],
h3[style*="color: #fff"],
h3[style*="color:#fff"],
h1[style*="color: #fff"],
h1[style*="color:#fff"] {
color: #0F172A !important;
}
/* Secondary descriptions */
div[style*="color: #E2E8F0"],
div[style*="color:#E2E8F0"],
span[style*="color: #E2E8F0"],
span[style*="color:#E2E8F0"],
div[style*="color: #F1F5F9"],
div[style*="color:#F1F5F9"] {
color: #1E293B !important;
}
/* Muted labels & dates */
div[style*="color: #94A3B8"],
div[style*="color:#94A3B8"],
span[style*="color: #94A3B8"],
span[style*="color:#94A3B8"],
p[style*="color: #94A3B8"],
p[style*="color:#94A3B8"],
div[style*="color: #64748B"],
div[style*="color:#64748B"] {
color: #475569 !important;
}
/* 7. Inner Stats Grid and Blocks (e.g. Station detail stats cards) */
div[style*="background: #040B16"],
div[style*="background:#040B16"] {
background: #F1F5F9 !important;
border-color: #CBD5E1 !important;
}
div[style*="background: #040B16"] div[style*="color: #fff"],
div[style*="background:#040B16"] div[style*="color:#fff"] {
color: #0F172A !important;
}
/* Separator lines in cards */
div[style*="border-bottom: 1px solid rgba(255,255,255,0.04)"],
div[style*="border-bottom:1px solid rgba(255,255,255,0.04)"],
div[style*="border-top: 1px solid rgba(255,255,255,0.04)"],
div[style*="border-top:1px solid rgba(255,255,255,0.04)"],
div[style*="border-top: 1px solid rgba(255,255,255,0.06)"],
div[style*="border-top:1px solid rgba(255,255,255,0.06)"] {
border-color: #E2E8F0 !important;
}
/* 8. Forms & Inputs */
/* Field Labels */
label[style*="color: #64748B"],
label[style*="color:#64748B"],
label[style*="color: #475569"],
label[style*="color:#475569"] {
color: #475569 !important;
}
/* Input Fields and Select Dropdowns */
input[style*="background: rgba(255, 255, 255, 0.04)"],
input[style*="background:rgba(255,255,255,0.04)"],
select[style*="background: rgba(255, 255, 255, 0.04)"],
select[style*="background:rgba(255,255,255,0.04)"],
textarea[style*="background: rgba(255, 255, 255, 0.04)"],
textarea[style*="background:rgba(255,255,255,0.04)"] {
background: #FFFFFF !important;
border: 1px solid #CBD5E1 !important;
color: #0F172A !important;
}
input[style*="background: rgba(255, 255, 255, 0.04)"]:focus,
select[style*="background: rgba(255, 255, 255, 0.04)"]:focus,
textarea[style*="background: rgba(255, 255, 255, 0.04)"]:focus {
border-color: #06B6D4 !important;
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.1) !important;
}
/* Select options backgrounds */
option[style*="background: #0F172A"],
option[style*="background:#0F172A"],
option[style*="background: #0D1526"],
option[style*="background:#0D1526"] {
background: #FFFFFF !important;
color: #0F172A !important;
}
/* Optgroups */
optgroup {
background: #FFFFFF !important;
color: #0F172A !important;
}
/* 9. Secondary Buttons (e.g. Cancel, Edit, Sync, etc) */
button[style*="background: rgba(255, 255, 255, 0.04)"],
button[style*="background:rgba(255,255,255,0.04)"],
button[style*="background: rgba(255,255,255,0.03)"],
button[style*="background:rgba(255,255,255,0.03)"] {
background: #FFFFFF !important;
border: 1px solid #CBD5E1 !important;
color: #475569 !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02) !important;
}
button[style*="background: rgba(255, 255, 255, 0.04)"]:hover,
button[style*="background:rgba(255,255,255,0.04)"]:hover {
background: #F8FAFC !important;
color: #0F172A !important;
}
/* 10. Modals */
/* Background backdrop layer */
div[style*="background: rgba(0,0,0,0.7)"],
div[style*="background:rgba(0,0,0,0.7)"],
div[style*="background: rgba(0,0,0,0.75)"],
div[style*="background:rgba(0,0,0,0.75)"] {
background: rgba(15, 23, 42, 0.3) !important;
backdrop-filter: blur(8px) !important;
}
/* Modal box wrapper styling */
div[style*="background: #0F172A"][style*="border: 1px solid rgba(255,255,255,0.1)"],
div[style*="background:#0F172A"][style*="border:1px solid rgba(255,255,255,0.1)"],
div[style*="background: #0D1526"][style*="border: 1px solid rgba(255,255,255,0.1)"],
div[style*="background:#0D1526"][style*="border:1px solid rgba(255,255,255,0.1)"] {
background: #FFFFFF !important;
border: 1px solid rgba(15, 23, 42, 0.08) !important;
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.12) !important;
color: #1E293B !important;
}
/* Modal Inner Header */
div[style*="background: linear-gradient(135deg,rgba(6,182,212,0.12),transparent)"],
div[style*="background:linear-gradient(135deg,rgba(6,182,212,0.12),transparent)"] {
background: linear-gradient(135deg, rgba(6, 182, 212, 0.06), transparent) !important;
border-bottom: 1px solid rgba(15, 23, 42, 0.06) !important;
}
/* Modal close button */
div[style*="background: #0F172A"] button[style*="background: rgba(255, 255, 255, 0.05)"],
div[style*="background:#0F172A"] button[style*="background:rgba(255,255,255,0.05)"],
div[style*="background: #0D1526"] button[style*="background: rgba(255, 255, 255, 0.06)"],
div[style*="background:#0D1526"] button[style*="background:rgba(255,255,255,0.06)"] {
background: #F1F5F9 !important;
border: 1px solid #E2E8F0 !important;
color: #475569 !important;
}
/* GPS input coordination section in modal */
div[style*="background: rgba(255,255,255,0.03)"][style*="border: 1px solid rgba(255,255,255,0.06)"],
div[style*="background:rgba(255,255,255,0.03)"][style*="border:1px solid rgba(255,255,255,0.06)"],
div[style*="background: rgba(255,255,255,0.03)"][style*="border: 1px solid rgba(16,185,129,0.3)"] {
background: #F1F5F9 !important;
border-color: #E2E8F0 !important;
color: #475569 !important;
}
/* 11. Tables inside components */
table th {
color: #475569 !important;
background: #F1F5F9 !important;
border-bottom: 1px solid #CBD5E1 !important;
}
table tr {
border-bottom: 1px solid #E2E8F0 !important;
}
table tr:hover {
background: #F8FAFC !important;
}
/* 12. Crew Scheduling Page Specific Items */
/* Roster timeline assignments list containers */
div[style*="background: rgba(6,182,212,0.04)"][style*="border: 1px solid rgba(6,182,212,0.08)"],
div[style*="background:rgba(6,182,212,0.04)"][style*="border:1px solid rgba(6,182,212,0.08)"] {
background: rgba(6, 182, 212, 0.06) !important;
border: 1px solid rgba(6, 182, 212, 0.15) !important;
}
div[style*="background: rgba(59,130,246,0.04)"][style*="border: 1px solid rgba(59,130,246,0.08)"],
div[style*="background:rgba(59,130,246,0.04)"][style*="border:1px solid rgba(59,130,246,0.08)"] {
background: rgba(59, 130, 246, 0.06) !important;
border: 1px solid rgba(59, 130, 246, 0.15) !important;
}
div[style*="background: rgba(16,185,129,0.04)"][style*="border: 1px solid rgba(16,185,129,0.08)"],
div[style*="background:rgba(16,185,129,0.04)"][style*="border:1px solid rgba(16,185,129,0.08)"] {
background: rgba(16, 185, 129, 0.06) !important;
border: 1px solid rgba(16, 185, 129, 0.15) !important;
}
/* 13. Empty / Placeholder States */
div[style*="border: 1px dashed rgba(255,255,255,0.08)"],
div[style*="border:1px dashed rgba(255,255,255,0.08)"],
div[style*="border: 1px dashed rgba(255,255,255,0.1)"],
div[style*="border:1px dashed rgba(255,255,255,0.1)"] {
background: #FFFFFF !important;
border: 1px dashed #CBD5E1 !important;
}
/* 14. Broad Wildcard Overrides (Ensuring 100% full light theme coverage) */
.fleet-dashboard-content div[style*="rgba(255, 255, 255, 0.0"],
.fleet-dashboard-content div[style*="rgba(255,255,255,0.0"],
.fleet-dashboard-content div[style*="rgba(255, 255, 255, 0.1"],
.fleet-dashboard-content div[style*="rgba(255,255,255,0.1"] {
background: #FFFFFF !important;
border-color: rgba(15, 23, 42, 0.08) !important;
color: #1E293B !important;
}
.fleet-dashboard-content div[style*="color: #fff"],
.fleet-dashboard-content div[style*="color:#fff"],
.fleet-dashboard-content div[style*="color: #FFFFFF"],
.fleet-dashboard-content div[style*="color:#FFFFFF"],
.fleet-dashboard-content div[style*="color: rgb(255, 255, 255)"],
.fleet-dashboard-content p[style*="color: #fff"],
.fleet-dashboard-content p[style*="color:#fff"],
.fleet-dashboard-content span[style*="color: #fff"],
.fleet-dashboard-content span[style*="color:#fff"] {
color: #0F172A !important;
}
.fleet-dashboard-content div[style*="color: rgb(148, 163, 184)"],
.fleet-dashboard-content div[style*="color: #94A3B8"],
.fleet-dashboard-content div[style*="color:#94A3B8"] {
color: #475569 !important;
}
.fleet-dashboard-content input,
.fleet-dashboard-content select,
.fleet-dashboard-content textarea {
background: #FFFFFF !important;
border: 1px solid #CBD5E1 !important;
color: #0F172A !important;
}
/* 15. Global Uniform Font Family */
.fleet-dashboard-container,
.fleet-dashboard-container *,
.sidebar,
.sidebar * {
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
}
/* 16. Custom Stations Search Input override (High-specificity to beat broad content overrides) */
.fleet-dashboard-content .stations-search-input,
.fleet-dashboard-content input.stations-search-input {
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
margin: 0 !important;
width: 100% !important;
color: #0F172A !important;
}
.fleet-dashboard-content .stations-search-input:focus,
.fleet-dashboard-content input.stations-search-input:focus {
background: transparent !important;
border: none !important;
box-shadow: none !important;
outline: none !important;
}

View File

@@ -39,15 +39,16 @@ interface StationForm {
} }
const cardStyle: React.CSSProperties = { const cardStyle: React.CSSProperties = {
background: 'rgba(255,255,255,0.03)', background: '#FFFFFF',
border: '1px solid rgba(255,255,255,0.08)', border: '1px solid rgba(15, 23, 42, 0.08)',
borderRadius: '16px', borderRadius: '16px',
padding: '24px', padding: '24px',
boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)',
}; };
const labelStyle: React.CSSProperties = { const labelStyle: React.CSSProperties = {
fontSize: '0.7rem', fontSize: '0.7rem',
color: '#64748B', color: '#475569',
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: '1px', letterSpacing: '1px',
marginBottom: '6px', marginBottom: '6px',
@@ -55,11 +56,11 @@ const labelStyle: React.CSSProperties = {
}; };
const inputStyle: React.CSSProperties = { const inputStyle: React.CSSProperties = {
background: 'rgba(255,255,255,0.04)', background: '#FFFFFF',
border: '1px solid rgba(255,255,255,0.08)', border: '1px solid #CBD5E1',
padding: '11px 14px', padding: '11px 14px',
borderRadius: '10px', borderRadius: '10px',
color: '#fff', color: '#0F172A',
fontSize: '0.875rem', fontSize: '0.875rem',
outline: 'none', outline: 'none',
width: '100%', width: '100%',
@@ -77,7 +78,9 @@ const EMPTY_FORM: StationForm = {
export const FleetOrganization: React.FC = () => { export const FleetOrganization: React.FC = () => {
const [activeTab, setActiveTab] = useState<'stations' | 'profile'>('stations'); const [activeTab, setActiveTab] = useState<'stations' | 'profile'>('stations');
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [isSearchFocused, setIsSearchFocused] = useState(false);
const [stations, setStations] = useState<Station[]>([]); const [stations, setStations] = useState<Station[]>([]);
const [selectedStationForManifest, setSelectedStationForManifest] = useState<Station | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [editingStationId, setEditingStationId] = useState<string | null>(null); const [editingStationId, setEditingStationId] = useState<string | null>(null);
@@ -140,14 +143,57 @@ export const FleetOrganization: React.FC = () => {
else if (json?.data && Array.isArray(json.data)) list = json.data; else if (json?.data && Array.isArray(json.data)) list = json.data;
else if (Array.isArray(json)) list = json; else if (Array.isArray(json)) list = json;
setStations(list); // 2. Fetch Vehicles for each station in parallel using the specific URL: /v1/fleet/vehicles?station_id=...
const mergedList = await Promise.all(
list.map(async (s) => {
let stationVehicles: any[] = [];
try {
const vehiclesUrl = `https://teleems-api-gateway.onrender.com/v1/fleet/vehicles?station_id=${s.id}`;
const vehiclesRes = await fetch(vehiclesUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
}
});
if (vehiclesRes.ok) {
const vJson = await vehiclesRes.json();
stationVehicles = vJson?.data?.data || vJson?.data || (Array.isArray(vJson) ? vJson : []);
}
} catch (err) {
console.error(`Failed to fetch vehicles for station ${s.id}:`, err);
}
let staffSet = new Set<string>();
stationVehicles.forEach((v: any) => {
if (v.activeRoster) {
const r = v.activeRoster;
if (r.driver?.user?.name) staffSet.add(r.driver.user.name);
if (r.staff?.user?.name) staffSet.add(r.staff.user.name);
}
if (v.activeShift) {
const sh = v.activeShift;
if (sh.driver?.user?.name) staffSet.add(sh.driver.user.name);
if (sh.staff?.user?.name) staffSet.add(sh.staff.user.name);
}
});
return {
...s,
vehiclesAssigned: stationVehicles.length,
staffAssigned: staffSet.size
};
})
);
setStations(mergedList);
} catch (e) { } catch (e) {
setFetchError('Network error — unable to reach the server. Check your connection.'); setFetchError('Network error — unable to reach the server. Check your connection.');
console.error('Failed to fetch stations:', e); console.error('Failed to fetch stations:', e);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, []); }, [organisationId]);
useEffect(() => { fetchStations(); }, [fetchStations]); useEffect(() => { fetchStations(); }, [fetchStations]);
@@ -361,19 +407,19 @@ export const FleetOrganization: React.FC = () => {
{showModal && ( {showModal && (
<motion.div <motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', zIndex: 10000, display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(4px)' }} style={{ position: 'fixed', inset: 0, background: 'rgba(15, 23, 42, 0.3)', zIndex: 10000, display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(4px)' }}
onClick={(e) => { if (e.target === e.currentTarget) setShowModal(false); }} onClick={(e) => { if (e.target === e.currentTarget) setShowModal(false); }}
> >
<motion.div <motion.div
initial={{ scale: 0.92, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.92, opacity: 0 }} initial={{ scale: 0.92, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.92, opacity: 0 }}
style={{ background: '#0F172A', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '20px', padding: '24px 32px', width: '880px', maxWidth: '95vw', maxHeight: '96vh', overflowY: 'hidden', display: 'flex', flexDirection: 'column' }} style={{ background: '#FFFFFF', border: '1px solid rgba(15, 23, 42, 0.08)', borderRadius: '20px', padding: '24px 32px', width: '880px', maxWidth: '95vw', maxHeight: '96vh', overflowY: 'hidden', display: 'flex', flexDirection: 'column', boxShadow: '0 20px 50px rgba(15, 23, 42, 0.12)' }}
> >
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', flexShrink: 0 }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', flexShrink: 0 }}>
<div> <div>
<h2 style={{ fontSize: '1.3rem', fontWeight: 900, color: '#fff', margin: 0 }}>{editingStationId ? 'Edit Station' : 'Add New Station'}</h2> <h2 style={{ fontSize: '1.3rem', fontWeight: 900, color: '#0F172A', margin: 0 }}>{editingStationId ? 'Edit Station' : 'Add New Station'}</h2>
<p style={{ fontSize: '0.78rem', color: '#64748B', margin: '2px 0 0' }}>{editingStationId ? 'Update dispatch station details' : 'Create a new dispatch station'}</p> <p style={{ fontSize: '0.78rem', color: '#475569', margin: '2px 0 0' }}>{editingStationId ? 'Update dispatch station details' : 'Create a new dispatch station'}</p>
</div> </div>
<button type="button" onClick={() => setShowModal(false)} style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '8px', padding: '8px', color: '#94A3B8', cursor: 'pointer', display: 'flex' }}> <button type="button" onClick={() => setShowModal(false)} style={{ background: '#F1F5F9', border: '1px solid #E2E8F0', borderRadius: '8px', padding: '8px', color: '#475569', cursor: 'pointer', display: 'flex' }}>
<X size={18} /> <X size={18} />
</button> </button>
</div> </div>
@@ -405,8 +451,8 @@ export const FleetOrganization: React.FC = () => {
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div> <div>
<label style={labelStyle}>GPS Location Click on map to set pin</label> <label style={labelStyle}>GPS Location Click on map to set pin</label>
<div style={{ position: 'relative', borderRadius: '12px', overflow: 'hidden', border: '1px solid rgba(255,255,255,0.08)' }}> <div style={{ position: 'relative', borderRadius: '12px', overflow: 'hidden', border: '1px solid rgba(15, 23, 42, 0.08)' }}>
<div ref={mapContainerRef} style={{ height: '210px', width: '100%', background: '#1E293B' }} /> <div ref={mapContainerRef} style={{ height: '210px', width: '100%', background: '#F1F5F9' }} />
{/* Overlay UI */} {/* Overlay UI */}
<button <button
type="button" type="button"
@@ -419,7 +465,7 @@ export const FleetOrganization: React.FC = () => {
</button> </button>
</div> </div>
<div style={{ marginTop: '8px' }}> <div style={{ marginTop: '8px' }}>
<div style={{ padding: '8px 12px', borderRadius: '8px', background: gpsCoords ? 'rgba(16,185,129,0.08)' : 'rgba(255,255,255,0.03)', border: `1px solid ${gpsCoords ? 'rgba(16,185,129,0.3)' : 'rgba(255,255,255,0.06)'}`, color: gpsCoords ? '#10B981' : '#64748B', fontSize: '0.75rem', fontFamily: 'monospace' }}> <div style={{ padding: '8px 12px', borderRadius: '8px', background: gpsCoords ? 'rgba(16,185,129,0.08)' : '#F1F5F9', border: `1px solid ${gpsCoords ? 'rgba(16,185,129,0.2)' : '#E2E8F0'}`, color: gpsCoords ? '#10B981' : '#475569', fontSize: '0.75rem', fontFamily: 'monospace' }}>
{gpsCoords ? `Lat: ${gpsCoords.lat.toFixed(6)} | Lon: ${gpsCoords.lon.toFixed(6)}` : '📍 Click on the map to place a pin'} {gpsCoords ? `Lat: ${gpsCoords.lat.toFixed(6)} | Lon: ${gpsCoords.lon.toFixed(6)}` : '📍 Click on the map to place a pin'}
</div> </div>
</div> </div>
@@ -428,7 +474,7 @@ export const FleetOrganization: React.FC = () => {
{/* Action buttons placed cleanly inside the right column at the bottom */} {/* Action buttons placed cleanly inside the right column at the bottom */}
<div style={{ display: 'flex', gap: '12px', marginTop: '12px' }}> <div style={{ display: 'flex', gap: '12px', marginTop: '12px' }}>
<button type="button" onClick={() => setShowModal(false)} <button type="button" onClick={() => setShowModal(false)}
style={{ flex: 1, padding: '11px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '12px', color: '#94A3B8', cursor: 'pointer', fontWeight: 600, fontSize: '0.85rem' }}> style={{ flex: 1, padding: '11px', background: '#FFFFFF', border: '1px solid #CBD5E1', borderRadius: '12px', color: '#475569', cursor: 'pointer', fontWeight: 600, fontSize: '0.85rem' }}>
Cancel Cancel
</button> </button>
<button type="submit" disabled={submitting} <button type="submit" disabled={submitting}
@@ -509,10 +555,60 @@ export const FleetOrganization: React.FC = () => {
) : ( ) : (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}> <motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', background: 'rgba(255,255,255,0.03)', padding: '8px 16px', borderRadius: '12px', border: '1px solid rgba(255,255,255,0.06)' }}> <div style={{
<Search size={16} color="#64748B" /> display: 'flex',
<input type="text" placeholder="Search stations..." value={searchQuery} onChange={e => setSearchQuery(e.target.value)} alignItems: 'center',
style={{ background: 'transparent', border: 'none', color: '#fff', fontSize: '0.875rem', outline: 'none', width: '220px' }} /> gap: '10px',
background: '#FFFFFF',
padding: '10px 16px',
borderRadius: '12px',
border: isSearchFocused ? '1px solid #06B6D4' : '1px solid #CBD5E1',
boxShadow: isSearchFocused ? '0 0 0 3px rgba(6, 182, 212, 0.15)' : '0 2px 4px rgba(15, 23, 42, 0.01)',
transition: 'all 0.2s ease',
width: '320px',
position: 'relative'
}}>
<Search size={16} color={isSearchFocused ? "#06B6D4" : "#64748B"} style={{ transition: 'color 0.2s' }} />
<input
type="text"
placeholder="Search stations, incharge..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onFocus={() => setIsSearchFocused(true)}
onBlur={() => setIsSearchFocused(false)}
className="stations-search-input"
style={{
background: 'transparent',
border: 'none',
color: '#0F172A',
fontSize: '0.875rem',
outline: 'none',
width: '100%',
paddingRight: searchQuery ? '24px' : '0'
}}
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
style={{
position: 'absolute',
right: '12px',
background: 'transparent',
border: 'none',
cursor: 'pointer',
color: '#94A3B8',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2px',
borderRadius: '50%',
}}
onMouseEnter={e => e.currentTarget.style.color = '#475569'}
onMouseLeave={e => e.currentTarget.style.color = '#94A3B8'}
>
<X size={14} />
</button>
)}
</div> </div>
<button onClick={openCreateModal} <button onClick={openCreateModal}
style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '11px 22px', background: 'linear-gradient(135deg, #06B6D4, #3B82F6)', border: 'none', borderRadius: '12px', color: '#fff', fontWeight: 700, cursor: 'pointer', boxShadow: '0 4px 12px rgba(6,182,212,0.3)', fontSize: '0.875rem' }}> style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '11px 22px', background: 'linear-gradient(135deg, #06B6D4, #3B82F6)', border: 'none', borderRadius: '12px', color: '#fff', fontWeight: 700, cursor: 'pointer', boxShadow: '0 4px 12px rgba(6,182,212,0.3)', fontSize: '0.875rem' }}>
@@ -554,24 +650,24 @@ export const FleetOrganization: React.FC = () => {
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: '20px' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: '20px' }}>
{filteredStations.map(station => ( {filteredStations.map(station => (
<div key={station.id} style={{ ...cardStyle, position: 'relative', overflow: 'hidden', transition: 'transform 0.2s, box-shadow 0.2s', cursor: 'pointer' }} <div key={station.id} style={{ ...cardStyle, position: 'relative', overflow: 'hidden', transition: 'transform 0.2s, box-shadow 0.2s', cursor: 'pointer' }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.transform = 'translateY(-4px)'; (e.currentTarget as HTMLElement).style.boxShadow = '0 12px 32px rgba(6,182,212,0.1)'; }} onMouseEnter={e => { (e.currentTarget as HTMLElement).style.transform = 'translateY(-4px)'; (e.currentTarget as HTMLElement).style.boxShadow = '0 12px 32px rgba(15, 23, 42, 0.06)'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = 'translateY(0)'; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }} onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = 'translateY(0)'; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
> >
<div style={{ position: 'absolute', top: 0, left: 0, width: '4px', height: '100%', background: station.status === 'INACTIVE' ? '#EF4444' : '#10B981', borderRadius: '4px 0 0 4px' }} /> <div style={{ position: 'absolute', top: 0, left: 0, width: '4px', height: '100%', background: station.status === 'INACTIVE' ? '#EF4444' : '#10B981', borderRadius: '4px 0 0 4px' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '16px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '16px' }}>
<div> <div>
<h3 style={{ fontSize: '1rem', fontWeight: 800, color: '#fff' }}>{station.name}</h3> <h3 style={{ fontSize: '1rem', fontWeight: 800, color: '#0F172A' }}>{station.name}</h3>
<div style={{ fontSize: '0.7rem', color: '#64748B', marginTop: '4px', display: 'flex', alignItems: 'center', gap: '4px' }}> <div style={{ fontSize: '0.7rem', color: '#475569', marginTop: '4px', display: 'flex', alignItems: 'center', gap: '4px' }}>
<Globe size={11} /> {station.id?.substring(0, 8)}... <Globe size={11} /> {station.id?.substring(0, 8)}...
</div> </div>
</div> </div>
<button style={{ background: 'transparent', border: 'none', color: '#64748B', cursor: 'pointer', padding: '4px' }}><MoreVertical size={18} /></button> <button style={{ background: 'transparent', border: 'none', color: '#475569', cursor: 'pointer', padding: '4px' }}><MoreVertical size={18} /></button>
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '20px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '20px' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '10px' }}> <div style={{ display: 'flex', alignItems: 'flex-start', gap: '10px' }}>
<MapPin size={15} color="#06B6D4" style={{ marginTop: '2px', flexShrink: 0 }} /> <MapPin size={15} color="#06B6D4" style={{ marginTop: '2px', flexShrink: 0 }} />
<div> <div>
<div style={{ fontSize: '0.85rem', color: '#E2E8F0' }}>{station.address}</div> <div style={{ fontSize: '0.85rem', color: '#334155' }}>{station.address}</div>
{(station.gps_lat || station.gps_lon) && ( {(station.gps_lat || station.gps_lon) && (
<div style={{ fontSize: '0.68rem', color: '#475569', fontFamily: 'monospace', marginTop: '2px' }}> <div style={{ fontSize: '0.68rem', color: '#475569', fontFamily: 'monospace', marginTop: '2px' }}>
GPS: {station.gps_lat}, {station.gps_lon} GPS: {station.gps_lat}, {station.gps_lon}
@@ -582,36 +678,36 @@ export const FleetOrganization: React.FC = () => {
{station.incharge_name && ( {station.incharge_name && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<Users size={15} color="#06B6D4" /> <Users size={15} color="#06B6D4" />
<span style={{ fontSize: '0.85rem', color: '#94A3B8' }}>Incharge: <span style={{ fontWeight: 700, color: '#E2E8F0' }}>{station.incharge_name}</span></span> <span style={{ fontSize: '0.85rem', color: '#475569' }}>Incharge: <span style={{ fontWeight: 700, color: '#0F172A' }}>{station.incharge_name}</span></span>
</div> </div>
)} )}
{station.phone && ( {station.phone && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<Phone size={15} color="#06B6D4" /> <Phone size={15} color="#06B6D4" />
<span style={{ fontSize: '0.85rem', color: '#94A3B8' }}>{station.phone}</span> <span style={{ fontSize: '0.85rem', color: '#475569' }}>{station.phone}</span>
</div> </div>
)} )}
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1px', background: 'rgba(255,255,255,0.06)', borderRadius: '10px', overflow: 'hidden', marginBottom: '16px' }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1px', background: '#E2E8F0', borderRadius: '10px', overflow: 'hidden', marginBottom: '16px', border: '1px solid #E2E8F0' }}>
<div style={{ textAlign: 'center', padding: '14px', background: '#040B16' }}> <div style={{ textAlign: 'center', padding: '14px', background: '#F8FAFC' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#fff' }}>{station.vehiclesAssigned ?? '—'}</div> <div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#0F172A' }}>{station.vehiclesAssigned ?? '—'}</div>
<div style={{ fontSize: '0.62rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px', marginTop: '4px' }}>Vehicles</div> <div style={{ fontSize: '0.62rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '0.5px', marginTop: '4px' }}>Vehicles</div>
</div> </div>
<div style={{ textAlign: 'center', padding: '14px', background: '#040B16' }}> <div style={{ textAlign: 'center', padding: '14px', background: '#F8FAFC' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#fff' }}>{station.staffAssigned ?? '—'}</div> <div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#0F172A' }}>{station.staffAssigned ?? '—'}</div>
<div style={{ fontSize: '0.62rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px', marginTop: '4px' }}>Staff</div> <div style={{ fontSize: '0.62rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '0.5px', marginTop: '4px' }}>Staff</div>
</div> </div>
</div> </div>
<div style={{ display: 'flex', gap: '10px' }}> <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<button <button
onClick={() => openEditModal(station)} onClick={(e) => { e.stopPropagation(); openEditModal(station); }}
disabled={loadingDetailsId !== null} disabled={loadingDetailsId !== null}
style={{ style={{
flex: 1, padding: '9px', flex: '1 1 70px', padding: '9px',
background: 'rgba(255,255,255,0.04)', background: '#FFFFFF',
border: '1px solid rgba(255,255,255,0.08)', border: '1px solid #CBD5E1',
borderRadius: '8px', borderRadius: '8px',
color: loadingDetailsId === station.id ? '#06B6D4' : '#94A3B8', color: loadingDetailsId === station.id ? '#06B6D4' : '#475569',
fontSize: '0.75rem', fontWeight: 600, fontSize: '0.75rem', fontWeight: 600,
cursor: loadingDetailsId !== null ? 'not-allowed' : 'pointer', cursor: loadingDetailsId !== null ? 'not-allowed' : 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px',
@@ -625,9 +721,15 @@ export const FleetOrganization: React.FC = () => {
)} )}
{loadingDetailsId === station.id ? 'FETCHING...' : 'EDIT'} {loadingDetailsId === station.id ? 'FETCHING...' : 'EDIT'}
</button> </button>
<button style={{ flex: 1, padding: '9px', background: 'rgba(6,182,212,0.08)', border: '1px solid rgba(6,182,212,0.2)', borderRadius: '8px', color: '#06B6D4', fontSize: '0.75rem', fontWeight: 600, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}> <button
<Truck size={13} /> ASSIGN ASSETS onClick={(e) => { e.stopPropagation(); setSelectedStationForManifest(station); }}
style={{ flex: '1.2 1 100px', padding: '9px', background: 'rgba(16, 185, 129, 0.08)', border: '1px solid rgba(16, 185, 129, 0.2)', borderRadius: '8px', color: '#10B981', fontSize: '0.75rem', fontWeight: 600, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}
>
<Users size={13} /> MANIFEST
</button> </button>
{/* <button style={{ flex: '1.5 1 110px', padding: '9px', background: 'rgba(6,182,212,0.08)', border: '1px solid rgba(6,182,212,0.2)', borderRadius: '8px', color: '#06B6D4', fontSize: '0.75rem', fontWeight: 600, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}>
<Truck size={13} /> ASSIGN ASSETS
</button> */}
</div> </div>
</div> </div>
))} ))}
@@ -636,7 +738,258 @@ export const FleetOrganization: React.FC = () => {
</motion.div> </motion.div>
)} )}
{/* Station Manifest Modal */}
{createPortal(
<AnimatePresence>
{selectedStationForManifest && (
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
style={{ position: 'fixed', inset: 0, background: 'rgba(15, 23, 42, 0.3)', zIndex: 10000, display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(4px)' }}
onClick={() => setSelectedStationForManifest(null)}
>
<motion.div
initial={{ scale: 0.92, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.92, opacity: 0 }}
style={{ background: '#FFFFFF', border: '1px solid rgba(15, 23, 42, 0.08)', borderRadius: '20px', padding: '24px 32px', width: '750px', maxWidth: '95vw', maxHeight: '90vh', overflowY: 'auto', display: 'flex', flexDirection: 'column', boxShadow: '0 20px 50px rgba(15, 23, 42, 0.12)' }}
onClick={(e) => e.stopPropagation()}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', borderBottom: '1px solid #F1F5F9', paddingBottom: '16px' }}>
<div>
<h2 style={{ fontSize: '1.25rem', fontWeight: 900, color: '#0F172A', margin: 0 }}>
{selectedStationForManifest.name.toUpperCase()} MANIFEST
</h2>
<p style={{ fontSize: '0.78rem', color: '#64748B', margin: '4px 0 0' }}>
📍 {selectedStationForManifest.address}
</p>
</div>
<button type="button" onClick={() => setSelectedStationForManifest(null)} style={{ background: '#F1F5F9', border: '1px solid #E2E8F0', borderRadius: '8px', padding: '8px', color: '#475569', cursor: 'pointer', display: 'flex' }}>
<X size={18} />
</button>
</div>
<StationManifestContent stationId={selectedStationForManifest.id} />
</motion.div>
</motion.div>
)}
</AnimatePresence>,
document.body
)}
<style>{`@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .spin { animation: spin 1s linear infinite; }`}</style> <style>{`@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .spin { animation: spin 1s linear infinite; }`}</style>
</div> </div>
); );
}; };
interface ManifestVehicle {
id: string;
registration_number: string;
brand?: string;
model?: string;
vehicle_type?: string;
status: string;
activeRoster?: {
driver?: { user?: { name: string; phone?: string } };
staff?: { user?: { name: string; phone?: string } };
};
activeShift?: {
driver?: { user?: { name: string; phone?: string } };
staff?: { user?: { name: string; phone?: string } };
};
}
const StationManifestContent: React.FC<{ stationId: string }> = ({ stationId }) => {
const [vehicles, setVehicles] = useState<ManifestVehicle[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let active = true;
const load = async () => {
setLoading(true);
setError(null);
try {
const authToken = localStorage.getItem('teleems_token') || '';
if (!authToken) throw new Error('Session expired — please log in again.');
const url = `https://teleems-api-gateway.onrender.com/v1/fleet/vehicles?station_id=${stationId}`;
console.log('[Manifest] Calling:', url);
const res = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
},
});
if (!res.ok) {
throw new Error(`HTTP error ${res.status}`);
}
const json = await res.json();
if (active) {
let list = json?.data?.data || json?.data || (Array.isArray(json) ? json : []);
setVehicles(list);
}
} catch (err: any) {
if (active) {
setError(err.message || 'Failed to fetch station assets');
}
} finally {
if (active) {
setLoading(false);
}
}
};
load();
return () => { active = false; };
}, [stationId]);
const crewList = React.useMemo(() => {
const map = new Map<string, { name: string; role: string; phone?: string }>();
vehicles.forEach(v => {
const roster = v.activeRoster;
if (roster) {
if (roster.driver?.user?.name) {
map.set(roster.driver.user.name, {
name: roster.driver.user.name,
role: 'Pilot (Driver)',
phone: roster.driver.user.phone
});
}
if (roster.staff?.user?.name) {
map.set(roster.staff.user.name, {
name: roster.staff.user.name,
role: 'EMT (Medic)',
phone: roster.staff.user.phone
});
}
}
const shift = v.activeShift;
if (shift) {
if (shift.driver?.user?.name) {
map.set(shift.driver.user.name, {
name: shift.driver.user.name,
role: 'Pilot (Driver)',
phone: shift.driver.user.phone
});
}
if (shift.staff?.user?.name) {
map.set(shift.staff.user.name, {
name: shift.staff.user.name,
role: 'EMT (Medic)',
phone: shift.staff.user.phone
});
}
}
});
return Array.from(map.values());
}, [vehicles]);
if (loading) {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '60px 0', gap: '12px' }}>
<Loader2 className="spin" size={28} color="#06B6D4" style={{ animation: 'spin 1s linear infinite' }} />
<span style={{ color: '#64748B', fontSize: '0.85rem', fontWeight: 600 }}>FETCHING STATION ASSETS...</span>
</div>
);
}
if (error) {
return (
<div style={{ padding: '24px', textAlign: 'center', color: '#EF4444', background: 'rgba(239, 68, 68, 0.05)', borderRadius: '12px', border: '1px solid rgba(239, 68, 68, 0.15)' }}>
<AlertCircle size={28} style={{ marginBottom: '8px', display: 'inline-block' }} />
<div style={{ fontSize: '0.875rem', fontWeight: 700 }}>Error Loading Manifest</div>
<div style={{ fontSize: '0.8rem', opacity: 0.8, marginTop: '4px' }}>{error}</div>
</div>
);
}
return (
<div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1fr', gap: '24px', minHeight: '300px' }}>
{/* Vehicles Column */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<h4 style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.9rem', fontWeight: 800, color: '#0F172A', margin: 0, textTransform: 'uppercase', letterSpacing: '0.5px' }}>
<Truck size={18} color="#06B6D4" /> Deployed Fleet ({vehicles.length})
</h4>
{vehicles.length === 0 ? (
<div style={{ padding: '40px 16px', textAlign: 'center', background: '#F8FAFC', borderRadius: '12px', border: '1px dashed #E2E8F0', color: '#64748B', fontSize: '0.82rem', fontStyle: 'italic' }}>
No vehicles currently docked at this station.
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', maxHeight: '50vh', overflowY: 'auto', paddingRight: '4px' }}>
{vehicles.map(v => {
const driverName = v.activeRoster?.driver?.user?.name || v.activeShift?.driver?.user?.name;
const emtName = v.activeRoster?.staff?.user?.name || v.activeShift?.staff?.user?.name;
return (
<div key={v.id} style={{ padding: '14px', background: '#F8FAFC', borderRadius: '12px', border: '1px solid #E2E8F0', display: 'flex', flexDirection: 'column', gap: '10px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#0F172A' }}>{v.registration_number}</div>
<div style={{ fontSize: '0.72rem', color: '#475569', marginTop: '2px' }}>{v.brand} {v.model} {v.vehicle_type}</div>
</div>
<span style={{
fontSize: '0.68rem',
fontWeight: 700,
padding: '4px 10px',
borderRadius: '20px',
background: v.status === 'AVAILABLE' ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
color: v.status === 'AVAILABLE' ? '#10B981' : '#EF4444'
}}>
{v.status}
</span>
</div>
{(driverName || emtName) && (
<div style={{ borderTop: '1px solid #E2E8F0', paddingTop: '8px', display: 'flex', flexDirection: 'column', gap: '4px' }}>
{driverName && (
<div style={{ fontSize: '0.75rem', color: '#475569', display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ color: '#94A3B8', fontWeight: 600 }}>Pilot:</span>
<strong style={{ color: '#0F172A' }}>{driverName}</strong>
</div>
)}
{emtName && (
<div style={{ fontSize: '0.75rem', color: '#475569', display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ color: '#94A3B8', fontWeight: 600 }}>EMT:</span>
<strong style={{ color: '#0F172A' }}>{emtName}</strong>
</div>
)}
</div>
)}
</div>
);
})}
</div>
)}
</div>
{/* Staff Column */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<h4 style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.9rem', fontWeight: 800, color: '#0F172A', margin: 0, textTransform: 'uppercase', letterSpacing: '0.5px' }}>
<Users size={18} color="#06B6D4" /> Active Crew ({crewList.length})
</h4>
{crewList.length === 0 ? (
<div style={{ padding: '40px 16px', textAlign: 'center', background: '#F8FAFC', borderRadius: '12px', border: '1px dashed #E2E8F0', color: '#64748B', fontSize: '0.82rem', fontStyle: 'italic' }}>
No crew members currently on duty.
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', maxHeight: '50vh', overflowY: 'auto' }}>
{crewList.map((c, i) => (
<div key={i} style={{ padding: '12px 14px', background: '#F8FAFC', borderRadius: '12px', border: '1px solid #E2E8F0', display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ width: '34px', height: '34px', borderRadius: '50%', background: 'rgba(6, 182, 212, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#06B6D4' }}>
<Users size={16} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '0.85rem', fontWeight: 700, color: '#0F172A' }}>{c.name}</div>
<div style={{ fontSize: '0.7rem', color: '#475569', marginTop: '2px' }}>{c.role} {c.phone ? `${c.phone}` : ''}</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,393 @@
import React, { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
ArrowLeft,
Activity,
AlertTriangle,
Package,
CheckCircle,
Clock,
ChevronLeft,
ChevronRight,
Search,
Check,
Truck
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../components/Common';
import { fleetApi } from '../../api/fleet';
export const FleetPendingRequests: React.FC = () => {
const [, setSearchParams] = useSearchParams();
const [requests, setRequests] = useState<any[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// Filtering & Pagination states
const [statusFilter, setStatusFilter] = useState<string>('PENDING');
const [searchQuery, setSearchQuery] = useState<string>('');
const [isSearchFocused, setIsSearchFocused] = useState<boolean>(false);
const [currentPage, setCurrentPage] = useState<number>(1);
const itemsPerPage = 8;
// Inline loading when clicking action buttons
const [processingId, setProcessingId] = useState<string | null>(null);
const fetchRequests = async () => {
setLoading(true);
setError(null);
try {
const token = localStorage.getItem('teleems_token') || '';
if (!token) {
setError('Session token expired. Please log in.');
return;
}
// Fetch with specific status filter (PENDING, APPROVED, COMPLETED, or '' for all)
const res = await fleetApi.getRestockRequests(token, statusFilter || undefined);
const data = res?.data?.data || res?.data || [];
setRequests(Array.isArray(data) ? data : []);
setCurrentPage(1);
} catch (err: any) {
console.error('Failed to fetch restock requests:', err);
setError(err?.message || 'Failed to fetch restock requests.');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchRequests();
}, [statusFilter]);
const handleUpdateStatus = async (requestId: string, nextStatus: 'APPROVED' | 'COMPLETED') => {
if (!requestId) return;
setProcessingId(requestId);
try {
const token = localStorage.getItem('teleems_token') || '';
await fleetApi.updateRestockRequestStatus(requestId, nextStatus, token);
alert(`Request has been successfully marked as ${nextStatus}!`);
// Reload current tab requests
fetchRequests();
} catch (err: any) {
console.error('Failed to update request status:', err);
alert(err?.message || `Failed to mark request as ${nextStatus}. Please try again.`);
} finally {
setProcessingId(null);
}
};
// Filter local requests on top of status filter using search query
const filteredRequests = requests.filter(req => {
const itemName = req.item_master?.name || '';
const itemCategory = req.item_master?.category || '';
const requestedBy = typeof req.requested_by === 'object' && req.requested_by !== null
? (req.requested_by.name || req.requested_by.username || '')
: String(req.requested_by || '');
return (
itemName.toLowerCase().includes(searchQuery.toLowerCase()) ||
itemCategory.toLowerCase().includes(searchQuery.toLowerCase()) ||
requestedBy.toLowerCase().includes(searchQuery.toLowerCase())
);
});
// Pagination calculation
const totalPages = Math.ceil(filteredRequests.length / itemsPerPage);
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = filteredRequests.slice(indexOfFirstItem, indexOfLastItem);
return (
<div className="animate-in fade-in duration-500" style={{ fontFamily: "'Inter', sans-serif" }}>
{/* Top action bar */}
<div style={{ marginBottom: '24px', display: 'flex', gap: '12px', alignItems: 'center' }}>
<button
onClick={() => setSearchParams({ tab: 'warehouse' })}
style={{
background: 'rgba(255,255,255,0.05)',
border: '1px solid rgba(255,255,255,0.1)',
color: '#94A3B8',
borderRadius: '12px',
width: '52px',
height: '42px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
transition: 'all 0.2s',
flexShrink: 0
}}
onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.1)'}
onMouseLeave={e => e.currentTarget.style.background = 'rgba(255,255,255,0.05)'}
title="Back to Warehouse Stock"
>
<ArrowLeft size={18} />
</button>
{/* Search filter input */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
background: '#FFFFFF',
padding: '10px 16px',
borderRadius: '12px',
border: isSearchFocused ? '1px solid #06B6D4' : '1px solid #CBD5E1',
boxShadow: isSearchFocused ? '0 0 0 3px rgba(6, 182, 212, 0.15)' : '0 2px 4px rgba(15, 23, 42, 0.01)',
transition: 'all 0.2s ease',
width: '320px',
position: 'relative'
}}>
<Search size={16} color={isSearchFocused ? "#06B6D4" : "#64748B"} />
<input
type="text"
placeholder="Search request items or users..."
value={searchQuery}
onChange={e => {
setSearchQuery(e.target.value);
setCurrentPage(1);
}}
onFocus={() => setIsSearchFocused(true)}
onBlur={() => setIsSearchFocused(false)}
className="stations-search-input"
style={{
background: 'transparent',
border: 'none',
color: '#0F172A',
fontSize: '0.875rem',
outline: 'none',
width: '100%'
}}
/>
</div>
{/* Status filters */}
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}>
{[
{ id: 'PENDING', label: 'Pending Approval', count: statusFilter === 'PENDING' ? filteredRequests.length : null },
{ id: 'APPROVED', label: 'Approved (Awaiting Handover)', count: statusFilter === 'APPROVED' ? filteredRequests.length : null }
].map(tab => (
<button
key={tab.id}
onClick={() => {
setStatusFilter(tab.id);
setSearchQuery('');
}}
style={{
background: statusFilter === tab.id ? '#06B6D4' : '#FFFFFF',
color: statusFilter === tab.id ? '#FFFFFF' : '#475569',
border: statusFilter === tab.id ? '1px solid #06B6D4' : '1px solid #CBD5E1',
borderRadius: '12px',
padding: '10px 18px',
fontSize: '0.8125rem',
fontWeight: 700,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
boxShadow: statusFilter === tab.id ? '0 4px 12px rgba(6, 182, 212, 0.2)' : '0 1px 2px rgba(15, 23, 42, 0.05)',
transition: 'all 0.2s ease'
}}
>
{tab.label}
{tab.count !== null && (
<span style={{
background: statusFilter === tab.id ? '#FFFFFF' : 'rgba(6, 182, 212, 0.1)',
color: statusFilter === tab.id ? '#06B6D4' : '#06B6D4',
padding: '2px 6px',
borderRadius: '6px',
fontSize: '0.7rem',
fontWeight: 800
}}>
{tab.count}
</span>
)}
</button>
))}
</div>
</div>
<Card title={`${statusFilter.charAt(0) + statusFilter.slice(1).toLowerCase()} Restock Requests Ledger`}>
{loading ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '100px', gap: '16px', color: '#64748B' }}>
<Activity size={32} className="spin" style={{ color: '#06B6D4' }} />
<span style={{ fontSize: '0.875rem', fontWeight: 600 }}>FETCHING TRANSACTION RECORDS...</span>
</div>
) : error ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '80px 24px', textAlign: 'center', color: '#EF4444' }}>
<AlertTriangle size={48} style={{ marginBottom: '16px', opacity: 0.8 }} />
<h3 style={{ fontSize: '1.1rem', fontWeight: 700, marginBottom: '8px' }}>Synchronization Failed</h3>
<p style={{ fontSize: '0.82rem', color: '#94A3B8', maxWidth: '360px', margin: '0 auto' }}>{error}</p>
</div>
) : currentItems.length === 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '100px 24px', color: '#64748B' }}>
<Package size={48} style={{ opacity: 0.2, marginBottom: '16px', color: '#06B6D4' }} />
<h3 style={{ fontSize: '1rem', fontWeight: 700, color: '#475569', marginBottom: '4px' }}>No Supply Requests</h3>
<p style={{ fontSize: '0.8125rem' }}>There are no restock requests matching this state filter right now.</p>
</div>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ textAlign: 'left', opacity: 0.5, fontSize: '0.65rem', textTransform: 'uppercase', borderBottom: '1px solid rgba(15, 23, 42, 0.08)' }}>
<th style={{ padding: '16px 12px', color: '#0F172A' }}>Requested Supply Item</th>
<th style={{ padding: '16px 12px', color: '#0F172A' }}>Quantity Demanded</th>
<th style={{ padding: '16px 12px', color: '#0F172A' }}>Requested By</th>
<th style={{ padding: '16px 12px', color: '#0F172A' }}>Request Date</th>
<th style={{ padding: '16px 12px', color: '#0F172A' }}>Status Badge</th>
<th style={{ padding: '16px 12px', color: '#0F172A', textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody>
{currentItems.map((req) => {
const requestedUser = typeof req.requested_by === 'object' && req.requested_by !== null
? (req.requested_by.name || req.requested_by.username || 'Unknown User')
: (req.requested_by || 'N/A');
return (
<tr key={req.id} style={{ borderBottom: '1px solid rgba(15, 23, 42, 0.04)', transition: 'background 0.2s' }} className="hover-glow">
<td style={{ padding: '16px 12px' }}>
<div style={{ fontWeight: 700, fontSize: '0.875rem', color: '#0F172A' }}>
{req.item_master?.name || 'Unknown Item'}
</div>
<div style={{ fontSize: '0.7rem', color: '#64748B', marginTop: '2px', fontWeight: 500 }}>
Category: {req.item_master?.category || 'General'}
</div>
</td>
<td style={{ padding: '16px 12px' }}>
<div style={{ fontWeight: 900, fontSize: '1rem', color: '#06B6D4' }}>
{req.quantity} <span style={{ fontSize: '0.7rem', color: '#64748B', fontWeight: 500 }}>Units</span>
</div>
</td>
<td style={{ padding: '16px 12px' }}>
<div style={{ fontWeight: 600, fontSize: '0.8rem', color: '#475569' }}>
{requestedUser}
</div>
</td>
<td style={{ padding: '16px 12px' }}>
<div style={{ fontSize: '0.75rem', color: '#64748B' }}>
{new Date(req.createdAt || req.updatedAt).toLocaleDateString()}
</div>
</td>
<td style={{ padding: '16px 12px' }}>
<span style={{
fontSize: '0.65rem',
fontWeight: 900,
textTransform: 'uppercase',
color: statusFilter === 'PENDING' ? '#D97706' : statusFilter === 'APPROVED' ? '#2563EB' : '#16A34A',
background: statusFilter === 'PENDING' ? 'rgba(245, 158, 11, 0.1)' : statusFilter === 'APPROVED' ? 'rgba(59, 130, 246, 0.1)' : 'rgba(34, 197, 94, 0.1)',
padding: '4px 10px',
borderRadius: '6px'
}}>
{req.status}
</span>
</td>
<td style={{ padding: '16px 12px', textAlign: 'right' }}>
{statusFilter === 'PENDING' && (
<button
disabled={processingId !== null}
onClick={() => handleUpdateStatus(req.id, 'APPROVED')}
style={{
background: '#10B981',
border: 'none',
color: '#FFFFFF',
borderRadius: '8px',
padding: '6px 12px',
fontSize: '0.75rem',
fontWeight: 700,
cursor: processingId !== null ? 'not-allowed' : 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
opacity: processingId !== null ? 0.6 : 1,
transition: 'all 0.2s',
boxShadow: '0 2px 4px rgba(16, 185, 129, 0.15)'
}}
>
{processingId === req.id ? (
<Activity size={12} className="spin" />
) : (
<Check size={12} strokeWidth={3} />
)}
APPROVE
</button>
)}
{statusFilter === 'APPROVED' && (
<button
disabled={processingId !== null}
onClick={() => handleUpdateStatus(req.id, 'COMPLETED')}
style={{
background: '#3B82F6',
border: 'none',
color: '#FFFFFF',
borderRadius: '8px',
padding: '6px 12px',
fontSize: '0.75rem',
fontWeight: 700,
cursor: processingId !== null ? 'not-allowed' : 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
opacity: processingId !== null ? 0.6 : 1,
transition: 'all 0.2s',
boxShadow: '0 2px 4px rgba(59, 130, 246, 0.15)'
}}
>
{processingId === req.id ? (
<Activity size={12} className="spin" />
) : (
<Truck size={12} />
)}
HAND OVER (COMPLETE)
</button>
)}
{statusFilter === 'COMPLETED' && (
<div style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', color: '#10B981', fontSize: '0.75rem', fontWeight: 700 }}>
<CheckCircle size={14} /> TRANSACTION CLOSED
</div>
)}
</td>
</tr>
);
})}
</tbody>
</table>
)}
{!loading && !error && filteredRequests.length > itemsPerPage && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px', borderTop: '1px solid rgba(15, 23, 42, 0.08)', marginTop: '8px' }}>
<div style={{ fontSize: '0.8125rem', color: '#64748B' }}>
Showing {indexOfFirstItem + 1} to {Math.min(indexOfLastItem, filteredRequests.length)} of {filteredRequests.length} transactions
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
style={{
padding: '6px 12px', background: 'transparent', border: '1px solid #CBD5E1', borderRadius: '6px',
color: currentPage === 1 ? '#94A3B8' : '#475569', cursor: currentPage === 1 ? 'not-allowed' : 'pointer',
display: 'flex', alignItems: 'center', gap: '4px', fontSize: '0.8125rem', fontWeight: 600, transition: 'all 0.2s'
}}
>
<ChevronLeft size={14} /> Prev
</button>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 8px', fontSize: '0.8125rem', fontWeight: 700, color: '#0F172A' }}>
{currentPage} / {totalPages}
</div>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
style={{
padding: '6px 12px', background: 'transparent', border: '1px solid #CBD5E1', borderRadius: '6px',
color: currentPage === totalPages ? '#94A3B8' : '#475569', cursor: currentPage === totalPages ? 'not-allowed' : 'pointer',
display: 'flex', alignItems: 'center', gap: '4px', fontSize: '0.8125rem', fontWeight: 600, transition: 'all 0.2s'
}}
>
Next <ChevronRight size={14} />
</button>
</div>
</div>
)}
</Card>
</div>
);
};

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { UserPlus, Search, ShieldCheck, ChevronLeft, ChevronRight, Loader2, AlertCircle, Phone, Mail, X, Eye, EyeOff } from 'lucide-react'; import { UserPlus, Search, ShieldCheck, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Loader2, AlertCircle, Phone, Mail, X, Eye, EyeOff } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
interface Staff { interface Staff {
@@ -9,18 +9,17 @@ interface Staff {
} }
const ROLE_COLORS: Record<string, { color: string; bg: string }> = { const ROLE_COLORS: Record<string, { color: string; bg: string }> = {
DRIVER: { color: '#06B6D4', bg: 'rgba(6,182,212,0.12)' }, DRIVER: { color: '#06B6D4', bg: 'rgba(6,182,212,0.08)' },
EMT: { color: '#3B82F6', bg: 'rgba(59,130,246,0.12)' }, EMT: { color: '#3B82F6', bg: 'rgba(59,130,246,0.08)' },
DOCTOR: { color: '#10B981', bg: 'rgba(16,185,129,0.12)' }, DOCTOR: { color: '#10B981', bg: 'rgba(16,185,129,0.08)' },
}; };
const DEF = { color: '#94A3B8', bg: 'rgba(148,163,184,0.1)' }; const DEF = { color: '#475569', bg: 'rgba(71,85,105,0.06)' };
const STATUS_CFG: Record<string, { label: string; color: string }> = { const STATUS_CFG: Record<string, { label: string; color: string }> = {
ON_DUTY: { label: 'On Duty', color: '#10B981' }, OFF_DUTY: { label: 'Off Duty', color: '#64748B' }, ON_DUTY: { label: 'On Duty', color: '#10B981' }, OFF_DUTY: { label: 'Off Duty', color: '#475569' },
ON_LEAVE: { label: 'On Leave', color: '#F59E0B' }, ACTIVE: { label: 'Active', color: '#10B981' }, INACTIVE: { label: 'Inactive', color: '#EF4444' }, ON_LEAVE: { label: 'On Leave', color: '#F59E0B' }, ACTIVE: { label: 'Active', color: '#10B981' }, INACTIVE: { label: 'Inactive', color: '#EF4444' },
}; };
const FILTERS = ['ALL', 'DRIVER', 'EMT', 'DOCTOR'] as const; const FILTERS = ['ALL', 'DRIVER', 'EMT', 'DOCTOR'] as const;
const PAGE_SIZE = 5; const th: React.CSSProperties = { padding: '14px 18px', fontSize: '0.68rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, textAlign: 'left', borderBottom: '1px solid #CBD5E1', background: '#F1F5F9', whiteSpace: 'nowrap' };
const th: React.CSSProperties = { padding: '14px 18px', fontSize: '0.68rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, textAlign: 'left', borderBottom: '1px solid rgba(255,255,255,0.06)', background: 'rgba(255,255,255,0.02)', whiteSpace: 'nowrap' };
const norm = (s: Staff) => { const norm = (s: Staff) => {
const pd = s.professional_details; const pd = s.professional_details;
@@ -41,7 +40,9 @@ export const FleetPersonnel: React.FC = () => {
const [fetchError, setFetchError] = useState(''); const [fetchError, setFetchError] = useState('');
const [filter, setFilter] = useState<typeof FILTERS[number]>('ALL'); const [filter, setFilter] = useState<typeof FILTERS[number]>('ALL');
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [isSearchFocused, setIsSearchFocused] = useState(false);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [selectedRaw, setSelectedRaw] = useState<Staff | null>(null); const [selectedRaw, setSelectedRaw] = useState<Staff | null>(null);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@@ -96,9 +97,9 @@ export const FleetPersonnel: React.FC = () => {
const normalized = staffList.map(norm); const normalized = staffList.map(norm);
const filtered = normalized.filter(s => (filter === 'ALL' || s.role === filter) && (s.name.toLowerCase().includes(search.toLowerCase()) || s.role.toLowerCase().includes(search.toLowerCase()))); const filtered = normalized.filter(s => (filter === 'ALL' || s.role === filter) && (s.name.toLowerCase().includes(search.toLowerCase()) || s.role.toLowerCase().includes(search.toLowerCase())));
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage));
const safePage = Math.min(page, totalPages); const safePage = Math.min(page, totalPages);
const pageData = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE); const pageData = filtered.slice((safePage - 1) * itemsPerPage, safePage * itemsPerPage);
const onDuty = normalized.filter(s => ['ON_DUTY', 'ACTIVE'].includes(s.status)).length; const onDuty = normalized.filter(s => ['ON_DUTY', 'ACTIVE'].includes(s.status)).length;
const offDuty = normalized.filter(s => s.status === 'OFF_DUTY').length; const offDuty = normalized.filter(s => s.status === 'OFF_DUTY').length;
const onLeave = normalized.filter(s => s.status === 'ON_LEAVE').length; const onLeave = normalized.filter(s => s.status === 'ON_LEAVE').length;
@@ -112,24 +113,24 @@ export const FleetPersonnel: React.FC = () => {
const ini = s.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase(); const ini = s.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
const cw = s.certExpiry ? new Date(s.certExpiry) < new Date(Date.now() + 60 * 24 * 60 * 60 * 1000) : false; const cw = s.certExpiry ? new Date(s.certExpiry) < new Date(Date.now() + 60 * 24 * 60 * 60 * 1000) : false;
const row = (label: string, value: string, mono = false) => ( const row = (label: string, value: string, mono = false) => (
<div style={{ padding: '14px 16px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: '12px' }}> <div style={{ padding: '14px 16px', background: '#FFFFFF', border: '1px solid rgba(15, 23, 42, 0.08)', borderRadius: '12px' }}>
<div style={{ fontSize: '0.62rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '5px' }}>{label}</div> <div style={{ fontSize: '0.62rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '5px' }}>{label}</div>
<div style={{ fontSize: '0.9rem', color: '#E2E8F0', fontWeight: 600, fontFamily: mono ? 'monospace' : undefined, wordBreak: 'break-all' }}>{value || '—'}</div> <div style={{ fontSize: '0.9rem', color: '#0F172A', fontWeight: 600, fontFamily: mono ? 'monospace' : undefined, wordBreak: 'break-all' }}>{value || '—'}</div>
</div> </div>
); );
return ( return (
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} style={{ color: '#F8FAFC' }}> <motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} style={{ color: '#0F172A' }}>
<button onClick={() => setSelectedRaw(null)} style={{ display: 'flex', alignItems: 'center', gap: '8px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '10px', padding: '9px 16px', color: '#94A3B8', cursor: 'pointer', fontSize: '0.82rem', fontWeight: 600, marginBottom: '24px' }}> <button onClick={() => setSelectedRaw(null)} style={{ display: 'flex', alignItems: 'center', gap: '8px', background: '#FFFFFF', border: '1px solid #CBD5E1', borderRadius: '10px', padding: '9px 16px', color: '#475569', cursor: 'pointer', fontSize: '0.82rem', fontWeight: 600, marginBottom: '24px' }}>
<ChevronLeft size={16} /> Back to Staff List <ChevronLeft size={16} /> Back to Staff List
</button> </button>
{/* Hero */} {/* Hero */}
<div style={{ background: `linear-gradient(135deg,${rc.color}18,rgba(255,255,255,0.02))`, border: `1px solid ${rc.color}30`, borderRadius: '20px', padding: '32px', marginBottom: '20px', position: 'relative', overflow: 'hidden' }}> <div style={{ background: `linear-gradient(135deg,${rc.color}08,#FFFFFF)`, border: `1px solid ${rc.color}30`, borderRadius: '20px', padding: '32px', marginBottom: '20px', position: 'relative', overflow: 'hidden', boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)' }}>
<div style={{ position: 'absolute', top: -40, right: -40, width: 180, height: 180, borderRadius: '50%', background: `${rc.color}10` }} /> <div style={{ position: 'absolute', top: -40, right: -40, width: 180, height: 180, borderRadius: '50%', background: `${rc.color}10` }} />
<div style={{ display: 'flex', alignItems: 'center', gap: '24px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '24px' }}>
<div style={{ width: 80, height: 80, borderRadius: 22, background: rc.bg, border: `2px solid ${rc.color}60`, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '1.6rem', fontWeight: 900, color: rc.color, boxShadow: `0 0 28px ${rc.color}30`, flexShrink: 0 }}>{ini}</div> <div style={{ width: 80, height: 80, borderRadius: 22, background: rc.bg, border: `2px solid ${rc.color}60`, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '1.6rem', fontWeight: 900, color: rc.color, boxShadow: `0 0 28px ${rc.color}30`, flexShrink: 0 }}>{ini}</div>
<div> <div>
<h1 style={{ fontSize: '1.6rem', fontWeight: 900, color: '#fff', margin: '0 0 10px' }}>{s.name}</h1> <h1 style={{ fontSize: '1.6rem', fontWeight: 900, color: '#0F172A', margin: '0 0 10px' }}>{s.name}</h1>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<span style={{ padding: '4px 12px', borderRadius: 7, fontSize: '0.72rem', fontWeight: 800, background: rc.bg, color: rc.color, border: `1px solid ${rc.color}40` }}>{s.role}</span> <span style={{ padding: '4px 12px', borderRadius: 7, fontSize: '0.72rem', fontWeight: 800, background: rc.bg, color: rc.color, border: `1px solid ${rc.color}40` }}>{s.role}</span>
<span style={{ display: 'flex', alignItems: 'center', gap: 5, padding: '4px 12px', borderRadius: 7, fontSize: '0.72rem', fontWeight: 700, background: `${sc.color}18`, color: sc.color }}> <span style={{ display: 'flex', alignItems: 'center', gap: 5, padding: '4px 12px', borderRadius: 7, fontSize: '0.72rem', fontWeight: 700, background: `${sc.color}18`, color: sc.color }}>
@@ -145,24 +146,24 @@ export const FleetPersonnel: React.FC = () => {
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
{/* Contact */} {/* Contact */}
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 16, padding: 22 }}> <div style={{ background: '#FFFFFF', border: '1px solid rgba(15, 23, 42, 0.08)', borderRadius: 16, padding: 22, boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)' }}>
<div style={{ fontSize: '0.65rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 14 }}>📞 Contact</div> <div style={{ fontSize: '0.65rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 14 }}>📞 Contact</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 10 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', background: '#F8FAFC', border: '1px solid rgba(15, 23, 42, 0.06)', borderRadius: 10 }}>
<Phone size={16} color="#06B6D4" /> <Phone size={16} color="#06B6D4" />
<div><div style={{ fontSize: '0.62rem', color: '#64748B', marginBottom: 2 }}>Phone</div><div style={{ fontSize: '0.9rem', color: '#E2E8F0', fontWeight: 600 }}>{s.phone}</div></div> <div><div style={{ fontSize: '0.62rem', color: '#475569', marginBottom: 2 }}>Phone</div><div style={{ fontSize: '0.9rem', color: '#0F172A', fontWeight: 600 }}>{s.phone}</div></div>
</div> </div>
{s.email && ( {s.email && (
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 10 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', background: '#F8FAFC', border: '1px solid rgba(15, 23, 42, 0.06)', borderRadius: 10 }}>
<Mail size={16} color="#06B6D4" /> <Mail size={16} color="#06B6D4" />
<div><div style={{ fontSize: '0.62rem', color: '#64748B', marginBottom: 2 }}>Email</div><div style={{ fontSize: '0.88rem', color: '#E2E8F0', fontWeight: 600 }}>{s.email}</div></div> <div><div style={{ fontSize: '0.62rem', color: '#475569', marginBottom: 2 }}>Email</div><div style={{ fontSize: '0.88rem', color: '#0F172A', fontWeight: 600 }}>{s.email}</div></div>
</div> </div>
)} )}
</div> </div>
</div> </div>
{/* Identity */} {/* Identity */}
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 16, padding: 22 }}> <div style={{ background: '#FFFFFF', border: '1px solid rgba(15, 23, 42, 0.08)', borderRadius: 16, padding: 22, boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)' }}>
<div style={{ fontSize: '0.65rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 14 }}>🪪 Identity</div> <div style={{ fontSize: '0.65rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 14 }}>🪪 Identity</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{row('Staff ID', s.id, true)} {row('Staff ID', s.id, true)}
@@ -173,7 +174,7 @@ export const FleetPersonnel: React.FC = () => {
{/* Professional — full width */} {/* Professional — full width */}
{pd && ( {pd && (
<div style={{ gridColumn: '1 / -1', background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 16, padding: 22 }}> <div style={{ gridColumn: '1 / -1', background: '#FFFFFF', border: '1px solid rgba(15, 23, 42, 0.08)', borderRadius: 16, padding: 22, boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)' }}>
<div style={{ fontSize: '0.65rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 14 }}>🎓 Professional Details</div> <div style={{ fontSize: '0.65rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 14 }}>🎓 Professional Details</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(240px,1fr))', gap: 10 }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(240px,1fr))', gap: 10 }}>
{pd.qualification && row('Qualification', pd.qualification)} {pd.qualification && row('Qualification', pd.qualification)}
@@ -183,7 +184,7 @@ export const FleetPersonnel: React.FC = () => {
{s.specialization && row('Specialization', s.specialization)} {s.specialization && row('Specialization', s.specialization)}
{s.certExpiry && ( {s.certExpiry && (
<div style={{ padding: '14px 16px', background: cw ? 'rgba(245,158,11,0.08)' : 'rgba(16,185,129,0.07)', border: `1px solid ${cw ? 'rgba(245,158,11,0.25)' : 'rgba(16,185,129,0.2)'}`, borderRadius: 12 }}> <div style={{ padding: '14px 16px', background: cw ? 'rgba(245,158,11,0.08)' : 'rgba(16,185,129,0.07)', border: `1px solid ${cw ? 'rgba(245,158,11,0.25)' : 'rgba(16,185,129,0.2)'}`, borderRadius: 12 }}>
<div style={{ fontSize: '0.62rem', color: '#64748B', marginBottom: 5, display: 'flex', alignItems: 'center', gap: 5, textTransform: 'uppercase' }}> <div style={{ fontSize: '0.62rem', color: '#475569', marginBottom: 5, display: 'flex', alignItems: 'center', gap: 5, textTransform: 'uppercase' }}>
<ShieldCheck size={11} color={cw ? '#F59E0B' : '#10B981'} /> Cert Expiry <ShieldCheck size={11} color={cw ? '#F59E0B' : '#10B981'} /> Cert Expiry
</div> </div>
<div style={{ fontSize: '0.95rem', fontWeight: 800, color: cw ? '#F59E0B' : '#10B981' }}>{s.certExpiry}</div> <div style={{ fontSize: '0.95rem', fontWeight: 800, color: cw ? '#F59E0B' : '#10B981' }}>{s.certExpiry}</div>
@@ -206,17 +207,17 @@ export const FleetPersonnel: React.FC = () => {
<AnimatePresence> <AnimatePresence>
{showModal && ( {showModal && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)', zIndex: 3000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20, backdropFilter: 'blur(6px)' }} style={{ position: 'fixed', inset: 0, background: 'rgba(15, 23, 42, 0.3)', zIndex: 3000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20, backdropFilter: 'blur(6px)' }}
onClick={e => { if (e.target === e.currentTarget) setShowModal(false); }} onClick={e => { if (e.target === e.currentTarget) setShowModal(false); }}
> >
<motion.div initial={{ scale: 0.93, y: 20, opacity: 0 }} animate={{ scale: 1, y: 0, opacity: 1 }} exit={{ scale: 0.93, y: 20, opacity: 0 }} <motion.div initial={{ scale: 0.93, y: 20, opacity: 0 }} animate={{ scale: 1, y: 0, opacity: 1 }} exit={{ scale: 0.93, y: 20, opacity: 0 }}
style={{ background: '#0D1526', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 24, width: 780, maxWidth: '96vw', position: 'relative' }} style={{ background: '#FFFFFF', border: '1px solid rgba(15, 23, 42, 0.08)', borderRadius: 24, width: 780, maxWidth: '96vw', position: 'relative', boxShadow: '0 20px 50px rgba(15, 23, 42, 0.12)' }}
> >
{/* Modal Header */} {/* Modal Header */}
<div style={{ background: 'linear-gradient(135deg,rgba(6,182,212,0.12),transparent)', borderBottom: '1px solid rgba(255,255,255,0.07)', padding: '20px 28px 16px', borderRadius: '24px 24px 0 0' }}> <div style={{ background: 'linear-gradient(135deg,rgba(6,182,212,0.08),transparent)', borderBottom: '1px solid rgba(15, 23, 42, 0.08)', padding: '20px 28px 16px', borderRadius: '24px 24px 0 0' }}>
<button onClick={() => setShowModal(false)} style={{ position: 'absolute', top: 14, right: 14, background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, padding: 6, color: '#94A3B8', cursor: 'pointer', display: 'flex' }}><X size={15} /></button> <button onClick={() => setShowModal(false)} style={{ position: 'absolute', top: 14, right: 14, background: '#F1F5F9', border: '1px solid #E2E8F0', borderRadius: 8, padding: 6, color: '#475569', cursor: 'pointer', display: 'flex' }}><X size={15} /></button>
<h3 style={{ margin: 0, fontSize: '1.05rem', fontWeight: 900, color: '#fff' }}>Register New Staff</h3> <h3 style={{ margin: 0, fontSize: '1.05rem', fontWeight: 900, color: '#0F172A' }}>Register New Staff</h3>
<p style={{ margin: '4px 0 0', fontSize: '0.75rem', color: '#64748B' }}>Fill details based on staff type</p> <p style={{ margin: '4px 0 0', fontSize: '0.75rem', color: '#475569' }}>Fill details based on staff type</p>
</div> </div>
<div style={{ padding: '20px 28px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '20px 28px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
@@ -227,7 +228,7 @@ export const FleetPersonnel: React.FC = () => {
const rc = ROLE_COLORS[t]; const rc = ROLE_COLORS[t];
return ( return (
<button key={t} onClick={() => setStaffType(t)} <button key={t} onClick={() => setStaffType(t)}
style={{ flex: 1, padding: '9px 0', borderRadius: 10, border: `2px solid ${staffType === t ? rc.color : 'rgba(255,255,255,0.08)'}`, background: staffType === t ? rc.bg : 'rgba(255,255,255,0.03)', color: staffType === t ? rc.color : '#64748B', fontWeight: 800, fontSize: '0.8rem', cursor: 'pointer', transition: 'all 0.2s' }}> style={{ flex: 1, padding: '9px 0', borderRadius: 10, border: `2px solid ${staffType === t ? rc.color : '#E2E8F0'}`, background: staffType === t ? rc.bg : '#F1F5F9', color: staffType === t ? rc.color : '#475569', fontWeight: 800, fontSize: '0.8rem', cursor: 'pointer', transition: 'all 0.2s' }}>
{t} {t}
</button> </button>
); );
@@ -241,17 +242,17 @@ export const FleetPersonnel: React.FC = () => {
<div style={{ fontSize: '0.6rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 5 }}>{f.label}</div> <div style={{ fontSize: '0.6rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 5 }}>{f.label}</div>
<input value={(form as Record<string, string | boolean>)[f.k] as string} onChange={e => setF(f.k, e.target.value)} placeholder={f.ph} <input value={(form as Record<string, string | boolean>)[f.k] as string} onChange={e => setF(f.k, e.target.value)} placeholder={f.ph}
autoComplete="off" autoComplete="off"
style={{ width: '100%', padding: '9px 12px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 9, color: '#fff', fontSize: '0.85rem', outline: 'none', boxSizing: 'border-box' }} /> style={{ width: '100%', padding: '9px 12px', background: '#FFFFFF', border: '1px solid #CBD5E1', borderRadius: 9, color: '#0F172A', fontSize: '0.85rem', outline: 'none', boxSizing: 'border-box' }} />
</div> </div>
))} ))}
{/* Password — same row as Aadhaar */} {/* Password — same row as Aadhaar */}
<div> <div>
<div style={{ fontSize: '0.6rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 5 }}>Password <span style={{ color: '#334155', fontWeight: 400, textTransform: 'none', letterSpacing: 0 }}>(optional)</span></div> <div style={{ fontSize: '0.6rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 5 }}>Password <span style={{ color: '#64748B', fontWeight: 400, textTransform: 'none', letterSpacing: 0 }}>(optional)</span></div>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<input type={showPw ? 'text' : 'password'} value={form.password} onChange={e => setF('password', e.target.value)} placeholder="Min 8 chars" <input type={showPw ? 'text' : 'password'} value={form.password} onChange={e => setF('password', e.target.value)} placeholder="Min 8 chars"
autoComplete="new-password" autoComplete="new-password"
style={{ width: '100%', padding: '9px 36px 9px 12px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 9, color: '#fff', fontSize: '0.85rem', outline: 'none', boxSizing: 'border-box' }} /> style={{ width: '100%', padding: '9px 36px 9px 12px', background: '#FFFFFF', border: '1px solid #CBD5E1', borderRadius: 9, color: '#0F172A', fontSize: '0.85rem', outline: 'none', boxSizing: 'border-box' }} />
<button onClick={() => setShowPw(p => !p)} style={{ position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', color: '#64748B', cursor: 'pointer', display: 'flex' }}> <button onClick={() => setShowPw(p => !p)} style={{ position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', color: '#475569', cursor: 'pointer', display: 'flex' }}>
{showPw ? <EyeOff size={14} /> : <Eye size={14} />} {showPw ? <EyeOff size={14} /> : <Eye size={14} />}
</button> </button>
</div> </div>
@@ -260,14 +261,14 @@ export const FleetPersonnel: React.FC = () => {
{/* DRIVER fields */} {/* DRIVER fields */}
{staffType === 'DRIVER' && ( {staffType === 'DRIVER' && (
<div style={{ padding: '14px 16px', background: 'rgba(6,182,212,0.05)', border: '1px solid rgba(6,182,212,0.15)', borderRadius: 12 }}> <div style={{ padding: '14px 16px', background: 'rgba(6,182,212,0.03)', border: '1px solid rgba(6,182,212,0.15)', borderRadius: 12 }}>
<div style={{ fontSize: '0.6rem', color: '#06B6D4', textTransform: 'uppercase', fontWeight: 700, marginBottom: 10 }}>Driver Professional Details</div> <div style={{ fontSize: '0.6rem', color: '#06B6D4', textTransform: 'uppercase', fontWeight: 700, marginBottom: 10 }}>Driver Professional Details</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}>
{[{ k:'license_number', label:'License Number', ph:'DL-KA-2024-DR1' }, { k:'license_category', label:'License Category', ph:'MCWG/LMV' }, { k:'license_expiry', label:'License Expiry', ph:'2034-01-01' }].map(f => ( {[{ k:'license_number', label:'License Number', ph:'DL-KA-2024-DR1' }, { k:'license_category', label:'License Category', ph:'MCWG/LMV' }, { k:'license_expiry', label:'License Expiry', ph:'2034-01-01' }].map(f => (
<div key={f.k}> <div key={f.k}>
<div style={{ fontSize: '0.58rem', color: '#475569', textTransform: 'uppercase', fontWeight: 700, marginBottom: 4 }}>{f.label}</div> <div style={{ fontSize: '0.58rem', color: '#475569', textTransform: 'uppercase', fontWeight: 700, marginBottom: 4 }}>{f.label}</div>
<input value={(form as Record<string, string | boolean>)[f.k] as string} onChange={e => setF(f.k, e.target.value)} placeholder={f.ph} <input value={(form as Record<string, string | boolean>)[f.k] as string} onChange={e => setF(f.k, e.target.value)} placeholder={f.ph}
style={{ width: '100%', padding: '8px 10px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, color: '#fff', fontSize: '0.82rem', outline: 'none', boxSizing: 'border-box' }} /> style={{ width: '100%', padding: '8px 10px', background: '#FFFFFF', border: '1px solid #CBD5E1', borderRadius: 8, color: '#0F172A', fontSize: '0.82rem', outline: 'none', boxSizing: 'border-box' }} />
</div> </div>
))} ))}
</div> </div>
@@ -276,14 +277,14 @@ export const FleetPersonnel: React.FC = () => {
{/* EMT fields */} {/* EMT fields */}
{staffType === 'EMT' && ( {staffType === 'EMT' && (
<div style={{ padding: '14px 16px', background: 'rgba(59,130,246,0.05)', border: '1px solid rgba(59,130,246,0.15)', borderRadius: 12 }}> <div style={{ padding: '14px 16px', background: 'rgba(59,130,246,0.03)', border: '1px solid rgba(59,130,246,0.15)', borderRadius: 12 }}>
<div style={{ fontSize: '0.6rem', color: '#3B82F6', textTransform: 'uppercase', fontWeight: 700, marginBottom: 10 }}>EMT Professional Details</div> <div style={{ fontSize: '0.6rem', color: '#3B82F6', textTransform: 'uppercase', fontWeight: 700, marginBottom: 10 }}>EMT Professional Details</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{[{ k:'qualification', label:'Qualification', ph:'Diploma in EMT' }, { k:'certification_body', label:'Certification Body', ph:'Indian Resuscitation Council' }, { k:'certificate_number', label:'Certificate Number', ph:'EMT-CERT-9901' }, { k:'certificate_expiry', label:'Certificate Expiry', ph:'2029-08-15' }].map(f => ( {[{ k:'qualification', label:'Qualification', ph:'Diploma in EMT' }, { k:'certification_body', label:'Certification Body', ph:'Indian Resuscitation Council' }, { k:'certificate_number', label:'Certificate Number', ph:'EMT-CERT-9901' }, { k:'certificate_expiry', label:'Certificate Expiry', ph:'2029-08-15' }].map(f => (
<div key={f.k}> <div key={f.k}>
<div style={{ fontSize: '0.58rem', color: '#475569', textTransform: 'uppercase', fontWeight: 700, marginBottom: 4 }}>{f.label}</div> <div style={{ fontSize: '0.58rem', color: '#475569', textTransform: 'uppercase', fontWeight: 700, marginBottom: 4 }}>{f.label}</div>
<input value={(form as Record<string, string | boolean>)[f.k] as string} onChange={e => setF(f.k, e.target.value)} placeholder={f.ph} <input value={(form as Record<string, string | boolean>)[f.k] as string} onChange={e => setF(f.k, e.target.value)} placeholder={f.ph}
style={{ width: '100%', padding: '8px 10px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, color: '#fff', fontSize: '0.82rem', outline: 'none', boxSizing: 'border-box' }} /> style={{ width: '100%', padding: '8px 10px', background: '#FFFFFF', border: '1px solid #CBD5E1', borderRadius: 8, color: '#0F172A', fontSize: '0.82rem', outline: 'none', boxSizing: 'border-box' }} />
</div> </div>
))} ))}
</div> </div>
@@ -292,17 +293,17 @@ export const FleetPersonnel: React.FC = () => {
{/* DOCTOR fields */} {/* DOCTOR fields */}
{staffType === 'DOCTOR' && ( {staffType === 'DOCTOR' && (
<div style={{ padding: '14px 16px', background: 'rgba(16,185,129,0.05)', border: '1px solid rgba(16,185,129,0.15)', borderRadius: 12 }}> <div style={{ padding: '14px 16px', background: 'rgba(16,185,129,0.03)', border: '1px solid rgba(16,185,129,0.15)', borderRadius: 12 }}>
<div style={{ fontSize: '0.6rem', color: '#10B981', textTransform: 'uppercase', fontWeight: 700, marginBottom: 10 }}>Doctor Professional Details</div> <div style={{ fontSize: '0.6rem', color: '#10B981', textTransform: 'uppercase', fontWeight: 700, marginBottom: 10 }}>Doctor Professional Details</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{[{ k:'medical_registration_number', label:'Medical Reg. Number', ph:'MCI/DMC/2024/77' }, { k:'specialization', label:'Specialization', ph:'Emergency Medicine' }, { k:'qualification', label:'Qualification', ph:'MBBS, MD (Emergency)' }].map(f => ( {[{ k:'medical_registration_number', label:'Medical Reg. Number', ph:'MCI/DMC/2024/77' }, { k:'specialization', label:'Specialization', ph:'Emergency Medicine' }, { k:'qualification', label:'Qualification', ph:'MBBS, MD (Emergency)' }].map(f => (
<div key={f.k}> <div key={f.k}>
<div style={{ fontSize: '0.58rem', color: '#475569', textTransform: 'uppercase', fontWeight: 700, marginBottom: 4 }}>{f.label}</div> <div style={{ fontSize: '0.58rem', color: '#475569', textTransform: 'uppercase', fontWeight: 700, marginBottom: 4 }}>{f.label}</div>
<input value={(form as Record<string, string | boolean>)[f.k] as string} onChange={e => setF(f.k, e.target.value)} placeholder={f.ph} <input value={(form as Record<string, string | boolean>)[f.k] as string} onChange={e => setF(f.k, e.target.value)} placeholder={f.ph}
style={{ width: '100%', padding: '8px 10px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, color: '#fff', fontSize: '0.82rem', outline: 'none', boxSizing: 'border-box' }} /> style={{ width: '100%', padding: '8px 10px', background: '#FFFFFF', border: '1px solid #CBD5E1', borderRadius: 8, color: '#0F172A', fontSize: '0.82rem', outline: 'none', boxSizing: 'border-box' }} />
</div> </div>
))} ))}
<label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: '0.82rem', color: '#94A3B8', gridColumn: '1 / -1' }}> <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: '0.82rem', color: '#475569', gridColumn: '1 / -1' }}>
<input type="checkbox" checked={form.teleconsult_available} onChange={e => setF('teleconsult_available', e.target.checked)} style={{ accentColor: '#10B981', width: 15, height: 15 }} /> <input type="checkbox" checked={form.teleconsult_available} onChange={e => setF('teleconsult_available', e.target.checked)} style={{ accentColor: '#10B981', width: 15, height: 15 }} />
Teleconsult Available Teleconsult Available
</label> </label>
@@ -326,29 +327,80 @@ export const FleetPersonnel: React.FC = () => {
{/* Stats */} {/* Stats */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 14, marginBottom: 24 }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 14, marginBottom: 24 }}>
{[{ label: 'Total Staff', value: normalized.length, color: '#06B6D4' }, { label: 'On Duty', value: onDuty, color: '#10B981' }, { label: 'Off Duty', value: offDuty, color: '#64748B' }, { label: 'On Leave', value: onLeave, color: '#F59E0B' }].map(s => ( {[{ label: 'Total Staff', value: normalized.length, color: '#06B6D4' }, { label: 'On Duty', value: onDuty, color: '#10B981' }, { label: 'Off Duty', value: offDuty, color: '#64748B' }, { label: 'On Leave', value: onLeave, color: '#F59E0B' }].map(s => (
<div key={s.label} style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 14, padding: '18px 20px', borderLeft: `4px solid ${s.color}` }}> <div key={s.label} style={{ background: '#FFFFFF', border: '1px solid rgba(15, 23, 42, 0.08)', borderRadius: 14, padding: '18px 20px', borderLeft: `4px solid ${s.color}`, boxShadow: '0 4px 12px rgba(15, 23, 42, 0.01)' }}>
<div style={{ fontSize: '1.8rem', fontWeight: 900, color: '#fff', lineHeight: 1 }}>{loading ? '—' : s.value}</div> <div style={{ fontSize: '1.8rem', fontWeight: 900, color: '#0F172A', lineHeight: 1 }}>{loading ? '—' : s.value}</div>
<div style={{ fontSize: '0.7rem', color: '#64748B', marginTop: 6, textTransform: 'uppercase', letterSpacing: '0.5px' }}>{s.label}</div> <div style={{ fontSize: '0.7rem', color: '#475569', marginTop: 6, textTransform: 'uppercase', letterSpacing: '0.5px' }}>{s.label}</div>
</div> </div>
))} ))}
</div> </div>
{/* Toolbar */} {/* Toolbar */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, flexWrap: 'wrap', gap: 12 }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, flexWrap: 'wrap', gap: 12 }}>
<div style={{ display: 'flex', gap: 6, background: 'rgba(255,255,255,0.03)', padding: 4, borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)' }}> <div style={{ display: 'flex', gap: 6, background: '#E2E8F0', padding: 4, borderRadius: 12, border: '1px solid #CBD5E1' }}>
{FILTERS.map(f => ( {FILTERS.map(f => (
<button key={f} onClick={() => { setFilter(f); setPage(1); }} style={{ padding: '7px 14px', borderRadius: 8, border: 'none', cursor: 'pointer', fontSize: '0.78rem', fontWeight: filter === f ? 700 : 500, background: filter === f ? 'linear-gradient(135deg,#06B6D4,#3B82F6)' : 'transparent', color: filter === f ? '#fff' : '#64748B', transition: 'all 0.2s' }}> <button key={f} onClick={() => { setFilter(f); setPage(1); }} style={{ padding: '7px 14px', borderRadius: 8, border: 'none', cursor: 'pointer', fontSize: '0.78rem', fontWeight: filter === f ? 700 : 500, background: filter === f ? 'linear-gradient(135deg,#06B6D4,#3B82F6)' : 'transparent', color: filter === f ? '#fff' : '#475569', transition: 'all 0.2s' }}>
{f === 'ALL' ? 'All' : f} {f === 'ALL' ? 'All' : f}
</button> </button>
))} ))}
</div> </div>
<div style={{ display: 'flex', gap: 10 }}> <div style={{ display: 'flex', gap: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, background: 'rgba(255,255,255,0.03)', padding: '8px 14px', borderRadius: 10, border: '1px solid rgba(255,255,255,0.06)' }}> <div style={{
<Search size={14} color="#64748B" /> display: 'flex',
<input value={search} onChange={e => { setSearch(e.target.value); setPage(1); }} placeholder="Search staff..." style={{ background: 'transparent', border: 'none', color: '#fff', fontSize: '0.85rem', outline: 'none', width: 170 }} /> alignItems: 'center',
gap: '10px',
background: '#FFFFFF',
padding: '10px 16px',
borderRadius: '12px',
border: isSearchFocused ? '1px solid #06B6D4' : '1px solid #CBD5E1',
boxShadow: isSearchFocused ? '0 0 0 3px rgba(6, 182, 212, 0.15)' : 'none',
transition: 'all 0.2s ease',
width: '260px',
position: 'relative'
}}>
<Search size={16} color={isSearchFocused ? "#06B6D4" : "#64748B"} style={{ transition: 'color 0.2s' }} />
<input
type="text"
placeholder="Search staff..."
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
onFocus={() => setIsSearchFocused(true)}
onBlur={() => setIsSearchFocused(false)}
className="stations-search-input"
style={{
background: 'transparent',
border: 'none',
color: '#0F172A',
fontSize: '0.875rem',
outline: 'none',
width: '100%',
paddingRight: search ? '24px' : '0'
}}
/>
{search && (
<button
onClick={() => { setSearch(''); setPage(1); }}
style={{
position: 'absolute',
right: '12px',
background: 'transparent',
border: 'none',
cursor: 'pointer',
color: '#94A3B8',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2px',
borderRadius: '50%',
}}
onMouseEnter={e => e.currentTarget.style.color = '#475569'}
onMouseLeave={e => e.currentTarget.style.color = '#94A3B8'}
>
<X size={14} />
</button>
)}
</div> </div>
<button onClick={() => { resetModal(); setShowModal(true); }} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '9px 18px', background: 'linear-gradient(135deg,#06B6D4,#3B82F6)', border: 'none', borderRadius: 10, color: '#fff', fontWeight: 700, cursor: 'pointer', fontSize: '0.82rem', whiteSpace: 'nowrap', boxShadow: '0 4px 12px rgba(6,182,212,0.3)' }}> <button onClick={() => { resetModal(); setShowModal(true); }} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '11px 22px', background: '#0F172A', border: 'none', borderRadius: '12px', color: '#fff', fontWeight: 700, cursor: 'pointer', fontSize: '0.875rem', whiteSpace: 'nowrap' }}>
<UserPlus size={15} /> ADD STAFF <UserPlus size={18} /> ADD STAFF
</button> </button>
</div> </div>
</div> </div>
@@ -364,7 +416,7 @@ export const FleetPersonnel: React.FC = () => {
)} )}
{!loading && !fetchError && ( {!loading && !fetchError && (
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 16, overflow: 'hidden' }}> <div style={{ background: '#FFFFFF', border: '1px solid rgba(15, 23, 42, 0.08)', borderRadius: 16, overflow: 'hidden', boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}> <table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead> <thead>
<tr> <tr>
@@ -376,23 +428,23 @@ export const FleetPersonnel: React.FC = () => {
<tr><td colSpan={8} style={{ textAlign: 'center', padding: 48, color: '#64748B' }}>{staffList.length === 0 ? 'No staff registered yet.' : 'No results match your filter.'}</td></tr> <tr><td colSpan={8} style={{ textAlign: 'center', padding: 48, color: '#64748B' }}>{staffList.length === 0 ? 'No staff registered yet.' : 'No results match your filter.'}</td></tr>
) : pageData.map((s, idx) => { ) : pageData.map((s, idx) => {
const rc = ROLE_COLORS[s.role] || DEF; const rc = ROLE_COLORS[s.role] || DEF;
const sc = STATUS_CFG[s.status] || { label: s.status, color: '#94A3B8' }; const sc = STATUS_CFG[s.status] || { label: s.status, color: '#475569' };
const certSoon = s.certExpiry ? new Date(s.certExpiry) < new Date(Date.now() + 60 * 24 * 60 * 60 * 1000) : false; const certSoon = s.certExpiry ? new Date(s.certExpiry) < new Date(Date.now() + 60 * 24 * 60 * 60 * 1000) : false;
return ( return (
<tr key={s.id} <tr key={s.id}
onClick={() => { const raw = staffList.find(r => r.id === s.id); if (raw) setSelectedRaw(raw); }} onClick={() => { const raw = staffList.find(r => r.id === s.id); if (raw) setSelectedRaw(raw); }}
style={{ borderBottom: '1px solid rgba(255,255,255,0.05)', cursor: 'pointer', transition: 'background 0.15s' }} style={{ borderBottom: '1px solid rgba(15, 23, 42, 0.06)', cursor: 'pointer', transition: 'background 0.15s' }}
onMouseEnter={e => (e.currentTarget.style.background = 'rgba(255,255,255,0.04)')} onMouseEnter={e => (e.currentTarget.style.background = '#F8FAFC')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
> >
<td style={{ padding: '14px 18px', color: '#475569', fontSize: '0.78rem', fontWeight: 600 }}>{(safePage - 1) * PAGE_SIZE + idx + 1}</td> <td style={{ padding: '14px 18px', color: '#475569', fontSize: '0.78rem', fontWeight: 600 }}>{(safePage - 1) * itemsPerPage + idx + 1}</td>
<td style={{ padding: '14px 18px' }}> <td style={{ padding: '14px 18px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 38, height: 38, borderRadius: 10, background: rc.bg, border: `1.5px solid ${rc.color}40`, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.85rem', fontWeight: 900, color: rc.color, flexShrink: 0 }}> <div style={{ width: 38, height: 38, borderRadius: 10, background: rc.bg, border: `1.5px solid ${rc.color}40`, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.85rem', fontWeight: 900, color: rc.color, flexShrink: 0 }}>
{s.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase()} {s.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase()}
</div> </div>
<div> <div>
<div style={{ fontWeight: 700, fontSize: '0.88rem', color: '#F1F5F9' }}>{s.name}</div> <div style={{ fontWeight: 700, fontSize: '0.88rem', color: '#0F172A' }}>{s.name}</div>
</div> </div>
</div> </div>
</td> </td>
@@ -403,11 +455,11 @@ export const FleetPersonnel: React.FC = () => {
<span style={{ fontSize: '0.75rem', fontWeight: 700, color: sc.color }}>{sc.label}</span> <span style={{ fontSize: '0.75rem', fontWeight: 700, color: sc.color }}>{sc.label}</span>
</div> </div>
</td> </td>
<td style={{ padding: '14px 18px' }}><div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: '0.78rem', color: '#94A3B8' }}><Phone size={12} color="#06B6D4" />{s.phone}</div></td> <td style={{ padding: '14px 18px' }}><div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: '0.78rem', color: '#475569' }}><Phone size={12} color="#06B6D4" />{s.phone}</div></td>
<td style={{ padding: '14px 18px' }}><span style={{ fontSize: '0.78rem', color: '#94A3B8' }}>{s.specialization || '—'}</span></td> <td style={{ padding: '14px 18px' }}><span style={{ fontSize: '0.78rem', color: '#475569' }}>{s.specialization || '—'}</span></td>
<td style={{ padding: '14px 18px' }}><span style={{ fontSize: '0.78rem', color: '#94A3B8' }}>{s.joinedDate || '—'}</span></td> <td style={{ padding: '14px 18px' }}><span style={{ fontSize: '0.78rem', color: '#475569' }}>{s.joinedDate || '—'}</span></td>
<td style={{ padding: '14px 18px' }}> <td style={{ padding: '14px 18px' }}>
{s.certExpiry ? <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}><ShieldCheck size={14} color={certSoon ? '#F59E0B' : '#10B981'} /><span style={{ fontSize: '0.75rem', fontWeight: 600, color: certSoon ? '#F59E0B' : '#94A3B8' }}>{s.certExpiry}</span></div> : <span style={{ color: '#475569' }}></span>} {s.certExpiry ? <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}><ShieldCheck size={14} color={certSoon ? '#F59E0B' : '#10B981'} /><span style={{ fontSize: '0.75rem', fontWeight: 600, color: certSoon ? '#F59E0B' : '#475569' }}>{s.certExpiry}</span></div> : <span style={{ color: '#64748B' }}></span>}
</td> </td>
</tr> </tr>
); );
@@ -415,14 +467,40 @@ export const FleetPersonnel: React.FC = () => {
</tbody> </tbody>
</table> </table>
{/* Pagination */} {/* Pagination */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '14px 20px', borderTop: '1px solid rgba(255,255,255,0.06)' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '14px 20px', borderTop: '1px solid rgba(15, 23, 42, 0.08)' }}>
<span style={{ fontSize: '0.78rem', color: '#64748B' }}>Showing <strong style={{ color: '#94A3B8' }}>{filtered.length === 0 ? 0 : (safePage - 1) * PAGE_SIZE + 1}{Math.min(safePage * PAGE_SIZE, filtered.length)}</strong> of <strong style={{ color: '#94A3B8' }}>{filtered.length}</strong> staff</span> <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span style={{ fontSize: '0.78rem', color: '#475569' }}>
Showing <strong style={{ color: '#0F172A' }}>{filtered.length === 0 ? 0 : (safePage - 1) * itemsPerPage + 1}{Math.min(safePage * itemsPerPage, filtered.length)}</strong> of <strong style={{ color: '#0F172A' }}>{filtered.length}</strong> staff
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: '0.75rem', color: '#64748B' }}>Rows per page:</span>
<select
value={itemsPerPage}
onChange={e => { setItemsPerPage(Number(e.target.value)); setPage(1); }}
style={{ padding: '4px 8px', borderRadius: 6, border: '1px solid #CBD5E1', background: '#FFFFFF', color: '#0F172A', fontSize: '0.75rem', outline: 'none', cursor: 'pointer' }}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
</select>
</div>
</div>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={safePage === 1} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid rgba(255,255,255,0.08)', background: 'rgba(255,255,255,0.04)', color: safePage === 1 ? '#334155' : '#94A3B8', cursor: safePage === 1 ? 'not-allowed' : 'pointer' }}><ChevronLeft size={15} /></button> <button onClick={() => setPage(1)} disabled={safePage === 1} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid #E2E8F0', background: '#F1F5F9', color: safePage === 1 ? '#94A3B8' : '#475569', cursor: safePage === 1 ? 'not-allowed' : 'pointer' }}><ChevronsLeft size={15} /></button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(p => ( <button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={safePage === 1} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid #E2E8F0', background: '#F1F5F9', color: safePage === 1 ? '#94A3B8' : '#475569', cursor: safePage === 1 ? 'not-allowed' : 'pointer' }}><ChevronLeft size={15} /></button>
<button key={p} onClick={() => setPage(p)} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: 'none', background: p === safePage ? 'linear-gradient(135deg,#06B6D4,#3B82F6)' : 'rgba(255,255,255,0.04)', color: p === safePage ? '#fff' : '#64748B', fontWeight: p === safePage ? 700 : 500, cursor: 'pointer', fontSize: '0.82rem' }}>{p}</button>
{Array.from({ length: totalPages }, (_, i) => i + 1)
.filter(p => p === 1 || p === totalPages || Math.abs(p - safePage) <= 1)
.map((p, i, arr) => (
<React.Fragment key={p}>
{i > 0 && arr[i - 1] !== p - 1 && <span style={{ padding: '0 4px', color: '#94A3B8' }}>...</span>}
<button onClick={() => setPage(p)} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: 'none', background: p === safePage ? 'linear-gradient(135deg,#06B6D4,#3B82F6)' : '#F1F5F9', color: p === safePage ? '#fff' : '#475569', fontWeight: p === safePage ? 700 : 500, cursor: 'pointer', fontSize: '0.82rem' }}>{p}</button>
</React.Fragment>
))} ))}
<button onClick={() => setPage(p => Math.min(totalPages, p + 1))} disabled={safePage === totalPages} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid rgba(255,255,255,0.08)', background: 'rgba(255,255,255,0.04)', color: safePage === totalPages ? '#334155' : '#94A3B8', cursor: safePage === totalPages ? 'not-allowed' : 'pointer' }}><ChevronRight size={15} /></button>
<button onClick={() => setPage(p => Math.min(totalPages, p + 1))} disabled={safePage === totalPages} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid #E2E8F0', background: '#F1F5F9', color: safePage === totalPages ? '#94A3B8' : '#475569', cursor: safePage === totalPages ? 'not-allowed' : 'pointer' }}><ChevronRight size={15} /></button>
<button onClick={() => setPage(totalPages)} disabled={safePage === totalPages} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid #E2E8F0', background: '#F1F5F9', color: safePage === totalPages ? '#94A3B8' : '#475569', cursor: safePage === totalPages ? 'not-allowed' : 'pointer' }}><ChevronsRight size={15} /></button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { import {
Clock, Clock,
Plus, Plus,
@@ -13,6 +14,7 @@ import {
Calendar Calendar
} from 'lucide-react'; } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { fleetApi } from '../../api/fleet';
// --- INTERFACES --- // --- INTERFACES ---
interface Assignment { interface Assignment {
@@ -53,32 +55,8 @@ const SHIFTS = [
{ key: 'NIGHT', label: 'Night', time: '22:00 06:00', color: '#8B5CF6', bg: 'rgba(139,92,246,0.1)' }, { key: 'NIGHT', label: 'Night', time: '22:00 06:00', color: '#8B5CF6', bg: 'rgba(139,92,246,0.1)' },
] as const; ] as const;
// Fallback collections if live database list is initially empty
const FALLBACK_STAFF = {
DRIVERS: [
{ id: '48360abc-ec1c-4cd3-a8e6-fed791249d8b', name: 'Vikram Singh' },
{ id: 'e971d2b1-9ee8-4c4c-897d-6fd81f8f307a', name: 'Suresh Kumar' },
{ id: '9cfd91fb-db92-491b-8f35-dcba1f2ea8b2', name: 'Karan Mehra' }
],
EMTS: [
{ id: '1a6b6c77-26a4-4776-8681-79f9d961050c', name: 'Rahul Verma' },
{ id: '7d04e5d8-df6c-47ea-bc64-1da0f3da97e2', name: 'Amit Roy' },
{ id: '24b42b10-09a7-47b2-a42d-7bbbb3ef3d4c', name: 'Priya Das' }
],
DOCTORS: [
{ id: '893fbf2a-cb0c-4e89-a292-0b2a60714b60', name: 'Dr. Ananya Iyer' },
{ id: '57c3e387-a3a8-44fb-9389-702b85e05445', name: 'Dr. Sameer Gupta' }
]
};
const FALLBACK_VEHICLES = [
{ id: '20c5f964-aa6c-4df0-a656-06d5b9893607', registration_number: 'TN-06-AM-1001', vehicle_type: 'ALS' },
{ id: '46c4f347-695c-4869-9da2-0c9f8ef4422c', registration_number: 'TN-06-AM-1002', vehicle_type: 'BLS' },
{ id: 'd9cfa42f-77fa-4a2b-bc6b-31a89bf16ab8', registration_number: 'TN-06-AM-1003', vehicle_type: 'ALS' },
{ id: '8c4e4776-8bf0-427c-bc70-4f812328ba8a', registration_number: 'TN-06-AM-1004', vehicle_type: 'TRANS' }
];
export const FleetScheduling: React.FC = () => { export const FleetScheduling: React.FC = () => {
const navigate = useNavigate();
// --- STATE --- // --- STATE ---
const [selectedDate] = useState<string>(new Date().toISOString().split('T')[0]); const [selectedDate] = useState<string>(new Date().toISOString().split('T')[0]);
const [view, setView] = useState<'DAY' | 'WEEK' | 'MONTH'>('DAY'); const [view, setView] = useState<'DAY' | 'WEEK' | 'MONTH'>('DAY');
@@ -91,12 +69,7 @@ export const FleetScheduling: React.FC = () => {
const [rosterSuccess, setRosterSuccess] = useState(''); const [rosterSuccess, setRosterSuccess] = useState('');
const [rosterErr, setRosterErr] = useState(''); const [rosterErr, setRosterErr] = useState('');
const [assignments, setAssignments] = useState<Assignment[]>([ const [assignments, setAssignments] = useState<Assignment[]>([]);
{ id: 'AS-1001', date: new Date().toISOString().split('T')[0], vehicleId: '20c5f964-aa6c-4df0-a656-06d5b9893607', vehicleReg: 'TN-06-AM-1001', vehicleType: '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', date: new Date().toISOString().split('T')[0], vehicleId: '46c4f347-695c-4869-9da2-0c9f8ef4422c', vehicleReg: 'TN-06-AM-1002', vehicleType: 'BLS', shift: 'MORNING', driver: 'Suresh Kumar', emt: 'Amit Roy', status: 'ON_DUTY', startTime: '06:00', endTime: '14:00' },
{ id: 'AS-1003', date: new Date().toISOString().split('T')[0], vehicleId: 'd9cfa42f-77fa-4a2b-bc6b-31a89bf16ab8', vehicleReg: 'TN-06-AM-1003', vehicleType: 'ALS', shift: 'EVENING', driver: 'Karan Mehra', emt: 'Priya Das', doctor: 'Dr. Sameer Gupta', status: 'SCHEDULED', startTime: '14:00', endTime: '22:00' },
{ id: 'AS-1004', date: new Date().toISOString().split('T')[0], vehicleId: '8c4e4776-8bf0-427c-bc70-4f812328ba8a', vehicleReg: 'TN-06-AM-1004', vehicleType: 'TRANS', shift: 'MORNING', driver: 'Ravi Teja', emt: 'Sneha Rao', status: 'SCHEDULED', startTime: '06:00', endTime: '14:00' },
]);
// Modals state // Modals state
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
@@ -117,26 +90,107 @@ export const FleetScheduling: React.FC = () => {
if (!token) return; if (!token) return;
setLoadingAssets(true); setLoadingAssets(true);
try { try {
const vRes = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/vehicles', { // 1. Fetch Vehicles
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, const vJson = await fleetApi.getVehicles(token);
});
const vJson = await vRes.json();
let vList: APIVehicle[] = []; let vList: APIVehicle[] = [];
if (vJson?.data?.data && Array.isArray(vJson.data.data)) vList = vJson.data.data; if (vJson?.data?.data && Array.isArray(vJson.data.data)) vList = vJson.data.data;
else if (vJson?.data && Array.isArray(vJson.data)) vList = vJson.data; else if (vJson?.data && Array.isArray(vJson.data)) vList = vJson.data;
else if (Array.isArray(vJson)) vList = vJson; else if (Array.isArray(vJson)) vList = vJson;
setApiVehicles(vList); setApiVehicles(vList);
const sRes = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/staff', { // 2. Fetch Staff
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, const sJson = await fleetApi.getStaff(token);
});
const sJson = await sRes.json();
let sList: APIStaff[] = []; let sList: APIStaff[] = [];
if (sJson?.data?.data && Array.isArray(sJson.data.data)) sList = sJson.data.data; if (sJson?.data?.data && Array.isArray(sJson.data.data)) sList = sJson.data.data;
else if (sJson?.data && Array.isArray(sJson.data)) sList = sJson.data; else if (sJson?.data && Array.isArray(sJson.data)) sList = sJson.data;
else if (Array.isArray(sJson)) sList = sJson; else if (Array.isArray(sJson)) sList = sJson;
setApiStaff(sList); setApiStaff(sList);
// 3. Fetch Scheduled Rosters
let rList: any[] = [];
try {
const rJson = await fleetApi.getRoster(token);
if (rJson) {
rList = rJson?.data?.data || rJson?.data || (Array.isArray(rJson) ? rJson : []);
}
} catch (err) {
console.error('Failed to fetch roster list:', err);
}
// 4. Map to assignments (using actual data only, completely removing mocks)
const parsedAssignments: Assignment[] = [];
const addedIds = new Set<string>();
// Add from roster query
rList.forEach((r: any) => {
if (!r.id) return;
const vehicle = r.vehicle || vList.find((v: any) => v.id === r.vehicleId);
const driverName = r.driver?.user?.name || 'Unknown Pilot';
const staffName = r.staff?.user?.name || 'Unknown EMT';
parsedAssignments.push({
id: r.id,
date: r.startDate || new Date().toISOString().split('T')[0],
vehicleId: r.vehicleId,
vehicleReg: vehicle?.registration_number || r.vehicleId,
vehicleType: (vehicle?.vehicle_type as any) || 'ALS',
shift: r.shiftType === 'NIGHT' ? 'NIGHT' : (r.shiftType === 'DAY' ? 'MORNING' : 'EVENING'),
driver: driverName,
emt: staffName,
status: 'SCHEDULED',
startTime: r.shiftType === 'NIGHT' ? '22:00' : (r.shiftType === 'DAY' ? '06:00' : '14:00'),
endTime: r.shiftType === 'NIGHT' ? '06:00' : (r.shiftType === 'DAY' ? '14:00' : '22:00')
});
addedIds.add(r.id);
});
// Add active rosters from live vehicles list
vList.forEach((v: any) => {
if (v.activeRoster && v.activeRoster.id && !addedIds.has(v.activeRoster.id)) {
const r = v.activeRoster;
const driverName = r.driver?.user?.name || 'Unknown Pilot';
const staffName = r.staff?.user?.name || 'Unknown EMT';
parsedAssignments.push({
id: r.id,
date: new Date().toISOString().split('T')[0],
vehicleId: v.id,
vehicleReg: v.registration_number,
vehicleType: (v.vehicle_type as any) || 'ALS',
shift: 'MORNING',
driver: driverName,
emt: staffName,
status: 'ON_DUTY',
startTime: '06:00',
endTime: '14:00'
});
addedIds.add(r.id);
}
if (v.activeShift && v.activeShift.id && !addedIds.has(v.activeShift.id)) {
const sh = v.activeShift;
const driverName = sh.driver?.user?.name || 'Unknown Pilot';
const staffName = sh.staff?.user?.name || 'Unknown EMT';
parsedAssignments.push({
id: sh.id,
date: new Date().toISOString().split('T')[0],
vehicleId: v.id,
vehicleReg: v.registration_number,
vehicleType: (v.vehicle_type as any) || 'ALS',
shift: 'EVENING',
driver: driverName,
emt: staffName,
status: 'ON_DUTY',
startTime: '14:00',
endTime: '22:00'
});
addedIds.add(sh.id);
}
});
setAssignments(parsedAssignments);
} catch (e) { } catch (e) {
console.error('[Scheduling API Check] Failed to sync assets:', e); console.error('[Scheduling API Check] Failed to sync assets:', e);
} finally { } finally {
@@ -149,23 +203,19 @@ export const FleetScheduling: React.FC = () => {
}, [fetchAssets]); }, [fetchAssets]);
const driverOptions = useMemo(() => { const driverOptions = useMemo(() => {
const list = apiStaff.filter(s => s.type === 'DRIVER').map(s => ({ id: s.id, name: s.user?.name || 'Unknown Pilot' })); return apiStaff.filter(s => s.type === 'DRIVER').map(s => ({ id: s.id, name: s.user?.name || 'Unknown Pilot' }));
return list.length > 0 ? list : FALLBACK_STAFF.DRIVERS;
}, [apiStaff]); }, [apiStaff]);
const emtOptions = useMemo(() => { const emtOptions = useMemo(() => {
const list = apiStaff.filter(s => s.type === 'EMT').map(s => ({ id: s.id, name: s.user?.name || 'Unknown EMT' })); return apiStaff.filter(s => s.type === 'EMT').map(s => ({ id: s.id, name: s.user?.name || 'Unknown EMT' }));
return list.length > 0 ? list : FALLBACK_STAFF.EMTS;
}, [apiStaff]); }, [apiStaff]);
const doctorOptions = useMemo(() => { const doctorOptions = useMemo(() => {
const list = apiStaff.filter(s => s.type === 'DOCTOR').map(s => ({ id: s.id, name: s.user?.name || 'Unknown Doctor' })); return apiStaff.filter(s => s.type === 'DOCTOR').map(s => ({ id: s.id, name: s.user?.name || 'Unknown Doctor' }));
return list.length > 0 ? list : FALLBACK_STAFF.DOCTORS;
}, [apiStaff]); }, [apiStaff]);
const vehicleOptions = useMemo(() => { const vehicleOptions = useMemo(() => {
const list = apiVehicles.map(v => ({ id: v.id, registration_number: v.registration_number, vehicle_type: v.vehicle_type || 'ALS' })); return apiVehicles.map(v => ({ id: v.id, registration_number: v.registration_number, vehicle_type: v.vehicle_type || 'ALS' }));
return list.length > 0 ? list : FALLBACK_VEHICLES;
}, [apiVehicles]); }, [apiVehicles]);
const currentFilteredAssignments = useMemo(() => { const currentFilteredAssignments = useMemo(() => {
@@ -239,74 +289,23 @@ export const FleetScheduling: React.FC = () => {
setSubmittingRoster(true); setSubmittingRoster(true);
try { try {
const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/roster', { const json = await fleetApi.createRoster(payload, token);
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const json = await res.json();
if (!res.ok) { if (json && json.status && json.status >= 400) {
throw new Error(json?.message || `API Response Error ${res.status}`); throw new Error(json?.message || `API Response Error ${json.status}`);
} }
setRosterSuccess('Roster scheduled successfully on backend API!'); setRosterSuccess('Roster scheduled successfully on backend API!');
const selectedV = vehicleOptions.find(v => v.id === formVehicleId);
const selectedDriver = driverOptions.find(d => d.id === formDriverId);
const selectedStaff = emtOptions.find(e => e.id === formStaffId) || doctorOptions.find(doc => doc.id === formStaffId);
const newAs: Assignment = {
id: json?.data?.id || `AS-${1000 + assignments.length + 1}`,
date: formStartDate,
vehicleId: formVehicleId,
vehicleReg: selectedV?.registration_number || formVehicleId,
vehicleType: (selectedV?.vehicle_type as any) || 'ALS',
shift: formShiftType === 'NIGHT' ? 'NIGHT' : (formShiftType === 'DAY' ? 'MORNING' : 'EVENING'),
driver: selectedDriver?.name || 'Driver',
emt: selectedStaff?.name || 'EMT',
status: 'SCHEDULED',
startTime: formShiftType === 'NIGHT' ? '22:00' : (formShiftType === 'DAY' ? '06:00' : '14:00'),
endTime: formShiftType === 'NIGHT' ? '06:00' : (formShiftType === 'DAY' ? '14:00' : '22:00')
};
setAssignments(prev => [newAs, ...prev]);
setTimeout(() => { setTimeout(() => {
fetchAssets(); // Refresh assignments from real database endpoints
setShowCreateModal(false); setShowCreateModal(false);
setRosterSuccess(''); setRosterSuccess('');
}, 1500); }, 1500);
} catch (err: any) { } catch (err: any) {
console.error('[Create Roster Error]:', err); console.error('[Create Roster Error]:', err);
setRosterSuccess('Roster simulated locally (Backend API offline or demo credentials)'); setRosterErr(err?.message || 'Failed to schedule roster assignment on backend.');
const selectedV = vehicleOptions.find(v => v.id === formVehicleId);
const selectedDriver = driverOptions.find(d => d.id === formDriverId);
const selectedStaff = emtOptions.find(e => e.id === formStaffId) || doctorOptions.find(doc => doc.id === formStaffId);
const newAs: Assignment = {
id: `AS-${1000 + assignments.length + 1}`,
date: formStartDate,
vehicleId: formVehicleId,
vehicleReg: selectedV?.registration_number || formVehicleId,
vehicleType: (selectedV?.vehicle_type as any) || 'ALS',
shift: formShiftType === 'NIGHT' ? 'NIGHT' : (formShiftType === 'DAY' ? 'MORNING' : 'EVENING'),
driver: selectedDriver?.name || 'Driver',
emt: selectedStaff?.name || 'EMT',
status: 'SCHEDULED',
startTime: formShiftType === 'NIGHT' ? '22:00' : (formShiftType === 'DAY' ? '06:00' : '14:00'),
endTime: formShiftType === 'NIGHT' ? '06:00' : (formShiftType === 'DAY' ? '14:00' : '22:00')
};
setAssignments(prev => [newAs, ...prev]);
setTimeout(() => {
setShowCreateModal(false);
setRosterSuccess('');
}, 1800);
} finally { } finally {
setSubmittingRoster(false); setSubmittingRoster(false);
} }
@@ -330,6 +329,9 @@ export const FleetScheduling: React.FC = () => {
{/* Action Buttons */} {/* Action Buttons */}
<div style={{ display: 'flex', gap: 10 }}> <div style={{ display: 'flex', gap: 10 }}>
<button onClick={() => navigate('?tab=active-shifts')} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 12px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 10, color: '#10B981', fontSize: '0.75rem', fontWeight: 700, cursor: 'pointer' }}>
<Activity size={13} /> Active Shifts
</button>
<button onClick={fetchAssets} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 12px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 10, color: '#94A3B8', fontSize: '0.75rem', fontWeight: 700, cursor: 'pointer' }}> <button onClick={fetchAssets} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 12px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 10, color: '#94A3B8', fontSize: '0.75rem', fontWeight: 700, cursor: 'pointer' }}>
{loadingAssets ? <Loader2 size={13} className="spin" /> : '↺ Sync Assets'} {loadingAssets ? <Loader2 size={13} className="spin" /> : '↺ Sync Assets'}
</button> </button>
@@ -399,7 +401,7 @@ export const FleetScheduling: React.FC = () => {
<User size={15} color="#06B6D4" /> <User size={15} color="#06B6D4" />
<div> <div>
<div style={{ fontSize: '0.58rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px', fontWeight: 700 }}>Pilot Driver</div> <div style={{ fontSize: '0.58rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px', fontWeight: 700 }}>Pilot Driver</div>
<div style={{ fontSize: '0.85rem', fontWeight: 700, color: '#E2E8F0' }}>{a.driver}</div> <div style={{ fontSize: '0.85rem', fontWeight: 700, color: '#000000' }}>{a.driver}</div>
</div> </div>
</div> </div>
@@ -407,7 +409,7 @@ export const FleetScheduling: React.FC = () => {
<Activity size={15} color="#3B82F6" /> <Activity size={15} color="#3B82F6" />
<div> <div>
<div style={{ fontSize: '0.58rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px', fontWeight: 700 }}>Emergency Medical Technician (EMT)</div> <div style={{ fontSize: '0.58rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px', fontWeight: 700 }}>Emergency Medical Technician (EMT)</div>
<div style={{ fontSize: '0.85rem', fontWeight: 700, color: '#E2E8F0' }}>{a.emt}</div> <div style={{ fontSize: '0.85rem', fontWeight: 700, color: '#000000' }}>{a.emt}</div>
</div> </div>
</div> </div>
@@ -416,7 +418,7 @@ export const FleetScheduling: React.FC = () => {
<Stethoscope size={15} color="#10B981" /> <Stethoscope size={15} color="#10B981" />
<div> <div>
<div style={{ fontSize: '0.58rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px', fontWeight: 700 }}>Advanced Medical Doctor</div> <div style={{ fontSize: '0.58rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px', fontWeight: 700 }}>Advanced Medical Doctor</div>
<div style={{ fontSize: '0.85rem', fontWeight: 700, color: '#E2E8F0' }}>{a.doctor || 'Dr. Alok Mehta'}</div> <div style={{ fontSize: '0.85rem', fontWeight: 700, color: '#000000' }}>{a.doctor || 'Dr. Alok Mehta'}</div>
</div> </div>
</div> </div>
)} )}

View File

@@ -0,0 +1,339 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Search, MapPin, Truck, Activity, Navigation,
User, Calendar, Clock, AlertTriangle, Filter,
ChevronDown, X, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
interface Vehicle {
id: string;
registration_number: string;
vehicle_type: string;
brand?: string;
model?: string;
station_id?: string;
status?: string;
gps_lat?: string;
gps_lon?: string;
activeShift?: any;
activeRoster?: any;
}
export const FleetTrips: React.FC = () => {
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState<string>('');
const [statusFilter, setStatusFilter] = useState<string>('');
const [isSearchFocused, setIsSearchFocused] = useState<boolean>(false);
// Pagination
const [page, setPage] = useState<number>(1);
const [itemsPerPage, setItemsPerPage] = useState<number>(5);
const fetchVehicles = useCallback(async () => {
setLoading(true);
setError(null);
try {
const token = localStorage.getItem('teleems_token') || '';
const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/vehicles', {
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
});
const json = await res.json();
let list: Vehicle[] = [];
if (json?.data?.data && Array.isArray(json.data.data)) list = json.data.data;
else if (json?.data && Array.isArray(json.data)) list = json.data;
else if (Array.isArray(json)) list = json;
setVehicles(list);
} catch (e: any) {
console.error('Failed to fetch vehicles for trip management:', e);
setError(e?.message || 'Failed to fetch vehicles');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchVehicles();
}, [fetchVehicles]);
const filteredVehicles = vehicles.filter(v => {
const searchMatch = v.registration_number?.toLowerCase().includes(searchQuery.toLowerCase()) ||
v.activeShift?.driver?.user?.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
v.activeShift?.staff?.user?.name?.toLowerCase().includes(searchQuery.toLowerCase());
const statusMatch = statusFilter ? v.status === statusFilter : true;
return searchMatch && statusMatch;
});
const totalPages = Math.max(1, Math.ceil(filteredVehicles.length / itemsPerPage));
const safePage = Math.min(page, totalPages);
const pageData = filteredVehicles.slice((safePage - 1) * itemsPerPage, safePage * itemsPerPage);
const getStatusColor = (status?: string) => {
switch (status) {
case 'BUSY':
return { bg: 'rgba(239, 68, 68, 0.1)', color: '#EF4444', border: 'rgba(239, 68, 68, 0.2)' };
case 'AVAILABLE':
return { bg: 'rgba(34, 197, 94, 0.1)', color: '#22C55E', border: 'rgba(34, 197, 94, 0.2)' };
default:
return { bg: 'rgba(245, 158, 11, 0.1)', color: '#F59E0B', border: 'rgba(245, 158, 11, 0.2)' };
}
};
const activeTripsCount = vehicles.filter(v => v.status === 'BUSY').length;
const availableCount = vehicles.filter(v => v.status === 'AVAILABLE').length;
return (
<div className="fleet-trips 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)', background: '#FFFFFF', boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ color: '#3B82F6' }}><Truck size={20} /></div>
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#0F172A' }}>{loading ? '...' : vehicles.length}</div>
<div style={{ fontSize: '0.7rem', color: '#64748B', textTransform: 'uppercase', fontWeight: 700 }}>Total Vehicles</div>
</div>
<div className="glass" style={{ padding: '20px', borderRadius: '16px', border: '1px solid rgba(239, 68, 68, 0.2)', background: '#FFFFFF', boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ color: '#EF4444' }}><Activity size={20} /></div>
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#EF4444' }}>{loading ? '...' : activeTripsCount}</div>
<div style={{ fontSize: '0.7rem', color: '#64748B', textTransform: 'uppercase', fontWeight: 700 }}>Active Trips (Busy)</div>
</div>
<div className="glass" style={{ padding: '20px', borderRadius: '16px', border: '1px solid rgba(34, 197, 94, 0.2)', background: '#FFFFFF', boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ color: '#22C55E' }}><Navigation size={20} /></div>
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#22C55E' }}>{loading ? '...' : availableCount}</div>
<div style={{ fontSize: '0.7rem', color: '#64748B', textTransform: 'uppercase', fontWeight: 700 }}>Available Units</div>
</div>
<div className="glass" style={{ padding: '20px', borderRadius: '16px', border: '1px solid rgba(245, 158, 11, 0.2)', background: '#FFFFFF', boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ color: '#F59E0B' }}><AlertTriangle size={20} /></div>
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#F59E0B' }}>{loading ? '...' : vehicles.filter(v => !v.activeShift).length}</div>
<div style={{ fontSize: '0.7rem', color: '#64748B', textTransform: 'uppercase', fontWeight: 700 }}>No Active Shift</div>
</div>
</div>
{/* Toolbar */}
<div style={{ display: 'flex', gap: '12px', alignItems: 'center', marginBottom: 16 }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
background: '#FFFFFF',
padding: '10px 16px',
borderRadius: '12px',
border: isSearchFocused ? '1px solid #06B6D4' : '1px solid #CBD5E1',
boxShadow: isSearchFocused ? '0 0 0 3px rgba(6, 182, 212, 0.15)' : '0 2px 4px rgba(15, 23, 42, 0.01)',
transition: 'all 0.2s ease',
flex: 1,
position: 'relative'
}}>
<Search size={16} color={isSearchFocused ? "#06B6D4" : "#64748B"} style={{ transition: 'color 0.2s' }} />
<input
type="text"
placeholder="Search by vehicle reg, driver, or EMT name..."
value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setPage(1); }}
onFocus={() => setIsSearchFocused(true)}
onBlur={() => setIsSearchFocused(false)}
className="stations-search-input"
style={{ background: 'transparent', border: 'none', color: '#0F172A', fontSize: '0.875rem', outline: 'none', width: '100%', paddingRight: searchQuery ? '24px' : '0' }}
/>
{searchQuery && (
<button
onClick={() => { setSearchQuery(''); setPage(1); }}
style={{
position: 'absolute',
right: '12px',
background: 'transparent',
border: 'none',
cursor: 'pointer',
color: '#94A3B8',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2px',
borderRadius: '50%',
}}
onMouseEnter={e => e.currentTarget.style.color = '#475569'}
onMouseLeave={e => e.currentTarget.style.color = '#94A3B8'}
>
<X size={14} />
</button>
)}
</div>
<div style={{ position: 'relative', display: 'inline-block' }}>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
style={{
background: '#FFFFFF', border: '1px solid #CBD5E1', padding: '11px 36px 11px 16px',
borderRadius: '12px', color: '#0F172A', fontSize: '0.875rem', outline: 'none',
cursor: 'pointer', appearance: 'none', minWidth: '180px', boxShadow: '0 2px 4px rgba(15, 23, 42, 0.01)',
}}
>
<option value="">All Statuses</option>
<option value="AVAILABLE">Available</option>
<option value="BUSY">Busy</option>
</select>
<ChevronDown size={16} color="#64748B" style={{ position: 'absolute', right: '12px', top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }} />
</div>
</div>
{/* Content */}
{loading ? (
<div style={{ padding: '60px', textAlign: 'center', color: '#64748B', background: '#FFFFFF', borderRadius: '16px', border: '1px solid #CBD5E1' }}>
<Activity className="spin" size={24} style={{ margin: '0 auto 12px' }} />
Loading trips...
</div>
) : error ? (
<div style={{ padding: '24px', background: '#FEF2F2', border: '1px solid #FCA5A5', color: '#991B1B', borderRadius: '16px' }}>
<AlertTriangle size={24} style={{ marginBottom: '8px' }} />
<div style={{ fontWeight: 600 }}>Error loading trips</div>
<div>{error}</div>
</div>
) : filteredVehicles.length === 0 ? (
<div style={{ padding: '60px', textAlign: 'center', color: '#64748B', background: '#FFFFFF', borderRadius: '16px', border: '1px solid #CBD5E1' }}>
<MapPin size={48} style={{ opacity: 0.2, margin: '0 auto 16px' }} />
<div style={{ fontSize: '1.1rem', fontWeight: 600, color: '#475569' }}>No trips found</div>
<div style={{ fontSize: '0.875rem' }}>Adjust your filters to see more results</div>
</div>
) : (
<div style={{ display: 'grid', gap: '16px' }}>
{pageData.map(v => {
const sc = getStatusColor(v.status);
const shift = v.activeShift;
const roster = v.activeRoster;
const driverName = shift?.driver?.user?.name || roster?.driver?.user?.name || 'Unassigned';
const emtName = shift?.staff?.user?.name || roster?.staff?.user?.name || 'Unassigned';
return (
<motion.div
key={v.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
style={{
background: '#FFFFFF', border: '1px solid #CBD5E1', borderRadius: '16px', padding: '20px',
display: 'flex', flexDirection: 'column', gap: '16px', borderLeft: `4px solid ${sc.color}`,
boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
<div style={{ width: '48px', height: '48px', borderRadius: '12px', background: '#F1F5F9', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#475569' }}>
<Truck size={24} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
<h3 style={{ margin: 0, fontSize: '1.1rem', fontWeight: 800, color: '#0F172A' }}>{v.registration_number}</h3>
<span style={{ fontSize: '0.65rem', padding: '4px 8px', borderRadius: '6px', background: '#F1F5F9', color: '#475569', fontWeight: 700 }}>{v.vehicle_type} UNIT</span>
</div>
<div style={{ fontSize: '0.8rem', color: '#64748B', display: 'flex', alignItems: 'center', gap: '6px' }}>
<MapPin size={14} />
{v.gps_lat && v.gps_lat !== "0.0000000" ? `${v.gps_lat}, ${v.gps_lon}` : 'Location Unavailable'}
</div>
</div>
</div>
<div style={{ padding: '6px 12px', borderRadius: '8px', fontSize: '0.75rem', fontWeight: 800, background: sc.bg, color: sc.color }}>
{v.status || 'UNKNOWN'}
</div>
</div>
<div style={{ height: '1px', background: '#F1F5F9' }} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px' }}>
<div>
<div style={{ fontSize: '0.7rem', fontWeight: 700, color: '#94A3B8', textTransform: 'uppercase', marginBottom: '6px' }}>Driver / Pilot</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.875rem', color: '#0F172A', fontWeight: 600 }}>
<User size={16} color="#64748B" /> {driverName}
</div>
</div>
<div>
<div style={{ fontSize: '0.7rem', fontWeight: 700, color: '#94A3B8', textTransform: 'uppercase', marginBottom: '6px' }}>Paramedic / EMT</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.875rem', color: '#0F172A', fontWeight: 600 }}>
<User size={16} color="#64748B" /> {emtName}
</div>
</div>
{shift && (
<div>
<div style={{ fontSize: '0.7rem', fontWeight: 700, color: '#94A3B8', textTransform: 'uppercase', marginBottom: '6px' }}>Shift Details</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.875rem', color: '#0F172A' }}>
<Clock size={16} color="#64748B" />
Started: {new Date(shift.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
)}
</div>
</motion.div>
);
})}
</div>
)}
{/* Pagination Controls */}
{!loading && filteredVehicles.length > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px', background: '#FFFFFF', borderRadius: '16px', border: '1px solid #CBD5E1', marginTop: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span style={{ fontSize: '0.875rem', color: '#64748B' }}>Rows per page:</span>
<select
value={itemsPerPage}
onChange={(e) => { setItemsPerPage(Number(e.target.value)); setPage(1); }}
style={{ background: '#F8FAFC', border: '1px solid #E2E8F0', borderRadius: '8px', padding: '6px 28px 6px 12px', fontSize: '0.875rem', color: '#0F172A', outline: 'none', cursor: 'pointer', appearance: 'none' }}
>
{[5, 10, 20, 50].map(size => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '24px' }}>
<span style={{ fontSize: '0.875rem', color: '#64748B' }}>
{Math.min((safePage - 1) * itemsPerPage + 1, filteredVehicles.length)} - {Math.min(safePage * itemsPerPage, filteredVehicles.length)} of {filteredVehicles.length}
</span>
<div style={{ display: 'flex', gap: '4px' }}>
<button
onClick={() => setPage(1)}
disabled={safePage === 1}
style={{ padding: '6px', borderRadius: '6px', border: 'none', background: 'transparent', color: safePage === 1 ? '#CBD5E1' : '#64748B', cursor: safePage === 1 ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<ChevronsLeft size={18} />
</button>
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={safePage === 1}
style={{ padding: '6px', borderRadius: '6px', border: 'none', background: 'transparent', color: safePage === 1 ? '#CBD5E1' : '#64748B', cursor: safePage === 1 ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<ChevronLeft size={18} />
</button>
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={safePage === totalPages}
style={{ padding: '6px', borderRadius: '6px', border: 'none', background: 'transparent', color: safePage === totalPages ? '#CBD5E1' : '#64748B', cursor: safePage === totalPages ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<ChevronRight size={18} />
</button>
<button
onClick={() => setPage(totalPages)}
disabled={safePage === totalPages}
style={{ padding: '6px', borderRadius: '6px', border: 'none', background: 'transparent', color: safePage === totalPages ? '#CBD5E1' : '#64748B', cursor: safePage === totalPages ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<ChevronsRight size={18} />
</button>
</div>
</div>
</div>
)}
<style>{`
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.spin { animation: spin 1s linear infinite; }
`}</style>
</div>
);
};

View File

@@ -0,0 +1,944 @@
import React, { useEffect, useState } from 'react';
import { Database, Search, Activity, AlertTriangle, ArrowLeft, X, ChevronLeft, ChevronRight, ShoppingCart, Truck, PlusCircle, Package, ClipboardList } from 'lucide-react';
import { useSearchParams } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { fleetApi } from '../../api/fleet';
import { Card } from '../../components/Common';
export const FleetWarehouseStock: React.FC = () => {
const [, setSearchParams] = useSearchParams();
const [stockList, setStockList] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [isSearchFocused, setIsSearchFocused] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
// Master Inventory for dropdowns
const [inventory, setInventory] = useState<any[]>([]);
// Bulk Restock states
interface BulkRestockRow {
itemId: string;
quantity: number;
reason: string;
}
const [showBulkRestockModal, setShowBulkRestockModal] = useState<boolean>(false);
const [bulkRestockRows, setBulkRestockRows] = useState<BulkRestockRow[]>([
{ itemId: '', quantity: 100, reason: 'Shipment from HealthCare Distributors' }
]);
const [bulkRestockIsSubmitting, setBulkRestockIsSubmitting] = useState<boolean>(false);
const [bulkRestockError, setBulkRestockError] = useState<string | null>(null);
// Assign to Vehicle states
interface AssignVehicleRow {
itemId: string;
quantity: number;
batch_number: string;
expiry_date: string;
}
const [showAssignVehicleModal, setShowAssignVehicleModal] = useState<boolean>(false);
const [assignVehicleId, setAssignVehicleId] = useState<string>('');
const [assignSupplierName, setAssignSupplierName] = useState<string>('Central Warehouse');
const [assignReason, setAssignReason] = useState<string>('Ambulance Initial Stocking');
const [assignVehicleRows, setAssignVehicleRows] = useState<AssignVehicleRow[]>([
{ itemId: '', quantity: 100, batch_number: '', expiry_date: '' }
]);
const [assignVehicleIsSubmitting, setAssignVehicleIsSubmitting] = useState<boolean>(false);
const [assignVehicleError, setAssignVehicleError] = useState<string | null>(null);
const [vehiclesList, setVehiclesList] = useState<any[]>([]);
// Pending restock requests states
const [pendingRequests, setPendingRequests] = useState<any[]>([]);
const [loadingRequests, setLoadingRequests] = useState<boolean>(true);
const fetchRequests = async () => {
try {
setLoadingRequests(true);
const token = localStorage.getItem('teleems_token') || '';
if (!token) return;
const res = await fleetApi.getPendingRestockRequests(token);
const data = res?.data?.data || res?.data || [];
setPendingRequests(Array.isArray(data) ? data : []);
} catch (err) {
console.error('Failed to fetch pending requests:', err);
} finally {
setLoadingRequests(false);
}
};
const fetchStock = async () => {
setLoading(true);
setError(null);
try {
const token = localStorage.getItem('teleems_token') || '';
const response = await fleetApi.getWarehouseStock(token);
const data = response?.data?.data?.data || response?.data?.data || response?.data || [];
setStockList(Array.isArray(data) ? data : []);
} catch (err: any) {
console.error('Failed to fetch warehouse stock:', err);
setError(err?.message || 'Failed to fetch warehouse stock data.');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStock();
fetchRequests();
}, []);
useEffect(() => {
const fetchInventory = async () => {
try {
const token = localStorage.getItem('teleems_token') || '';
const response = await fleetApi.getInventoryMaster(token);
const data = response?.data?.data || response?.data || [];
setInventory(Array.isArray(data) ? data : []);
} catch (err) {
console.error('Failed to fetch inventory master:', err);
}
};
fetchInventory();
}, []);
useEffect(() => {
const fetchVehicles = async () => {
try {
const token = localStorage.getItem('teleems_token') || '';
const userStr = localStorage.getItem('teleems_user');
let orgId = '';
if (userStr) {
const u = JSON.parse(userStr);
orgId = u.organisationId || '';
}
const res = await fleetApi.getVehicles(token, orgId);
const data = res?.data?.data || res?.data || [];
setVehiclesList(Array.isArray(data) ? data : []);
} catch (e) {
console.error('Failed to fetch vehicles:', e);
}
};
fetchVehicles();
}, []);
const handleSubmitBulkRestock = async (e: React.FormEvent) => {
e.preventDefault();
setBulkRestockError(null);
setBulkRestockIsSubmitting(true);
if (bulkRestockRows.length === 0) {
setBulkRestockError('Please add at least one item to restock.');
setBulkRestockIsSubmitting(false);
return;
}
const invalidRow = bulkRestockRows.find(row => !row.itemId || row.quantity <= 0);
if (invalidRow) {
setBulkRestockError('Please ensure all rows have an item selected and a positive quantity.');
setBulkRestockIsSubmitting(false);
return;
}
try {
const token = localStorage.getItem('teleems_token') || '';
const payload = bulkRestockRows.map(row => ({
itemId: row.itemId,
quantity: Number(row.quantity),
reason: row.reason || 'Bulk Shipment'
}));
await fleetApi.restockInventory(payload, token);
alert(`Successfully processed bulk restock for ${bulkRestockRows.length} items!`);
setShowBulkRestockModal(false);
fetchStock();
fetchRequests();
} catch (err: any) {
console.error('Failed bulk restock:', err);
setBulkRestockError(err?.message || 'Failed to submit bulk restock. Check item compatibility or try again.');
} finally {
setBulkRestockIsSubmitting(false);
}
};
const handleSubmitAssignVehicle = async (e: React.FormEvent) => {
e.preventDefault();
setAssignVehicleError(null);
setAssignVehicleIsSubmitting(true);
if (!assignVehicleId) {
setAssignVehicleError('Please select a vehicle.');
setAssignVehicleIsSubmitting(false);
return;
}
if (assignVehicleRows.length === 0) {
setAssignVehicleError('Please add at least one item to assign.');
setAssignVehicleIsSubmitting(false);
return;
}
const invalidRow = assignVehicleRows.find(row => !row.itemId || row.quantity <= 0);
if (invalidRow) {
setAssignVehicleError('Please ensure all rows have an item selected and a positive quantity.');
setAssignVehicleIsSubmitting(false);
return;
}
try {
const token = localStorage.getItem('teleems_token') || '';
const payload = {
supplier_name: assignSupplierName,
reason: assignReason,
items: assignVehicleRows.map(row => ({
itemId: row.itemId,
quantity: Number(row.quantity),
batch_number: row.batch_number || 'N/A',
expiry_date: row.expiry_date || new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString().split('T')[0]
}))
};
await fleetApi.assignToVehicle(assignVehicleId, payload, token);
alert(`Successfully assigned inventory to vehicle!`);
setShowAssignVehicleModal(false);
fetchStock();
fetchRequests();
} catch (err: any) {
console.error('Failed to assign to vehicle:', err);
setAssignVehicleError(err?.message || 'Failed to submit assignment. Check item details or try again.');
} finally {
setAssignVehicleIsSubmitting(false);
}
};
useEffect(() => {
setCurrentPage(1);
}, [searchQuery]);
const filteredStock = stockList.filter(item => {
const name = item.item_master?.name || '';
const category = item.item_master?.category || '';
return name.toLowerCase().includes(searchQuery.toLowerCase()) || category.toLowerCase().includes(searchQuery.toLowerCase());
});
const totalPages = Math.ceil(filteredStock.length / itemsPerPage);
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = filteredStock.slice(indexOfFirstItem, indexOfLastItem);
return (
<div className="animate-in fade-in duration-500">
<div style={{ marginBottom: '24px', display: 'flex', gap: '12px' }}>
<button
onClick={() => setSearchParams({ tab: 'inventory' })}
style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', color: '#94A3B8', borderRadius: '12px', width: '52px', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'all 0.2s', flexShrink: 0 }}
onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.1)'}
onMouseLeave={e => e.currentTarget.style.background = 'rgba(255,255,255,0.05)'}
title="Back to Inventory"
>
<ArrowLeft size={18} />
</button>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
background: '#FFFFFF',
padding: '10px 16px',
borderRadius: '12px',
border: isSearchFocused ? '1px solid #06B6D4' : '1px solid #CBD5E1',
boxShadow: isSearchFocused ? '0 0 0 3px rgba(6, 182, 212, 0.15)' : '0 2px 4px rgba(15, 23, 42, 0.01)',
transition: 'all 0.2s ease',
width: '380px',
position: 'relative'
}}>
<Search size={16} color={isSearchFocused ? "#06B6D4" : "#64748B"} style={{ transition: 'color 0.2s' }} />
<input
type="text"
placeholder="Search warehouse stock by name or category..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onFocus={() => setIsSearchFocused(true)}
onBlur={() => setIsSearchFocused(false)}
className="stations-search-input"
style={{
background: 'transparent',
border: 'none',
color: '#0F172A',
fontSize: '0.875rem',
outline: 'none',
width: '100%',
paddingRight: searchQuery ? '24px' : '0'
}}
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
style={{
position: 'absolute',
right: '12px',
background: 'transparent',
border: 'none',
cursor: 'pointer',
color: '#94A3B8',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2px',
borderRadius: '50%',
}}
onMouseEnter={e => e.currentTarget.style.color = '#475569'}
onMouseLeave={e => e.currentTarget.style.color = '#94A3B8'}
>
<X size={14} />
</button>
)}
</div>
<button
onClick={() => {
const initialItemId = inventory.length > 0 ? inventory[0].id : '';
setBulkRestockRows([
{ itemId: initialItemId, quantity: 100, reason: 'Shipment from HealthCare Distributors' }
]);
setBulkRestockError(null);
setShowBulkRestockModal(true);
}}
className="btn-secondary"
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'rgba(59, 130, 246, 0.1)',
border: '1px solid rgba(59, 130, 246, 0.3)',
color: '#3B82F6',
borderRadius: '12px',
padding: '8px 16px',
fontSize: '0.8125rem',
fontWeight: 700,
cursor: 'pointer',
transition: 'all 0.2s',
marginLeft: 'auto'
}}
onMouseEnter={e => {
e.currentTarget.style.background = 'rgba(59, 130, 246, 0.2)';
}}
onMouseLeave={e => {
e.currentTarget.style.background = 'rgba(59, 130, 246, 0.1)';
}}
>
<ShoppingCart size={16} /> BULK RESTOCK
</button>
<button
onClick={() => {
const initialItemId = inventory.length > 0 ? inventory[0].id : '';
setAssignVehicleRows([
{ itemId: initialItemId, quantity: 100, batch_number: '', expiry_date: '' }
]);
setAssignVehicleError(null);
setShowAssignVehicleModal(true);
}}
className="btn-secondary"
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'rgba(245, 158, 11, 0.1)',
border: '1px solid rgba(245, 158, 11, 0.3)',
color: '#F59E0B',
borderRadius: '12px',
padding: '8px 16px',
fontSize: '0.8125rem',
fontWeight: 700,
cursor: 'pointer',
transition: 'all 0.2s'
}}
onMouseEnter={e => {
e.currentTarget.style.background = 'rgba(245, 158, 11, 0.2)';
}}
onMouseLeave={e => {
e.currentTarget.style.background = 'rgba(245, 158, 11, 0.1)';
}}
>
<Truck size={16} /> ASSIGN TO VEHICLE
</button>
<button
onClick={() => setSearchParams({ tab: 'pending-requests' })}
className="btn-secondary"
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'rgba(6, 182, 212, 0.1)',
border: '1px solid rgba(6, 182, 212, 0.3)',
color: '#06B6D4',
borderRadius: '12px',
padding: '8px 16px',
fontSize: '0.8125rem',
fontWeight: 700,
cursor: 'pointer',
transition: 'all 0.2s'
}}
onMouseEnter={e => {
e.currentTarget.style.background = 'rgba(6, 182, 212, 0.2)';
}}
onMouseLeave={e => {
e.currentTarget.style.background = 'rgba(6, 182, 212, 0.1)';
}}
>
<ClipboardList size={16} /> STOCK REQUESTS
</button>
</div>
<Card title="Warehouse Inventory Catalog">
{loading ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '100px', gap: '16px', color: 'var(--text-secondary)' }}>
<Activity size={32} className="spin" style={{ color: 'var(--accent-cyan)' }} />
<span style={{ fontSize: '0.875rem', fontWeight: 600 }}>SYNCHRONIZING WAREHOUSE DATA...</span>
</div>
) : error ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '80px 24px', textAlign: 'center', color: '#EF4444' }}>
<AlertTriangle size={48} style={{ marginBottom: '16px', opacity: 0.8 }} />
<h3 style={{ fontSize: '1.1rem', fontWeight: 700, marginBottom: '8px' }}>Warehouse Connection Failed</h3>
<p style={{ fontSize: '0.82rem', color: '#94A3B8', maxWidth: '360px', margin: '0 auto' }}>{error}</p>
</div>
) : filteredStock.length === 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '100px 24px', color: '#64748B' }}>
<Database size={48} style={{ opacity: 0.2, marginBottom: '16px' }} />
<h3 style={{ fontSize: '1rem', fontWeight: 700, color: '#94A3B8', marginBottom: '4px' }}>Warehouse is Empty</h3>
<p style={{ fontSize: '0.8125rem' }}>No stock data found or search returned no results.</p>
</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: '16px 12px' }}>Supply Item</th>
<th style={{ padding: '16px 12px' }}>Category</th>
<th style={{ padding: '16px 12px' }}>Quantity Available</th>
<th style={{ padding: '16px 12px' }}>Reorder Point</th>
<th style={{ padding: '16px 12px' }}>Last Updated</th>
</tr>
</thead>
<tbody>
{currentItems.map((item) => (
<tr key={item.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)', transition: 'background 0.2s' }} className="hover-glow">
<td style={{ padding: '16px 12px' }}>
<div style={{ fontWeight: 700, fontSize: '0.875rem' }}>
{item.item_master?.name || 'Unknown Item'}
</div>
</td>
<td style={{ padding: '16px 12px' }}>
<span style={{ fontSize: '0.7rem', fontWeight: 700, color: '#06B6D4', background: 'rgba(6, 182, 212, 0.1)', padding: '4px 8px', borderRadius: '6px' }}>
{item.item_master?.category || 'GENERAL'}
</span>
</td>
<td style={{ padding: '16px 12px' }}>
<div style={{ fontWeight: 900, fontSize: '1.1rem', color: '#10B981' }}>
{item.quantity}
<span style={{ fontSize: '0.7rem', color: '#64748B', fontWeight: 500, marginLeft: '6px' }}>Units</span>
</div>
</td>
<td style={{ padding: '16px 12px' }}>
<div style={{ fontSize: '0.8rem', color: '#94A3B8', fontWeight: 600 }}>
{item.item_master?.min_stock_threshold || 0}
</div>
</td>
<td style={{ padding: '16px 12px' }}>
<div style={{ fontSize: '0.75rem', color: '#64748B' }}>
{new Date(item.updatedAt).toLocaleString()}
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
{!loading && !error && filteredStock.length > itemsPerPage && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px', borderTop: '1px solid rgba(15, 23, 42, 0.08)', marginTop: '8px' }}>
<div style={{ fontSize: '0.8125rem', color: '#64748B' }}>
Showing {indexOfFirstItem + 1} to {Math.min(indexOfLastItem, filteredStock.length)} of {filteredStock.length} items
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
style={{
padding: '6px 12px', background: 'transparent', border: '1px solid #CBD5E1', borderRadius: '6px',
color: currentPage === 1 ? '#94A3B8' : '#475569', cursor: currentPage === 1 ? 'not-allowed' : 'pointer',
display: 'flex', alignItems: 'center', gap: '4px', fontSize: '0.8125rem', fontWeight: 600, transition: 'all 0.2s'
}}
>
<ChevronLeft size={14} /> Prev
</button>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 8px', fontSize: '0.8125rem', fontWeight: 700, color: '#0F172A' }}>
{currentPage} / {totalPages}
</div>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
style={{
padding: '6px 12px', background: 'transparent', border: '1px solid #CBD5E1', borderRadius: '6px',
color: currentPage === totalPages ? '#94A3B8' : '#475569', cursor: currentPage === totalPages ? 'not-allowed' : 'pointer',
display: 'flex', alignItems: 'center', gap: '4px', fontSize: '0.8125rem', fontWeight: 600, transition: 'all 0.2s'
}}
>
Next <ChevronRight size={14} />
</button>
</div>
</div>
)}
</Card>
{/* Bulk Restock Modal */}
{showBulkRestockModal && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(15, 23, 42, 0.4)',
backdropFilter: 'blur(8px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1500,
animation: 'fadeIn 0.25s ease-out'
}}>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
transition={{ duration: 0.25, ease: 'easeOut' }}
style={{
width: '100%',
maxWidth: '820px',
background: '#FFFFFF',
border: '1px solid #E2E8F0',
borderRadius: '20px',
padding: '30px',
boxShadow: '0 20px 40px rgba(15, 23, 42, 0.15)',
fontFamily: "'Inter', sans-serif",
boxSizing: 'border-box',
maxHeight: '90vh',
display: 'flex',
flexDirection: 'column'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px', borderBottom: '1px solid #F1F5F9', paddingBottom: '16px', flexShrink: 0 }}>
<div>
<h3 style={{ fontSize: '1.25rem', fontWeight: 900, color: '#0F172A', margin: 0, letterSpacing: '-0.5px' }}>Bulk Restock Intake Ledger</h3>
<span style={{ fontSize: '0.75rem', color: '#0284C7', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '1px' }}>Warehouse bulk stock ingestion</span>
</div>
<button
onClick={() => setShowBulkRestockModal(false)}
style={{ background: 'transparent', border: 'none', color: '#64748B', cursor: 'pointer', fontSize: '1.1rem' }}
onMouseEnter={e => e.currentTarget.style.color = '#0F172A'}
onMouseLeave={e => e.currentTarget.style.color = '#64748B'}
>
</button>
</div>
<form onSubmit={handleSubmitBulkRestock} style={{ display: 'flex', flexDirection: 'column', gap: '20px', overflow: 'hidden', flex: 1 }}>
{bulkRestockError && (
<div style={{ background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.2)', color: '#EF4444', padding: '12px 16px', borderRadius: '10px', fontSize: '0.8rem', display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}>
<AlertTriangle size={16} />
<span>{bulkRestockError}</span>
</div>
)}
{/* Rows List Container */}
<div style={{ overflowY: 'auto', paddingRight: '4px', display: 'flex', flexDirection: 'column', gap: '12px', flex: 1, minHeight: '150px' }}>
{bulkRestockRows.map((row, index) => {
const matchedItem = inventory.find(item => item.id === row.itemId);
return (
<div
key={index}
style={{
display: 'grid',
gridTemplateColumns: '1.5fr 1fr 1.5fr auto',
gap: '12px',
alignItems: 'center',
background: '#F8FAFC',
border: '1px solid #E2E8F0',
borderRadius: '12px',
padding: '14px'
}}
>
{/* Item Select */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<label style={{ fontSize: '0.65rem', fontWeight: 800, color: '#64748B', textTransform: 'uppercase' }}>Select Supply Item</label>
<select
value={row.itemId}
onChange={(e) => {
const newRows = [...bulkRestockRows];
newRows[index].itemId = e.target.value;
setBulkRestockRows(newRows);
}}
style={{
background: '#FFFFFF',
border: '1px solid #D1D5DB',
borderRadius: '8px',
padding: '8px 12px',
color: '#0F172A',
fontSize: '0.8rem',
outline: 'none',
cursor: 'pointer'
}}
>
<option value="">-- Choose Item --</option>
{inventory.map(item => (
<option key={item.id} value={item.id}>
{item.name} ({item.category})
</option>
))}
</select>
</div>
{/* Quantity Input */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<label style={{ fontSize: '0.65rem', fontWeight: 800, color: '#64748B', textTransform: 'uppercase' }}>
Quantity {matchedItem ? `(${matchedItem.unit_of_measure || matchedItem.unit || 'Units'})` : ''}
</label>
<input
type="number"
min="1"
value={row.quantity}
onChange={(e) => {
const newRows = [...bulkRestockRows];
newRows[index].quantity = Number(e.target.value);
setBulkRestockRows(newRows);
}}
style={{
background: '#FFFFFF',
border: '1px solid #D1D5DB',
borderRadius: '8px',
padding: '8px 12px',
color: '#0F172A',
fontSize: '0.8rem',
outline: 'none',
fontWeight: 700
}}
/>
</div>
{/* Reason Select/Input */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<label style={{ fontSize: '0.65rem', fontWeight: 800, color: '#64748B', textTransform: 'uppercase' }}>Reason / Supplier</label>
<select
value={row.reason}
onChange={(e) => {
const newRows = [...bulkRestockRows];
newRows[index].reason = e.target.value;
setBulkRestockRows(newRows);
}}
style={{
background: '#FFFFFF',
border: '1px solid #D1D5DB',
borderRadius: '8px',
padding: '8px 12px',
color: '#0F172A',
fontSize: '0.8rem',
outline: 'none',
cursor: 'pointer'
}}
>
<option value="Shipment from HealthCare Distributors">Shipment from HealthCare Distributors</option>
<option value="Shipment from MediStore">Shipment from MediStore</option>
<option value="Routine Stock Replenishment">Routine Stock Replenishment</option>
<option value="Emergency Intake">Emergency Intake</option>
</select>
</div>
{/* Remove Action */}
<button
type="button"
onClick={() => {
const newRows = bulkRestockRows.filter((_, rIdx) => rIdx !== index);
setBulkRestockRows(newRows);
}}
style={{
background: 'rgba(239, 68, 68, 0.1)',
border: 'none',
color: '#EF4444',
padding: '8px',
borderRadius: '8px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
alignSelf: 'end',
marginBottom: '2px',
transition: 'all 0.2s'
}}
onMouseEnter={e => e.currentTarget.style.background = 'rgba(239, 68, 68, 0.2)'}
onMouseLeave={e => e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)'}
title="Remove row"
>
<X size={16} />
</button>
</div>
);
})}
{/* Add Row Button */}
<button
type="button"
onClick={() => {
const firstId = inventory.length > 0 ? inventory[0].id : '';
setBulkRestockRows([
...bulkRestockRows,
{ itemId: firstId, quantity: 100, reason: 'Shipment from HealthCare Distributors' }
]);
}}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
background: 'rgba(2, 132, 199, 0.05)',
border: '1px dashed rgba(2, 132, 199, 0.3)',
color: '#0284C7',
borderRadius: '12px',
padding: '12px',
fontSize: '0.8rem',
fontWeight: 700,
cursor: 'pointer',
transition: 'all 0.2s'
}}
onMouseEnter={e => {
e.currentTarget.style.background = 'rgba(2, 132, 199, 0.1)';
}}
onMouseLeave={e => {
e.currentTarget.style.background = 'rgba(2, 132, 199, 0.05)';
}}
>
<PlusCircle size={16} /> ADD ANOTHER ITEM ROW
</button>
</div>
{/* Bottom Buttons */}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', borderTop: '1px solid #F1F5F9', paddingTop: '20px', flexShrink: 0 }}>
<button
type="button"
onClick={() => setShowBulkRestockModal(false)}
className="btn-ghost"
style={{ padding: '10px 20px', fontSize: '0.8rem', color: '#475569', background: 'transparent', border: 'none', cursor: 'pointer', fontWeight: 600 }}
>
CANCEL
</button>
<button
type="submit"
className="btn-primary"
disabled={bulkRestockIsSubmitting}
style={{ padding: '10px 24px', fontSize: '0.8rem', display: 'flex', alignItems: 'center', gap: '8px', background: '#22C55E', color: '#FFFFFF', border: 'none', borderRadius: '8px', fontWeight: 700, cursor: 'pointer' }}
>
{bulkRestockIsSubmitting ? (
<>
<Activity size={14} className="spin" /> PROCESSING...
</>
) : (
'SUBMIT BULK INGESTION'
)}
</button>
</div>
</form>
</motion.div>
</div>
)}
{/* Assign to Vehicle Modal */}
{showAssignVehicleModal && (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(15, 23, 42, 0.4)', zIndex: 10000, display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(4px)' }} onClick={(e) => { if (e.target === e.currentTarget) setShowAssignVehicleModal(false); }}>
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} style={{ background: '#FFFFFF', borderRadius: '16px', width: '900px', maxWidth: '95vw', maxHeight: '90vh', overflow: 'hidden', display: 'flex', flexDirection: 'column', boxShadow: '0 20px 40px rgba(15, 23, 42, 0.2)' }}>
{/* Header */}
<div style={{ padding: '20px 24px', borderBottom: '1px solid #F1F5F9', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#F8FAFC' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ background: 'rgba(245, 158, 11, 0.1)', padding: '10px', borderRadius: '12px', color: '#F59E0B' }}>
<Truck size={20} />
</div>
<div>
<h2 style={{ margin: 0, fontSize: '1.1rem', fontWeight: 800, color: '#0F172A' }}>Assign to Vehicle</h2>
<p style={{ margin: '4px 0 0', fontSize: '0.8rem', color: '#64748B' }}>Transfer inventory stock to an active fleet vehicle</p>
</div>
</div>
<button onClick={() => setShowAssignVehicleModal(false)} style={{ background: '#FFFFFF', border: '1px solid #E2E8F0', padding: '6px', borderRadius: '8px', cursor: 'pointer', color: '#64748B' }}>
<X size={16} />
</button>
</div>
{/* Form */}
<form onSubmit={handleSubmitAssignVehicle} style={{ flex: 1, overflowY: 'auto', padding: '24px', display: 'flex', flexDirection: 'column', gap: '24px' }}>
{assignVehicleError && (
<div style={{ padding: '12px 16px', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.2)', borderRadius: '8px', color: '#EF4444', fontSize: '0.85rem', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '8px' }}>
<AlertTriangle size={16} /> {assignVehicleError}
</div>
)}
{/* Top Configuration Details */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px', background: '#F8FAFC', padding: '16px', borderRadius: '12px', border: '1px solid #E2E8F0' }}>
<div>
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: 700, color: '#475569', marginBottom: '6px' }}>Select Target Vehicle *</label>
<select
required
value={assignVehicleId}
onChange={(e) => setAssignVehicleId(e.target.value)}
style={{ width: '100%', background: '#FFFFFF', border: '1px solid #D1D5DB', borderRadius: '8px', padding: '8px 12px', color: '#0F172A', fontSize: '0.8rem', outline: 'none' }}
>
<option value="" disabled>-- Select Vehicle --</option>
{vehiclesList.map(v => (
<option key={v.id} value={v.id}>{v.registration_number} {v.brand ? `(${v.brand})` : ''}</option>
))}
</select>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: 700, color: '#475569', marginBottom: '6px' }}>Supplier / Source</label>
<input
type="text"
required
value={assignSupplierName}
onChange={(e) => setAssignSupplierName(e.target.value)}
style={{ width: '100%', background: '#FFFFFF', border: '1px solid #D1D5DB', borderRadius: '8px', padding: '8px 12px', color: '#0F172A', fontSize: '0.8rem', outline: 'none' }}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: 700, color: '#475569', marginBottom: '6px' }}>Assignment Reason</label>
<select
value={assignReason}
onChange={(e) => setAssignReason(e.target.value)}
style={{ width: '100%', background: '#FFFFFF', border: '1px solid #D1D5DB', borderRadius: '8px', padding: '8px 12px', color: '#0F172A', fontSize: '0.8rem', outline: 'none' }}
>
<option value="Ambulance Initial Stocking">Ambulance Initial Stocking</option>
<option value="Routine Restock">Routine Restock</option>
<option value="Emergency Restock">Emergency Restock</option>
</select>
</div>
</div>
{/* Rows */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<h3 style={{ margin: 0, fontSize: '0.9rem', fontWeight: 700, color: '#0F172A', borderBottom: '1px solid #E2E8F0', paddingBottom: '8px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<Package size={16} color="#0284C7" /> Items to Assign
</h3>
{assignVehicleRows.map((row, index) => (
<div key={index} style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr 1fr auto', gap: '12px', alignItems: 'center', background: '#FFFFFF', padding: '12px', borderRadius: '12px', border: '1px solid #E2E8F0' }}>
<div>
<label style={{ display: 'block', fontSize: '0.65rem', fontWeight: 700, color: '#64748B', marginBottom: '4px', textTransform: 'uppercase' }}>Select Item</label>
<select
required
value={row.itemId}
onChange={(e) => {
const newRows = [...assignVehicleRows];
newRows[index].itemId = e.target.value;
setAssignVehicleRows(newRows);
}}
style={{ width: '100%', background: '#F8FAFC', border: '1px solid #D1D5DB', borderRadius: '6px', padding: '8px', color: '#0F172A', fontSize: '0.8rem', outline: 'none' }}
>
<option value="" disabled>-- Select item --</option>
{inventory.map(invItem => (
<option key={invItem.id} value={invItem.id}>{invItem.name} ({invItem.unit_of_measure || invItem.unit || 'Units'})</option>
))}
</select>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.65rem', fontWeight: 700, color: '#64748B', marginBottom: '4px', textTransform: 'uppercase' }}>Quantity</label>
<input
type="number"
min="1"
required
value={row.quantity}
onChange={(e) => {
const newRows = [...assignVehicleRows];
newRows[index].quantity = Number(e.target.value);
setAssignVehicleRows(newRows);
}}
style={{ width: '100%', background: '#F8FAFC', border: '1px solid #D1D5DB', borderRadius: '6px', padding: '8px', color: '#0F172A', fontSize: '0.8rem', outline: 'none' }}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.65rem', fontWeight: 700, color: '#64748B', marginBottom: '4px', textTransform: 'uppercase' }}>Batch No</label>
<input
type="text"
placeholder="e.g. B-2024"
value={row.batch_number}
onChange={(e) => {
const newRows = [...assignVehicleRows];
newRows[index].batch_number = e.target.value;
setAssignVehicleRows(newRows);
}}
style={{ width: '100%', background: '#F8FAFC', border: '1px solid #D1D5DB', borderRadius: '6px', padding: '8px', color: '#0F172A', fontSize: '0.8rem', outline: 'none' }}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.65rem', fontWeight: 700, color: '#64748B', marginBottom: '4px', textTransform: 'uppercase' }}>Expiry Date</label>
<input
type="date"
required
value={row.expiry_date}
onChange={(e) => {
const newRows = [...assignVehicleRows];
newRows[index].expiry_date = e.target.value;
setAssignVehicleRows(newRows);
}}
style={{ width: '100%', background: '#F8FAFC', border: '1px solid #D1D5DB', borderRadius: '6px', padding: '8px', color: '#0F172A', fontSize: '0.8rem', outline: 'none' }}
/>
</div>
<button
type="button"
onClick={() => setAssignVehicleRows(assignVehicleRows.filter((_, rIdx) => rIdx !== index))}
style={{ background: 'rgba(239, 68, 68, 0.1)', border: 'none', color: '#EF4444', padding: '8px', borderRadius: '8px', cursor: 'pointer', alignSelf: 'end', marginBottom: '2px' }}
>
<X size={16} />
</button>
</div>
))}
<button
type="button"
onClick={() => {
const firstId = inventory.length > 0 ? inventory[0].id : '';
setAssignVehicleRows([...assignVehicleRows, { itemId: firstId, quantity: 100, batch_number: '', expiry_date: '' }]);
}}
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', background: 'rgba(245, 158, 11, 0.05)', border: '1px dashed rgba(245, 158, 11, 0.3)', color: '#F59E0B', borderRadius: '12px', padding: '12px', fontSize: '0.8rem', fontWeight: 700, cursor: 'pointer' }}
>
<PlusCircle size={16} /> ADD ANOTHER ITEM
</button>
</div>
{/* Bottom Buttons */}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', borderTop: '1px solid #F1F5F9', paddingTop: '20px', flexShrink: 0 }}>
<button
type="button"
onClick={() => setShowAssignVehicleModal(false)}
style={{ padding: '10px 20px', fontSize: '0.8rem', color: '#475569', background: 'transparent', border: 'none', cursor: 'pointer', fontWeight: 600 }}
>
CANCEL
</button>
<button
type="submit"
disabled={assignVehicleIsSubmitting}
style={{ padding: '10px 24px', fontSize: '0.8rem', display: 'flex', alignItems: 'center', gap: '8px', background: '#F59E0B', color: '#FFFFFF', border: 'none', borderRadius: '8px', fontWeight: 700, cursor: 'pointer' }}
>
{assignVehicleIsSubmitting ? <><Activity size={14} className="spin" /> PROCESSING...</> : 'CONFIRM ASSIGNMENT'}
</button>
</div>
</form>
</motion.div>
</div>
)}
<style>{`
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.spin { animation: spin 1s linear infinite; }
.hover-glow:hover { background: rgba(255,255,255,0.02); }
`}</style>
</div>
);
};

View File

@@ -0,0 +1,749 @@
/// <reference types="@types/google.maps" />
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { io, Socket } from 'socket.io-client';
import {
Truck, Search, MapPin, Radio, Activity, X, Check,
Loader2, Compass, Play, RefreshCw
} from 'lucide-react';
// import { motion } from 'framer-motion';
// --- Types ---
interface Vehicle {
id: string;
registration_number: string;
chassis_number?: string | null;
brand?: string;
model?: string;
vehicle_type: string; // ALS, BLS, etc.
status: string; // BUSY, AVAILABLE, OFFLINE
gps_lat: string | number;
gps_lon: string | number;
activeShift?: {
id: string;
status: string;
staff?: {
user?: {
name: string;
phone: string;
};
};
} | null;
}
// --- Google Maps Script Helper ---
let mapsScriptLoaded = false;
let mapsScriptLoadingPromise: Promise<void> | null = null;
const loadGoogleMapsScript = (apiKey: string): Promise<void> => {
if (mapsScriptLoaded) return Promise.resolve();
if (mapsScriptLoadingPromise) return mapsScriptLoadingPromise;
mapsScriptLoadingPromise = new Promise((resolve, reject) => {
if ((window as any).google && (window as any).google.maps) {
mapsScriptLoaded = true;
resolve();
return;
}
// Set dynamic callback name
const callbackName = 'initGoogleMapsCallback';
(window as any)[callbackName] = () => {
mapsScriptLoaded = true;
resolve();
};
const script = document.createElement('script');
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&callback=${callbackName}&loading=async`;
script.async = true;
script.defer = true;
script.onerror = (err) => {
console.error('Google Maps Script load failed:', err);
reject(err);
};
document.head.appendChild(script);
});
return mapsScriptLoadingPromise;
};
// Map custom aesthetic style (sleek dark/light theme balancing high-contrast premium feel)
const mapStyles = [
{
"elementType": "geometry",
"stylers": [{ "color": "#f5f5f5" }]
},
{
"elementType": "labels.icon",
"stylers": [{ "visibility": "off" }]
},
{
"elementType": "labels.text.fill",
"stylers": [{ "color": "#616161" }]
},
{
"elementType": "labels.text.stroke",
"stylers": [{ "color": "#f5f5f5" }]
},
{
"featureType": "administrative.land_parcel",
"elementType": "labels.text.fill",
"stylers": [{ "color": "#bdbdbd" }]
},
{
"featureType": "poi",
"elementType": "geometry",
"stylers": [{ "color": "#eeeeee" }]
},
{
"featureType": "poi",
"elementType": "labels.text.fill",
"stylers": [{ "color": "#757575" }]
},
{
"featureType": "road",
"elementType": "geometry",
"stylers": [{ "color": "#ffffff" }]
},
{
"featureType": "road.arterial",
"elementType": "labels.text.fill",
"stylers": [{ "color": "#757575" }]
},
{
"featureType": "road.highway",
"elementType": "geometry",
"stylers": [{ "color": "#dadada" }]
},
{
"featureType": "road.highway",
"elementType": "labels.text.fill",
"stylers": [{ "color": "#616161" }]
},
{
"featureType": "road.local",
"elementType": "labels.text.fill",
"stylers": [{ "color": "#9e9e9e" }]
},
{
"featureType": "transit.line",
"elementType": "geometry",
"stylers": [{ "color": "#e5e5e5" }]
},
{
"featureType": "transit.station",
"elementType": "geometry",
"stylers": [{ "color": "#eeeeee" }]
},
{
"featureType": "water",
"elementType": "geometry",
"stylers": [{ "color": "#c9c9c9" }]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [{ "color": "#9e9e9e" }]
}
];
export const LiveDashboard: React.FC = () => {
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [isSearchFocused, setIsSearchFocused] = useState(false);
const [loading, setLoading] = useState(true);
const [socketStatus, setSocketStatus] = useState<'disconnected' | 'connecting' | 'connected'>('connecting');
const [selectedVehicle, setSelectedVehicle] = useState<Vehicle | null>(null);
// References to prevent stale closures inside socket events
const vehiclesRef = useRef<Vehicle[]>([]);
const selectedVehicleRef = useRef<Vehicle | null>(null);
useEffect(() => {
vehiclesRef.current = vehicles;
}, [vehicles]);
useEffect(() => {
selectedVehicleRef.current = selectedVehicle;
}, [selectedVehicle]);
// Stats
const [totalVehicles, setTotalVehicles] = useState(0);
const [busyCount, setBusyCount] = useState(0);
const [availableCount, setAvailableCount] = useState(0);
// References
const mapContainerRef = useRef<HTMLDivElement>(null);
const mapInstanceRef = useRef<google.maps.Map | null>(null);
const markersRef = useRef<Record<string, google.maps.Marker>>({});
const infoWindowRef = useRef<google.maps.InfoWindow | null>(null);
const socketRef = useRef<Socket | null>(null);
const API_KEY = 'AIzaSyA9fe3iFd2XxDDUQNGJLs6Tp5iRewpHdhs';
const token = localStorage.getItem('teleems_token') || '';
// Get status details
const getStatusCfg = (status: string) => {
switch ((status || '').toUpperCase()) {
case 'BUSY':
return { label: 'Busy', color: '#F97316', bg: '#FFF7ED', border: '#FFEDD5' };
case 'AVAILABLE':
return { label: 'Available', color: '#10B981', bg: '#ECFDF5', border: '#D1FAE5' };
default:
return { label: 'Offline', color: '#64748B', bg: '#F8FAFC', border: '#F1F5F9' };
}
};
// Fetch initial list of vehicles
const fetchVehicles = useCallback(async () => {
setLoading(true);
try {
const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/vehicles', {
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
});
const json = await res.json();
let list: Vehicle[] = [];
if (json?.data?.data && Array.isArray(json.data.data)) list = json.data.data;
else if (json?.data && Array.isArray(json.data)) list = json.data;
else if (Array.isArray(json)) list = json;
setVehicles(list);
setTotalVehicles(list.length);
setBusyCount(list.filter(v => v.status === 'BUSY').length);
setAvailableCount(list.filter(v => v.status === 'AVAILABLE').length);
} catch (err) {
console.error('Error fetching vehicles:', err);
} finally {
setLoading(false);
}
}, [token]);
// Center map on specific marker
const centerOnVehicle = (vehicle: Vehicle) => {
const lat = parseFloat(vehicle.gps_lat as string);
const lon = parseFloat(vehicle.gps_lon as string);
if (isNaN(lat) || isNaN(lon) || (lat === 0 && lon === 0)) {
return;
}
setSelectedVehicle(vehicle);
if (mapInstanceRef.current) {
mapInstanceRef.current.setZoom(15);
mapInstanceRef.current.panTo({ lat, lng: lon });
// Open infowindow
const marker = markersRef.current[vehicle.id];
if (marker && infoWindowRef.current) {
const sc = getStatusCfg(vehicle.status);
const driverName = vehicle.activeShift?.staff?.user?.name || 'No Pilot Assigned';
const content = `
<div style="padding: 10px; font-family: 'Inter', sans-serif; min-width: 180px; color: #0F172A;">
<div style="font-size: 0.65rem; font-weight: 800; color: #64748B; text-transform: uppercase; margin-bottom: 3px;">${vehicle.vehicle_type} AMBULANCE</div>
<div style="font-size: 0.95rem; font-weight: 800; color: #0F172A; margin-bottom: 6px;">${vehicle.registration_number}</div>
<div style="font-size: 0.75rem; color: #475569; margin-bottom: 8px;">${vehicle.brand || ''} ${vehicle.model || ''}</div>
<div style="display: flex; gap: 6px; align-items: center; margin-top: 4px;">
<span style="display: inline-block; width: 6px; height: 6px; border-radius: 50%; background-color: ${sc.color};"></span>
<span style="font-size: 0.72rem; font-weight: 700; color: ${sc.color};">${sc.label}</span>
</div>
<div style="font-size: 0.7rem; color: #64748B; margin-top: 8px; border-top: 1px solid #E2E8F0; padding-top: 6px;">
Pilot: <strong>${driverName}</strong>
</div>
</div>
`;
infoWindowRef.current.setContent(content);
infoWindowRef.current.open(mapInstanceRef.current, marker);
}
}
};
// Setup maps and socket connections
useEffect(() => {
fetchVehicles();
}, [fetchVehicles]);
// Google Maps setup once vehicles list is ready and element exists
useEffect(() => {
if (loading || vehicles.length === 0 || !mapContainerRef.current) return;
loadGoogleMapsScript(API_KEY)
.then(() => {
// Find center based on active coords
const validCoords = vehicles
.map(v => ({ lat: parseFloat(v.gps_lat as string), lng: parseFloat(v.gps_lon as string) }))
.filter(c => !isNaN(c.lat) && !isNaN(c.lng) && c.lat !== 0 && c.lng !== 0);
const defaultCenter = validCoords.length > 0
? validCoords[0]
: { lat: 18.5204, lng: 73.8567 }; // Default Pune center
const mapOptions: google.maps.MapOptions = {
center: defaultCenter,
zoom: 12,
styles: mapStyles,
mapTypeControl: false,
streetViewControl: false,
fullscreenControl: false,
zoomControl: true,
zoomControlOptions: {
position: google.maps.ControlPosition.RIGHT_CENTER
}
};
const map = new google.maps.Map(mapContainerRef.current!, mapOptions);
mapInstanceRef.current = map;
infoWindowRef.current = new google.maps.InfoWindow();
// Create Markers
vehicles.forEach(v => {
const lat = parseFloat(v.gps_lat as string);
const lon = parseFloat(v.gps_lon as string);
if (isNaN(lat) || isNaN(lon) || (lat === 0 && lon === 0)) return;
const sc = getStatusCfg(v.status);
// High quality SVG markers for vehicles
const svgIcon = {
path: 'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z',
fillColor: sc.color,
fillOpacity: 1,
strokeColor: '#FFFFFF',
strokeWeight: 2,
scale: 1.8,
anchor: new google.maps.Point(12, 24)
};
const marker = new google.maps.Marker({
position: { lat, lng: lon },
map,
title: v.registration_number,
icon: svgIcon,
animation: google.maps.Animation.DROP
});
marker.addListener('click', () => {
centerOnVehicle(v);
});
markersRef.current[v.id] = marker;
});
})
.catch(err => {
console.error('Google Maps Load Error:', err);
});
return () => {
// Cleanup markers
Object.values(markersRef.current).forEach(m => m.setMap(null));
markersRef.current = {};
};
}, [loading]);
// Real-time socket client connection
useEffect(() => {
setSocketStatus('connecting');
const socket = io('https://teleems-api-gateway.onrender.com', {
transports: ['websocket'],
reconnectionAttempts: 5,
reconnectionDelay: 3000
});
socketRef.current = socket;
socket.on('connect', () => {
console.log('Socket.io connected successfully!');
setSocketStatus('connected');
// Emit subscription event
socket.emit('subscribe_fleet');
});
socket.on('disconnect', () => {
console.log('Socket.io disconnected!');
setSocketStatus('disconnected');
});
// Real-time updates listener
socket.on('fleet:location_updated', (data: any) => {
console.log('Real-time location received:', data);
const vId = data.vehicle_id || data.id;
const regNo = data.registration_number;
const lat = parseFloat(data.lat || data.gps_lat);
const lon = parseFloat(data.lon || data.gps_lon);
if (!vId || isNaN(lat) || isNaN(lon)) return;
// Update vehicle state coordinates in vehicle array dynamically
setVehicles(prev => {
const index = prev.findIndex(v => v.id === vId);
if (index === -1) return prev;
const copy = [...prev];
copy[index] = {
...copy[index],
gps_lat: lat,
gps_lon: lon
};
return copy;
});
// Update marker coordinates dynamically on map
let marker = markersRef.current[vId];
if (marker) {
const newPos = new google.maps.LatLng(lat, lon);
marker.setPosition(newPos);
// Soft focus visual if selected
if (selectedVehicleRef.current?.id === vId) {
if (mapInstanceRef.current) {
mapInstanceRef.current.panTo(newPos);
}
}
} else if (mapInstanceRef.current) {
// Create marker dynamically if it didn't exist originally due to missing coordinates
const vehicleInfo = vehiclesRef.current.find(v => v.id === vId);
const sc = getStatusCfg(vehicleInfo?.status || 'AVAILABLE');
const svgIcon = {
path: 'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z',
fillColor: sc.color,
fillOpacity: 1,
strokeColor: '#FFFFFF',
strokeWeight: 2,
scale: 1.8,
anchor: new google.maps.Point(12, 24)
};
marker = new google.maps.Marker({
position: { lat, lng: lon },
map: mapInstanceRef.current,
title: regNo || vehicleInfo?.registration_number || 'Ambulance',
icon: svgIcon,
animation: google.maps.Animation.DROP
});
marker.addListener('click', () => {
if (vehicleInfo) centerOnVehicle(vehicleInfo);
});
markersRef.current[vId] = marker;
}
});
return () => {
socket.disconnect();
};
}, []);
// Handle simulation trigger
const runMoveSimulation = () => {
// FindPune Vehicle or first with valid coords
const activeV = vehicles.find(v => parseFloat(v.gps_lat as string) > 0);
if (!activeV) return;
let currentLat = parseFloat(activeV.gps_lat as string);
let currentLon = parseFloat(activeV.gps_lon as string);
// Simulate standard movement increments
const interval = setInterval(() => {
currentLat += (Math.random() - 0.5) * 0.0015;
currentLon += (Math.random() - 0.5) * 0.0015;
// Send to Socket receiver locally to test standard workflow
if (socketRef.current) {
socketRef.current.emit('simulate_move_debug', {
vehicle_id: activeV.id,
registration_number: activeV.registration_number,
lat: currentLat,
lon: currentLon
});
}
// Locally update as fallback to show live feedback immediately
const marker = markersRef.current[activeV.id];
if (marker) {
const newPos = new google.maps.LatLng(currentLat, currentLon);
marker.setPosition(newPos);
if (mapInstanceRef.current) mapInstanceRef.current.panTo(newPos);
}
setVehicles(prev => {
const idx = prev.findIndex(v => v.id === activeV.id);
if (idx === -1) return prev;
const copy = [...prev];
copy[idx] = { ...copy[idx], gps_lat: currentLat, gps_lon: currentLon };
return copy;
});
}, 1200);
// Clear simulation after 10 seconds
setTimeout(() => {
clearInterval(interval);
}, 10000);
};
const filtered = vehicles.filter(v =>
v.registration_number.toLowerCase().includes(searchQuery.toLowerCase()) ||
(v.brand && v.brand.toLowerCase().includes(searchQuery.toLowerCase())) ||
(v.model && v.model.toLowerCase().includes(searchQuery.toLowerCase()))
);
return (
<div style={{ display: 'flex', height: 'calc(100vh - 128px)', gap: '20px', color: '#0F172A', fontFamily: 'sans-serif' }}>
{/* Sidebar: Vehicles List Panel */}
<div style={{
width: '380px',
background: '#FFFFFF',
border: '1px solid rgba(15, 23, 42, 0.08)',
borderRadius: '16px',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)',
overflow: 'hidden'
}}>
{/* Header / Stats */}
<div style={{ padding: '20px', borderBottom: '1px solid rgba(15, 23, 42, 0.06)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '14px' }}>
<h2 style={{ fontSize: '1.1rem', fontWeight: 900, color: '#0F172A', margin: 0, display: 'flex', alignItems: 'center', gap: '8px' }}>
<Radio size={18} color="#06B6D4" style={{ animation: 'pulse 2s infinite' }} /> Active Ambulances
</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.72rem', background: socketStatus === 'connected' ? '#ECFDF5' : '#FEF2F2', padding: '4px 10px', borderRadius: '50px', border: `1px solid ${socketStatus === 'connected' ? '#A7F3D0' : '#FCA5A5'}`, color: socketStatus === 'connected' ? '#047857' : '#B91C1C', fontWeight: 800 }}>
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: socketStatus === 'connected' ? '#10B981' : '#EF4444' }} />
{socketStatus === 'connected' ? 'Connected' : socketStatus === 'connecting' ? 'Reconnecting' : 'Disconnected'}
</div>
</div>
{/* Quick Mini Stats */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '8px' }}>
<div style={{ background: '#F8FAFC', padding: '8px 10px', borderRadius: '10px', border: '1px solid rgba(15, 23, 42, 0.04)', textAlign: 'center' }}>
<div style={{ fontSize: '1.2rem', fontWeight: 900, color: '#0F172A' }}>{totalVehicles}</div>
<div style={{ fontSize: '0.62rem', color: '#64748B', marginTop: '2px', textTransform: 'uppercase' }}>Total</div>
</div>
<div style={{ background: '#FFF7ED', padding: '8px 10px', borderRadius: '10px', border: '1px solid #FFEDD5', textAlign: 'center' }}>
<div style={{ fontSize: '1.2rem', fontWeight: 900, color: '#F97316' }}>{busyCount}</div>
<div style={{ fontSize: '0.62rem', color: '#EA580C', marginTop: '2px', textTransform: 'uppercase' }}>Busy</div>
</div>
<div style={{ background: '#ECFDF5', padding: '8px 10px', borderRadius: '10px', border: '1px solid #D1FAE5', textAlign: 'center' }}>
<div style={{ fontSize: '1.2rem', fontWeight: 900, color: '#10B981' }}>{availableCount}</div>
<div style={{ fontSize: '0.62rem', color: '#059669', marginTop: '2px', textTransform: 'uppercase' }}>Available</div>
</div>
</div>
</div>
{/* Unified borderless search bar */}
<div style={{ padding: '16px 20px', borderBottom: '1px solid rgba(15, 23, 42, 0.06)' }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
background: '#FFFFFF',
padding: '10px 14px',
borderRadius: '12px',
border: isSearchFocused ? '1px solid #06B6D4' : '1px solid #E2E8F0',
boxShadow: isSearchFocused ? '0 0 0 3px rgba(6, 182, 212, 0.15)' : 'none',
transition: 'all 0.2s ease',
position: 'relative'
}}>
<Search size={15} color={isSearchFocused ? "#06B6D4" : "#94A3B8"} />
<input
type="text"
placeholder="Search registration or model..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setIsSearchFocused(true)}
onBlur={() => setIsSearchFocused(false)}
className="stations-search-input"
style={{
background: 'transparent',
border: 'none',
color: '#0F172A',
fontSize: '0.82rem',
outline: 'none',
width: '100%',
paddingRight: searchQuery ? '24px' : '0'
}}
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
style={{ position: 'absolute', right: '12px', background: 'transparent', border: 'none', cursor: 'pointer', color: '#94A3B8', display: 'flex', padding: '2px' }}
>
<X size={13} />
</button>
)}
</div>
</div>
{/* Scrollable List Container */}
<div style={{ flex: 1, overflowY: 'auto', padding: '14px 20px', display: 'flex', flexDirection: 'column', gap: '10px' }}>
{loading && (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '160px', color: '#64748B', gap: '10px' }}>
<Loader2 size={24} className="spin" />
<span style={{ fontSize: '0.8rem' }}>Loading fleet list...</span>
</div>
)}
{!loading && filtered.length === 0 && (
<div style={{ textAlign: 'center', padding: '32px 10px', color: '#64748B' }}>
<Truck size={36} style={{ opacity: 0.15, marginBottom: '10px' }} />
<div style={{ fontSize: '0.85rem', fontWeight: 600 }}>No vehicles found</div>
</div>
)}
{!loading && filtered.length > 0 && filtered.map(v => {
const sc = getStatusCfg(v.status);
const isSelected = selectedVehicle?.id === v.id;
const hasCoords = parseFloat(v.gps_lat as string) !== 0 && parseFloat(v.gps_lon as string) !== 0;
return (
<div
key={v.id}
onClick={() => { if (hasCoords) centerOnVehicle(v); }}
style={{
padding: '12px 14px',
borderRadius: '12px',
border: isSelected ? '1px solid #06B6D4' : '1px solid rgba(15, 23, 42, 0.05)',
background: isSelected ? 'rgba(6,182,212,0.03)' : '#FFFFFF',
cursor: hasCoords ? 'pointer' : 'default',
transition: 'all 0.15s ease',
position: 'relative'
}}
onMouseEnter={e => { if (hasCoords && !isSelected) e.currentTarget.style.border = '1px solid rgba(6,182,212,0.3)'; }}
onMouseLeave={e => { if (!isSelected) e.currentTarget.style.border = '1px solid rgba(15, 23, 42, 0.05)'; }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '6px' }}>
<div>
<div style={{ fontSize: '0.62rem', fontWeight: 900, color: '#64748B', textTransform: 'uppercase' }}>
{v.vehicle_type} UNIT
</div>
<div style={{ fontSize: '0.92rem', fontWeight: 800, color: '#0F172A', marginTop: '2px' }}>
{v.registration_number}
</div>
</div>
<div style={{ padding: '3px 8px', borderRadius: '6px', fontSize: '0.62rem', fontWeight: 900, color: sc.color, background: sc.bg, border: `1px solid ${sc.border}` }}>
{sc.label}
</div>
</div>
<div style={{ fontSize: '0.75rem', color: '#475569' }}>
{v.brand} {v.model}
</div>
{v.activeShift?.staff?.user?.name && (
<div style={{ fontSize: '0.68rem', color: '#64748B', marginTop: '6px', display: 'flex', alignItems: 'center', gap: '4px' }}>
<Check size={12} color="#10B981" /> Active Pilot: <strong>{v.activeShift.staff.user.name}</strong>
</div>
)}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '10px', borderTop: '1px solid rgba(15, 23, 42, 0.04)', paddingTop: '8px' }}>
<div style={{ fontSize: '0.65rem', color: '#64748B', display: 'flex', alignItems: 'center', gap: '4px' }}>
<MapPin size={11} color={hasCoords ? '#06B6D4' : '#94A3B8'} />
{hasCoords ? `${parseFloat(v.gps_lat as string).toFixed(4)}, ${parseFloat(v.gps_lon as string).toFixed(4)}` : 'Coords Unavailable'}
</div>
{hasCoords && (
<button
onClick={(e) => { e.stopPropagation(); centerOnVehicle(v); }}
style={{ background: 'transparent', border: 'none', color: '#06B6D4', fontSize: '0.68rem', fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '2px' }}
>
<Compass size={11} /> TRACK
</button>
)}
</div>
</div>
);
})}
</div>
{/* Simulate Movement Button */}
<div style={{ padding: '16px 20px', background: '#F8FAFC', borderTop: '1px solid rgba(15, 23, 42, 0.06)', display: 'flex', gap: '8px' }}>
<button
onClick={runMoveSimulation}
style={{
flex: 1,
padding: '10px',
background: '#0F172A',
color: '#FFFFFF',
border: 'none',
borderRadius: '10px',
fontWeight: 700,
fontSize: '0.78rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px',
boxShadow: '0 2px 8px rgba(15, 23, 42, 0.08)'
}}
>
<Play size={14} /> SIMULATE MOVE (DEBUG)
</button>
<button
onClick={fetchVehicles}
style={{
padding: '10px',
background: '#FFFFFF',
color: '#475569',
border: '1px solid #CBD5E1',
borderRadius: '10px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<RefreshCw size={14} />
</button>
</div>
</div>
{/* Main Panel: Google Map Canvas */}
<div style={{
flex: 1,
background: '#FFFFFF',
border: '1px solid rgba(15, 23, 42, 0.08)',
borderRadius: '16px',
position: 'relative',
boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)',
overflow: 'hidden'
}}>
{/* Loading Overlay */}
{loading && (
<div style={{ position: 'absolute', inset: 0, background: '#FFFFFF', zIndex: 10, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '14px' }}>
<Loader2 size={36} className="spin" color="#06B6D4" />
<h3 style={{ fontSize: '1.05rem', fontWeight: 800, color: '#0F172A', margin: 0 }}>Initializing Google Maps...</h3>
<p style={{ fontSize: '0.8rem', color: '#64748B', margin: 0 }}>Establishing secure connection to Dispatch Gateway</p>
</div>
)}
{/* Map div container */}
<div
ref={mapContainerRef}
style={{ width: '100%', height: '100%' }}
/>
{/* Dynamic Float Controls */}
<div style={{ position: 'absolute', top: '16px', left: '16px', zIndex: 5, background: 'rgba(15, 23, 42, 0.95)', padding: '10px 14px', borderRadius: '10px', color: '#FFFFFF', fontSize: '0.72rem', display: 'flex', alignItems: 'center', gap: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.2)', border: '1px solid rgba(255,255,255,0.08)' }}>
<Activity size={14} color="#06B6D4" style={{ animation: 'pulse 1.5s infinite' }} />
<span>Real-time Telemetry Active Channel: <strong>fleet:location_updated</strong></span>
</div>
</div>
<style>{`
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.92); }
}
`}</style>
</div>
);
};