diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index 8d5b568dc..513dd1300 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -59,6 +59,8 @@ import FleetResourcePage from "./pages/fleet/FleetResourcePage"; import RoutesPage from "./pages/fleet/RoutesPage"; import FuelPurchasePage from "./pages/fleet/FuelPurchasePage"; import FuelStatsPage from "./pages/fleet/FuelStatsPage"; +import { MaintenancePage } from "./pages/fleet/MaintenancePage"; +import { FinancialReportsPage } from "./pages/fleet/FinancialReportsPage"; import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources"; import RuleEngineLegacyRedirect from "./pages/ruleEngine/RuleEngineLegacyRedirect"; import RuleEngineResourcePage from "./pages/ruleEngine/RuleEngineResourcePage"; @@ -214,6 +216,18 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [ icon: , permission: FREIGHT_PERMS.fleet.view, }, + { + label: "Maintenance", + href: "/dashboard/maintenance", + icon: , + permission: FREIGHT_PERMS.fleet.view, + }, + { + label: "Financial Reports", + href: "/dashboard/financial-reports", + icon: , + permission: FREIGHT_PERMS.fleet.view, + }, // { // label: "Containers", // href: "/dashboard/containers", @@ -755,6 +769,22 @@ const App = () => { } /> + + + + } + /> + + + + } + /> ["overview", "customers", range ?? "30d"] as const, staffTab: (range?: string) => ["overview", "staff", range ?? "30d"] as const, }, + + FUEL: { + ROOT: ["fuel"] as const, + purchases: (vehicleId?: string) => ["fuel", "purchases", vehicleId ?? "all"] as const, + stats: (vehicleId?: string) => ["fuel", "stats", vehicleId ?? "all"] as const, + }, + + MAINTENANCE: { + ROOT: ["maintenance"] as const, + schedules: (vehicleId?: string) => ["maintenance", "schedules", vehicleId ?? "all"] as const, + upcoming: (vehicleId?: string) => ["maintenance", "upcoming", vehicleId ?? "all"] as const, + history: (vehicleId?: string) => ["maintenance", "history", vehicleId ?? "all"] as const, + stats: (vehicleId?: string) => ["maintenance", "stats", vehicleId ?? "all"] as const, + }, + + FINANCIAL_REPORTS: { + ROOT: ["financial-reports"] as const, + fleet: (vehicleId?: string, months?: number) => + ["financial-reports", "fleet", vehicleId ?? "all", months ?? 12] as const, + }, } as const; diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FinancialReportsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FinancialReportsPage.tsx new file mode 100644 index 000000000..31d7a7a23 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FinancialReportsPage.tsx @@ -0,0 +1,245 @@ +import { useMemo, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { Card, Button, Stack, Group, Grid, Select, Text, ThemeIcon, RingProgress } from '@mantine/core'; +import { QUERY_KEYS } from '@/constants/QUERY_KEYS'; +import { api } from '@/services/api'; +import { vehiclesService } from '@/services/vehicles.service'; + +interface FuelStats { + vehicleId: string; + totalPurchases: number; + totalFuel: number; + totalCost: number; + averageCostPerLiter: number; +} + +interface MaintenanceStats { + vehicleId: string; + totalCost: number; + numberOfMaintenanceItems: number; + averageCostPerMaintenance: number; + costByType: Record; +} + +interface CombinedReport { + vehicleId: string; + fuelCost: number; + maintenanceCost: number; + totalOperatingCost: number; + fuelPercentage: number; + maintenancePercentage: number; +} + +export function FinancialReportsPage() { + const [selectedVehicle, setSelectedVehicle] = useState(null); + const [months, setMonths] = useState('12'); + + const { data: vehicles } = useQuery({ + queryKey: QUERY_KEYS.VEHICLES.list(), + queryFn: () => vehiclesService.getAll({ limit: 1000 }), + }); + + 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 }) => ( + + + + {label} + + + {value} + + + + ); + + return ( + + + + Fleet Financial Analysis + + + + setMonths(v || '12')} + style={{ flex: 1 }} + /> + + + + + {report && ( + <> + + + + + + + + + + + + + + + Monthly Avg + + + ${(report.totalOperatingCost / parseInt(months)).toFixed(2)} + + + + + + + + + + + Cost Breakdown + + + + + + + Fuel + + {report.fuelPercentage}% + + + {report.fuelPercentage}% + + } + size={100} + thickness={4} + /> + + + + + Maintenance + + {report.maintenancePercentage}% + + + {report.maintenancePercentage}% + + } + size={100} + thickness={4} + /> + + + + + + + + + + Operational Insights + + + +
+ + Fuel Purchases + + {fuelStats?.totalPurchases || 0} transactions +
+
+ + Fuel Efficiency + + + {fuelStats?.fuelEfficiency?.toFixed(2) || 'N/A'} km/L + +
+
+ + Maintenance Items + + {maintenanceStats?.numberOfMaintenanceItems || 0} records +
+
+ + Avg Maintenance Cost + + ${maintenanceStats?.averageCostPerMaintenance?.toFixed(2) || '0.00'} +
+
+
+
+
+
+ + )} + + {!selectedVehicle && ( + + + Select a vehicle to view financial reports + + + )} +
+ ); +} diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/MaintenancePage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/MaintenancePage.tsx new file mode 100644 index 000000000..162ee5f21 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/MaintenancePage.tsx @@ -0,0 +1,197 @@ +import { useState, useMemo } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { Card, Button, Modal, Stack, Group, Grid, Select, TextInput, NumberInput, Table, Badge, Text } from '@mantine/core'; +import { DateInput } from '@mantine/dates'; +import { QUERY_KEYS } from '@/constants/QUERY_KEYS'; +import { api } from '@/services/api'; +import { vehiclesService } from '@/services/vehicles.service'; + +interface MaintenanceSchedule { + id: string; + vehicleId: string; + maintenanceType: string; + description: string; + scheduledDate: string; + completedDate?: string; + status: string; + estimatedCost?: number; + actualCost?: number; + serviceProvider?: string; +} + +export function MaintenancePage() { + const [selectedVehicle, setSelectedVehicle] = useState(null); + const [openScheduleModal, setOpenScheduleModal] = useState(false); + const [formData, setFormData] = useState({ + maintenanceType: 'PREVENTIVE', + description: '', + scheduledDate: new Date(), + estimatedCost: 0, + serviceProvider: '', + notes: '', + }); + + const queryClient = useQueryClient(); + + const { data: vehicles } = useQuery({ + queryKey: QUERY_KEYS.VEHICLES.list(), + queryFn: () => vehiclesService.getAll({ limit: 1000 }), + }); + + const { data: upcoming, isLoading } = useQuery({ + queryKey: QUERY_KEYS.MAINTENANCE.upcoming(selectedVehicle || ''), + queryFn: () => selectedVehicle ? api.get(`/maintenance/upcoming/${selectedVehicle}`) : Promise.resolve([]), + enabled: !!selectedVehicle, + }); + + const scheduleMutation = useMutation({ + mutationFn: async () => { + if (!selectedVehicle) return; + return api.post('/maintenance/schedules', { + vehicleId: selectedVehicle, + ...formData, + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: QUERY_KEYS.MAINTENANCE.upcoming(selectedVehicle || '') }); + setOpenScheduleModal(false); + setFormData({ + maintenanceType: 'PREVENTIVE', + description: '', + scheduledDate: new Date(), + estimatedCost: 0, + serviceProvider: '', + notes: '', + }); + }, + }); + + const vehicleOptions = useMemo( + () => vehicles?.map(v => ({ label: v.registrationNumber || v.id, value: v.id })) || [], + [vehicles] + ); + + const statusColor = (status: string) => { + const colors: Record = { + SCHEDULED: 'blue', + IN_PROGRESS: 'yellow', + COMPLETED: 'green', + OVERDUE: 'red', + }; + return colors[status] || 'gray'; + }; + + return ( + + + + + Schedule Maintenance + + + + + setFormData({ ...formData, maintenanceType: v || 'PREVENTIVE' })} + /> + setFormData({ ...formData, description: e.currentTarget.value })} + /> + setFormData({ ...formData, scheduledDate: d || new Date() })} + /> + setFormData({ ...formData, estimatedCost: Number(v) })} + /> + setFormData({ ...formData, serviceProvider: e.currentTarget.value })} + /> + setFormData({ ...formData, notes: e.currentTarget.value })} + /> + + + + + + + + ); +}