add remaining fleet operator functionality
This commit is contained in:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user