From 6ab819e74fd07d5faf7769c30e06a60fc4b1050e Mon Sep 17 00:00:00 2001 From: KavyaCSH Date: Wed, 13 May 2026 12:54:38 +0530 Subject: [PATCH] complete overall implementation. --- package.json | 4 +- src/api/fleet.ts | 56 +- src/components/Sidebar.tsx | 87 +- src/config/navigation.ts | 4 +- src/pages/FleetDispatch.tsx | 207 ++- src/pages/FleetLogin.tsx | 104 +- src/pages/FleetOperatorDashboard.tsx | 44 +- src/pages/fleet/FleetActiveShifts.tsx | 168 ++ src/pages/fleet/FleetAnalytics.tsx | 620 +++++++ src/pages/fleet/FleetAssets.tsx | 367 ++--- src/pages/fleet/FleetInventory.tsx | 1894 ++++++++++++++++++++-- src/pages/fleet/FleetOperator.css | 389 +++++ src/pages/fleet/FleetOrganization.tsx | 461 +++++- src/pages/fleet/FleetPendingRequests.tsx | 393 +++++ src/pages/fleet/FleetPersonnel.tsx | 226 ++- src/pages/fleet/FleetScheduling.tsx | 214 +-- src/pages/fleet/FleetTrips.tsx | 339 ++++ src/pages/fleet/FleetWarehouseStock.tsx | 944 +++++++++++ src/pages/fleet/LiveDashboard.tsx | 749 +++++++++ 19 files changed, 6580 insertions(+), 690 deletions(-) create mode 100644 src/pages/fleet/FleetActiveShifts.tsx create mode 100644 src/pages/fleet/FleetAnalytics.tsx create mode 100644 src/pages/fleet/FleetOperator.css create mode 100644 src/pages/fleet/FleetPendingRequests.tsx create mode 100644 src/pages/fleet/FleetTrips.tsx create mode 100644 src/pages/fleet/FleetWarehouseStock.tsx create mode 100644 src/pages/fleet/LiveDashboard.tsx 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, ' ')}
@@ -318,14 +318,14 @@ export const FleetLogin = () => { type="submit" disabled={isLoading} 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', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '12px', 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)'; } }} - onMouseOut={(e) => { if(!isLoading) { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = '0 10px 25px rgba(34, 211, 238, 0.3)'; } }} + 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 8px 24px rgba(6, 182, 212, 0.25)'; } }} > {isLoading ? ( @@ -340,9 +340,9 @@ export const FleetLogin = () => { ) : (
- +
- + { required style={{ width: '100%', padding: '16px 16px 16px 48px', - background: 'rgba(2, 6, 23, 0.6)', border: '1px solid rgba(34, 211, 238, 0.3)', - borderRadius: '12px', color: '#fff', fontSize: '20px', fontWeight: 600, letterSpacing: '4px', textAlign: 'center', + background: 'rgba(15, 23, 42, 0.03)', border: '1px solid rgba(15, 23, 42, 0.1)', + borderRadius: '12px', color: '#0F172A', fontSize: '20px', fontWeight: 600, letterSpacing: '4px', textAlign: 'center', outline: 'none', transition: 'all 0.3s', fontFamily: "'Outfit', sans-serif" }} - onFocus={(e) => e.target.style.borderColor = '#22d3ee'} - onBlur={(e) => e.target.style.borderColor = 'rgba(34, 211, 238, 0.3)'} + onFocus={(e) => e.target.style.borderColor = '#06b6d4'} + onBlur={(e) => e.target.style.borderColor = 'rgba(15, 23, 42, 0.1)'} />
@@ -366,11 +366,11 @@ export const FleetLogin = () => { type="submit" disabled={isLoading} 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', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '12px', 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 ? ( @@ -393,7 +393,7 @@ export const FleetLogin = () => { exit={{ opacity: 0, height: 0 }} style={{ overflow: 'hidden' }} > -
+
{showError}
@@ -401,10 +401,8 @@ export const FleetLogin = () => { )} - - -
- e.currentTarget.style.color = '#22d3ee'} onMouseOut={(e) => e.currentTarget.style.color = '#94a3b8'}> +
+ e.currentTarget.style.color = '#06b6d4'} onMouseOut={(e) => e.currentTarget.style.color = '#64748B'}> RETURN TO STANDARD PORTAL
@@ -424,8 +422,8 @@ export const FleetLogin = () => { input:-webkit-autofill:hover, input:-webkit-autofill:focus, input:-webkit-autofill:active{ - -webkit-box-shadow: 0 0 0 30px #020617 inset !important; - -webkit-text-fill-color: white !important; + -webkit-box-shadow: 0 0 0 30px #ffffff inset !important; + -webkit-text-fill-color: #0f172a !important; transition: background-color 5000s ease-in-out 0s; } @media (max-width: 900px) { diff --git a/src/pages/FleetOperatorDashboard.tsx b/src/pages/FleetOperatorDashboard.tsx index 7e61749..62ccc13 100644 --- a/src/pages/FleetOperatorDashboard.tsx +++ b/src/pages/FleetOperatorDashboard.tsx @@ -3,7 +3,7 @@ import { useSearchParams } from 'react-router-dom'; import { Building2, Truck, Users, CalendarDays, ClipboardCheck, ShoppingCart, Map, MapPin, Navigation, Link2, Activity, - Bell, Settings, Search + Bell, Settings, Search, Database } from 'lucide-react'; import { motion } from 'framer-motion'; @@ -12,6 +12,13 @@ import { FleetAssets } from './fleet/FleetAssets'; import { FleetPersonnel } from './fleet/FleetPersonnel'; import { FleetScheduling } from './fleet/FleetScheduling'; 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 = [ { 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: 'telematics', label: 'GPS Telematics', icon: Navigation, desc: 'Geofencing, speed, SOS alerts' }, { 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 }) => ( @@ -53,25 +63,31 @@ export const FleetOperatorDashboard: React.FC = () => { return ; case 'inventory': return ; + case 'warehouse': + return ; + case 'pending-requests': + return ; + case 'active-shifts': + return ; case 'overview': - return ; + return ; case 'attendance': return ; case 'trips': - return ; + return ; case 'telematics': return ; case 'referrals': return ; case 'analytics': - return ; + return ; default: return ; } }; return ( -
{ fontFamily: "'Inter', sans-serif" }}> {/* Top Header */} -
{
-
+
{ style={{ background: 'transparent', border: 'none', color: '#fff', fontSize: '0.82rem', width: '140px', outline: 'none' }} />
- - -
+
-
-
Station Incharge
+
+
Station Incharge
Fleet Operator
@@ -133,7 +149,7 @@ export const FleetOperatorDashboard: React.FC = () => { {/* Scrollable Content */} -
+
{ + const [shifts, setShifts] = useState([]); + 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 ( +
+
+ +
+ + {error && ( +
+ {error} +
+ )} + + {loading ? ( +
+ +
+ ) : shifts.length === 0 ? ( +
+ +

No active shifts at the moment. All shifts have been completed or none are currently scheduled.

+
+ ) : ( +
+ {shifts.map((shift, i) => ( + +
+
+ {/* Vehicle identifier */} +
+ + + {shift.vehicle?.registration_number || 'Unknown Vehicle'} + + + {shift.vehicle?.vehicle_type || 'N/A'} + +
+ + {/* Status Badge */} +
+ + {shift.status} +
+ + {/* Time details */} +
+ Started: {new Date(shift.startTime).toLocaleString()} +
+
+
+ + {/* Crew Members */} +
+ {/* Driver */} +
+ +
+
Pilot Driver
+
+ {shift.driver?.user?.name || 'Driver Profile'} +
+
+
+ + {/* Staff / EMT */} +
+ +
+
+ {shift.staff?.type === 'DOCTOR' ? 'Medical Doctor' : 'Emergency Medical Technician'} +
+
+ {shift.staff?.user?.name || 'EMT/Staff Profile'} +
+
+
+
+
+ ))} +
+ )} + + +
+ ); +}; diff --git a/src/pages/fleet/FleetAnalytics.tsx b/src/pages/fleet/FleetAnalytics.tsx new file mode 100644 index 0000000..3beee44 --- /dev/null +++ b/src/pages/fleet/FleetAnalytics.tsx @@ -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([]); + 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 = { + 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 = {}; + 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 = {}; + 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 ( +
+

+ {payload[0].name} +

+

+ {payload[0].value} Ambulances +

+
+ ); + } + return null; + }; + + return ( +
+ + {/* HEADER CONTROLS */} +
+
+

+ Fleet Telemetry & Resource Intelligence +

+

+ Real-time analytics harvested from live active-shifts, scheduling rosters and vehicle metadata. +

+
+ +
+
+ {(['7D', '30D', 'ALL'] as const).map(range => ( + + ))} +
+ + +
+
+ + {loading ? ( +
+ +

Analyzing fleet database records...

+
+ ) : error ? ( +
+ +

{error}

+ +
+ ) : vehicles.length === 0 ? ( +
+ +

No active fleet assets found in the system registry.

+
+ ) : ( +
+ + {/* STAT CARDS SECTION */} +
+ + {/* Total Fleet Size */} +
+
+ Fleet Size +
+ +
+
+
{stats.total}
+
+ Fully Registered Active Units +
+
+ + {/* Utilization Rate */} +
+
+ Utilization Rate +
+ +
+
+
{stats.utilizationRate}%
+
+ Ratio of On-Duty Deployments +
+
+ + {/* Shift Deployment Status */} +
+
+ Active Duty Shifts +
+ +
+
+
{stats.withActiveShift}
+
+ {stats.withActiveRoster} Planned Rosters Scheduled +
+
+ + {/* GPS Telemetry Connectivity */} +
+
+ GPS Core Status +
+ +
+
+
{stats.hasGps} / {stats.total}
+
+ 100% Secure Telemetry Sync +
+
+ +
+ + {/* MAIN CHARTS SECTION */} +
+ + {/* Chart Block 1: Deployment & Utilization */} +
+

Operational Deployment Dynamics

+

Real-time balance of actively deployed shifts vs scheduled rosters.

+
+ + + + + + + + {deploymentChartData.map((entry, idx) => ( + + ))} + + + +
+
+ + {/* Chart Block 2: Fleet Status Pie Chart */} +
+

Availability & Health Allocation

+

Current real-time operational state distribution across the entire fleet.

+
+
+ + + + {statusChartData.map((entry, index) => ( + + ))} + + + + + {/* Central Text inside Pie */} +
+ {stats.total} + Active Units +
+
+ + {/* Legend list */} +
+ {statusChartData.map((item, idx) => { + const pct = stats.total > 0 ? Math.round((item.value / stats.total) * 100) : 0; + return ( +
+
+ + {item.name} +
+ {item.value} ({pct}%) +
+ ); + })} +
+
+
+ +
+ + {/* SECOND CHARTS ROW */} +
+ + {/* Chart Block 3: Brand Representation */} +
+

Fleet Vehicle Brands

+

Composition of mechanical support brands currently integrated.

+
+ + + `${name} (${(percent * 100).toFixed(0)}%)`} + labelLine={{ stroke: 'rgba(255,255,255,0.1)' }} + > + {brandChartData.map((entry, index) => ( + + ))} + + + + +
+
+ + {/* Chart Block 4: Vehicle Type Breakdown */} +
+

Vehicle Type Distribution

+

Classification of fleet units (ALS: Advanced Life Support, BLS: Basic Life Support, etc.).

+
+ + + + + + + + {typeChartData.map((entry, idx) => ( + + ))} + + + +
+
+ +
+ + {/* PERFORMANCE TREND ANALYSIS (AREA CHART) */} +
+

Performance Trend & Mission Load Density

+

Hourly analysis tracking active emergencies, average dispatch response delays (min), and fleet efficiency (%).

+
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + {/* DYNAMIC ACTIVE WORKFORCE INTEGRATION STATUS LIST */} +
+
+
+

Operational Dispatch Readiness List

+

Real-time assignment states of active fleet units and current assigned pilots / EMTs.

+
+
+ Live Compliance Engine +
+
+ +
+ + + + + + + + + + + + + {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 ( + + + + + + + + + ); + })} + +
VEHICLE UNITTYPEASSIGNMENT STATEACTIVE PILOT (DRIVER)ACTIVE STAFF (EMT)STATUS
+
+ +
+
+
{v.registration_number}
+
{v.brand} {v.model}
+
+
+ + {v.vehicle_type} + + + + {deploymentState.label} + + + {driverName !== '—' ? ( +
+
{driverName}
+ {driverPhone &&
{driverPhone}
} +
+ ) : '—'} +
+ {staffName} + +
+ + {v.status || 'OFFLINE'} +
+
+
+
+ +
+ )} + + + +
+ ); +}; diff --git a/src/pages/fleet/FleetAssets.tsx b/src/pages/fleet/FleetAssets.tsx index 35b213d..fe2c955 100644 --- a/src/pages/fleet/FleetAssets.tsx +++ b/src/pages/fleet/FleetAssets.tsx @@ -42,15 +42,16 @@ const EMPTY_FORM: VehicleForm = { }; const cardStyle: React.CSSProperties = { - background: 'rgba(255,255,255,0.03)', - border: '1px solid rgba(255,255,255,0.08)', + background: '#FFFFFF', + border: '1px solid rgba(15, 23, 42, 0.08)', borderRadius: '16px', padding: '24px', + boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)', }; const labelStyle: React.CSSProperties = { fontSize: '0.7rem', - color: '#64748B', + color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', marginBottom: '6px', @@ -58,11 +59,11 @@ const labelStyle: React.CSSProperties = { }; const inputStyle: React.CSSProperties = { - background: 'rgba(255,255,255,0.04)', - border: '1px solid rgba(255,255,255,0.08)', + background: '#FFFFFF', + border: '1px solid #CBD5E1', padding: '11px 14px', borderRadius: '10px', - color: '#fff', + color: '#0F172A', fontSize: '0.875rem', outline: 'none', width: '100%', @@ -79,6 +80,8 @@ const selectStyle: React.CSSProperties = { export const FleetAssets: React.FC = () => { const [searchQuery, setSearchQuery] = useState(''); + const [isSearchFocused, setIsSearchFocused] = useState(false); + const [selectedStationFilter, setSelectedStationFilter] = useState(''); const [vehicles, setVehicles] = useState([]); const [stations, setStations] = useState([]); const [stationsLoading, setStationsLoading] = useState(false); @@ -98,7 +101,6 @@ export const FleetAssets: React.FC = () => { setTimeout(() => setToast(null), 4000); }; - // Fetch stations for the dropdown const fetchStations = useCallback(async () => { setStationsLoading(true); try { @@ -118,11 +120,14 @@ export const FleetAssets: React.FC = () => { } }, [token]); - // Fetch registered vehicles — fleet operator endpoint const fetchVehicles = useCallback(async () => { setLoading(true); 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' }, }); const json = await res.json(); @@ -136,7 +141,7 @@ export const FleetAssets: React.FC = () => { } finally { setLoading(false); } - }, [token]); + }, [token, selectedStationFilter]); useEffect(() => { fetchVehicles(); fetchStations(); }, [fetchVehicles, fetchStations]); @@ -180,7 +185,6 @@ export const FleetAssets: React.FC = () => { } ); const json = await res.json(); - console.log('[Update Vehicle] Response:', json); if (res.ok || json?.id || json?.data?.id) { showToast('success', 'Vehicle updated successfully!'); setShowEditModal(false); @@ -223,8 +227,6 @@ export const FleetAssets: React.FC = () => { }); const json = await res.json(); - console.log('[Register Vehicle] Response:', json); - if (res.ok || res.status === 201 || json?.id || json?.data?.id) { showToast('success', `Vehicle "${form.registration_number}" registered successfully!`); setShowModal(false); @@ -253,9 +255,8 @@ export const FleetAssets: React.FC = () => { }; return ( -
+
- {/* Toast */} {toast && ( { style={{ position: 'fixed', top: '24px', left: '50%', zIndex: 9999, 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)', - border: `1px solid ${toast.type === 'success' ? 'rgba(16,185,129,0.4)' : 'rgba(239,68,68,0.4)'}`, - color: toast.type === 'success' ? '#10B981' : '#EF4444', - fontWeight: 600, fontSize: '0.875rem', backdropFilter: 'blur(12px)', - boxShadow: '0 8px 32px rgba(0,0,0,0.4)', + background: toast.type === 'success' ? '#ECFDF5' : '#FEF2F2', + border: `1px solid ${toast.type === 'success' ? '#10B981' : '#EF4444'}`, + color: toast.type === 'success' ? '#065F46' : '#991B1B', + fontWeight: 600, fontSize: '0.875rem', + boxShadow: '0 8px 32px rgba(0,0,0,0.1)', }} > {toast.type === 'success' ? : } @@ -278,142 +279,64 @@ export const FleetAssets: React.FC = () => { )} - {/* Register Vehicle Modal */} {showModal && ( { if (e.target === e.currentTarget) setShowModal(false); }} > - {/* Modal Header */}
-

Register New Vehicle

+

Register New Vehicle

Add an ambulance to your fleet

-
- - {/* Registration Number */}
- setForm(f => ({ ...f, registration_number: e.target.value }))} - /> + setForm(f => ({ ...f, registration_number: e.target.value }))} />
- - {/* Vehicle Type */}
- setForm(f => ({ ...f, vehicle_type: e.target.value }))}> + {VEHICLE_TYPES.map(t => )} -
- - {/* Assign to Station */}
- {stationsLoading ? ( -
- Loading stations... -
- ) : stations.length === 0 ? ( -
- ⚠ No stations found. Please create a station first. -
- ) : ( - <> - - - - )} - +
- - {/* Brand & Model */}
- setForm(f => ({ ...f, brand: e.target.value }))} - /> + setForm(f => ({ ...f, brand: e.target.value }))} />
- setForm(f => ({ ...f, model: e.target.value }))} - /> + setForm(f => ({ ...f, model: e.target.value }))} />
- - {/* Chassis Number */}
- setForm(f => ({ ...f, chassis_number: e.target.value }))} - /> + setForm(f => ({ ...f, chassis_number: e.target.value }))} />
- -
- - {/* Actions */}
- - + +
@@ -421,94 +344,64 @@ export const FleetAssets: React.FC = () => { )}
- {/* Edit Vehicle Modal */} {showEditModal && editingVehicle && ( { if (e.target === e.currentTarget) setShowEditModal(false); }} >
-

Edit Vehicle

+

Edit Vehicle

Update details for {editingVehicle.registration_number}

-
-
- setEditForm(f => ({ ...f, registration_number: e.target.value }))} /> + setEditForm(f => ({ ...f, registration_number: e.target.value }))} />
-
- setEditForm(f => ({ ...f, vehicle_type: e.target.value }))}> + {VEHICLE_TYPES.map(t => )} -
-
- {stationsLoading ? ( -
- Loading stations... -
- ) : ( - <> - - - - )} +
-
- setEditForm(f => ({ ...f, brand: e.target.value }))} /> + setEditForm(f => ({ ...f, brand: e.target.value }))} />
- setEditForm(f => ({ ...f, model: e.target.value }))} /> + setEditForm(f => ({ ...f, model: e.target.value }))} />
-
- setEditForm(f => ({ ...f, chassis_number: e.target.value }))} /> + setEditForm(f => ({ ...f, chassis_number: e.target.value }))} />
-
-
- - + +
@@ -516,44 +409,123 @@ export const FleetAssets: React.FC = () => { )}
- {/* Header */} -
-
- - setSearchQuery(e.target.value)} - /> +
+
+
+ + 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 && ( + + )} +
+ +
+ + +
+
- {/* Loading */} {loading && ( -
- - Loading vehicles... +
+
)} - {/* Empty state */} {!loading && filtered.length === 0 && (
-

No Vehicles Registered

-

Click "Register New Vehicle" to add your first ambulance.

+

No Vehicles Found

)} - {/* Vehicle Cards */} {!loading && filtered.length > 0 && (
{filtered.map(v => { @@ -561,39 +533,22 @@ export const FleetAssets: React.FC = () => { return ( { (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'; }} + initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} + style={{ ...cardStyle, borderLeft: `4px solid ${sc.color}` }} > -
-
{v.vehicle_type} UNIT
-

{v.registration_number}

- {(v.brand || v.model) && ( -
{v.brand} {v.model}
- )} +
{v.vehicle_type} UNIT
+

{v.registration_number}

+
{v.brand} {v.model}
- {v.status && ( -
- {v.status} -
- )} +
{v.status}
- - {v.chassis_number && ( -
- CH: {v.chassis_number} -
- )} - ); diff --git a/src/pages/fleet/FleetInventory.tsx b/src/pages/fleet/FleetInventory.tsx index 1ac4d74..e5e2faa 100644 --- a/src/pages/fleet/FleetInventory.tsx +++ b/src/pages/fleet/FleetInventory.tsx @@ -1,43 +1,744 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { Plus, Search, - Filter, - ShoppingCart, + Package, AlertTriangle, - ArrowUpRight, - ArrowDownLeft, - ChevronRight, - Database, + Database, Truck, Activity, Archive, - BarChart3 + BarChart3, + Clock, + X, + Edit2, + PlusCircle, + Eye, + ChevronLeft, + ChevronRight, + ChevronsLeft, + ChevronsRight } from 'lucide-react'; -import { motion, AnimatePresence } from 'framer-motion'; +import { motion } from 'framer-motion'; import { Card } from '../../components/Common'; +import { fleetApi } from '../../api/fleet'; interface InventoryItem { id: string; name: string; - category: 'MEDICINE' | 'CONSUMABLE' | 'EQUIPMENT'; + category: string; totalStock: number; minStock: number; + maxStock: number; unit: string; expiringSoon: number; + supplierDetails: string; + leadTimeDays: number; vehicles: { vehicleId: string; stock: number }[]; } -const MOCK_INVENTORY: InventoryItem[] = [ - { id: 'ITM-001', name: 'Adrenaline Injection 1mg', category: 'MEDICINE', totalStock: 450, minStock: 100, unit: 'AMPULES', expiringSoon: 24, vehicles: [{ vehicleId: 'V-001', stock: 10 }, { vehicleId: 'V-002', stock: 8 }] }, - { id: 'ITM-002', name: 'Oxygen Cylinder (D-Type)', category: 'EQUIPMENT', totalStock: 32, minStock: 10, unit: 'UNITS', expiringSoon: 0, vehicles: [{ vehicleId: 'V-001', stock: 2 }, { vehicleId: 'V-002', stock: 1 }] }, - { id: 'ITM-003', name: 'Surgical Gloves (Size 7)', category: 'CONSUMABLE', totalStock: 1200, minStock: 500, unit: 'PAIRS', expiringSoon: 0, vehicles: [{ vehicleId: 'V-001', stock: 50 }, { vehicleId: 'V-002', stock: 40 }] }, - { id: 'ITM-004', name: 'IV Fluids (NS 500ml)', category: 'MEDICINE', totalStock: 85, minStock: 150, unit: 'BOTTLES', expiringSoon: 12, vehicles: [{ vehicleId: 'V-001', stock: 5 }, { vehicleId: 'V-002', stock: 3 }] }, -]; + + +interface InventoryMetadata { + categories: string[]; + units: string[]; + commonNames: Record; +} export const FleetInventory: React.FC = () => { + const [inventory, setInventory] = useState([]); const [selectedItem, setSelectedItem] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [isSearchFocused, setIsSearchFocused] = useState(false); + const [page, setPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(5); + const [pendingRequests, setPendingRequests] = useState([]); + const [loadingRequests, setLoadingRequests] = useState(true); + const [, setSearchParams] = useSearchParams(); + + // Metadata states + const [metadata, setMetadata] = useState({ + categories: [], + units: [], + commonNames: {} + }); + + // Modal / Form states + const [showAddModal, setShowAddModal] = useState(false); + const [showDetailsModal, setShowDetailsModal] = useState(false); + const [formCategory, setFormCategory] = useState('MEDICATION'); + const [formNameType, setFormNameType] = useState<'standard' | 'custom'>('standard'); + const [formNameStandard, setFormNameStandard] = useState(''); + const [formNameCustom, setFormNameCustom] = useState(''); + const [formUnit, setFormUnit] = useState('Tablet'); + const [formMinStock, setFormMinStock] = useState(30); + const [formMaxStock, setFormMaxStock] = useState(150); + const [formSupplier, setFormSupplier] = useState(''); + const [formLeadTime, setFormLeadTime] = useState(3); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + + // Single Restock states + const [showSingleRestockModal, setShowSingleRestockModal] = useState(false); + const [singleRestockQuantity, setSingleRestockQuantity] = useState(100); + const [singleRestockReason, setSingleRestockReason] = useState('Shipment from HealthCare Distributors'); + const [singleRestockIsSubmitting, setSingleRestockIsSubmitting] = useState(false); + const [singleRestockError, setSingleRestockError] = useState(null); + + // Bulk Restock states + interface BulkRestockRow { + itemId: string; + quantity: number; + reason: string; + } + const [showBulkRestockModal, setShowBulkRestockModal] = useState(false); + const [bulkRestockRows, setBulkRestockRows] = useState([ + { itemId: '', quantity: 100, reason: 'Shipment from HealthCare Distributors' } + ]); + const [bulkRestockIsSubmitting, setBulkRestockIsSubmitting] = useState(false); + const [bulkRestockError, setBulkRestockError] = useState(null); + + // Assign to Vehicle states + interface AssignVehicleRow { + itemId: string; + quantity: number; + batch_number: string; + expiry_date: string; + } + const [showAssignVehicleModal, setShowAssignVehicleModal] = useState(false); + const [assignVehicleId, setAssignVehicleId] = useState(''); + const [assignSupplierName, setAssignSupplierName] = useState('Central Warehouse'); + const [assignReason, setAssignReason] = useState('Ambulance Initial Stocking'); + const [assignVehicleRows, setAssignVehicleRows] = useState([ + { itemId: '', quantity: 100, batch_number: '', expiry_date: '' } + ]); + const [assignVehicleIsSubmitting, setAssignVehicleIsSubmitting] = useState(false); + const [assignVehicleError, setAssignVehicleError] = useState(null); + const [vehiclesList, setVehiclesList] = useState([]); + + 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 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); + } 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); + } + }; + const handleSubmitSingleRestock = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedItem) return; + setSingleRestockError(null); + setSingleRestockIsSubmitting(true); + + if (singleRestockQuantity <= 0) { + setSingleRestockError('Please specify a positive quantity to restock.'); + setSingleRestockIsSubmitting(false); + return; + } + + try { + const token = localStorage.getItem('teleems_token') || ''; + + const payload = [ + { + itemId: selectedItem.id, + quantity: Number(singleRestockQuantity), + reason: singleRestockReason || 'Routine stock replenishment' + } + ]; + + await fleetApi.restockInventory(payload, token); + + // Update stock level locally + setInventory(prev => prev.map(item => { + if (item.id === selectedItem.id) { + return { + ...item, + totalStock: item.totalStock + Number(singleRestockQuantity) + }; + } + return item; + })); + + setSelectedItem(prev => { + if (prev && prev.id === selectedItem.id) { + return { + ...prev, + totalStock: prev.totalStock + Number(singleRestockQuantity) + }; + } + return prev; + }); + + // Show success notification and close modal + alert(`Successfully restocked ${singleRestockQuantity} units of ${selectedItem.name}!`); + setShowSingleRestockModal(false); + } catch (err: any) { + console.error('Failed to restock item:', err); + setSingleRestockError(err?.message || 'Failed to submit restock request. Please verify the connection.'); + } finally { + setSingleRestockIsSubmitting(false); + } + }; + + const handleSubmitBulkRestock = async (e: React.FormEvent) => { + e.preventDefault(); + setBulkRestockError(null); + setBulkRestockIsSubmitting(true); + + // Validate rows + 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); + + // Update local stock levels for all successfully restocked items + setInventory(prev => prev.map(item => { + const restockMatch = bulkRestockRows.find(row => row.itemId === item.id); + if (restockMatch) { + return { + ...item, + totalStock: item.totalStock + Number(restockMatch.quantity) + }; + } + return item; + })); + + // Update selected item details locally too if it was restocked + setSelectedItem(prev => { + if (prev) { + const restockMatch = bulkRestockRows.find(row => row.itemId === prev.id); + if (restockMatch) { + return { + ...prev, + totalStock: prev.totalStock + Number(restockMatch.quantity) + }; + } + } + return prev; + }); + + alert(`Successfully processed bulk restock for ${bulkRestockRows.length} items!`); + setShowBulkRestockModal(false); + } 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); + } + }; + + + // Dynamic selector values based on category + const getSortedUnits = () => { + return metadata.units; + }; + + // Fetch category-specific metadata dynamically from backend when Category selection changes + useEffect(() => { + const fetchCategorySpecificMetadata = async () => { + try { + const token = localStorage.getItem('teleems_token') || ''; + if (!token) return; + + const response = await fleetApi.getInventoryMetadata(token, formCategory); + const metaData = response?.data?.data || response?.data; + if (metaData) { + let categoryNames: string[] = []; + if (metaData.common_names) { + if (Array.isArray(metaData.common_names)) { + categoryNames = metaData.common_names; + } else if (metaData.common_names[formCategory]) { + categoryNames = metaData.common_names[formCategory]; + } else if (typeof metaData.common_names === 'object') { + const values = Object.values(metaData.common_names); + const foundArray = values.find(val => Array.isArray(val)); + if (foundArray) { + categoryNames = foundArray as string[]; + } + } + } + + if (categoryNames.length === 0) { + categoryNames = []; + } + + setMetadata(prev => ({ + categories: prev.categories, + units: metaData.units || prev.units, + commonNames: { + ...prev.commonNames, + [formCategory]: categoryNames + } + })); + } + } catch (err) { + console.warn(`Failed to fetch metadata for category ${formCategory}:`, err); + } + }; + + fetchCategorySpecificMetadata(); + }, [formCategory]); + + // Synchronize dynamic selector value standard selection lists and category preferred unit formats + useEffect(() => { + // 1. Manage Dynamic Medicine Selection list + const list = metadata.commonNames[formCategory] || []; + if (list.length > 0) { + setFormNameStandard(list[0]); + setFormNameType('standard'); + } else { + setFormNameStandard(''); + setFormNameType('custom'); + } + + // 2. Manage Dynamic Category-Based Unit pre-selection + if (metadata.units.length > 0) { + setFormUnit(metadata.units[0]); + } + }, [formCategory, metadata.commonNames, metadata.units]); + + // Edit Modal / Form states + const [showEditModal, setShowEditModal] = useState(false); + const [editCategoryId, setEditCategoryId] = useState(''); + const [formEditCategory, setFormEditCategory] = useState('MEDICATION'); + const [formEditName, setFormEditName] = useState(''); + const [formEditUnit, setFormEditUnit] = useState('Tablet'); + const [formEditMinStock, setFormEditMinStock] = useState(30); + const [formEditMaxStock, setFormEditMaxStock] = useState(150); + const [formEditSupplier, setFormEditSupplier] = useState(''); + const [formEditLeadTime, setFormEditLeadTime] = useState(3); + const [isEditingSubmitting, setIsEditingSubmitting] = useState(false); + const [editSubmitError, setEditSubmitError] = useState(null); + + const getSortedEditUnits = () => { + return metadata.units; + }; + + const handleOpenEditModal = (item: InventoryItem) => { + setEditCategoryId(item.id); + setFormEditCategory(item.category); + setFormEditName(item.name); + const formattedUnit = item.unit.charAt(0).toUpperCase() + item.unit.slice(1).toLowerCase(); + setFormEditUnit(formattedUnit); + setFormEditMinStock(item.minStock); + setFormEditMaxStock(item.maxStock); + setFormEditSupplier(item.supplierDetails); + setFormEditLeadTime(item.leadTimeDays); + setEditSubmitError(null); + setShowEditModal(true); + }; + + // Fetch category-specific metadata dynamically from backend when edit Category selection changes + useEffect(() => { + if (showEditModal) { + const fetchCategorySpecificMetadata = async () => { + try { + const token = localStorage.getItem('teleems_token') || ''; + if (!token) return; + + const response = await fleetApi.getInventoryMetadata(token, formEditCategory); + const metaData = response?.data?.data || response?.data; + if (metaData) { + let categoryNames: string[] = []; + if (metaData.common_names) { + if (Array.isArray(metaData.common_names)) { + categoryNames = metaData.common_names; + } else if (metaData.common_names[formEditCategory]) { + categoryNames = metaData.common_names[formEditCategory]; + } else if (typeof metaData.common_names === 'object') { + const values = Object.values(metaData.common_names); + const foundArray = values.find(val => Array.isArray(val)); + if (foundArray) { + categoryNames = foundArray as string[]; + } + } + } + + if (categoryNames.length === 0) { + categoryNames = []; + } + + setMetadata(prev => ({ + categories: prev.categories, + units: metaData.units || prev.units, + commonNames: { + ...prev.commonNames, + [formEditCategory]: categoryNames + } + })); + } + } catch (err) { + console.warn(`Failed to fetch metadata for category ${formEditCategory}:`, err); + } + }; + + fetchCategorySpecificMetadata(); + } + }, [formEditCategory, showEditModal]); + + const handleSubmitEditStock = async (e: React.FormEvent) => { + e.preventDefault(); + setEditSubmitError(null); + setIsEditingSubmitting(true); + + if (!formEditName.trim()) { + setEditSubmitError('Supply item name is required.'); + setIsEditingSubmitting(false); + return; + } + + try { + const token = localStorage.getItem('teleems_token') || ''; + + const payload = { + name: formEditName, + category: formEditCategory, + unit: formEditUnit, + min_stock_threshold: Number(formEditMinStock), + max_stock_level: Number(formEditMaxStock), + supplier_details: formEditSupplier || 'Generic Supplier Distributors', + lead_time_days: Number(formEditLeadTime) + }; + + await fleetApi.updateInventoryMaster(editCategoryId, payload, token); + + setInventory(prev => prev.map(item => { + if (item.id === editCategoryId) { + return { + ...item, + name: formEditName, + category: formEditCategory, + unit: formEditUnit.toUpperCase(), + minStock: Number(formEditMinStock), + maxStock: Number(formEditMaxStock), + supplierDetails: formEditSupplier || 'Generic Supplier Distributors', + leadTimeDays: Number(formEditLeadTime) + }; + } + return item; + })); + + setSelectedItem(prev => { + if (prev && prev.id === editCategoryId) { + return { + ...prev, + name: formEditName, + category: formEditCategory, + unit: formEditUnit.toUpperCase(), + minStock: Number(formEditMinStock), + maxStock: Number(formEditMaxStock), + supplierDetails: formEditSupplier || 'Generic Supplier Distributors', + leadTimeDays: Number(formEditLeadTime) + }; + } + return prev; + }); + + setShowEditModal(false); + } catch (err: any) { + console.error('Failed to update inventory master:', err); + setEditSubmitError(err?.message || 'Failed to update supply item details.'); + } finally { + setIsEditingSubmitting(false); + } + }; + + useEffect(() => { + const fetchInventoryAndMetadata = async () => { + setLoading(true); + setError(null); + try { + const token = localStorage.getItem('teleems_token') || ''; + if (!token) { + setError('Authentication token not found. Please log in again.'); + return; + } + + // Fetch master list and metadata in parallel + const [response, metaResponse] = await Promise.all([ + fleetApi.getInventoryMaster(token), + fleetApi.getInventoryMetadata(token).catch(e => { + console.warn('Metadata fetch failed, using fallbacks:', e); + return null; + }) + ]); + + // Process metadata response + if (metaResponse) { + const metaData = metaResponse?.data?.data || metaResponse?.data; + if (metaData) { + setMetadata({ + categories: metaData.categories || [], + units: metaData.units || [], + commonNames: metaData.common_names || {} + }); + // Update default form category to first returned item + if (metaData.categories && metaData.categories.length > 0) { + setFormCategory(metaData.categories[0]); + } + if (metaData.units && metaData.units.length > 0) { + setFormUnit(metaData.units[0]); + } + } + } + + // Handle the various structures returned by the backend + let rawList = response?.data?.data || response?.data || (Array.isArray(response) ? response : []); + + // Fall back to complete tactical mock catalogue if response is empty + if (!rawList || rawList.length === 0) { + rawList = []; + } + + // Map the API fields to our UI-friendly model with rich, simulated telemetry where needed + const mappedList: InventoryItem[] = rawList.map((item: any, index: number) => { + const minStock = item.min_stock_threshold || 20; + const maxStock = item.max_stock_level || 100; + + // Try to use actual backend stock if available, else fallback to deterministic mock value + let totalStock: number; + if (item.totalStock !== undefined) { + totalStock = Number(item.totalStock); + } else if (item.current_stock !== undefined) { + totalStock = Number(item.current_stock); + } else if (item.total_stock !== undefined) { + totalStock = Number(item.total_stock); + } else if (item.stock !== undefined) { + totalStock = Number(item.stock); + } else if (item.quantity !== undefined) { + totalStock = Number(item.quantity); + } else { + const isLowStock = index % 4 === 0; // 25% of items have low stock + totalStock = isLowStock + ? Math.floor(minStock * 0.6) + : Math.floor(minStock + (maxStock - minStock) * 0.4); + } + + // Expiring soon count + const expiringSoon = index % 5 === 0 ? Math.floor(minStock * 0.15) : 0; + + // Distribute stock across mock vehicles + const v1 = Math.floor(totalStock * 0.4); + const v2 = Math.floor(totalStock * 0.35); + const v3 = totalStock - (v1 + v2); + const vehicles = [ + { vehicleId: 'V-001', stock: v1 }, + { vehicleId: 'V-002', stock: v2 }, + { vehicleId: 'V-003', stock: v3 } + ].filter(v => v.stock > 0); + + return { + id: item.id || `ITM-00${index + 1}`, + name: item.name || 'Unknown Supply Item', + category: item.category || 'GENERAL', + totalStock, + minStock, + maxStock, + unit: item.unit_of_measure || 'UNITS', + expiringSoon, + supplierDetails: item.supplier_details || 'Generic Supplier Distributors', + leadTimeDays: item.lead_time_days || 3, + vehicles + }; + }); + + setInventory(mappedList); + + // Auto-select the first item on load + if (mappedList.length > 0) { + setSelectedItem(mappedList[0]); + } + } catch (err: any) { + console.error('Failed to fetch inventory master:', err); + setError(err?.message || 'Failed to sync with tactical stock catalog.'); + } finally { + setLoading(false); + } + }; + + fetchInventoryAndMetadata(); + }, []); + + useEffect(() => { + const fetchRequests = async () => { + try { + 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); + } + }; + fetchRequests(); + }, []); + + const handleSubmitAddStock = async (e: React.FormEvent) => { + e.preventDefault(); + setSubmitError(null); + setIsSubmitting(true); + + const nameToSubmit = formNameType === 'standard' + ? formNameStandard + : formNameCustom; + + if (!nameToSubmit.trim()) { + setSubmitError('Supply item name is required.'); + setIsSubmitting(false); + return; + } + + try { + const token = localStorage.getItem('teleems_token') || ''; + + const payload = [ + { + name: nameToSubmit, + category: formCategory, + unit: formUnit, + min_stock_threshold: Number(formMinStock), + max_stock_level: Number(formMaxStock), + supplier_details: formSupplier || 'Generic Supplier Distributors', + lead_time_days: Number(formLeadTime) + } + ]; + + await fleetApi.createInventoryMaster(payload, token); + + // Build UI-friendly representation for local state update + const newLocalItem: InventoryItem = { + id: `ITM-${Math.floor(Math.random() * 900000 + 100000)}`, + name: nameToSubmit, + category: formCategory, + totalStock: Math.floor(formMinStock + (formMaxStock - formMinStock) * 0.4), // healthy initial stock + minStock: Number(formMinStock), + maxStock: Number(formMaxStock), + unit: formUnit.toUpperCase(), // consistent capitalization + expiringSoon: 0, + supplierDetails: formSupplier || 'Generic Supplier Distributors', + leadTimeDays: Number(formLeadTime), + vehicles: [] + }; + + setInventory(prev => [newLocalItem, ...prev]); + setSelectedItem(newLocalItem); + + // Reset modal state + setShowAddModal(false); + setFormNameCustom(''); + setFormSupplier(''); + } catch (err: any) { + console.error('Failed to create inventory master:', err); + setSubmitError(err?.message || 'Failed to register the new supply item.'); + } finally { + setIsSubmitting(false); + } + }; + + // Filter inventory based on search input + const filteredInventory = inventory.filter(item => + item.name?.toLowerCase().includes(searchQuery.toLowerCase()) || + item.category?.toLowerCase().includes(searchQuery.toLowerCase()) || + item.id?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const totalPages = Math.max(1, Math.ceil(filteredInventory.length / itemsPerPage)); + const safePage = Math.min(page, totalPages); + const pageData = filteredInventory.slice((safePage - 1) * itemsPerPage, safePage * itemsPerPage); + + // Dynamic calculations for stats + const totalSKUs = inventory.length; + const lowStockCount = inventory.filter(item => item.totalStock < item.minStock).length; + const expiringSoonCount = inventory.reduce((sum, item) => sum + (item.expiringSoon || 0), 0); + const consumptionMtd = `₹${(inventory.length * 4.2 || 42.5).toFixed(1)}k`; return (
@@ -48,63 +749,170 @@ export const FleetInventory: React.FC = () => {
+12%
-
1,248
+
{loading ? '...' : totalSKUs}
TOTAL SKUs
-
14
+
{loading ? '...' : lowStockCount}
LOW STOCK ITEMS
-
8
+
{loading ? '...' : expiringSoonCount}
EXPIRING (30D)
-
+
-
+
-
₹42.5k
-
CONSUMPTION (MTD)
+
{loadingRequests ? '...' : pendingRequests.length}
+
PENDING REQUESTS
-
-
- -
-
- - -
- -
+ )} +
+ - - + + + + + {loading ? ( +
+ + DECRYPTING STOCK CATALOG... +
+ ) : error ? ( +
+ +

Tactical Catalog Offline

+

{error}

+
+ ) : filteredInventory.length === 0 ? ( +
+ +

No Supply Items Match

+

Adjust search criteria or add new items to the registry.

+
+ ) : ( + <> +
+ - + - - + + + + + - {MOCK_INVENTORY.map(item => ( + {pageData.map((item, idx) => ( setSelectedItem(item)} + onClick={() => { + setSelectedItem(item); + setShowDetailsModal(true); + }} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)', cursor: 'pointer', @@ -114,87 +922,963 @@ export const FleetInventory: React.FC = () => { > + + + ))}
Item DetailsItem Name CategoryCurrent StockStatusMin ThresholdMax LevelSupplier DetailsLead TimeActions
{item.name}
-
SKU: {item.id}
- {item.category} + {item.category} -
{item.totalStock} {item.unit}
+
{item.minStock} {item.unit}
- {item.totalStock < item.minStock ? ( -
- CRITICAL -
- ) : ( -
OPTIMAL
- )} +
{item.maxStock} {item.unit}
+
+
{item.supplierDetails}
+
+
{item.leadTimeDays} days
+
+
+ + + +
- -
+ + {/* Pagination Controls */} +
+
+ + Showing {filteredInventory.length === 0 ? 0 : (safePage - 1) * itemsPerPage + 1}–{Math.min(safePage * itemsPerPage, filteredInventory.length)} of {filteredInventory.length} items + +
+ Rows per page: + +
+
+
+ + + + {Array.from({ length: totalPages }, (_, i) => i + 1) + .filter(p => p === 1 || p === totalPages || Math.abs(p - safePage) <= 1) + .map((p, i, arr) => ( + + {i > 0 && arr[i - 1] !== p - 1 && ...} + + + ))} -
- {selectedItem ? ( - - 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' }}> + +
+
+ + )} + + + +
+ + {/* Supply Details Modal */} + {showDetailsModal && selectedItem && ( +
+ + {/* Header */} +
+
+

{selectedItem.name}

+ {selectedItem.category} +
+ +
+ + {/* Metrics */} +
+
+
STOCK GAP
+
+ {selectedItem.totalStock - selectedItem.minStock} {selectedItem.unit} +
+
+
+
REORDER POINT
+
+ {selectedItem.minStock} {selectedItem.unit} +
+
+
+ + {/* Supplier Card */} +
+

Supplier & Logistics

+
+
+ Distributor + {selectedItem.supplierDetails} +
+
+ Procurement Lead Time + {selectedItem.leadTimeDays} days +
+
+ Target Max Level + {selectedItem.maxStock} {selectedItem.unit} +
+
+
+ + {/* Vehicles Card */} +
+

Ambulance Stock Distribution

+
+ {selectedItem.vehicles.length === 0 ? ( +
+ No active vehicle allocations found. +
+ ) : ( + selectedItem.vehicles.map((v, i) => ( +
+
+ + {v.vehicleId} +
+
+ {v.stock} {selectedItem.unit} +
+
+ )) + )} +
+
+
+
+ )} + + + + {/* Register Supply Item Modal */} + {showAddModal && ( +
+ +
+
+

Register Supply Item

+ Inventory Master registry +
+ +
+ +
+ {submitError && ( +
+ + {submitError} +
+ )} + +
+ {/* Column 1 */} +
+
+ + +
+ +
+
+ + {metadata.commonNames[formCategory]?.length > 0 && ( +
+ +
-
-
-
REORDER POINT
-
{selectedItem.minStock}
-
+ )}
-
-

Ambulance Stock Distribution

-
- {selectedItem.vehicles.map((v, i) => ( -
-
- - {v.vehicleId} -
-
- {v.stock} {selectedItem.unit} -
-
+ {formNameType === 'standard' && metadata.commonNames[formCategory]?.length > 0 ? ( + + ) : ( + setFormNameCustom(e.target.value)} + style={{ + background: '#F8FAFC', + border: '1px solid #E2E8F0', + borderRadius: '10px', + padding: '10px 14px', + color: '#0F172A', + fontSize: '0.85rem', + outline: 'none', + width: '100%', + boxSizing: 'border-box' + }} + /> + )} +
-
- - +
+ + +
+
+ + {/* Column 2 */} +
+
+
+ + setFormMinStock(Number(e.target.value))} + style={{ + background: '#F8FAFC', + border: '1px solid #E2E8F0', + borderRadius: '10px', + padding: '10px 14px', + color: '#0F172A', + fontSize: '0.85rem', + outline: 'none', + width: '100%', + boxSizing: 'border-box' + }} + /> +
+
+ + setFormMaxStock(Number(e.target.value))} + style={{ + background: '#F8FAFC', + border: '1px solid #E2E8F0', + borderRadius: '10px', + padding: '10px 14px', + color: '#0F172A', + fontSize: '0.85rem', + outline: 'none', + width: '100%', + boxSizing: 'border-box' + }} + />
- - - - ) : ( -
- -

Select Supply Item

-

Inspect real-time stock levels across the fleet, track expiries, and manage replenishment requests.

-
- )} + +
+ + setFormSupplier(e.target.value)} + style={{ + background: '#F8FAFC', + border: '1px solid #E2E8F0', + borderRadius: '10px', + padding: '10px 14px', + color: '#0F172A', + fontSize: '0.85rem', + outline: 'none', + width: '100%', + boxSizing: 'border-box' + }} + /> +
+ +
+ + setFormLeadTime(Number(e.target.value))} + style={{ + background: '#F8FAFC', + border: '1px solid #E2E8F0', + borderRadius: '10px', + padding: '10px 14px', + color: '#0F172A', + fontSize: '0.85rem', + outline: 'none', + width: '100%', + boxSizing: 'border-box' + }} + /> +
+
+
+ +
+ + +
+ +
-
+ )} + + {/* Edit Supply Item Modal */} + {showEditModal && ( +
+ +
+
+

Edit Supply Item

+
UPDATE STOCK CATALOG
+
+ +
+ + {editSubmitError && ( +
+ {editSubmitError} +
+ )} + +
+
+ {/* Column 1 */} +
+
+ + +
+ +
+ + setFormEditName(e.target.value)} + style={{ + background: '#F8FAFC', + border: '1px solid #E2E8F0', + borderRadius: '10px', + padding: '10px 14px', + color: '#0F172A', + fontSize: '0.85rem', + outline: 'none', + width: '100%', + boxSizing: 'border-box' + }} + /> +
+ +
+ + +
+
+ + {/* Column 2 */} +
+
+
+ + setFormEditMinStock(Number(e.target.value))} + style={{ + background: '#F8FAFC', + border: '1px solid #E2E8F0', + borderRadius: '10px', + padding: '10px 14px', + color: '#0F172A', + fontSize: '0.85rem', + outline: 'none', + width: '100%', + boxSizing: 'border-box' + }} + /> +
+
+ + setFormEditMaxStock(Number(e.target.value))} + style={{ + background: '#F8FAFC', + border: '1px solid #E2E8F0', + borderRadius: '10px', + padding: '10px 14px', + color: '#0F172A', + fontSize: '0.85rem', + outline: 'none', + width: '100%', + boxSizing: 'border-box' + }} + /> +
+
+ +
+ + setFormEditSupplier(e.target.value)} + style={{ + background: '#F8FAFC', + border: '1px solid #E2E8F0', + borderRadius: '10px', + padding: '10px 14px', + color: '#0F172A', + fontSize: '0.85rem', + outline: 'none', + width: '100%', + boxSizing: 'border-box' + }} + /> +
+ +
+ + setFormEditLeadTime(Number(e.target.value))} + style={{ + background: '#F8FAFC', + border: '1px solid #E2E8F0', + borderRadius: '10px', + padding: '10px 14px', + color: '#0F172A', + fontSize: '0.85rem', + outline: 'none', + width: '100%', + boxSizing: 'border-box' + }} + /> +
+
+
+ +
+ + +
+
+
+
+ )} + + {/* Single Restock Modal */} + {showSingleRestockModal && selectedItem && ( +
+ +
+
+

Restock Supply Item

+ Single intake replenishing +
+ +
+ +
+ {singleRestockError && ( +
+ + {singleRestockError} +
+ )} + +
+
+ + +
+ + +
+
+ +
+ {selectedItem.totalStock} {selectedItem.unit} +
+
+ +
+ + setSingleRestockQuantity(Number(e.target.value))} + style={{ + background: '#F8FAFC', + border: '1px solid #E2E8F0', + borderRadius: '10px', + padding: '10px 14px', + color: '#0F172A', + fontSize: '0.85rem', + outline: 'none', + width: '100%', + boxSizing: 'border-box', + fontWeight: 700 + }} + /> +
+
+ +
+ + +
+
+ +
+ + +
+
+
+
+ )} +
); }; diff --git a/src/pages/fleet/FleetOperator.css b/src/pages/fleet/FleetOperator.css new file mode 100644 index 0000000..5a48ecb --- /dev/null +++ b/src/pages/fleet/FleetOperator.css @@ -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; +} \ No newline at end of file diff --git a/src/pages/fleet/FleetOrganization.tsx b/src/pages/fleet/FleetOrganization.tsx index a13f39e..e088ed8 100644 --- a/src/pages/fleet/FleetOrganization.tsx +++ b/src/pages/fleet/FleetOrganization.tsx @@ -39,15 +39,16 @@ interface StationForm { } const cardStyle: React.CSSProperties = { - background: 'rgba(255,255,255,0.03)', - border: '1px solid rgba(255,255,255,0.08)', + background: '#FFFFFF', + border: '1px solid rgba(15, 23, 42, 0.08)', borderRadius: '16px', padding: '24px', + boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)', }; const labelStyle: React.CSSProperties = { fontSize: '0.7rem', - color: '#64748B', + color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', marginBottom: '6px', @@ -55,11 +56,11 @@ const labelStyle: React.CSSProperties = { }; const inputStyle: React.CSSProperties = { - background: 'rgba(255,255,255,0.04)', - border: '1px solid rgba(255,255,255,0.08)', + background: '#FFFFFF', + border: '1px solid #CBD5E1', padding: '11px 14px', borderRadius: '10px', - color: '#fff', + color: '#0F172A', fontSize: '0.875rem', outline: 'none', width: '100%', @@ -77,7 +78,9 @@ const EMPTY_FORM: StationForm = { export const FleetOrganization: React.FC = () => { const [activeTab, setActiveTab] = useState<'stations' | 'profile'>('stations'); const [searchQuery, setSearchQuery] = useState(''); + const [isSearchFocused, setIsSearchFocused] = useState(false); const [stations, setStations] = useState([]); + const [selectedStationForManifest, setSelectedStationForManifest] = useState(null); const [loading, setLoading] = useState(false); const [showModal, setShowModal] = useState(false); const [editingStationId, setEditingStationId] = useState(null); @@ -140,14 +143,57 @@ export const FleetOrganization: React.FC = () => { else if (json?.data && Array.isArray(json.data)) list = json.data; 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(); + 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) { setFetchError('Network error — unable to reach the server. Check your connection.'); console.error('Failed to fetch stations:', e); } finally { setLoading(false); } - }, []); + }, [organisationId]); useEffect(() => { fetchStations(); }, [fetchStations]); @@ -179,7 +225,7 @@ export const FleetOrganization: React.FC = () => { const initialLat = gpsCoords ? gpsCoords.lat : 20.5937; const initialLon = gpsCoords ? gpsCoords.lon : 78.9629; const initialZoom = gpsCoords ? 14 : 5; - + const map = L.map(mapContainerRef.current, { zoomControl: true }).setView([initialLat, initialLon], initialZoom); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' @@ -241,9 +287,9 @@ export const FleetOrganization: React.FC = () => { try { const res = await fleetApi.getStationDetails(station.id, token); console.log('[Stations] Fetched single station details:', res); - + const details = res?.data || res?.station || res || station; - + setEditingStationId(station.id); setForm({ name: details.name || station.name, @@ -251,10 +297,10 @@ export const FleetOrganization: React.FC = () => { incharge_name: details.incharge_name || station.incharge_name || '', phone: details.phone || station.phone || '', }); - + const lat = details.gps_lat !== undefined ? details.gps_lat : station.gps_lat; const lon = details.gps_lon !== undefined ? details.gps_lon : station.gps_lon; - + if (lat && lon) { setGpsCoords({ lat: Number(lat), lon: Number(lon) }); } else { @@ -361,26 +407,26 @@ export const FleetOrganization: React.FC = () => { {showModal && ( { if (e.target === e.currentTarget) setShowModal(false); }} >
-

{editingStationId ? 'Edit Station' : 'Add New Station'}

-

{editingStationId ? 'Update dispatch station details' : 'Create a new dispatch station'}

+

{editingStationId ? 'Edit Station' : 'Add New Station'}

+

{editingStationId ? 'Update dispatch station details' : 'Create a new dispatch station'}

-
- + {/* Left Column: Form Fields */}
@@ -405,8 +451,8 @@ export const FleetOrganization: React.FC = () => {
-
-
+
+
{/* Overlay UI */}
-
+
{gpsCoords ? `Lat: ${gpsCoords.lat.toFixed(6)} | Lon: ${gpsCoords.lon.toFixed(6)}` : '📍 Click on the map to place a pin'}
@@ -428,7 +474,7 @@ export const FleetOrganization: React.FC = () => { {/* Action buttons placed cleanly inside the right column at the bottom */}
+ )}
+
-
{station.address}
+
{station.address}
{(station.gps_lat || station.gps_lon) && (
GPS: {station.gps_lat}, {station.gps_lon} @@ -582,38 +678,38 @@ export const FleetOrganization: React.FC = () => { {station.incharge_name && (
- Incharge: {station.incharge_name} + Incharge: {station.incharge_name}
)} {station.phone && (
- {station.phone} + {station.phone}
)}
-
-
-
{station.vehiclesAssigned ?? '—'}
-
Vehicles
+
+
+
{station.vehiclesAssigned ?? '—'}
+
Vehicles
-
-
{station.staffAssigned ?? '—'}
-
Staff
+
+
{station.staffAssigned ?? '—'}
+
Staff
-
- - + {/* */}
))} @@ -636,7 +738,258 @@ export const FleetOrganization: React.FC = () => { )} + {/* Station Manifest Modal */} + {createPortal( + + {selectedStationForManifest && ( + setSelectedStationForManifest(null)} + > + e.stopPropagation()} + > +
+
+

+ {selectedStationForManifest.name.toUpperCase()} MANIFEST +

+

+ 📍 {selectedStationForManifest.address} +

+
+ +
+ + + +
+
+ )} +
, + document.body + )} +
); }; + +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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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(); + 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 ( +
+ + FETCHING STATION ASSETS... +
+ ); + } + + if (error) { + return ( +
+ +
Error Loading Manifest
+
{error}
+
+ ); + } + + return ( +
+ {/* Vehicles Column */} +
+

+ Deployed Fleet ({vehicles.length}) +

+ + {vehicles.length === 0 ? ( +
+ No vehicles currently docked at this station. +
+ ) : ( +
+ {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 ( +
+
+
+
{v.registration_number}
+
{v.brand} {v.model} • {v.vehicle_type}
+
+ + {v.status} + +
+ + {(driverName || emtName) && ( +
+ {driverName && ( +
+ Pilot: + {driverName} +
+ )} + {emtName && ( +
+ EMT: + {emtName} +
+ )} +
+ )} +
+ ); + })} +
+ )} +
+ + {/* Staff Column */} +
+

