diff --git a/package.json b/package.json
index a0aed94..139d363 100644
--- a/package.json
+++ b/package.json
@@ -17,10 +17,12 @@
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.14.1",
- "recharts": "^3.8.1"
+ "recharts": "^3.8.1",
+ "socket.io-client": "^4.8.3"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
+ "@types/google.maps": "^3.64.0",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
diff --git a/src/api/fleet.ts b/src/api/fleet.ts
index f00ad75..66f87d9 100644
--- a/src/api/fleet.ts
+++ b/src/api/fleet.ts
@@ -21,12 +21,17 @@ export const fleetApi = {
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) => {
return apiClient.post('/v1/fleet/vehicles', vehicleData, { token });
},
- getVehicles: async (token: string, orgId: string) => {
- return apiClient.get(`/v1/fleet/vehicles?org_id=${orgId}`, { token });
+ getVehicles: async (token: string, orgId?: string) => {
+ 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) => {
@@ -37,8 +42,13 @@ export const fleetApi = {
return apiClient.post('/v1/fleet/staff', staffData, { token });
},
- getStaff: async (token: string, orgId: string) => {
- return apiClient.get(`/v1/fleet/staff?organisationId=${orgId}`, { token });
+ getStaff: async (token: string, orgId?: string) => {
+ 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) => {
@@ -51,5 +61,43 @@ export const fleetApi = {
getInventoryMaster: async (token: string) => {
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 });
}
};
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index 70cc170..a1148b6 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -6,7 +6,8 @@ import {
ChevronDown,
ChevronRight,
AlertCircle,
- MoreVertical
+ MoreVertical,
+ Monitor
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { PerspectiveSwitcher } from './PerspectiveSwitcher';
@@ -18,6 +19,7 @@ export const Sidebar: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
+ const isFleetPage = location.pathname.startsWith('/fleet-operator');
// Safely parse user data
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 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')
const activeRole = currentRole.toLowerCase().replace(/\s+/g, '_');
@@ -99,7 +126,7 @@ export const Sidebar: React.FC = () => {
};
return filterItems(NAVIGATION_CONFIG);
- }, [currentRole]);
+ }, [currentRole, isFleetPage, user.roles]);
const renderNavItem = (item: NavItem, isSubItem = false) => {
const Icon = item.icon || AlertCircle;
@@ -127,7 +154,7 @@ export const Sidebar: React.FC = () => {
to={itemTo}
title={isSidebarCollapsed ? item.label : undefined}
style={({ isActive: linkActive }) => {
- const active = linkActive || isActive;
+ const active = itemSearch ? isActive : linkActive;
return {
display: 'flex',
alignItems: 'center',
@@ -137,8 +164,8 @@ export const Sidebar: React.FC = () => {
margin: isSidebarCollapsed ? '4px 12px' : '2px 12px',
borderRadius: '12px',
textDecoration: 'none',
- color: active ? '#fff' : '#94A3B8',
- background: active ? 'linear-gradient(90deg, rgba(6, 182, 212, 0.15), rgba(59, 130, 246, 0.05))' : 'transparent',
+ color: active ? (isFleetPage ? '#06B6D4' : '#fff') : (isFleetPage ? '#475569' : '#94A3B8'),
+ 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',
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)',
@@ -146,10 +173,10 @@ export const Sidebar: React.FC = () => {
overflow: 'hidden'
};
}}
- className={(navData) => `nav-item-link ${navData.isActive || isActive ? 'active' : ''}`}
+ className={(navData) => `nav-item-link ${(itemSearch ? isActive : navData.isActive) ? 'active' : ''}`}
>
{({ isActive: linkActive }) => {
- const active = linkActive || isActive;
+ const active = itemSearch ? isActive : linkActive;
return (
<>
@@ -197,7 +224,7 @@ export const Sidebar: React.FC = () => {
style={{ overflow: 'hidden' }}
>
{
<>
@@ -225,8 +252,8 @@ export const Sidebar: React.FC = () => {
animate={{ width: isSidebarCollapsed ? 80 : 280 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
style={{
- background: '#040B16', // Deep dark aesthetic
- borderRight: '1px solid rgba(255,255,255,0.05)',
+ background: isFleetPage ? '#FFFFFF' : '#040B16',
+ borderRight: isFleetPage ? '1px solid rgba(15, 23, 42, 0.08)' : '1px solid rgba(255,255,255,0.05)',
display: 'flex',
flexDirection: 'column',
height: '100vh',
@@ -241,7 +268,7 @@ export const Sidebar: React.FC = () => {
display: 'flex',
alignItems: 'center',
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,
}}>
@@ -266,7 +293,7 @@ export const Sidebar: React.FC = () => {
{!isSidebarCollapsed && (
- CureSelect
+ CureSelect
Platform
)}
@@ -279,19 +306,19 @@ export const Sidebar: React.FC = () => {
onClick={() => setIsSidebarCollapsed(true)}
title="Collapse Menu"
style={{
- background: 'rgba(255,255,255,0.02)',
- border: '1px solid rgba(255,255,255,0.06)',
+ background: isFleetPage ? 'rgba(15, 23, 42, 0.02)' : 'rgba(255,255,255,0.02)',
+ border: isFleetPage ? '1px solid rgba(15, 23, 42, 0.06)' : '1px solid rgba(255,255,255,0.06)',
borderRadius: '8px',
padding: '6px',
- color: '#94A3B8',
+ color: isFleetPage ? '#475569' : '#94A3B8',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s'
}}
- onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}
- onMouseLeave={e => e.currentTarget.style.background = 'rgba(255,255,255,0.02)'}
+ 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 = isFleetPage ? 'rgba(15, 23, 42, 0.02)' : 'rgba(255,255,255,0.02)'}
>
@@ -301,17 +328,17 @@ export const Sidebar: React.FC = () => {
{/* Navigation Area */}
{/* User Footer Profile */}
-
+
{!isSidebarCollapsed && (
@@ -329,26 +356,26 @@ export const Sidebar: React.FC = () => {
{/* Premium User Card */}
{initials}
-
{displayName}
+
{displayName}
{currentRole.replace(/_/g, ' ')}