add remaining fleet operator functionality

This commit is contained in:
2026-06-08 12:32:20 +05:30
parent 6ab819e74f
commit 6b409c10fe
16 changed files with 1176 additions and 761 deletions
+37 -65
View File
@@ -2,9 +2,10 @@ import React, { useState, useEffect, useCallback } from 'react';
import {
Search, MapPin, Truck, Activity, Navigation,
User, Calendar, Clock, AlertTriangle, Filter,
ChevronDown, X, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight
ChevronDown, X, ChevronLeft, ChevronRight
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Pagination } from '../../components/Pagination';
interface Vehicle {
id: string;
@@ -22,6 +23,7 @@ interface Vehicle {
export const FleetTrips: React.FC = () => {
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
const [stats, setStats] = useState<any>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState<string>('');
@@ -32,20 +34,26 @@ export const FleetTrips: React.FC = () => {
const [page, setPage] = useState<number>(1);
const [itemsPerPage, setItemsPerPage] = useState<number>(5);
const fetchVehicles = useCallback(async () => {
const fetchVehicles = useCallback(async (regNumber: string = '') => {
setLoading(true);
setError(null);
try {
const token = localStorage.getItem('teleems_token') || '';
const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/vehicles', {
let url = 'https://teleems-api-gateway.onrender.com/v1/fleet/vehicles/stats';
if (regNumber) {
url += `?registration_number=${encodeURIComponent(regNumber)}`;
}
const res = await fetch(url, {
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);
const data = json?.data || {};
setStats(data);
const activeList = data.activeVehiclesList || [];
const inactiveList = data.inactiveVehiclesList || [];
setVehicles([...activeList, ...inactiveList]);
} catch (e: any) {
console.error('Failed to fetch vehicles for trip management:', e);
setError(e?.message || 'Failed to fetch vehicles');
@@ -55,8 +63,12 @@ export const FleetTrips: React.FC = () => {
}, []);
useEffect(() => {
fetchVehicles();
}, [fetchVehicles]);
const delayDebounceFn = setTimeout(() => {
fetchVehicles(searchQuery);
}, 500);
return () => clearTimeout(delayDebounceFn);
}, [searchQuery, fetchVehicles]);
const filteredVehicles = vehicles.filter(v => {
const searchMatch = v.registration_number?.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -92,29 +104,29 @@ export const FleetTrips: React.FC = () => {
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ color: '#3B82F6' }}><Truck size={20} /></div>
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#0F172A' }}>{loading ? '...' : vehicles.length}</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#0F172A' }}>{loading ? '...' : stats?.totalVehicles ?? vehicles.length}</div>
<div style={{ fontSize: '0.7rem', color: '#64748B', textTransform: 'uppercase', fontWeight: 700 }}>Total Vehicles</div>
</div>
<div className="glass" style={{ padding: '20px', borderRadius: '16px', border: '1px solid rgba(239, 68, 68, 0.2)', background: '#FFFFFF', boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ color: '#EF4444' }}><Activity size={20} /></div>
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#EF4444' }}>{loading ? '...' : activeTripsCount}</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#EF4444' }}>{loading ? '...' : stats?.busyVehiclesCount ?? activeTripsCount}</div>
<div style={{ fontSize: '0.7rem', color: '#64748B', textTransform: 'uppercase', fontWeight: 700 }}>Active Trips (Busy)</div>
</div>
<div className="glass" style={{ padding: '20px', borderRadius: '16px', border: '1px solid rgba(34, 197, 94, 0.2)', background: '#FFFFFF', boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ color: '#22C55E' }}><Navigation size={20} /></div>
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#22C55E' }}>{loading ? '...' : availableCount}</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#22C55E' }}>{loading ? '...' : stats?.availableVehiclesCount ?? availableCount}</div>
<div style={{ fontSize: '0.7rem', color: '#64748B', textTransform: 'uppercase', fontWeight: 700 }}>Available Units</div>
</div>
<div className="glass" style={{ padding: '20px', borderRadius: '16px', border: '1px solid rgba(245, 158, 11, 0.2)', background: '#FFFFFF', boxShadow: '0 4px 20px rgba(15, 23, 42, 0.02)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ color: '#F59E0B' }}><AlertTriangle size={20} /></div>
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#F59E0B' }}>{loading ? '...' : vehicles.filter(v => !v.activeShift).length}</div>
<div style={{ fontSize: '0.7rem', color: '#64748B', textTransform: 'uppercase', fontWeight: 700 }}>No Active Shift</div>
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#F59E0B' }}>{loading ? '...' : stats?.inactiveVehiclesCount ?? vehicles.filter(v => !v.activeShift).length}</div>
<div style={{ fontSize: '0.7rem', color: '#64748B', textTransform: 'uppercase', fontWeight: 700 }}>Inactive Vehicles</div>
</div>
</div>
@@ -279,57 +291,17 @@ export const FleetTrips: React.FC = () => {
{/* Pagination Controls */}
{!loading && filteredVehicles.length > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px', background: '#FFFFFF', borderRadius: '16px', border: '1px solid #CBD5E1', marginTop: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span style={{ fontSize: '0.875rem', color: '#64748B' }}>Rows per page:</span>
<select
value={itemsPerPage}
onChange={(e) => { setItemsPerPage(Number(e.target.value)); setPage(1); }}
style={{ background: '#F8FAFC', border: '1px solid #E2E8F0', borderRadius: '8px', padding: '6px 28px 6px 12px', fontSize: '0.875rem', color: '#0F172A', outline: 'none', cursor: 'pointer', appearance: 'none' }}
>
{[5, 10, 20, 50].map(size => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '24px' }}>
<span style={{ fontSize: '0.875rem', color: '#64748B' }}>
{Math.min((safePage - 1) * itemsPerPage + 1, filteredVehicles.length)} - {Math.min(safePage * itemsPerPage, filteredVehicles.length)} of {filteredVehicles.length}
</span>
<div style={{ display: 'flex', gap: '4px' }}>
<button
onClick={() => setPage(1)}
disabled={safePage === 1}
style={{ padding: '6px', borderRadius: '6px', border: 'none', background: 'transparent', color: safePage === 1 ? '#CBD5E1' : '#64748B', cursor: safePage === 1 ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<ChevronsLeft size={18} />
</button>
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={safePage === 1}
style={{ padding: '6px', borderRadius: '6px', border: 'none', background: 'transparent', color: safePage === 1 ? '#CBD5E1' : '#64748B', cursor: safePage === 1 ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<ChevronLeft size={18} />
</button>
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={safePage === totalPages}
style={{ padding: '6px', borderRadius: '6px', border: 'none', background: 'transparent', color: safePage === totalPages ? '#CBD5E1' : '#64748B', cursor: safePage === totalPages ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<ChevronRight size={18} />
</button>
<button
onClick={() => setPage(totalPages)}
disabled={safePage === totalPages}
style={{ padding: '6px', borderRadius: '6px', border: 'none', background: 'transparent', color: safePage === totalPages ? '#CBD5E1' : '#64748B', cursor: safePage === totalPages ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<ChevronsRight size={18} />
</button>
</div>
<div style={{ background: '#FFFFFF', borderRadius: '16px', border: '1px solid #CBD5E1', marginTop: '8px', overflow: 'hidden' }}>
<Pagination
currentPage={page}
totalItems={filteredVehicles.length}
itemsPerPage={itemsPerPage}
onPageChange={setPage}
onItemsPerPageChange={setItemsPerPage}
itemsLabel="vehicles"
/>
</div>
</div>
)}
)}
<style>{`
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.spin { animation: spin 1s linear infinite; }