mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 16:35:42 +00:00
Add px="lg" (24px) to all Container components: - FleetDashboard - FuelPurchasePage - FuelStatsPage - MaintenancePage - FinancialReportsPage - TrackingPage Reduces left/right padding from default to 24px, making better use of horizontal screen space. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
252 lines
8.5 KiB
TypeScript
252 lines
8.5 KiB
TypeScript
import { useMemo, useState } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { Card, Button, Stack, Group, Grid, Select, Text, ThemeIcon, RingProgress, Container } from '@mantine/core';
|
|
import { QUERY_KEYS } from '@/constants/QUERY_KEYS';
|
|
import { api } from '@/services/api';
|
|
import { vehiclesService } from '@/services/vehicles.service';
|
|
import { freightBrand } from '@/theme/freight-brand';
|
|
|
|
interface FuelStats {
|
|
vehicleId: string;
|
|
totalPurchases: number;
|
|
totalFuel: number;
|
|
totalCost: number;
|
|
averageCostPerLiter: number;
|
|
}
|
|
|
|
interface MaintenanceStats {
|
|
vehicleId: string;
|
|
totalCost: number;
|
|
numberOfMaintenanceItems: number;
|
|
averageCostPerMaintenance: number;
|
|
costByType: Record<string, number>;
|
|
}
|
|
|
|
interface CombinedReport {
|
|
vehicleId: string;
|
|
fuelCost: number;
|
|
maintenanceCost: number;
|
|
totalOperatingCost: number;
|
|
fuelPercentage: number;
|
|
maintenancePercentage: number;
|
|
}
|
|
|
|
export function FinancialReportsPage() {
|
|
const [selectedVehicle, setSelectedVehicle] = useState<string | null>(null);
|
|
const [months, setMonths] = useState('12');
|
|
|
|
const { data: vehicles } = useQuery({
|
|
queryKey: QUERY_KEYS.VEHICLES.list(),
|
|
queryFn: async () => {
|
|
const res = await vehiclesService.getAll({ limit: 1000 });
|
|
return res.data || [];
|
|
},
|
|
});
|
|
|
|
const { data: fuelStats } = useQuery({
|
|
queryKey: QUERY_KEYS.FUEL.stats(selectedVehicle || ''),
|
|
queryFn: () => selectedVehicle ? api.get(`/fuel/stats/${selectedVehicle}?months=${months}`) : Promise.resolve(null),
|
|
enabled: !!selectedVehicle,
|
|
});
|
|
|
|
const { data: maintenanceStats } = useQuery({
|
|
queryKey: QUERY_KEYS.MAINTENANCE.stats(selectedVehicle || ''),
|
|
queryFn: () => selectedVehicle ? api.get(`/maintenance/stats/${selectedVehicle}`) : Promise.resolve(null),
|
|
enabled: !!selectedVehicle,
|
|
});
|
|
|
|
const vehicleOptions = useMemo(
|
|
() => vehicles?.map(v => ({ label: v.registrationNumber || v.id, value: v.id })) || [],
|
|
[vehicles]
|
|
);
|
|
|
|
const report = useMemo(() => {
|
|
if (!fuelStats || !maintenanceStats) return null;
|
|
|
|
const fuelCost = Number(fuelStats.totalCost) || 0;
|
|
const maintenanceCost = Number(maintenanceStats.totalCost) || 0;
|
|
const total = fuelCost + maintenanceCost;
|
|
|
|
return {
|
|
vehicleId: selectedVehicle!,
|
|
fuelCost,
|
|
maintenanceCost,
|
|
totalOperatingCost: total,
|
|
fuelPercentage: total > 0 ? Math.round((fuelCost / total) * 100) : 0,
|
|
maintenancePercentage: total > 0 ? Math.round((maintenanceCost / total) * 100) : 0,
|
|
};
|
|
}, [fuelStats, maintenanceStats, selectedVehicle]);
|
|
|
|
const StatCard = ({ label, value }: { label: string; value: string }) => (
|
|
<Card withBorder>
|
|
<Card.Section p="md">
|
|
<Text size="sm" c="dimmed">
|
|
{label}
|
|
</Text>
|
|
<Text fw={700} size="lg">
|
|
{value}
|
|
</Text>
|
|
</Card.Section>
|
|
</Card>
|
|
);
|
|
|
|
return (
|
|
<Container size="xl" py="xl" px="lg">
|
|
<Stack gap="md">
|
|
<Card>
|
|
<Card.Section p="md" withBorder>
|
|
<Text fw={500}>Fleet Financial Analysis</Text>
|
|
</Card.Section>
|
|
<Card.Section p="md">
|
|
<Group>
|
|
<Select
|
|
label="Vehicle"
|
|
placeholder="Select a vehicle"
|
|
data={vehicleOptions}
|
|
value={selectedVehicle}
|
|
onChange={setSelectedVehicle}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<Select
|
|
label="Period"
|
|
data={[
|
|
{ label: 'Last 3 months', value: '3' },
|
|
{ label: 'Last 6 months', value: '6' },
|
|
{ label: 'Last 12 months', value: '12' },
|
|
]}
|
|
value={months}
|
|
onChange={v => setMonths(v || '12')}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
</Group>
|
|
</Card.Section>
|
|
</Card>
|
|
|
|
{report && (
|
|
<>
|
|
<Grid>
|
|
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
|
<StatCard label="Total Operating Cost" value={`$${report.totalOperatingCost.toFixed(2)}`} />
|
|
</Grid.Col>
|
|
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
|
<StatCard label="Fuel Cost" value={`$${report.fuelCost.toFixed(2)}`} />
|
|
</Grid.Col>
|
|
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
|
<StatCard label="Maintenance Cost" value={`$${report.maintenanceCost.toFixed(2)}`} />
|
|
</Grid.Col>
|
|
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
|
<Card withBorder>
|
|
<Card.Section p="md">
|
|
<Text size="sm" c="dimmed">
|
|
Monthly Avg
|
|
</Text>
|
|
<Text fw={700} size="lg">
|
|
${(report.totalOperatingCost / parseInt(months)).toFixed(2)}
|
|
</Text>
|
|
</Card.Section>
|
|
</Card>
|
|
</Grid.Col>
|
|
</Grid>
|
|
|
|
<Grid>
|
|
<Grid.Col span={{ base: 12, sm: 6 }}>
|
|
<Card>
|
|
<Card.Section p="md" withBorder>
|
|
<Text fw={500}>Cost Breakdown</Text>
|
|
</Card.Section>
|
|
<Card.Section p="md">
|
|
<Stack gap="lg">
|
|
<Group justify="space-between">
|
|
<Stack gap={0}>
|
|
<Text size="sm" c="dimmed">
|
|
Fuel
|
|
</Text>
|
|
<Text fw={500}>{report.fuelPercentage}%</Text>
|
|
</Stack>
|
|
<RingProgress
|
|
sections={[{ value: report.fuelPercentage, color: 'edr-accent' }]}
|
|
label={
|
|
<Text size="xs" align="center">
|
|
{report.fuelPercentage}%
|
|
</Text>
|
|
}
|
|
size={100}
|
|
thickness={4}
|
|
/>
|
|
</Group>
|
|
<Group justify="space-between">
|
|
<Stack gap={0}>
|
|
<Text size="sm" c="dimmed">
|
|
Maintenance
|
|
</Text>
|
|
<Text fw={500}>{report.maintenancePercentage}%</Text>
|
|
</Stack>
|
|
<RingProgress
|
|
sections={[{ value: report.maintenancePercentage, color: 'edr-red' }]}
|
|
label={
|
|
<Text size="xs" align="center">
|
|
{report.maintenancePercentage}%
|
|
</Text>
|
|
}
|
|
size={100}
|
|
thickness={4}
|
|
/>
|
|
</Group>
|
|
</Stack>
|
|
</Card.Section>
|
|
</Card>
|
|
</Grid.Col>
|
|
|
|
<Grid.Col span={{ base: 12, sm: 6 }}>
|
|
<Card>
|
|
<Card.Section p="md" withBorder>
|
|
<Text fw={500}>Operational Insights</Text>
|
|
</Card.Section>
|
|
<Card.Section p="md">
|
|
<Stack gap="sm">
|
|
<div>
|
|
<Text size="sm" c="dimmed">
|
|
Fuel Purchases
|
|
</Text>
|
|
<Text fw={500}>{fuelStats?.totalPurchases || 0} transactions</Text>
|
|
</div>
|
|
<div>
|
|
<Text size="sm" c="dimmed">
|
|
Fuel Efficiency
|
|
</Text>
|
|
<Text fw={500}>
|
|
{fuelStats?.fuelEfficiency?.toFixed(2) || 'N/A'} km/L
|
|
</Text>
|
|
</div>
|
|
<div>
|
|
<Text size="sm" c="dimmed">
|
|
Maintenance Items
|
|
</Text>
|
|
<Text fw={500}>{maintenanceStats?.numberOfMaintenanceItems || 0} records</Text>
|
|
</div>
|
|
<div>
|
|
<Text size="sm" c="dimmed">
|
|
Avg Maintenance Cost
|
|
</Text>
|
|
<Text fw={500}>${maintenanceStats?.averageCostPerMaintenance?.toFixed(2) || '0.00'}</Text>
|
|
</div>
|
|
</Stack>
|
|
</Card.Section>
|
|
</Card>
|
|
</Grid.Col>
|
|
</Grid>
|
|
</>
|
|
)}
|
|
|
|
{!selectedVehicle && (
|
|
<Card>
|
|
<Card.Section p="md">
|
|
<Text c="dimmed">Select a vehicle to view financial reports</Text>
|
|
</Card.Section>
|
|
</Card>
|
|
)}
|
|
</Stack>
|
|
</Container>
|
|
);
|
|
}
|