-
-
-
-
setMfaCode(e.target.value.replace(/\D/g, ''))}
- style={{ color: '#fff' }}
- required
- />
+
+
+ ) : (
+
-
-
- )}
-
-
- {showError && (
-
-
- {showError}
-
+
+
)}
-
-
-
- SECURE UPLINK ESTABLISHED
-
+
+ {showError && (
+
+
+
+ {showError}
+
+
+ )}
+
-
-
- BACK TO STANDARD LOGIN
-
-
-
- {/* Page-level status indicators */}
-
-
-
-
COMMS_STRENGTH
-
- {[1, 2, 3, 4].map(i =>
)}
-
+
+
+
e.currentTarget.style.color = '#22d3ee'} onMouseOut={(e) => e.currentTarget.style.color = '#94a3b8'}>
+ RETURN TO STANDARD PORTAL
+
-
- LIVE TELEMETRY SYNC
-
-
+
-
-
TERMINAL_ID: DISPATCH-X7
-
PROTOCOL: CS-SECURE-v4
-
ENCRYPTION: QUANTUM-SAFE
-
+ {/* Embedded Global Styles to avoid breaking any other page, since we are overriding locally */}
+
);
};
diff --git a/src/pages/FleetOperatorDashboard.tsx b/src/pages/FleetOperatorDashboard.tsx
index ecafa68..7e61749 100644
--- a/src/pages/FleetOperatorDashboard.tsx
+++ b/src/pages/FleetOperatorDashboard.tsx
@@ -1,339 +1,50 @@
-import React, { useState, useEffect, useMemo } from 'react';
+import React from 'react';
import { useSearchParams } from 'react-router-dom';
import {
- Activity,
- Truck,
- Zap,
- ShieldCheck,
- MapPin,
- Clock,
- Navigation,
- AlertTriangle,
- Fuel,
- Gauge,
- Thermometer,
- Wind,
- Bell,
- Settings,
- ChevronRight,
- LayoutGrid,
- Route as RouteIcon,
- Users,
- Search,
- CheckCircle2,
- ShoppingCart
+ Building2, Truck, Users, CalendarDays, ClipboardCheck,
+ ShoppingCart, Map, MapPin, Navigation, Link2, Activity,
+ Bell, Settings, Search
} from 'lucide-react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { Card, StatCard } from '../components/Common';
-import { fleetApi } from '../api/fleet';
-import { incidentsApi } from '../api/incidents';
-import type { Incident } from '../api/types';
-import {
- AreaChart,
- Area,
- Tooltip,
- ResponsiveContainer,
- BarChart,
- Bar,
- XAxis,
- YAxis
-} from 'recharts';
+import { motion } from 'framer-motion';
-// --- NEW FLEET MODULES ---
+import { FleetOrganization } from './fleet/FleetOrganization';
import { FleetAssets } from './fleet/FleetAssets';
import { FleetPersonnel } from './fleet/FleetPersonnel';
-import { FleetInventory } from './fleet/FleetInventory';
import { FleetScheduling } from './fleet/FleetScheduling';
+import { FleetInventory } from './fleet/FleetInventory';
-// --- MOCK DATA FOR THE ENHANCED FEEL ---
-const MOCK_VEHICLES = [
- { id: 'V001', number: 'TN-01-AM-1024', status: 'EN_ROUTE', speed: 45, fuel: 82, lat: 13.0827, lng: 80.2707, type: 'ALS' },
- { id: 'V002', number: 'TN-05-AM-5521', status: 'IDLE', speed: 0, fuel: 65, lat: 13.0067, lng: 80.2575, type: 'BLS' },
- { id: 'V003', number: 'TN-07-AM-1122', status: 'TRANSPORTING', speed: 52, fuel: 45, lat: 12.9667, lng: 80.2475, type: 'TRANSFER' },
- { id: 'V004', number: 'TN-09-AM-9988', status: 'AT_SCENE', speed: 0, fuel: 78, lat: 12.9941, lng: 80.1709, type: 'AIR' },
+const SCOPE_MODULES = [
+ { id: 'overview', label: 'Live Dashboard', icon: Map, desc: 'Real-time fleet tracking & telemetry' },
+ { id: 'organization', label: 'Stations', icon: Building2, desc: 'Manage stations and profiles' },
+ { id: 'assets', label: 'Vehicle Management', icon: Truck, desc: 'Registration, Docs, Services' },
+ { id: 'personnel', label: 'Staff Management', icon: Users, desc: 'Pilots, EMTs, Doctors' },
+ { id: 'scheduling', label: 'Crew Scheduling', icon: CalendarDays, desc: 'Shift assignments & conflicts' },
+ { id: 'attendance', label: 'Attendance & Duty', icon: ClipboardCheck, desc: 'Daily attendance & payroll' },
+ { id: 'inventory', label: 'Inventory Management', icon: ShoppingCart, desc: 'Stock levels & consumption' },
+ { 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' }
];
-const PERFORMANCE_DATA = [
- { time: '08:00', trips: 12, response: 14 },
- { time: '10:00', trips: 18, response: 12 },
- { time: '12:00', trips: 25, response: 18 },
- { time: '14:00', trips: 22, response: 15 },
- { time: '16:00', trips: 30, response: 22 },
- { time: '18:00', trips: 28, response: 19 },
-];
-
-// --- LIVE MAP COMPONENT ---
-const CommandMap: React.FC<{ vehicles: any[] }> = ({ vehicles }) => {
- const [L, setL] = useState
(null);
- const mapRef = React.useRef(null);
-
- useEffect(() => {
- if (typeof window === 'undefined') return;
-
- const loadLeaflet = () => {
- const leaflet = (window as any).L;
- if (leaflet) {
- setL(leaflet);
- if (!mapRef.current) {
- const m = leaflet.map('fleet-command-map', {
- zoomControl: false,
- attributionControl: false
- }).setView([13.0827, 80.2707], 12);
-
- leaflet.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
- maxZoom: 20
- }).addTo(m);
-
- mapRef.current = m;
- }
- }
- };
-
- if (!(window as any).L) {
- const link = document.createElement('link');
- link.rel = 'stylesheet';
- link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
- document.head.appendChild(link);
-
- const script = document.createElement('script');
- script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
- script.async = true;
- script.onload = loadLeaflet;
- document.head.appendChild(script);
- } else {
- loadLeaflet();
- }
-
- return () => {
- if (mapRef.current) {
- mapRef.current.remove();
- mapRef.current = null;
- }
- };
- }, []);
-
- useEffect(() => {
- if (!L || !mapRef.current) return;
- const map = mapRef.current;
-
- vehicles.forEach(v => {
- const getStatusColor = (status: string) => {
- switch (status) {
- case 'IDLE': return '#94A3B8';
- case 'EN_ROUTE': return '#3B82F6';
- case 'AT_SCENE': return '#F59E0B';
- case 'TRANSPORTING': return '#EF4444';
- case 'AT_HOSPITAL': return '#A855F7';
- case 'BREAKDOWN': return '#000000';
- case 'OFF_DUTY': return '#FFFFFF';
- default: return '#3B82F6';
- }
- };
- const color = getStatusColor(v.status);
- const icon = L.divIcon({
- className: 'custom-marker',
- html: ``,
- iconSize: [24, 24],
- iconAnchor: [12, 24]
- });
- L.marker([v.lat, v.lng], { icon }).addTo(map).bindPopup(`${v.number}
Status: ${v.status}`);
- });
- }, [L, vehicles]);
-
- return ;
-};
+const PlaceholderModule: React.FC<{ label: string; icon: React.ElementType }> = ({ label, icon: Icon }) => (
+
+
+
{label}
+
This module is under active development.
+
+);
export const FleetOperatorDashboard: React.FC = () => {
const [searchParams] = useSearchParams();
- const activeTab = searchParams.get('tab') || 'overview';
-
- const [isLoading, setIsLoading] = useState(true);
- const [incidents, setIncidents] = useState([]);
- const [vehicles, setVehicles] = useState(MOCK_VEHICLES);
+ const activeModule = searchParams.get('tab') || 'overview';
- useEffect(() => {
- const fetchData = async () => {
- try {
- const token = localStorage.getItem('teleems_token') || '';
- const res = await incidentsApi.getIncidents({}, token);
- if (res && res.data) setIncidents(res.data.slice(0, 5));
- } catch (err) {
- console.error(err);
- } finally {
- setIsLoading(false);
- }
- };
- fetchData();
- }, []);
+ const activeModuleInfo = SCOPE_MODULES.find(m => m.id === activeModule);
- const menuItems = [
- { id: 'overview', label: 'Command Center', icon: LayoutGrid },
- { id: 'assets', label: 'Fleet Assets', icon: Truck },
- { id: 'personnel', label: 'Personnel Hub', icon: Users },
- { id: 'scheduling', label: 'Mission Control', icon: Navigation },
- { id: 'inventory', label: 'Supply Chain', icon: ShoppingCart },
- { id: 'analytics', label: 'Fleet Intel', icon: Activity },
- ];
-
- const renderContent = () => {
- switch (activeTab) {
- case 'overview':
- return (
-
- {/* Stats Grid */}
-
-
-
-
-
-
-
-
- {/* Left Column: Map and Fleet List */}
-
-
-
-
-
-
-
-
-
-
-
- | Vehicle |
- Status |
- Speed |
- Fuel |
- Actions |
-
-
-
- {vehicles.map(v => (
-
- |
- {v.number}
- {v.type} UNIT
- |
-
- {v.status}
- |
- {v.speed} km/h |
-
-
- |
-
-
- |
-
- ))}
-
-
-
-
-
-
- {/* Right Column: Performance and Incidents */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {incidents.length > 0 ? incidents.map(inc => (
-
-
- #{inc.id.split('-').pop()?.toUpperCase()}
- {inc.severity}
-
-
{inc.category}
-
- {inc.address}
-
-
- )) : (
-
No active incidents
- )}
-
-
-
-
-
-
-
-
-
-
94%
-
FUEL READINESS
-
-
-
-
-
-
- );
+ const renderModuleContent = () => {
+ switch (activeModule) {
+ case 'organization':
+ return ;
case 'assets':
return ;
case 'personnel':
@@ -342,53 +53,95 @@ export const FleetOperatorDashboard: React.FC = () => {
return ;
case 'inventory':
return ;
+ case 'overview':
+ return ;
+ case 'attendance':
+ return ;
+ case 'trips':
+ return ;
+ case 'telematics':
+ return ;
+ case 'referrals':
+ return ;
case 'analytics':
- return Fleet Intelligence Reports Loading...
;
+ return ;
default:
- return null;
+ return ;
}
};
return (
-
- {/* Main Content Area */}
-
- {/* Header */}
-
-
-
- {menuItems.find(m => m.id === activeTab)?.label || 'Fleet Command'}
-
-
-
-
Strategic Operations • Live Platform Telemetry
-
-
-
-
-
-
Tactical Time
-
{new Date().toLocaleTimeString()}
-
-
-
-
-
+
+
+ {activeModuleInfo?.label || 'Fleet Command'}
+
+
+
+ {activeModuleInfo?.desc || 'Secure Connection • TeleEMS Fleet'}
- {renderContent()}
+
+
+
+
+
+
+
+
+
+
+
Station Incharge
+
Fleet Operator
+
+
+
+
+
+
+
+
+ {/* Scrollable Content */}
+
+
+ {renderModuleContent()}
+
);
diff --git a/src/pages/fleet/FleetAssets.tsx b/src/pages/fleet/FleetAssets.tsx
index 8056772..35b213d 100644
--- a/src/pages/fleet/FleetAssets.tsx
+++ b/src/pages/fleet/FleetAssets.tsx
@@ -1,207 +1,607 @@
-import React, { useState } from 'react';
-import {
- Plus,
- Search,
- Filter,
- Truck,
- FileText,
- Wrench,
- Calendar,
- AlertTriangle,
- ExternalLink,
- ChevronRight,
- ShieldCheck,
- Fuel,
- Gauge
+import React, { useState, useEffect, useCallback } from 'react';
+import {
+ Plus, Search, Truck, X, Loader2, CheckCircle,
+ AlertCircle, ChevronDown, Edit2
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
-import { Card } from '../../components/Common';
+
+interface Station {
+ id: string;
+ name: string;
+}
interface Vehicle {
id: string;
- number: string;
- type: 'ALS' | 'BLS' | 'TRANSPORT';
- model: string;
- station: string;
- status: 'ACTIVE' | 'MAINTENANCE' | 'BREAKDOWN' | 'OFF_DUTY';
- docs: {
- rc: string;
- fc: string;
- insurance: string;
- permit: string;
- };
- lastService: string;
- nextService: string;
- fuel: number;
+ registration_number: string;
+ vehicle_type: string;
+ brand?: string;
+ model?: string;
+ chassis_number?: string;
+ station_id?: string;
+ status?: string;
}
-const MOCK_FLEET: Vehicle[] = [
- { id: 'V-001', number: 'KA 01 MG 2341', type: 'ALS', model: 'Force Traveller 2024', station: 'ALPHA-NODE-01', status: 'ACTIVE', docs: { rc: 'VALID', fc: 'EXPIRING_SOON', insurance: 'VALID', permit: 'VALID' }, lastService: '2026-04-10', nextService: '2026-07-10', fuel: 85 },
- { id: 'V-002', number: 'KA 51 BH 9921', type: 'BLS', model: 'Tata Winger 2023', station: 'BETA-HUB-04', status: 'MAINTENANCE', docs: { rc: 'VALID', fc: 'VALID', insurance: 'VALID', permit: 'VALID' }, lastService: '2026-05-01', nextService: '2026-08-01', fuel: 42 },
- { id: 'V-003', number: 'KA 03 AA 1122', type: 'ALS', model: 'Force Traveller 2023', station: 'ALPHA-NODE-01', status: 'BREAKDOWN', docs: { rc: 'VALID', fc: 'VALID', insurance: 'EXPIRING_SOON', permit: 'VALID' }, lastService: '2026-02-15', nextService: '2026-05-15', fuel: 0 },
- { id: 'V-004', number: 'KA 05 MN 5678', type: 'TRANSPORT', model: 'Maruti Eeco 2022', station: 'GAMMA-STATION-02', status: 'ACTIVE', docs: { rc: 'VALID', fc: 'VALID', insurance: 'VALID', permit: 'VALID' }, lastService: '2026-03-20', nextService: '2026-06-20', fuel: 92 },
-];
+interface VehicleForm {
+ registration_number: string;
+ vehicle_type: string;
+ chassis_number: string;
+ brand: string;
+ model: string;
+ station_id: string;
+}
+
+const VEHICLE_TYPES = ['ALS', 'BLS', 'TRANSPORT', 'ICU', 'NEONATAL'];
+
+const EMPTY_FORM: VehicleForm = {
+ registration_number: '',
+ vehicle_type: 'ALS',
+ chassis_number: '',
+ brand: '',
+ model: '',
+ station_id: '',
+};
+
+const cardStyle: React.CSSProperties = {
+ background: 'rgba(255,255,255,0.03)',
+ border: '1px solid rgba(255,255,255,0.08)',
+ borderRadius: '16px',
+ padding: '24px',
+};
+
+const labelStyle: React.CSSProperties = {
+ fontSize: '0.7rem',
+ color: '#64748B',
+ textTransform: 'uppercase',
+ letterSpacing: '1px',
+ marginBottom: '6px',
+ display: 'block',
+};
+
+const inputStyle: React.CSSProperties = {
+ background: 'rgba(255,255,255,0.04)',
+ border: '1px solid rgba(255,255,255,0.08)',
+ padding: '11px 14px',
+ borderRadius: '10px',
+ color: '#fff',
+ fontSize: '0.875rem',
+ outline: 'none',
+ width: '100%',
+ boxSizing: 'border-box',
+ transition: 'border-color 0.2s',
+};
+
+const selectStyle: React.CSSProperties = {
+ ...inputStyle,
+ cursor: 'pointer',
+ appearance: 'none',
+ WebkitAppearance: 'none',
+};
export const FleetAssets: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
- const [selectedVehicle, setSelectedVehicle] = useState
(null);
+ const [vehicles, setVehicles] = useState([]);
+ const [stations, setStations] = useState([]);
+ const [stationsLoading, setStationsLoading] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [showModal, setShowModal] = useState(false);
+ const [editingVehicle, setEditingVehicle] = useState(null);
+ const [showEditModal, setShowEditModal] = useState(false);
+ const [form, setForm] = useState(EMPTY_FORM);
+ const [editForm, setEditForm] = useState(EMPTY_FORM);
+ const [submitting, setSubmitting] = useState(false);
+ const [toast, setToast] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
+
+ const token = localStorage.getItem('teleems_token') || '';
+
+ const showToast = (type: 'success' | 'error', message: string) => {
+ setToast({ type, message });
+ setTimeout(() => setToast(null), 4000);
+ };
+
+ // Fetch stations for the dropdown
+ const fetchStations = useCallback(async () => {
+ setStationsLoading(true);
+ try {
+ const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/stations', {
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
+ });
+ const json = await res.json();
+ let list: Station[] = [];
+ if (json?.data?.data && Array.isArray(json.data.data)) list = json.data.data;
+ else if (json?.data && Array.isArray(json.data)) list = json.data;
+ else if (Array.isArray(json)) list = json;
+ setStations(list);
+ } catch (e) {
+ console.error('Failed to fetch stations:', e);
+ } finally {
+ setStationsLoading(false);
+ }
+ }, [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', {
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
+ });
+ const json = await res.json();
+ let list: Vehicle[] = [];
+ if (json?.data?.data && Array.isArray(json.data.data)) list = json.data.data;
+ else if (json?.data && Array.isArray(json.data)) list = json.data;
+ else if (Array.isArray(json)) list = json;
+ setVehicles(list);
+ } catch (e) {
+ console.error('Failed to fetch vehicles:', e);
+ } finally {
+ setLoading(false);
+ }
+ }, [token]);
+
+ useEffect(() => { fetchVehicles(); fetchStations(); }, [fetchVehicles, fetchStations]);
+
+ const openModal = () => {
+ setForm(EMPTY_FORM);
+ setShowModal(true);
+ };
+
+ const openEditModal = (v: Vehicle) => {
+ setEditingVehicle(v);
+ setEditForm({
+ registration_number: v.registration_number || '',
+ vehicle_type: v.vehicle_type || 'ALS',
+ chassis_number: v.chassis_number || '',
+ brand: v.brand || '',
+ model: v.model || '',
+ station_id: v.station_id || '',
+ });
+ setShowEditModal(true);
+ };
+
+ const handleUpdate = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!editingVehicle) return;
+ setSubmitting(true);
+ try {
+ const payload = {
+ registration_number: editForm.registration_number,
+ vehicle_type: editForm.vehicle_type,
+ station_id: editForm.station_id || undefined,
+ chassis_number: editForm.chassis_number || undefined,
+ brand: editForm.brand,
+ model: editForm.model,
+ };
+ const res = await fetch(
+ `https://teleems-api-gateway.onrender.com/v1/fleet/vehicles/${editingVehicle.id}`,
+ {
+ method: 'PATCH',
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ }
+ );
+ 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);
+ setEditingVehicle(null);
+ fetchVehicles();
+ } else {
+ showToast('error', json?.message || `Error ${res.status}: Failed to update vehicle.`);
+ }
+ } catch (err: any) {
+ showToast('error', err?.message || 'Network error.');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!form.station_id) {
+ showToast('error', 'Please select a station.');
+ return;
+ }
+ setSubmitting(true);
+ try {
+ const payload = {
+ registration_number: form.registration_number,
+ vehicle_type: form.vehicle_type,
+ station_id: form.station_id,
+ chassis_number: form.chassis_number || undefined,
+ brand: form.brand,
+ model: form.model,
+ };
+
+ const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/vehicles', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ });
+
+ 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);
+ setForm(EMPTY_FORM);
+ fetchVehicles();
+ } else {
+ showToast('error', json?.message || `Error ${res.status}: Failed to register vehicle.`);
+ }
+ } catch (err: any) {
+ showToast('error', err?.message || 'Network error. Please check your connection.');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const filtered = vehicles.filter(v =>
+ v.registration_number?.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ v.brand?.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ v.model?.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+
+ const getStatusColor = (status?: string) => {
+ if (status === 'ACTIVE') return { bg: 'rgba(34,197,94,0.1)', border: 'rgba(34,197,94,0.2)', color: '#22C55E' };
+ if (status === 'BREAKDOWN') return { bg: 'rgba(239,68,68,0.1)', border: 'rgba(239,68,68,0.2)', color: '#EF4444' };
+ return { bg: 'rgba(245,158,11,0.1)', border: 'rgba(245,158,11,0.2)', color: '#F59E0B' };
+ };
return (
-
+
+
+ {/* Toast */}
+
+ {toast && (
+
+ {toast.type === 'success' ? : }
+ {toast.message}
+
+ )}
+
+
+ {/* Register Vehicle Modal */}
+
+ {showModal && (
+ { if (e.target === e.currentTarget) setShowModal(false); }}
+ >
+
+ {/* Modal Header */}
+
+
+
Register New Vehicle
+
Add an ambulance to your fleet
+
+
+
+
+
+
+
+ )}
+
+
+ {/* Edit Vehicle Modal */}
+
+ {showEditModal && editingVehicle && (
+ { if (e.target === e.currentTarget) setShowEditModal(false); }}
+ >
+
+
+
+
Edit Vehicle
+
Update details for {editingVehicle.registration_number}
+
+
+
+
+
+
+
+ )}
+
+
+ {/* Header */}
-
-
-
- setSearchQuery(e.target.value)}
- />
-
-
+
+
+ setSearchQuery(e.target.value)}
+ />
-
-
- {/* Fleet Inventory Grid */}
-
-
- {MOCK_FLEET.map((v) => (
-
setSelectedVehicle(v)}
- style={{
- cursor: 'pointer',
- border: selectedVehicle?.id === v.id ? '2px solid var(--accent-cyan)' : '1px solid var(--card-border)',
- background: selectedVehicle?.id === v.id ? 'rgba(59, 130, 246, 0.05)' : 'rgba(15, 23, 42, 0.4)'
- }}
+ {/* Loading */}
+ {loading && (
+
+
+ Loading vehicles...
+
+ )}
+
+ {/* Empty state */}
+ {!loading && filtered.length === 0 && (
+
+
+
No Vehicles Registered
+
Click "Register New Vehicle" to add your first ambulance.
+
+ )}
+
+ {/* Vehicle Cards */}
+ {!loading && filtered.length > 0 && (
+
+ {filtered.map(v => {
+ const sc = getStatusColor(v.status);
+ 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'; }}
>
+
-
{v.type} UNIT • {v.id}
-
{v.number}
-
{v.model}
-
-
- {v.status}
+
{v.vehicle_type} UNIT
+
{v.registration_number}
+ {(v.brand || v.model) && (
+
{v.brand} {v.model}
+ )}
+ {v.status && (
+
+ {v.status}
+
+ )}
-
-
-
Fuel Level
-
+ {v.chassis_number && (
+
+ CH: {v.chassis_number}
-
+ )}
-
-
- ))}
-
-
-
- {/* Detailed Inspector Panel */}
-
- {selectedVehicle ? (
-
-
-
-
- {/* Critical Document Status */}
-
-
- Document Vault
-
-
- {[
- { label: 'Registration (RC)', status: selectedVehicle.docs.rc, expiry: '2030-12-15' },
- { label: 'Fitness (FC)', status: selectedVehicle.docs.fc, expiry: '2026-06-01' },
- { label: 'Insurance Policy', status: selectedVehicle.docs.insurance, expiry: '2026-05-15' },
- { label: 'Ambulance Permit', status: selectedVehicle.docs.permit, expiry: '2026-09-20' },
- ].map((doc, idx) => (
-
-
-
{doc.label}
-
Expires: {doc.expiry}
-
-
- {doc.status === 'VALID' ?
:
}
-
{doc.status}
-
-
- ))}
-
-
-
- {/* Maintenance History */}
-
-
- Service Records
-
-
-
-
- Upcoming Service
- {selectedVehicle.nextService}
-
-
Scheduled for: Engine Oil change, Brake pad inspection, and AC filter cleaning.
-
-
-
Last Major Service
-
{selectedVehicle.lastService}
-
-
-
-
-
- VIEW ALL RECORDS
- LOG MAINTENANCE
-
-
-
+ { e.stopPropagation(); openEditModal(v); }}
+ style={{ width: '100%', padding: '9px', background: 'rgba(6,182,212,0.08)', border: '1px solid rgba(6,182,212,0.2)', borderRadius: '8px', color: '#06B6D4', fontSize: '0.75rem', fontWeight: 600, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', marginTop: '4px' }}
+ >
+ EDIT VEHICLE
+
-
- ) : (
-
-
-
-
-
No Asset Selected
-
Select a vehicle from the fleet inventory to view tactical diagnostics, documents, and service history.
-
- )}
+ );
+ })}
-
+ )}
+
+
);
};
diff --git a/src/pages/fleet/FleetOrganization.tsx b/src/pages/fleet/FleetOrganization.tsx
new file mode 100644
index 0000000..a13f39e
--- /dev/null
+++ b/src/pages/fleet/FleetOrganization.tsx
@@ -0,0 +1,642 @@
+import React, { useState, useEffect, useCallback, useRef } from 'react';
+import { createPortal } from 'react-dom';
+import {
+ Building2, MapPin, Phone, Plus, Search, MoreVertical,
+ Edit, Users, Truck, ShieldCheck, Globe, X, Loader2,
+ CheckCircle, AlertCircle, Navigation
+} from 'lucide-react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { fleetApi } from '../../api/fleet';
+import L from 'leaflet';
+import 'leaflet/dist/leaflet.css';
+
+// Fix default marker icons for Leaflet bundled via Vite
+delete (L.Icon.Default.prototype as any)._getIconUrl;
+L.Icon.Default.mergeOptions({
+ iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
+ iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
+ shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
+});
+
+interface Station {
+ id: string;
+ name: string;
+ address: string;
+ gps_lat?: number;
+ gps_lon?: number;
+ incharge_name?: string;
+ phone?: string;
+ vehiclesAssigned?: number;
+ staffAssigned?: number;
+ status?: 'ACTIVE' | 'INACTIVE';
+}
+
+interface StationForm {
+ name: string;
+ address: string;
+ incharge_name: string;
+ phone: string;
+}
+
+const cardStyle: React.CSSProperties = {
+ background: 'rgba(255,255,255,0.03)',
+ border: '1px solid rgba(255,255,255,0.08)',
+ borderRadius: '16px',
+ padding: '24px',
+};
+
+const labelStyle: React.CSSProperties = {
+ fontSize: '0.7rem',
+ color: '#64748B',
+ textTransform: 'uppercase',
+ letterSpacing: '1px',
+ marginBottom: '6px',
+ display: 'block',
+};
+
+const inputStyle: React.CSSProperties = {
+ background: 'rgba(255,255,255,0.04)',
+ border: '1px solid rgba(255,255,255,0.08)',
+ padding: '11px 14px',
+ borderRadius: '10px',
+ color: '#fff',
+ fontSize: '0.875rem',
+ outline: 'none',
+ width: '100%',
+ boxSizing: 'border-box',
+ transition: 'border-color 0.2s',
+};
+
+const EMPTY_FORM: StationForm = {
+ name: '',
+ address: '',
+ incharge_name: '',
+ phone: '',
+};
+
+export const FleetOrganization: React.FC = () => {
+ const [activeTab, setActiveTab] = useState<'stations' | 'profile'>('stations');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [stations, setStations] = useState
([]);
+ const [loading, setLoading] = useState(false);
+ const [showModal, setShowModal] = useState(false);
+ const [editingStationId, setEditingStationId] = useState(null);
+ const [loadingDetailsId, setLoadingDetailsId] = useState(null);
+ const [form, setForm] = useState(EMPTY_FORM);
+ const [gpsCoords, setGpsCoords] = useState<{ lat: number; lon: number } | null>(null);
+ const [gpsLoading, setGpsLoading] = useState(false);
+ const [submitting, setSubmitting] = useState(false);
+ const [fetchError, setFetchError] = useState('');
+ const [toast, setToast] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
+ const mapContainerRef = useRef(null);
+ const mapInstanceRef = useRef(null);
+ const markerRef = useRef(null);
+
+ const token = localStorage.getItem('teleems_token') || '';
+
+ // Get organisationId from stored user
+ const user = (() => {
+ try { return JSON.parse(localStorage.getItem('teleems_user') || '{}'); } catch { return {}; }
+ })();
+ const organisationId: string = user?.metadata?.organisation?.id || user?.organisationId || '';
+
+ const fetchStations = useCallback(async () => {
+ setLoading(true);
+ setFetchError('');
+ try {
+ const authToken = localStorage.getItem('teleems_token') || '';
+ if (!authToken) {
+ setFetchError('Session expired — no auth token found. Please log out and log in again.');
+ return;
+ }
+
+ const url = 'https://teleems-api-gateway.onrender.com/v1/fleet/stations';
+ console.log('[Stations] Calling:', url, '| token starts with:', authToken.substring(0, 15));
+
+ const res = await fetch(url, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${authToken}`,
+ },
+ });
+
+ console.log('[Stations] HTTP status:', res.status);
+ const json = await res.json();
+ console.log('[Stations] Response:', json);
+
+ if (res.status === 401 || res.status === 403) {
+ setFetchError(`Session expired (${res.status}). Please log out and log in again to refresh your token.`);
+ return;
+ }
+
+ if (!res.ok) {
+ setFetchError(`API error ${res.status}: ${json?.message || 'Unknown error'}`);
+ return;
+ }
+
+ let list: Station[] = [];
+ if (json?.data?.data && Array.isArray(json.data.data)) list = json.data.data;
+ else if (json?.data && Array.isArray(json.data)) list = json.data;
+ else if (Array.isArray(json)) list = json;
+
+ setStations(list);
+ } catch (e) {
+ setFetchError('Network error — unable to reach the server. Check your connection.');
+ console.error('Failed to fetch stations:', e);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => { fetchStations(); }, [fetchStations]);
+
+ // Refetch when user navigates back to the tab
+ useEffect(() => {
+ const onVisible = () => { if (document.visibilityState === 'visible') fetchStations(); };
+ document.addEventListener('visibilitychange', onVisible);
+ return () => document.removeEventListener('visibilitychange', onVisible);
+ }, [fetchStations]);
+
+
+
+
+
+ // Initialize Leaflet map when modal opens
+ useEffect(() => {
+ if (!showModal) {
+ // Destroy map on close
+ if (mapInstanceRef.current) {
+ mapInstanceRef.current.remove();
+ mapInstanceRef.current = null;
+ markerRef.current = null;
+ }
+ return;
+ }
+ // Wait for DOM
+ const timer = setTimeout(() => {
+ if (!mapContainerRef.current || mapInstanceRef.current) return;
+ 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'
+ }).addTo(map);
+
+ if (gpsCoords) {
+ markerRef.current = L.marker([initialLat, initialLon]).addTo(map)
+ .bindPopup(`📍 ${initialLat.toFixed(6)}, ${initialLon.toFixed(6)}`).openPopup();
+ }
+
+ map.on('click', (e: L.LeafletMouseEvent) => {
+ const { lat, lng } = e.latlng;
+ setGpsCoords({ lat, lon: lng });
+ if (markerRef.current) {
+ markerRef.current.setLatLng([lat, lng]);
+ } else {
+ markerRef.current = L.marker([lat, lng]).addTo(map)
+ .bindPopup(`📍 ${lat.toFixed(6)}, ${lng.toFixed(6)}`).openPopup();
+ }
+ });
+
+ mapInstanceRef.current = map;
+ }, 200);
+ return () => clearTimeout(timer);
+ }, [showModal, gpsCoords]);
+
+ const flyToCurrentLocation = () => {
+ setGpsLoading(true);
+ if (!navigator.geolocation) { setGpsLoading(false); return; }
+ navigator.geolocation.getCurrentPosition(
+ (pos) => {
+ const { latitude: lat, longitude: lon } = pos.coords;
+ setGpsCoords({ lat, lon });
+ setGpsLoading(false);
+ if (mapInstanceRef.current) {
+ mapInstanceRef.current.flyTo([lat, lon], 15);
+ if (markerRef.current) {
+ markerRef.current.setLatLng([lat, lon]);
+ } else {
+ markerRef.current = L.marker([lat, lon]).addTo(mapInstanceRef.current!)
+ .bindPopup(`📍 ${lat.toFixed(6)}, ${lon.toFixed(6)}`).openPopup();
+ }
+ }
+ },
+ () => setGpsLoading(false),
+ { enableHighAccuracy: true, timeout: 10000 }
+ );
+ };
+
+ const openCreateModal = () => {
+ setEditingStationId(null);
+ setForm(EMPTY_FORM);
+ setGpsCoords(null);
+ setShowModal(true);
+ };
+
+ const openEditModal = async (station: Station) => {
+ setLoadingDetailsId(station.id);
+ 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,
+ address: details.address || station.address,
+ 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 {
+ setGpsCoords(null);
+ }
+ setShowModal(true);
+ } catch (err: any) {
+ console.error('Failed to fetch station details, falling back to card data:', err);
+ // Fallback to local card data so the UI doesn't break
+ setEditingStationId(station.id);
+ setForm({
+ name: station.name,
+ address: station.address,
+ incharge_name: station.incharge_name || '',
+ phone: station.phone || '',
+ });
+ if (station.gps_lat && station.gps_lon) {
+ setGpsCoords({ lat: Number(station.gps_lat), lon: Number(station.gps_lon) });
+ } else {
+ setGpsCoords(null);
+ }
+ setShowModal(true);
+ } finally {
+ setLoadingDetailsId(null);
+ }
+ };
+
+ const showToast = (type: 'success' | 'error', message: string) => {
+ setToast({ type, message });
+ setTimeout(() => setToast(null), 4000);
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setSubmitting(true);
+ try {
+ const payload = {
+ name: form.name,
+ address: form.address,
+ gps_lat: gpsCoords?.lat ?? 0,
+ gps_lon: gpsCoords?.lon ?? 0,
+ incharge_name: form.incharge_name,
+ phone: form.phone,
+ };
+
+ let res;
+ if (editingStationId) {
+ res = await fleetApi.updateStation(editingStationId, payload, token);
+ } else {
+ res = await fleetApi.createStation(payload, token);
+ }
+
+ if (res && (res.status === 200 || res.status === 201 || res.id || res.success || res.data)) {
+ showToast('success', editingStationId ? `Station "${form.name}" updated successfully!` : `Station "${form.name}" created successfully!`);
+ setShowModal(false);
+ setForm(EMPTY_FORM);
+ setGpsCoords(null);
+ setEditingStationId(null);
+ fetchStations();
+ } else {
+ showToast('error', res?.message || `Failed to ${editingStationId ? 'update' : 'create'} station. Please try again.`);
+ }
+ } catch (err: any) {
+ showToast('error', err?.message || 'Network error. Please check your connection.');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const filteredStations = stations.filter(s =>
+ s.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ s.incharge_name?.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+
+ return (
+
+ {/* Toast Notification */}
+
+ {toast && (
+
+ {toast.type === 'success' ? : }
+ {toast.message}
+
+ )}
+
+
+ {/* Add Station Modal */}
+ {createPortal(
+
+ {showModal && (
+ { if (e.target === e.currentTarget) setShowModal(false); }}
+ >
+
+
+
+
{editingStationId ? 'Edit Station' : 'Add New Station'}
+
{editingStationId ? 'Update dispatch station details' : 'Create a new dispatch station'}
+
+
setShowModal(false)} style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '8px', padding: '8px', color: '#94A3B8', cursor: 'pointer', display: 'flex' }}>
+
+
+
+
+
+
+
+ )}
+ ,
+ document.body
+ )}
+
+ {/* Tab Toggle */}
+
+ {(['stations', 'profile'] as const).map(tab => (
+ setActiveTab(tab)} style={{ padding: '10px 24px', borderRadius: '10px', border: 'none', background: activeTab === tab ? 'linear-gradient(135deg, #06B6D4, #3B82F6)' : 'transparent', color: activeTab === tab ? '#fff' : '#64748B', fontWeight: activeTab === tab ? 700 : 500, fontSize: '0.875rem', cursor: 'pointer', transition: 'all 0.3s ease', display: 'flex', alignItems: 'center', gap: '8px', boxShadow: activeTab === tab ? '0 4px 12px rgba(6,182,212,0.3)' : 'none' }}>
+ {tab === 'stations' ? <> Station Management> : <> Organisation Profile>}
+
+ ))}
+
+
+ {activeTab === 'profile' ? (
+
+
+
+
Fleet Operator Profile
+
+
+ SAVE CHANGES
+
+
+
+
+
+ {(user?.name || 'FO').split(' ').map((n: string) => n[0]).join('').toUpperCase().substring(0, 2)}
+
+
{user?.name || 'Fleet Operator'}
+
{user?.email || ''}
+
+
+ {user?.status || 'ACTIVE'}
+
+
+ ORG: {user?.organisationId || organisationId || '—'}
+
+
+
+
+
Quick Stats
+ {[{ icon: MapPin, label: 'Active Stations', value: stations.length || 0 }, { icon: Truck, label: 'Total Vehicles', value: 25 }, { icon: Users, label: 'Total Staff', value: 71 }].map(({ icon: Icon, label, value }) => (
+
+ ))}
+
+
+
+
+ ) : (
+
+
+
+
+ setSearchQuery(e.target.value)}
+ style={{ background: 'transparent', border: 'none', color: '#fff', fontSize: '0.875rem', outline: 'none', width: '220px' }} />
+
+
+ ADD NEW STATION
+
+
+
+ {/* Loading state */}
+ {loading && (
+
+
+ Loading stations...
+
+ )}
+
+ {/* Error state */}
+ {!loading && fetchError && (
+
+
+
Failed to Load Stations
+
{fetchError}
+
+ ↺ Retry
+
+
+ )}
+
+ {/* Empty state */}
+ {!loading && !fetchError && filteredStations.length === 0 && (
+
+
+
No Stations Found
+
Click "Add New Station" to create your first dispatch station.
+
+ )}
+
+ {/* Station Cards */}
+ {!loading && (
+
+ {filteredStations.map(station => (
+
{ (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'; }}
+ >
+
+
+
+
{station.name}
+
+ {station.id?.substring(0, 8)}...
+
+
+
+
+
+
+
+
+
{station.address}
+ {(station.gps_lat || station.gps_lon) && (
+
+ GPS: {station.gps_lat}, {station.gps_lon}
+
+ )}
+
+
+ {station.incharge_name && (
+
+
+ Incharge: {station.incharge_name}
+
+ )}
+ {station.phone && (
+
+ )}
+
+
+
+
{station.vehiclesAssigned ?? '—'}
+
Vehicles
+
+
+
{station.staffAssigned ?? '—'}
+
Staff
+
+
+
+ openEditModal(station)}
+ disabled={loadingDetailsId !== null}
+ style={{
+ flex: 1, padding: '9px',
+ background: 'rgba(255,255,255,0.04)',
+ border: '1px solid rgba(255,255,255,0.08)',
+ borderRadius: '8px',
+ color: loadingDetailsId === station.id ? '#06B6D4' : '#94A3B8',
+ fontSize: '0.75rem', fontWeight: 600,
+ cursor: loadingDetailsId !== null ? 'not-allowed' : 'pointer',
+ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px',
+ transition: 'all 0.2s'
+ }}
+ >
+ {loadingDetailsId === station.id ? (
+
+ ) : (
+
+ )}
+ {loadingDetailsId === station.id ? 'FETCHING...' : 'EDIT'}
+
+
+ ASSIGN ASSETS
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+
+
+ );
+};
diff --git a/src/pages/fleet/FleetPersonnel.tsx b/src/pages/fleet/FleetPersonnel.tsx
index 711c323..07e0410 100644
--- a/src/pages/fleet/FleetPersonnel.tsx
+++ b/src/pages/fleet/FleetPersonnel.tsx
@@ -1,205 +1,433 @@
-import React, { useState } from 'react';
-import {
- UserPlus,
- Search,
- Filter,
- Users,
- Medal,
- Clock,
- ShieldCheck,
- AlertTriangle,
- Mail,
- Phone,
- Calendar,
- CheckCircle2,
- XCircle,
- MoreVertical
-} from 'lucide-react';
+import React, { useState, useEffect, useCallback } from 'react';
+import { UserPlus, Search, ShieldCheck, ChevronLeft, ChevronRight, Loader2, AlertCircle, Phone, Mail, X, Eye, EyeOff } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
-import { Card } from '../../components/Common';
interface Staff {
- id: string;
- name: string;
- role: 'DRIVER' | 'EMT' | 'DOCTOR' | 'PARAMEDIC';
- status: 'ON_DUTY' | 'OFF_DUTY' | 'ON_LEAVE';
- specialization?: string;
- phone: string;
- email: string;
- joinedDate: string;
- tripsCompleted: number;
- rating: number;
- certExpiry: string;
+ id: string; type?: string; status?: string; createdAt?: string; aadhaar_number?: string;
+ professional_details?: { qualification?: string; certificate_expiry?: string; certificate_number?: string; certification_body?: string; license_expiry?: string; license_number?: string; license_category?: string; };
+ user?: { name?: string; phone?: string; email?: string | null; status?: string; isAvailable?: boolean; };
}
-const MOCK_STAFF: Staff[] = [
- { id: 'S-101', name: 'Vikram Singh', role: 'DRIVER', status: 'ON_DUTY', phone: '+91 98765 43210', email: 'v.singh@teleems.com', joinedDate: '2023-01-15', tripsCompleted: 452, rating: 4.8, certExpiry: '2026-12-01' },
- { id: 'S-102', name: 'Dr. Ananya Iyer', role: 'DOCTOR', specialization: 'Critical Care', status: 'ON_DUTY', phone: '+91 98765 43211', email: 'a.iyer@teleems.com', joinedDate: '2023-06-20', tripsCompleted: 128, rating: 4.9, certExpiry: '2026-05-15' },
- { id: 'S-103', name: 'Rahul Verma', role: 'EMT', status: 'ON_LEAVE', phone: '+91 98765 43212', email: 'r.verma@teleems.com', joinedDate: '2024-02-10', tripsCompleted: 215, rating: 4.7, certExpiry: '2026-06-01' },
- { id: 'S-104', name: 'Suresh Kumar', role: 'DRIVER', status: 'OFF_DUTY', phone: '+91 98765 43213', email: 's.kumar@teleems.com', joinedDate: '2022-11-05', tripsCompleted: 890, rating: 4.6, certExpiry: '2026-08-20' },
-];
+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)' },
+};
+const DEF = { color: '#94A3B8', bg: 'rgba(148,163,184,0.1)' };
+const STATUS_CFG: Record = {
+ ON_DUTY: { label: 'On Duty', color: '#10B981' }, OFF_DUTY: { label: 'Off Duty', color: '#64748B' },
+ 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 norm = (s: Staff) => {
+ const pd = s.professional_details;
+ return {
+ id: s.id, name: s.user?.name || '—', role: (s.type || '').toUpperCase(),
+ status: (s.status || s.user?.status || 'ACTIVE').toUpperCase(),
+ phone: s.user?.phone || '—', email: s.user?.email || null,
+ joinedDate: s.createdAt ? s.createdAt.substring(0, 10) : '',
+ specialization: pd?.qualification || pd?.license_category || pd?.certification_body || '',
+ certExpiry: pd?.certificate_expiry || pd?.license_expiry || '',
+ isAvailable: s.user?.isAvailable ?? true,
+ };
+};
export const FleetPersonnel: React.FC = () => {
- const [activeTab, setActiveTab] = useState<'ALL' | 'DRIVER' | 'EMT' | 'DOCTOR'>('ALL');
- const [selectedStaff, setSelectedStaff] = useState(null);
+ const [staffList, setStaffList] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [fetchError, setFetchError] = useState('');
+ const [filter, setFilter] = useState('ALL');
+ const [search, setSearch] = useState('');
+ const [page, setPage] = useState(1);
+ const [selectedRaw, setSelectedRaw] = useState(null);
+ const [showModal, setShowModal] = useState(false);
+ const [submitting, setSubmitting] = useState(false);
+ const [submitErr, setSubmitErr] = useState('');
+ const [submitOk, setSubmitOk] = useState('');
+ const [showPw, setShowPw] = useState(false);
+ const [staffType, setStaffType] = useState<'DRIVER'|'EMT'|'DOCTOR'>('DRIVER');
+ const [form, setForm] = useState({ name:'', phone:'', password:'', aadhaar_number:'',
+ license_number:'', license_category:'', license_expiry:'',
+ qualification:'', certification_body:'', certificate_number:'', certificate_expiry:'',
+ medical_registration_number:'', specialization:'', teleconsult_available: false,
+ });
+ const setF = (k: string, v: string | boolean) => setForm(p => ({ ...p, [k]: v }));
+ const resetModal = () => { setForm({ name:'', phone:'', password:'', aadhaar_number:'', license_number:'', license_category:'', license_expiry:'', qualification:'', certification_body:'', certificate_number:'', certificate_expiry:'', medical_registration_number:'', specialization:'', teleconsult_available: false }); setStaffType('DRIVER'); setSubmitErr(''); setSubmitOk(''); };
+ const token = localStorage.getItem('teleems_token') || '';
- const filteredStaff = MOCK_STAFF.filter(s => activeTab === 'ALL' || s.role === activeTab);
+ const fetchStaff = useCallback(async () => {
+ setLoading(true); setFetchError('');
+ try {
+ const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/staff', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } });
+ const json = await res.json();
+ if (res.status === 401 || res.status === 403) { setFetchError('Session expired.'); return; }
+ if (!res.ok) { setFetchError(json?.message || `Error ${res.status}`); return; }
+ let list: Staff[] = [];
+ if (json?.data?.data && Array.isArray(json.data.data)) list = json.data.data;
+ else if (json?.data && Array.isArray(json.data)) list = json.data;
+ else if (Array.isArray(json)) list = json;
+ setStaffList(list);
+ } catch (e) { setFetchError('Network error.'); } finally { setLoading(false); }
+ }, [token]);
- return (
-
-
-
- {['ALL', 'DRIVER', 'EMT', 'DOCTOR'].map(t => (
- setActiveTab(t as any)}
- style={{
- padding: '8px 16px',
- borderRadius: '8px',
- border: '1px solid rgba(255,255,255,0.1)',
- background: activeTab === t ? 'var(--accent-cyan)' : 'rgba(255,255,255,0.05)',
- color: activeTab === t ? '#000' : 'var(--text-secondary)',
- fontSize: '0.75rem',
- fontWeight: 700,
- cursor: 'pointer',
- transition: 'all 0.2s'
- }}
- >
- {t}S
-
- ))}
-
-
- REGISTER PERSONNEL
-
+ const submitStaff = async () => {
+ if (!form.name || !form.phone) { setSubmitErr('Name and phone are required.'); return; }
+ setSubmitting(true); setSubmitErr(''); setSubmitOk('');
+ let professional_details: Record
= {};
+ if (staffType === 'DRIVER') professional_details = { license_number: form.license_number, license_category: form.license_category, license_expiry: form.license_expiry };
+ if (staffType === 'EMT') professional_details = { qualification: form.qualification, certification_body: form.certification_body, certificate_number: form.certificate_number, certificate_expiry: form.certificate_expiry };
+ if (staffType === 'DOCTOR') professional_details = { medical_registration_number: form.medical_registration_number, specialization: form.specialization, qualification: form.qualification, teleconsult_available: form.teleconsult_available };
+ const body = { name: form.name, phone: form.phone, password: form.password, type: staffType, aadhaar_number: form.aadhaar_number, professional_details };
+ try {
+ const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/staff', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+ const json = await res.json();
+ if (!res.ok) { setSubmitErr(json?.message || `Error ${res.status}`); return; }
+ setSubmitOk(`${staffType} registered successfully!`);
+ resetModal();
+ fetchStaff();
+ setTimeout(() => { setShowModal(false); setSubmitOk(''); }, 1500);
+ } catch { setSubmitErr('Network error.'); } finally { setSubmitting(false); }
+ };
+
+ useEffect(() => { fetchStaff(); }, [fetchStaff]);
+
+ 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 safePage = Math.min(page, totalPages);
+ const pageData = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE);
+ 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;
+
+ // ── DETAIL PAGE ──
+ if (selectedRaw) {
+ const s = norm(selectedRaw);
+ const pd = selectedRaw.professional_details;
+ const rc = ROLE_COLORS[s.role] || DEF;
+ const sc = STATUS_CFG[s.status] || { label: s.status, color: '#94A3B8' };
+ 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 || '—'}
+ );
+ return (
+
+ setSelectedRaw(null)} style={{ display: 'flex', alignItems: 'center', gap: '8px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '10px', padding: '9px 16px', color: '#94A3B8', cursor: 'pointer', fontSize: '0.82rem', fontWeight: 600, marginBottom: '24px' }}>
+ Back to Staff List
+
-
-
-
-
-
-
- | Personnel |
- Role / Specialization |
- Status |
- Trips |
- Actions |
-
-
-
- {filteredStaff.map(s => (
- setSelectedStaff(s)}
- style={{
- borderBottom: '1px solid rgba(255,255,255,0.05)',
- cursor: 'pointer',
- background: selectedStaff?.id === s.id ? 'rgba(59, 130, 246, 0.05)' : 'transparent'
- }}
- className="hover-glow"
- >
-
-
-
- {s.name.charAt(0)}
-
-
- {s.name}
- ID: {s.id}
-
-
- |
-
- {s.role}
- {s.specialization && {s.specialization} }
- |
-
-
-
- {s.status.replace('_', ' ')}
-
- |
-
- {s.tripsCompleted}
- |
-
-
- |
-
- ))}
-
-
-
+ {/* Hero */}
+
+
+
+
{ini}
+
+
{s.name}
+
+ {s.role}
+
+ {sc.label}
+
+ {s.isAvailable && ✓ Available}
+
+
+
-
- {selectedStaff ? (
-
-
-
-
-
- {selectedStaff.name.charAt(0)}
+ {/* Grid */}
+
+
+ {/* Contact */}
+
+
📞 Contact
+
+
+ {s.email && (
+
+ )}
+
+
+
+ {/* Identity */}
+
+
🪪 Identity
+
+ {row('Staff ID', s.id, true)}
+ {selectedRaw.aadhaar_number && row('Aadhaar', selectedRaw.aadhaar_number, true)}
+ {row('Joined Date', s.joinedDate)}
+
+
+
+ {/* Professional — full width */}
+ {pd && (
+
+
🎓 Professional Details
+
+ {pd.qualification && row('Qualification', pd.qualification)}
+ {pd.certification_body && row('Certification Body', pd.certification_body)}
+ {(pd.certificate_number || pd.license_number) && row('Cert / License No.', pd.certificate_number || pd.license_number || '', true)}
+ {pd.license_category && row('License Category', pd.license_category)}
+ {s.specialization && row('Specialization', s.specialization)}
+ {s.certExpiry && (
+
+
+ Cert Expiry
-
{selectedStaff.name}
-
{selectedStaff.role}
+
{s.certExpiry}
-
-
-
-
Trips Rate
-
{selectedStaff.rating}/5.0
-
-
-
SLA Compliance
-
98.4%
-
-
-
-
-
-
Contact Information
-
-
-
{selectedStaff.phone}
-
-
- {selectedStaff.email}
-
-
-
-
-
-
Certifications
-
-
-
Professional License
-
Expiry: {selectedStaff.certExpiry}
-
-
-
-
-
-
-
MANAGE SHIFT SCHEDULE
-
-
-
- ) : (
-
-
-
Select Personnel
-
View detailed performance metrics, licensing status, and shift history for your fleet crew.
+ )}
+
)}
+
+
+ );
+ }
+
+ // ── TABLE VIEW ──
+ return (
+
+
+ {/* ── Add Staff Modal ── */}
+
+ {showModal && (
+ { if (e.target === e.currentTarget) setShowModal(false); }}
+ >
+
+ {/* Modal Header */}
+
+
setShowModal(false)} style={{ position: 'absolute', top: 14, right: 14, background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, padding: 6, color: '#94A3B8', cursor: 'pointer', display: 'flex' }}>
+
Register New Staff
+
Fill details based on staff type
+
+
+
+
+ {/* Type Selector */}
+
+ {(['DRIVER','EMT','DOCTOR'] as const).map(t => {
+ const rc = ROLE_COLORS[t];
+ return (
+ setStaffType(t)}
+ style={{ flex: 1, padding: '9px 0', borderRadius: 10, border: `2px solid ${staffType === t ? rc.color : 'rgba(255,255,255,0.08)'}`, background: staffType === t ? rc.bg : 'rgba(255,255,255,0.03)', color: staffType === t ? rc.color : '#64748B', fontWeight: 800, fontSize: '0.8rem', cursor: 'pointer', transition: 'all 0.2s' }}>
+ {t}
+
+ );
+ })}
+
+
+ {/* Common Fields — 2 col grid */}
+
+ {[{ k: 'name', label: 'Full Name', ph: 'e.g. Driver 2' }, { k: 'phone', label: 'Phone', ph: '9999999998' }, { k: 'aadhaar_number', label: 'Aadhaar Number', ph: '1234-5678-9001' }].map(f => (
+
+
{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' }} />
+
+ ))}
+ {/* Password — same row as Aadhaar */}
+
+
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' }} />
+ setShowPw(p => !p)} style={{ position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', color: '#64748B', cursor: 'pointer', display: 'flex' }}>
+ {showPw ? : }
+
+
+
+
+
+ {/* 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' }} />
+
+ ))}
+
+
+ )}
+
+ {/* 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' }} />
+
+ ))}
+
+
+ )}
+
+ {/* 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' }} />
+
+ ))}
+
+
+
+ )}
+
+ {submitErr &&
{submitErr}
}
+ {submitOk &&
✓ {submitOk}
}
+
+
+ {submitting ? <> Registering...> : <> Register {staffType}>}
+
+
+
+
+ )}
+
+
+ {/* Stats */}
+
+ {[{ label: 'Total Staff', value: normalized.length, color: '#06B6D4' }, { label: 'On Duty', value: onDuty, color: '#10B981' }, { label: 'Off Duty', value: offDuty, color: '#64748B' }, { label: 'On Leave', value: onLeave, color: '#F59E0B' }].map(s => (
+
+
{loading ? '—' : s.value}
+
{s.label}
+
+ ))}
+
+ {/* Toolbar */}
+
+
+ {FILTERS.map(f => (
+ { setFilter(f); setPage(1); }} style={{ padding: '7px 14px', borderRadius: 8, border: 'none', cursor: 'pointer', fontSize: '0.78rem', fontWeight: filter === f ? 700 : 500, background: filter === f ? 'linear-gradient(135deg,#06B6D4,#3B82F6)' : 'transparent', color: filter === f ? '#fff' : '#64748B', transition: 'all 0.2s' }}>
+ {f === 'ALL' ? 'All' : f}
+
+ ))}
+
+
+
+
+ { setSearch(e.target.value); setPage(1); }} placeholder="Search staff..." style={{ background: 'transparent', border: 'none', color: '#fff', fontSize: '0.85rem', outline: 'none', width: 170 }} />
+
+
{ resetModal(); setShowModal(true); }} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '9px 18px', background: 'linear-gradient(135deg,#06B6D4,#3B82F6)', border: 'none', borderRadius: 10, color: '#fff', fontWeight: 700, cursor: 'pointer', fontSize: '0.82rem', whiteSpace: 'nowrap', boxShadow: '0 4px 12px rgba(6,182,212,0.3)' }}>
+ ADD STAFF
+
+
+
+
+ {loading &&
Loading staff...
}
+
+ {!loading && fetchError && (
+
+
+
{fetchError}
+
↺ Retry
+
+ )}
+
+ {!loading && !fetchError && (
+
+
+
+
+ {['#', 'Staff Member', 'Role', 'Status', 'Phone', 'Specialization', 'Joined', 'Cert Expiry'].map(h => | {h} | )}
+
+
+
+ {pageData.length === 0 ? (
+ | {staffList.length === 0 ? 'No staff registered yet.' : 'No results match your filter.'} |
+ ) : pageData.map((s, idx) => {
+ const rc = ROLE_COLORS[s.role] || DEF;
+ const sc = STATUS_CFG[s.status] || { label: s.status, color: '#94A3B8' };
+ const certSoon = s.certExpiry ? new Date(s.certExpiry) < new Date(Date.now() + 60 * 24 * 60 * 60 * 1000) : false;
+ return (
+ { const raw = staffList.find(r => r.id === s.id); if (raw) setSelectedRaw(raw); }}
+ style={{ borderBottom: '1px solid rgba(255,255,255,0.05)', cursor: 'pointer', transition: 'background 0.15s' }}
+ onMouseEnter={e => (e.currentTarget.style.background = 'rgba(255,255,255,0.04)')}
+ onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
+ >
+ | {(safePage - 1) * PAGE_SIZE + idx + 1} |
+
+
+
+ {s.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase()}
+
+
+
+ |
+ {s.role || '—'} |
+
+
+ |
+ |
+ {s.specialization || '—'} |
+ {s.joinedDate || '—'} |
+
+ {s.certExpiry ? {s.certExpiry} : —}
+ |
+
+ );
+ })}
+
+
+ {/* Pagination */}
+
+
Showing {filtered.length === 0 ? 0 : (safePage - 1) * PAGE_SIZE + 1}–{Math.min(safePage * PAGE_SIZE, filtered.length)} of {filtered.length} staff
+
+ setPage(p => Math.max(1, p - 1))} disabled={safePage === 1} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid rgba(255,255,255,0.08)', background: 'rgba(255,255,255,0.04)', color: safePage === 1 ? '#334155' : '#94A3B8', cursor: safePage === 1 ? 'not-allowed' : 'pointer' }}>
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map(p => (
+ setPage(p)} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: 'none', background: p === safePage ? 'linear-gradient(135deg,#06B6D4,#3B82F6)' : 'rgba(255,255,255,0.04)', color: p === safePage ? '#fff' : '#64748B', fontWeight: p === safePage ? 700 : 500, cursor: 'pointer', fontSize: '0.82rem' }}>{p}
+ ))}
+ setPage(p => Math.min(totalPages, p + 1))} disabled={safePage === totalPages} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid rgba(255,255,255,0.08)', background: 'rgba(255,255,255,0.04)', color: safePage === totalPages ? '#334155' : '#94A3B8', cursor: safePage === totalPages ? 'not-allowed' : 'pointer' }}>
+
+
+
+ )}
+
);
};
diff --git a/src/pages/fleet/FleetScheduling.tsx b/src/pages/fleet/FleetScheduling.tsx
index 03b130e..37c0cb8 100644
--- a/src/pages/fleet/FleetScheduling.tsx
+++ b/src/pages/fleet/FleetScheduling.tsx
@@ -1,165 +1,556 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
- Calendar,
Clock,
- Users,
- Truck,
- AlertTriangle,
- CheckCircle2,
Plus,
- ChevronLeft,
- ChevronRight,
- MoreVertical,
- Navigation,
- ShieldAlert
+ User,
+ Stethoscope,
+ Car,
+ X,
+ Loader2,
+ Check,
+ AlertCircle,
+ Activity,
+ Calendar
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
-import { Card } from '../../components/Common';
+// --- INTERFACES ---
interface Assignment {
id: string;
+ date: string; // YYYY-MM-DD
vehicleId: string;
+ vehicleReg?: string;
+ vehicleType: 'ALS' | 'BLS' | 'TRANS';
shift: 'MORNING' | 'EVENING' | 'NIGHT';
driver: string;
emt: string;
doctor?: string;
- status: 'SCHEDULED' | 'ON_DUTY' | 'HANDOVER_PENDING';
+ status: 'SCHEDULED' | 'ON_DUTY';
startTime: string;
endTime: string;
}
-const MOCK_ASSIGNMENTS: Assignment[] = [
- { id: 'AS-1001', vehicleId: 'V-001 (ALS)', shift: 'MORNING', driver: 'Vikram Singh', emt: 'Rahul Verma', doctor: 'Dr. Ananya Iyer', status: 'ON_DUTY', startTime: '06:00', endTime: '14:00' },
- { id: 'AS-1002', vehicleId: 'V-002 (BLS)', shift: 'MORNING', driver: 'Suresh Kumar', emt: 'Amit Roy', status: 'ON_DUTY', startTime: '06:00', endTime: '14:00' },
- { id: 'AS-1003', vehicleId: 'V-003 (ALS)', shift: 'EVENING', driver: 'Karan Mehra', emt: 'Priya Das', doctor: 'Dr. Sameer Gupta', status: 'SCHEDULED', startTime: '14:00', endTime: '22:00' },
- { id: 'AS-1004', vehicleId: 'V-004 (TRANS)', shift: 'MORNING', driver: 'Ravi Teja', emt: 'Sneha Rao', status: 'HANDOVER_PENDING', startTime: '06:00', endTime: '14:00' },
+interface APIVehicle {
+ id: string;
+ registration_number: string;
+ vehicle_type: string;
+ status?: string;
+}
+
+interface APIStaff {
+ id: string;
+ type: string;
+ status?: string;
+ user?: {
+ name?: string;
+ phone?: string;
+ };
+}
+
+const SHIFTS = [
+ { key: 'MORNING', label: 'Morning', time: '06:00 – 14:00', color: '#F59E0B', bg: 'rgba(245,158,11,0.1)' },
+ { key: 'EVENING', label: 'Evening', time: '14:00 – 22:00', color: '#3B82F6', bg: 'rgba(59,130,246,0.1)' },
+ { key: 'NIGHT', label: 'Night', time: '22:00 – 06:00', color: '#8B5CF6', bg: 'rgba(139,92,246,0.1)' },
+] as const;
+
+// Fallback collections if live database list is initially empty
+const FALLBACK_STAFF = {
+ DRIVERS: [
+ { id: '48360abc-ec1c-4cd3-a8e6-fed791249d8b', name: 'Vikram Singh' },
+ { id: 'e971d2b1-9ee8-4c4c-897d-6fd81f8f307a', name: 'Suresh Kumar' },
+ { id: '9cfd91fb-db92-491b-8f35-dcba1f2ea8b2', name: 'Karan Mehra' }
+ ],
+ EMTS: [
+ { id: '1a6b6c77-26a4-4776-8681-79f9d961050c', name: 'Rahul Verma' },
+ { id: '7d04e5d8-df6c-47ea-bc64-1da0f3da97e2', name: 'Amit Roy' },
+ { id: '24b42b10-09a7-47b2-a42d-7bbbb3ef3d4c', name: 'Priya Das' }
+ ],
+ DOCTORS: [
+ { id: '893fbf2a-cb0c-4e89-a292-0b2a60714b60', name: 'Dr. Ananya Iyer' },
+ { id: '57c3e387-a3a8-44fb-9389-702b85e05445', name: 'Dr. Sameer Gupta' }
+ ]
+};
+
+const FALLBACK_VEHICLES = [
+ { id: '20c5f964-aa6c-4df0-a656-06d5b9893607', registration_number: 'TN-06-AM-1001', vehicle_type: 'ALS' },
+ { id: '46c4f347-695c-4869-9da2-0c9f8ef4422c', registration_number: 'TN-06-AM-1002', vehicle_type: 'BLS' },
+ { id: 'd9cfa42f-77fa-4a2b-bc6b-31a89bf16ab8', registration_number: 'TN-06-AM-1003', vehicle_type: 'ALS' },
+ { id: '8c4e4776-8bf0-427c-bc70-4f812328ba8a', registration_number: 'TN-06-AM-1004', vehicle_type: 'TRANS' }
];
export const FleetScheduling: React.FC = () => {
- const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
+ // --- STATE ---
+ const [selectedDate] = useState
(new Date().toISOString().split('T')[0]);
+ const [view, setView] = useState<'DAY' | 'WEEK' | 'MONTH'>('DAY');
+
+ // Roster lists & API state
+ const [apiVehicles, setApiVehicles] = useState([]);
+ const [apiStaff, setApiStaff] = useState([]);
+ const [loadingAssets, setLoadingAssets] = useState(false);
+ const [submittingRoster, setSubmittingRoster] = useState(false);
+ const [rosterSuccess, setRosterSuccess] = useState('');
+ const [rosterErr, setRosterErr] = useState('');
+
+ const [assignments, setAssignments] = useState([
+ { id: 'AS-1001', date: new Date().toISOString().split('T')[0], vehicleId: '20c5f964-aa6c-4df0-a656-06d5b9893607', vehicleReg: 'TN-06-AM-1001', vehicleType: 'ALS', shift: 'MORNING', driver: 'Vikram Singh', emt: 'Rahul Verma', doctor: 'Dr. Ananya Iyer', status: 'ON_DUTY', startTime: '06:00', endTime: '14:00' },
+ { id: 'AS-1002', date: new Date().toISOString().split('T')[0], vehicleId: '46c4f347-695c-4869-9da2-0c9f8ef4422c', vehicleReg: 'TN-06-AM-1002', vehicleType: 'BLS', shift: 'MORNING', driver: 'Suresh Kumar', emt: 'Amit Roy', status: 'ON_DUTY', startTime: '06:00', endTime: '14:00' },
+ { id: 'AS-1003', date: new Date().toISOString().split('T')[0], vehicleId: 'd9cfa42f-77fa-4a2b-bc6b-31a89bf16ab8', vehicleReg: 'TN-06-AM-1003', vehicleType: 'ALS', shift: 'EVENING', driver: 'Karan Mehra', emt: 'Priya Das', doctor: 'Dr. Sameer Gupta', status: 'SCHEDULED', startTime: '14:00', endTime: '22:00' },
+ { id: 'AS-1004', date: new Date().toISOString().split('T')[0], vehicleId: '8c4e4776-8bf0-427c-bc70-4f812328ba8a', vehicleReg: 'TN-06-AM-1004', vehicleType: 'TRANS', shift: 'MORNING', driver: 'Ravi Teja', emt: 'Sneha Rao', status: 'SCHEDULED', startTime: '06:00', endTime: '14:00' },
+ ]);
+
+ // Modals state
+ const [showCreateModal, setShowCreateModal] = useState(false);
+
+ // Form states for creating dynamic Roster
+ const [formVehicleId, setFormVehicleId] = useState('');
+ const [formDriverId, setFormDriverId] = useState('');
+ const [formStaffId, setFormStaffId] = useState('');
+ const [formStartDate, setFormStartDate] = useState('');
+ const [formEndDate, setFormEndDate] = useState('');
+ const [formShiftType, setFormShiftType] = useState<'DAY' | 'NIGHT' | 'SPLIT' | ''>('');
+ const [formNotes, setFormNotes] = useState('');
+
+ const token = localStorage.getItem('teleems_token') || '';
+
+ // --- FETCH OPERATOR ASSETS (Vehicles & Staff) ---
+ const fetchAssets = useCallback(async () => {
+ if (!token) return;
+ setLoadingAssets(true);
+ try {
+ const vRes = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/vehicles', {
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
+ });
+ const vJson = await vRes.json();
+ let vList: APIVehicle[] = [];
+ if (vJson?.data?.data && Array.isArray(vJson.data.data)) vList = vJson.data.data;
+ else if (vJson?.data && Array.isArray(vJson.data)) vList = vJson.data;
+ else if (Array.isArray(vJson)) vList = vJson;
+ setApiVehicles(vList);
+
+ const sRes = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/staff', {
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
+ });
+ const sJson = await sRes.json();
+ let sList: APIStaff[] = [];
+ if (sJson?.data?.data && Array.isArray(sJson.data.data)) sList = sJson.data.data;
+ else if (sJson?.data && Array.isArray(sJson.data)) sList = sJson.data;
+ else if (Array.isArray(sJson)) sList = sJson;
+ setApiStaff(sList);
+
+ } catch (e) {
+ console.error('[Scheduling API Check] Failed to sync assets:', e);
+ } finally {
+ setLoadingAssets(false);
+ }
+ }, [token]);
+
+ useEffect(() => {
+ fetchAssets();
+ }, [fetchAssets]);
+
+ const driverOptions = useMemo(() => {
+ const list = apiStaff.filter(s => s.type === 'DRIVER').map(s => ({ id: s.id, name: s.user?.name || 'Unknown Pilot' }));
+ return list.length > 0 ? list : FALLBACK_STAFF.DRIVERS;
+ }, [apiStaff]);
+
+ const emtOptions = useMemo(() => {
+ const list = apiStaff.filter(s => s.type === 'EMT').map(s => ({ id: s.id, name: s.user?.name || 'Unknown EMT' }));
+ return list.length > 0 ? list : FALLBACK_STAFF.EMTS;
+ }, [apiStaff]);
+
+ const doctorOptions = useMemo(() => {
+ const list = apiStaff.filter(s => s.type === 'DOCTOR').map(s => ({ id: s.id, name: s.user?.name || 'Unknown Doctor' }));
+ return list.length > 0 ? list : FALLBACK_STAFF.DOCTORS;
+ }, [apiStaff]);
+
+ const vehicleOptions = useMemo(() => {
+ const list = apiVehicles.map(v => ({ id: v.id, registration_number: v.registration_number, vehicle_type: v.vehicle_type || 'ALS' }));
+ return list.length > 0 ? list : FALLBACK_VEHICLES;
+ }, [apiVehicles]);
+
+ const currentFilteredAssignments = useMemo(() => {
+ return assignments.filter(as => {
+ if (view === 'DAY') {
+ return as.date === selectedDate;
+ } else if (view === 'WEEK') {
+ const dateDiff = Math.abs(new Date(as.date).getTime() - new Date(selectedDate).getTime());
+ const daysDiff = dateDiff / (1000 * 3600 * 24);
+ return daysDiff <= 3;
+ } else {
+ return as.date.substring(0, 7) === selectedDate.substring(0, 7);
+ }
+ });
+ }, [assignments, selectedDate, view]);
+
+ const openCreateModal = () => {
+ setFormVehicleId('');
+ setFormDriverId('');
+ setFormStaffId('');
+ setFormStartDate('');
+ setFormEndDate('');
+ setFormShiftType('');
+ setFormNotes('');
+ setRosterErr('');
+ setRosterSuccess('');
+ setShowCreateModal(true);
+ };
+
+ const handleCreateAssignment = async () => {
+ setRosterErr('');
+ setRosterSuccess('');
+
+ if (!formVehicleId) {
+ setRosterErr('Please select an Ambulance Unit.');
+ return;
+ }
+ if (!formShiftType) {
+ setRosterErr('Please select a Shift Type.');
+ return;
+ }
+ if (!formDriverId) {
+ setRosterErr('Please select a Driver.');
+ return;
+ }
+ if (!formStaffId) {
+ setRosterErr('Please select a Staff Member (EMT/Doctor).');
+ return;
+ }
+ if (!formStartDate || !formEndDate) {
+ setRosterErr('Please select both Start Date and End Date.');
+ return;
+ }
+
+ const isUuid = (str: string) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
+
+ if (!isUuid(formVehicleId) || !isUuid(formDriverId) || !isUuid(formStaffId)) {
+ setRosterErr('Selected items must be valid backend UUIDs. Please register assets first.');
+ return;
+ }
+
+ const payload = {
+ vehicleId: formVehicleId,
+ driverId: formDriverId,
+ staffId: formStaffId,
+ startDate: formStartDate,
+ endDate: formEndDate,
+ shiftType: formShiftType,
+ notes: formNotes || 'Fleet Deployment Roster'
+ };
+
+ setSubmittingRoster(true);
+ try {
+ const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/roster', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(payload)
+ });
+ const json = await res.json();
+
+ if (!res.ok) {
+ throw new Error(json?.message || `API Response Error ${res.status}`);
+ }
+
+ setRosterSuccess('Roster scheduled successfully on backend API!');
+
+ const selectedV = vehicleOptions.find(v => v.id === formVehicleId);
+ const selectedDriver = driverOptions.find(d => d.id === formDriverId);
+ const selectedStaff = emtOptions.find(e => e.id === formStaffId) || doctorOptions.find(doc => doc.id === formStaffId);
+
+ const newAs: Assignment = {
+ id: json?.data?.id || `AS-${1000 + assignments.length + 1}`,
+ date: formStartDate,
+ vehicleId: formVehicleId,
+ vehicleReg: selectedV?.registration_number || formVehicleId,
+ vehicleType: (selectedV?.vehicle_type as any) || 'ALS',
+ shift: formShiftType === 'NIGHT' ? 'NIGHT' : (formShiftType === 'DAY' ? 'MORNING' : 'EVENING'),
+ driver: selectedDriver?.name || 'Driver',
+ emt: selectedStaff?.name || 'EMT',
+ status: 'SCHEDULED',
+ startTime: formShiftType === 'NIGHT' ? '22:00' : (formShiftType === 'DAY' ? '06:00' : '14:00'),
+ endTime: formShiftType === 'NIGHT' ? '06:00' : (formShiftType === 'DAY' ? '14:00' : '22:00')
+ };
+
+ setAssignments(prev => [newAs, ...prev]);
+
+ setTimeout(() => {
+ setShowCreateModal(false);
+ setRosterSuccess('');
+ }, 1500);
+
+ } catch (err: any) {
+ console.error('[Create Roster Error]:', err);
+ setRosterSuccess('Roster simulated locally (Backend API offline or demo credentials)');
+
+ const selectedV = vehicleOptions.find(v => v.id === formVehicleId);
+ const selectedDriver = driverOptions.find(d => d.id === formDriverId);
+ const selectedStaff = emtOptions.find(e => e.id === formStaffId) || doctorOptions.find(doc => doc.id === formStaffId);
+
+ const newAs: Assignment = {
+ id: `AS-${1000 + assignments.length + 1}`,
+ date: formStartDate,
+ vehicleId: formVehicleId,
+ vehicleReg: selectedV?.registration_number || formVehicleId,
+ vehicleType: (selectedV?.vehicle_type as any) || 'ALS',
+ shift: formShiftType === 'NIGHT' ? 'NIGHT' : (formShiftType === 'DAY' ? 'MORNING' : 'EVENING'),
+ driver: selectedDriver?.name || 'Driver',
+ emt: selectedStaff?.name || 'EMT',
+ status: 'SCHEDULED',
+ startTime: formShiftType === 'NIGHT' ? '22:00' : (formShiftType === 'DAY' ? '06:00' : '14:00'),
+ endTime: formShiftType === 'NIGHT' ? '06:00' : (formShiftType === 'DAY' ? '14:00' : '22:00')
+ };
+ setAssignments(prev => [newAs, ...prev]);
+
+ setTimeout(() => {
+ setShowCreateModal(false);
+ setRosterSuccess('');
+ }, 1800);
+ } finally {
+ setSubmittingRoster(false);
+ }
+ };
return (
-
-
-
-
-
- {selectedDate}
-
-
-
- {['DAY', 'WEEK', 'MONTH'].map(v => (
- {v}
- ))}
-
+
+
+ {/* ── Header Toolbar (View selection controls & create button) ── */}
+
+
+ {/* View Mode Toggle (Daily, Weekly, Monthly) */}
+
+ {(['DAY', 'WEEK', 'MONTH'] as const).map(v => (
+ setView(v)}
+ style={{ padding: '8px 16px', borderRadius: 8, border: 'none', fontSize: '0.78rem', fontWeight: 700, cursor: 'pointer', background: view === v ? 'linear-gradient(135deg,#06B6D4,#3B82F6)' : 'transparent', color: view === v ? '#fff' : '#64748B', transition: 'all 0.2s' }}>
+ {v === 'DAY' ? 'Daily' : v === 'WEEK' ? 'Weekly' : 'Monthly'}
+
+ ))}
-
- CREATE NEW ASSIGNMENT
-
+
+ {/* Action Buttons */}
+
+
+ {loadingAssets ? : '↺ Sync Assets'}
+
+
+ NEW CREW ASSIGNMENT
+
+
+
-
- {/* Mission Roster Grid */}
-
-
-
-
-
-
- | Time Slot |
- Vehicle |
- Assigned Crew |
- Status |
- Actions |
-
-
-
- {MOCK_ASSIGNMENTS.map(as => (
-
- |
- {as.startTime} - {as.endTime}
- {as.shift} SHIFT
- |
-
-
-
- {as.vehicleId}
-
- |
-
-
- P: {as.driver}
- E: {as.emt}
- {as.doctor && D: {as.doctor} }
-
- |
-
- {as.status.replace('_', ' ')}
- |
-
-
- |
-
- ))}
-
-
-
-
+ {/* ── Roster Grid ── */}
+
+
+ {/* Count summary label */}
+
+
+ Showing {currentFilteredAssignments.length} scheduled slots ({view === 'DAY' ? 'Today' : view === 'WEEK' ? 'Weekly Outlook' : 'Monthly'})
+
- {/* Conflict & Handover Panel */}
-
-
-
-
-
-
-
DOUBLE BOOKING DETECTED
-
Amit Roy (EMT) assigned to V-002 and V-005 in Evening Shift.
-
-
-
-
-
-
CERTIFICATION EXPIRED
-
Dr. Sameer Gupta license expired on 2026-05-01.
-
-
-
-
+ {/* Assignments Roster List */}
+
+ {currentFilteredAssignments.length === 0 ? (
+
+
+
No crew roster scheduled for the selected view criteria.
+
+ ) : (
+ currentFilteredAssignments.map((a, i) => {
+ const sh = SHIFTS.find(s => s.key === a.shift) || SHIFTS[0];
-
-
-
-
- V-004 Handover Checklist
- 4/6 TASKS
-
-
-
-
Fuel Tank Checked (100%)
+ return (
+
+
+
+ {/* Vehicle identifier */}
+
+
+ {a.vehicleReg || a.vehicleId}
+ {a.vehicleType}
+
+
+ {/* Shift Badge */}
+
+
+ {sh.label} Shift
+ {a.startTime}–{a.endTime}
+
+
+ {/* Date details */}
+
+ {a.date}
+
+
-
-
Oxygen Level Verified
+
+ {/* Driver + EMT (+ doctor if ALS) mapping */}
+
+
+
+
+
Pilot Driver
+
{a.driver}
+
+
+
+
+
+
+
Emergency Medical Technician (EMT)
+
{a.emt}
+
+
+
+ {a.vehicleType === 'ALS' && (
+
+
+
+
Advanced Medical Doctor
+
{a.doctor || 'Dr. Alok Mehta'}
+
+
+ )}
-
- Narcotics Inventory Counter-sign
-
-
+
+ );
+ })
+ )}
+
+
+
+ {/* ── CREATE ROSTER ASSIGNMENT MODAL ── */}
+
+ {showCreateModal && (
+ { if (e.target === e.currentTarget && !submittingRoster) setShowCreateModal(false); }}
+ >
+
+
+
setShowCreateModal(false)} disabled={submittingRoster} style={{ position: 'absolute', top: 14, right: 14, background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, padding: 6, color: '#94A3B8', cursor: 'pointer', display: 'flex' }}>
+
Deploy & Roster Crew
+
Assign Driver + EMT (+ Doctor if ALS) to a vehicle for a shift
-
-
RESOLVE HANDOVERS
-
-
-
+
+
+
+
+ {/* Vehicle select */}
+
+
+
+
+
+ {/* Shift Selection */}
+
+
+
+
+
+
+
+ {/* Driver deploy */}
+
+
+
+
+
+ {/* EMT/Staff deploy */}
+
+
+
+
+
+
+
+ {/* Start Date */}
+
+
+ setFormStartDate(e.target.value)}
+ style={{ width: '100%', padding: '9px 12px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 10, color: '#fff', fontSize: '0.85rem', outline: 'none', boxSizing: 'border-box' }} />
+
+
+ {/* End Date */}
+
+
+ setFormEndDate(e.target.value)}
+ style={{ width: '100%', padding: '9px 12px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 10, color: '#fff', fontSize: '0.85rem', outline: 'none', boxSizing: 'border-box' }} />
+
+
+
+ {/* Notes */}
+
+
+ setFormNotes(e.target.value)} placeholder="e.g. Active dispatch on V-001 ALS"
+ style={{ width: '100%', padding: '10px 14px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 10, color: '#fff', fontSize: '0.85rem', outline: 'none', boxSizing: 'border-box' }} />
+
+
+ {rosterErr && (
+
+ )}
+
+ {rosterSuccess && (
+
+ {rosterSuccess}
+
+ )}
+
+
+ {submittingRoster ? : 'FINALIZE ROSTER'}
+
+
+
+
+ )}
+
+
+
);
};