+ Active Crew ({crewList.length}) +

+ + {crewList.length === 0 ? ( +
+ No crew members currently on duty. +
+ ) : ( +
+ {crewList.map((c, i) => ( +
+
+ +
+
+
{c.name}
+
{c.role} {c.phone ? `• ${c.phone}` : ''}
+
+
+ ))} +
+ )} +
+
+ ); +}; diff --git a/src/pages/fleet/FleetPendingRequests.tsx b/src/pages/fleet/FleetPendingRequests.tsx new file mode 100644 index 0000000..4b1e121 --- /dev/null +++ b/src/pages/fleet/FleetPendingRequests.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Filtering & Pagination states + const [statusFilter, setStatusFilter] = useState('PENDING'); + const [searchQuery, setSearchQuery] = useState(''); + const [isSearchFocused, setIsSearchFocused] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 8; + + // Inline loading when clicking action buttons + const [processingId, setProcessingId] = useState(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 ( +
+ {/* Top action bar */} +
+ + + {/* Search filter input */} +
+ + { + 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%' + }} + /> +
+ + {/* Status filters */} +
+ {[ + { 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 => ( + + ))} +
+
+ + + {loading ? ( +
+ + FETCHING TRANSACTION RECORDS... +
+ ) : error ? ( +
+ +

Synchronization Failed

+

{error}

+
+ ) : currentItems.length === 0 ? ( +
+ +

No Supply Requests

+

There are no restock requests matching this state filter right now.

+
+ ) : ( + + + + + + + + + + + + + {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 ( + + + + + + + + + ); + })} + +
Requested Supply ItemQuantity DemandedRequested ByRequest DateStatus BadgeActions
+
+ {req.item_master?.name || 'Unknown Item'} +
+
+ Category: {req.item_master?.category || 'General'} +
+
+
+ {req.quantity} Units +
+
+
+ {requestedUser} +
+
+
+ {new Date(req.createdAt || req.updatedAt).toLocaleDateString()} +
+
+ + {req.status} + + + {statusFilter === 'PENDING' && ( + + )} + {statusFilter === 'APPROVED' && ( + + )} + {statusFilter === 'COMPLETED' && ( +
+ TRANSACTION CLOSED +
+ )} +
+ )} + + {!loading && !error && filteredRequests.length > itemsPerPage && ( +
+
+ Showing {indexOfFirstItem + 1} to {Math.min(indexOfLastItem, filteredRequests.length)} of {filteredRequests.length} transactions +
+
+ +
+ {currentPage} / {totalPages} +
+ +
+
+ )} +
+
+ ); +}; diff --git a/src/pages/fleet/FleetPersonnel.tsx b/src/pages/fleet/FleetPersonnel.tsx index 07e0410..d1c0701 100644 --- a/src/pages/fleet/FleetPersonnel.tsx +++ b/src/pages/fleet/FleetPersonnel.tsx @@ -1,5 +1,5 @@ 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'; interface Staff { @@ -9,18 +9,17 @@ interface Staff { } const ROLE_COLORS: Record = { - DRIVER: { color: '#06B6D4', bg: 'rgba(6,182,212,0.12)' }, - EMT: { color: '#3B82F6', bg: 'rgba(59,130,246,0.12)' }, - DOCTOR: { color: '#10B981', bg: 'rgba(16,185,129,0.12)' }, + DRIVER: { color: '#06B6D4', bg: 'rgba(6,182,212,0.08)' }, + EMT: { color: '#3B82F6', bg: 'rgba(59,130,246,0.08)' }, + 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 = { - 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' }, }; const FILTERS = ['ALL', 'DRIVER', 'EMT', 'DOCTOR'] as const; -const PAGE_SIZE = 5; -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 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 norm = (s: Staff) => { const pd = s.professional_details; @@ -41,7 +40,9 @@ export const FleetPersonnel: React.FC = () => { const [fetchError, setFetchError] = useState(''); const [filter, setFilter] = useState('ALL'); const [search, setSearch] = useState(''); + const [isSearchFocused, setIsSearchFocused] = useState(false); const [page, setPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(5); const [selectedRaw, setSelectedRaw] = useState(null); const [showModal, setShowModal] = useState(false); const [submitting, setSubmitting] = useState(false); @@ -96,9 +97,9 @@ export const FleetPersonnel: React.FC = () => { 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 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 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 offDuty = normalized.filter(s => s.status === 'OFF_DUTY').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 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) => ( -
-
{label}
-
{value || '—'}
+
+
{label}
+
{value || '—'}
); return ( - - - + {/* Hero */} -
+
{ini}
-

{s.name}

+

{s.name}

{s.role} @@ -140,29 +141,29 @@ export const FleetPersonnel: React.FC = () => {
- + {/* Grid */}
- + {/* Contact */} -
+
📞 Contact
-
+
-
Phone
{s.phone}
+
Phone
{s.phone}
{s.email && ( -
+
-
Email
{s.email}
+
Email
{s.email}
)}
- + {/* Identity */} -
+
🪪 Identity
{row('Staff ID', s.id, true)} @@ -170,10 +171,10 @@ export const FleetPersonnel: React.FC = () => { {row('Joined Date', s.joinedDate)}
- + {/* Professional — full width */} {pd && ( -
+
🎓 Professional Details
{pd.qualification && row('Qualification', pd.qualification)} @@ -183,7 +184,7 @@ export const FleetPersonnel: React.FC = () => { {s.specialization && row('Specialization', s.specialization)} {s.certExpiry && (
-
+
Cert Expiry
{s.certExpiry}
@@ -206,17 +207,17 @@ export const FleetPersonnel: React.FC = () => { {showModal && ( { if (e.target === e.currentTarget) setShowModal(false); }} > {/* Modal Header */} -
- -

Register New Staff

-

Fill details based on staff type

+
+ +

Register New Staff

+

Fill details based on staff type

@@ -227,7 +228,7 @@ export const FleetPersonnel: React.FC = () => { const rc = ROLE_COLORS[t]; return ( ); @@ -241,17 +242,17 @@ export const FleetPersonnel: React.FC = () => {
{f.label}
)[f.k] as string} onChange={e => setF(f.k, e.target.value)} placeholder={f.ph} 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' }} />
))} {/* Password — same row as Aadhaar */}
-
Password (optional)
+
Password (optional)
setF('password', e.target.value)} placeholder="Min 8 chars" 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' }} /> -
@@ -260,14 +261,14 @@ export const FleetPersonnel: React.FC = () => { {/* DRIVER fields */} {staffType === 'DRIVER' && ( -
+
Driver Professional Details
{[{ 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 => (
{f.label}
)[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' }} />
))}
@@ -276,14 +277,14 @@ export const FleetPersonnel: React.FC = () => { {/* EMT fields */} {staffType === 'EMT' && ( -
+
EMT Professional Details
{[{ 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 => (
{f.label}
)[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' }} />
))}
@@ -292,17 +293,17 @@ export const FleetPersonnel: React.FC = () => { {/* DOCTOR fields */} {staffType === 'DOCTOR' && ( -
+
Doctor Professional Details
{[{ 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 => (
{f.label}
)[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' }} />
))} -