mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
Merge branch 'freight/hot_fix' of github.com:Tria-plc/edr-platform into freight/hot_fix
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card, Stack, Group, Grid, Text, ThemeIcon, Progress, Badge, Table, RingProgress, Container } from '@mantine/core';
|
||||
import { Truck, Fuel, Wrench, TrendingUp, AlertCircle } from 'lucide-react';
|
||||
import { Card, Stack, Group, Grid, Text, ThemeIcon, Progress, Badge, Table, RingProgress, Container, Title, Box, Tabs } from '@mantine/core';
|
||||
import { Truck, Fuel, Wrench, TrendingUp, AlertCircle, Users, User, MapPin, Calendar } from 'lucide-react';
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs';
|
||||
import { QUERY_KEYS } from '@/constants/QUERY_KEYS';
|
||||
import { api } from '@/auth/http';
|
||||
@@ -16,6 +16,16 @@ interface Vehicle {
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface Driver {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
licenseNumber?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
assignedVehicle?: string;
|
||||
}
|
||||
|
||||
interface FleetMetrics {
|
||||
totalVehicles: number;
|
||||
activeVehicles: number;
|
||||
@@ -24,23 +34,28 @@ interface FleetMetrics {
|
||||
totalMaintenanceSpend: number;
|
||||
averageFuelEfficiency: number;
|
||||
costPerKm: number;
|
||||
totalDrivers: number;
|
||||
assignedDrivers: number;
|
||||
}
|
||||
|
||||
const StatCard = ({ icon: Icon, label, value, color = 'blue' }: any) => (
|
||||
<Card withBorder>
|
||||
<Group justify="space-between">
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" c="dimmed">
|
||||
{label}
|
||||
</Text>
|
||||
<Text fw={700} size="lg">
|
||||
{value}
|
||||
</Text>
|
||||
</Stack>
|
||||
<ThemeIcon size="lg" radius="md" variant="light" color={color}>
|
||||
<Icon size={24} />
|
||||
const StatCard = ({ icon: Icon, label, value, color = 'blue', change }: any) => (
|
||||
<Card withBorder p="lg" radius="md">
|
||||
<Group justify="space-between" mb="sm">
|
||||
<ThemeIcon size="xl" radius="md" variant="gradient" gradient={{ from: color, to: color, deg: 135 }}>
|
||||
<Icon size={28} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" c="dimmed" fw={500} tt="uppercase">
|
||||
{label}
|
||||
</Text>
|
||||
<Group justify="space-between">
|
||||
<Text fw={700} size="xl">
|
||||
{value}
|
||||
</Text>
|
||||
{change && <Badge color={change > 0 ? 'green' : 'red'} size="lg">{change > 0 ? '+' : ''}{change}%</Badge>}
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -53,6 +68,18 @@ export function FleetDashboard() {
|
||||
},
|
||||
});
|
||||
|
||||
const { data: drivers = [] } = useQuery({
|
||||
queryKey: ['drivers'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const res = await api.get('/drivers');
|
||||
return res.data || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { data: fuelStats } = useQuery({
|
||||
queryKey: ['fleet-fuel-stats'],
|
||||
queryFn: async () => {
|
||||
@@ -80,6 +107,8 @@ export function FleetDashboard() {
|
||||
const metrics = useMemo((): FleetMetrics => {
|
||||
const totalVehicles = (vehicles as Vehicle[]).length;
|
||||
const activeVehicles = (vehicles as Vehicle[]).filter(v => v.status === 'ACTIVE').length;
|
||||
const totalDrivers = (drivers as Driver[]).length;
|
||||
const assignedDrivers = (drivers as Driver[]).filter(d => d.assignedVehicle).length;
|
||||
|
||||
const fuelTotal = fuelStats?.totalCost || 0;
|
||||
const maintenanceTotal = maintenanceStats?.totalCost || 0;
|
||||
@@ -92,8 +121,10 @@ export function FleetDashboard() {
|
||||
totalMaintenanceSpend: maintenanceTotal,
|
||||
averageFuelEfficiency: fuelStats?.averageEfficiency || 0,
|
||||
costPerKm: (fuelTotal + maintenanceTotal) / 100000, // Placeholder
|
||||
totalDrivers,
|
||||
assignedDrivers,
|
||||
};
|
||||
}, [vehicles, fuelStats, maintenanceStats]);
|
||||
}, [vehicles, drivers, fuelStats, maintenanceStats]);
|
||||
|
||||
const operatingCost = metrics.totalFuelSpend + metrics.totalMaintenanceSpend;
|
||||
const fuelPercent = operatingCost > 0 ? Math.round((metrics.totalFuelSpend / operatingCost) * 100) : 0;
|
||||
@@ -102,24 +133,29 @@ export function FleetDashboard() {
|
||||
<Container size="xl" py="xl">
|
||||
<Breadcrumbs items={[{ label: 'Fleet' }, { label: 'Dashboard' }]} />
|
||||
|
||||
<Text fw={700} size="xl" mb="lg">
|
||||
Fleet Overview
|
||||
</Text>
|
||||
<Box mb="xl">
|
||||
<Title order={1} mb="xs">
|
||||
Fleet Management Dashboard
|
||||
</Title>
|
||||
<Text c="dimmed" size="sm">
|
||||
Real-time fleet overview, vehicle & driver management
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Key Metrics */}
|
||||
<Grid mb="lg">
|
||||
{/* Primary Metrics */}
|
||||
<Grid mb="xl">
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<StatCard icon={Truck} label="Total Vehicles" value={metrics.totalVehicles} color="blue" />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<StatCard icon={Users} label="Total Drivers" value={metrics.totalDrivers} color="cyan" />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<StatCard icon={Fuel} label="Fuel Spend" value={`$${metrics.totalFuelSpend.toFixed(0)}`} color="orange" />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<StatCard icon={Wrench} label="Maintenance" value={`$${metrics.totalMaintenanceSpend.toFixed(0)}`} color="red" />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<StatCard icon={TrendingUp} label="Efficiency" value={`${metrics.averageFuelEfficiency.toFixed(1)} km/L`} color="green" />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{/* Fleet Status */}
|
||||
@@ -216,46 +252,108 @@ export function FleetDashboard() {
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{/* Fleet List */}
|
||||
{/* Vehicles & Drivers Tabs */}
|
||||
<Card withBorder>
|
||||
<Card.Section p="md" withBorder>
|
||||
<Text fw={500}>Fleet Vehicles</Text>
|
||||
</Card.Section>
|
||||
<Card.Section>
|
||||
{(vehicles as Vehicle[]).length > 0 ? (
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Registration</Table.Th>
|
||||
<Table.Th>Plate</Table.Th>
|
||||
<Table.Th>Model</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{(vehicles as Vehicle[]).slice(0, 10).map(v => (
|
||||
<Table.Tr key={v.id}>
|
||||
<Table.Td fw={500}>{v.registrationNumber}</Table.Td>
|
||||
<Table.Td>{v.plateNumber}</Table.Td>
|
||||
<Table.Td>
|
||||
{v.manufacturer} {v.model}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={v.status === 'ACTIVE' ? 'green' : 'gray'}>{v.status || 'UNKNOWN'}</Badge>
|
||||
</Table.Td>
|
||||
<Tabs defaultValue="vehicles" p="md">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="vehicles" leftSection={<Truck size={16} />}>
|
||||
Vehicles ({(vehicles as Vehicle[]).length})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="drivers" leftSection={<Users size={16} />}>
|
||||
Drivers ({(drivers as Driver[]).length})
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="vehicles" pt="md">
|
||||
{(vehicles as Vehicle[]).length > 0 ? (
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Registration</Table.Th>
|
||||
<Table.Th>Plate</Table.Th>
|
||||
<Table.Th>Model</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<Card.Section p="md">
|
||||
<Group gap="xs">
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{(vehicles as Vehicle[]).slice(0, 15).map(v => (
|
||||
<Table.Tr key={v.id}>
|
||||
<Table.Td fw={500}>{v.registrationNumber}</Table.Td>
|
||||
<Table.Td>{v.plateNumber}</Table.Td>
|
||||
<Table.Td>
|
||||
{v.manufacturer} {v.model}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={v.status === 'ACTIVE' ? 'green' : v.status === 'MAINTENANCE' ? 'yellow' : 'gray'}>
|
||||
{v.status || 'UNKNOWN'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<Group gap="xs" p="md">
|
||||
<AlertCircle size={20} />
|
||||
<Text c="dimmed">No vehicles in fleet</Text>
|
||||
</Group>
|
||||
</Card.Section>
|
||||
)}
|
||||
</Card.Section>
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="drivers" pt="md">
|
||||
{(drivers as Driver[]).length > 0 ? (
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Name</Table.Th>
|
||||
<Table.Th>License</Table.Th>
|
||||
<Table.Th>Contact</Table.Th>
|
||||
<Table.Th>Assigned Vehicle</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{(drivers as Driver[]).slice(0, 15).map(d => (
|
||||
<Table.Tr key={d.id}>
|
||||
<Table.Td fw={500}>
|
||||
<Group gap="xs">
|
||||
<ThemeIcon size="sm" radius="xl" variant="light" color="blue">
|
||||
<User size={14} />
|
||||
</ThemeIcon>
|
||||
{d.firstName} {d.lastName}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>{d.licenseNumber || 'N/A'}</Table.Td>
|
||||
<Table.Td>
|
||||
<Stack gap={0} size="xs">
|
||||
{d.phone && (
|
||||
<Text size="xs">
|
||||
<Group gap={4} inline>
|
||||
<MapPin size={12} /> {d.phone}
|
||||
</Group>
|
||||
</Text>
|
||||
)}
|
||||
{d.email && <Text size="xs">{d.email}</Text>}
|
||||
</Stack>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{d.assignedVehicle ? (
|
||||
<Badge color="green">Assigned</Badge>
|
||||
) : (
|
||||
<Badge color="gray">Unassigned</Badge>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<Group gap="xs" p="md">
|
||||
<AlertCircle size={20} />
|
||||
<Text c="dimmed">No drivers in system</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user