- ${operatingCost.toFixed(0)}
+ {etb(operatingCost)}
Total Cost
@@ -325,13 +339,12 @@ export function FleetDashboard() {
{d.licenseNumber || 'N/A'}
-
+
{d.phone && (
-
-
- {d.phone}
-
-
+
+
+ {d.phone}
+
)}
{d.email && {d.email}}
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx
index ff7caa9d2..f7bee5b41 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx
@@ -357,7 +357,7 @@ const FleetResourcePage = () => {
const itemLabel = config.label.toLowerCase();
return (
-
+
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FuelPurchasePage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FuelPurchasePage.tsx
index b3c168df3..6ca9f8870 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/FuelPurchasePage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FuelPurchasePage.tsx
@@ -1,11 +1,11 @@
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
- Box,
Button,
Card,
Container,
Group,
+ Loader,
Modal,
NumberInput,
Select,
@@ -17,13 +17,12 @@ import {
Badge,
Grid,
} from "@mantine/core";
-import { Plus, Trash2 } from "lucide-react";
+import { Plus } from "lucide-react";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import { useToast } from "@/hooks/use-toast";
import { api } from "@/auth/http";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
import { vehiclesService, type Vehicle as VehicleType } from "@/services/vehicles.service";
-import { freightBrand } from "@/theme/freight-brand";
interface FuelPurchase {
id: string;
@@ -67,7 +66,7 @@ export default function FuelPurchasePage() {
});
// Fetch fuel purchases
- const { data: purchasesData = [] } = useQuery({
+ const { data: purchasesData = [], isLoading: isLoadingPurchases } = useQuery({
queryKey: ["fuel-purchases"],
queryFn: async () => {
const res = await api.get("/fuel/purchases");
@@ -104,8 +103,8 @@ export default function FuelPurchasePage() {
onError: (error: any) => {
toast({
title: "Error recording purchase",
- message: error?.response?.data?.message || "Failed to record fuel purchase",
- color: "red",
+ description: error?.response?.data?.message || "Failed to record fuel purchase",
+ variant: "destructive",
});
},
});
@@ -118,6 +117,17 @@ export default function FuelPurchasePage() {
const totalCost = formData.liters * formData.costPerLiter;
+ // Aggregate stats (guarded against divide-by-zero when there are no purchases)
+ const totalLiters = (purchasesData as FuelPurchase[]).reduce(
+ (sum, p) => sum + Number(p.liters),
+ 0
+ );
+ const totalPurchaseCost = (purchasesData as FuelPurchase[]).reduce(
+ (sum, p) => sum + Number(p.totalCost),
+ 0
+ );
+ const avgPricePerLiter = totalLiters > 0 ? totalPurchaseCost / totalLiters : 0;
+
return (
@@ -147,10 +157,7 @@ export default function FuelPurchasePage() {
Total Liters
- {purchasesData
- .reduce((sum: number, p: FuelPurchase) => sum + Number(p.liters), 0)
- .toFixed(2)}{" "}
- L
+ {totalLiters.toFixed(2)} L
@@ -160,9 +167,7 @@ export default function FuelPurchasePage() {
Total Cost
- ETB {purchasesData
- .reduce((sum: number, p: FuelPurchase) => sum + Number(p.totalCost), 0)
- .toLocaleString("en-US", { maximumFractionDigits: 2 })}
+ ETB {totalPurchaseCost.toLocaleString("en-US", { maximumFractionDigits: 2 })}
@@ -172,11 +177,7 @@ export default function FuelPurchasePage() {
Avg Price/L
- ETB{" "}
- {(
- purchasesData.reduce((sum: number, p: FuelPurchase) => sum + Number(p.totalCost), 0) /
- purchasesData.reduce((sum: number, p: FuelPurchase) => sum + Number(p.liters), 0) || 0
- ).toFixed(2)}
+ ETB {avgPricePerLiter.toFixed(2)}
@@ -197,6 +198,23 @@ export default function FuelPurchasePage() {
+ {isLoadingPurchases ? (
+
+
+
+
+
+
+
+ ) : purchasesData.length === 0 ? (
+
+
+
+ No fuel purchases recorded yet.
+
+
+
+ ) : null}
{(purchasesData as FuelPurchase[])?.map((purchase) => (
{(purchase as any).vehicle?.registrationNumber || (purchase as any).vehicle?.plateNumber || purchase.vehicleId}
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FuelStatsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FuelStatsPage.tsx
index 04c872966..904347cf0 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/FuelStatsPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FuelStatsPage.tsx
@@ -1,20 +1,11 @@
import { useQuery } from "@tanstack/react-query";
-import { Box, Card, Container, Grid, Group, Select, Stack, Table, Text, Title, Badge } from "@mantine/core";
+import { Card, Container, Grid, Group, Loader, Select, Stack, Text, Title } from "@mantine/core";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import { api } from "@/auth/http";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
import { vehiclesService, type Vehicle as VehicleType } from "@/services/vehicles.service";
import { useState } from "react";
-interface FuelStats {
- vehicleId: string;
- totalPurchases: number;
- totalLiters: number;
- totalCost: number;
- averagePricePerLiter: number;
- dateRange: { startDate: string; endDate: string };
-}
-
export default function FuelStatsPage() {
const [selectedVehicleId, setSelectedVehicleId] = useState("");
const [monthsBack, setMonthsBack] = useState("12");
@@ -29,7 +20,7 @@ export default function FuelStatsPage() {
});
// Fetch fuel stats
- const { data: statsData } = useQuery({
+ const { data: statsData, isFetching: isStatsFetching } = useQuery({
queryKey: ["fuel-stats", selectedVehicleId, monthsBack],
queryFn: async () => {
if (!selectedVehicleId) return null;
@@ -102,7 +93,7 @@ export default function FuelStatsPage() {
Total Purchases
- {statsData.totalPurchases}
+ {Number(statsData.totalPurchases) || 0}
@@ -112,7 +103,7 @@ export default function FuelStatsPage() {
Total Fuel
- {statsData.totalLiters.toFixed(2)} L
+ {(Number(statsData.totalLiters) || 0).toFixed(2)} L
@@ -122,7 +113,7 @@ export default function FuelStatsPage() {
Total Cost
- ETB {statsData.totalCost.toLocaleString("en-US", { maximumFractionDigits: 2 })}
+ ETB {(Number(statsData.totalCost) || 0).toLocaleString("en-US", { maximumFractionDigits: 2 })}
@@ -132,7 +123,7 @@ export default function FuelStatsPage() {
Avg Price/L
- ETB {statsData.averagePricePerLiter.toFixed(2)}
+ ETB {(Number(statsData.averagePricePerLiter) || 0).toFixed(2)}
@@ -172,15 +163,15 @@ export default function FuelStatsPage() {
{selectedVehicle?.plateNumber} consumed{" "}
- {statsData.totalLiters.toFixed(2)} liters
+ {(Number(statsData.totalLiters) || 0).toFixed(2)} liters
{" "}
over the last {monthsBack} months, costing{" "}
- ETB {statsData.totalCost.toLocaleString("en-US", { maximumFractionDigits: 2 })}
+ ETB {(Number(statsData.totalCost) || 0).toLocaleString("en-US", { maximumFractionDigits: 2 })}
. Average fuel price was{" "}
- ETB {statsData.averagePricePerLiter.toFixed(2)} per liter
+ ETB {(Number(statsData.averagePricePerLiter) || 0).toFixed(2)} per liter
.
@@ -188,6 +179,12 @@ export default function FuelStatsPage() {
>
+ ) : selectedVehicleId && isStatsFetching ? (
+
+
+
+
+
) : (
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/MaintenancePage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/MaintenancePage.tsx
index b2bd155d3..3aa2506b5 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/MaintenancePage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/MaintenancePage.tsx
@@ -1,12 +1,26 @@
-import { useState, useMemo } from 'react';
+import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { Card, Button, Modal, Stack, Group, Grid, Select, TextInput, NumberInput, Table, Badge, Text, Container } from '@mantine/core';
-import { DateInput } from '@mantine/dates';
+import {
+ Card,
+ Button,
+ Modal,
+ Stack,
+ Group,
+ Select,
+ TextInput,
+ NumberInput,
+ Table,
+ Badge,
+ Text,
+ Title,
+ Container,
+} from '@mantine/core';
import { Plus } from 'lucide-react';
+import Breadcrumbs from '@/components/ui/Breadcrumbs';
+import { useToast } from '@/hooks/use-toast';
import { QUERY_KEYS } from '@/constants/QUERY_KEYS';
-import { api } from '@/services/api';
-import { vehiclesService } from '@/services/vehicles.service';
-import { freightBrand } from '@/theme/freight-brand';
+import { api } from '@/auth/http';
+import { vehiclesService, type Vehicle as VehicleType } from '@/services/vehicles.service';
interface MaintenanceSchedule {
id: string;
@@ -21,21 +35,23 @@ interface MaintenanceSchedule {
serviceProvider?: string;
}
+const emptyForm = {
+ maintenanceType: 'PREVENTIVE',
+ description: '',
+ scheduledDate: new Date().toISOString().split('T')[0],
+ estimatedCost: 0,
+ serviceProvider: '',
+ notes: '',
+};
+
export function MaintenancePage() {
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
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 [formData, setFormData] = useState(emptyForm);
- const queryClient = useQueryClient();
-
- const { data: vehicles } = useQuery({
+ const { data: vehiclesData } = useQuery({
queryKey: QUERY_KEYS.VEHICLES.list(),
queryFn: async () => {
const res = await vehiclesService.getAll({ limit: 1000 });
@@ -45,36 +61,49 @@ export function MaintenancePage() {
const { data: upcoming, isLoading } = useQuery({
queryKey: QUERY_KEYS.MAINTENANCE.upcoming(selectedVehicle || ''),
- queryFn: () => selectedVehicle ? api.get(`/maintenance/upcoming/${selectedVehicle}`) : Promise.resolve([]),
+ queryFn: async () => {
+ if (!selectedVehicle) return [];
+ const res = await api.get(`/maintenance/upcoming/${selectedVehicle}`);
+ return res.data || [];
+ },
enabled: !!selectedVehicle,
});
+ const upcomingList: MaintenanceSchedule[] = Array.isArray(upcoming) ? upcoming : [];
+
const scheduleMutation = useMutation({
mutationFn: async () => {
if (!selectedVehicle) return;
- return api.post('/maintenance/schedules', {
+ const res = await api.post('/maintenance/schedules', {
vehicleId: selectedVehicle,
...formData,
});
+ return res.data;
},
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: QUERY_KEYS.MAINTENANCE.upcoming(selectedVehicle || '') });
+ toast({ title: 'Maintenance scheduled' });
+ queryClient.invalidateQueries({
+ queryKey: QUERY_KEYS.MAINTENANCE.upcoming(selectedVehicle || ''),
+ });
setOpenScheduleModal(false);
- setFormData({
- maintenanceType: 'PREVENTIVE',
- description: '',
- scheduledDate: new Date(),
- estimatedCost: 0,
- serviceProvider: '',
- notes: '',
+ setFormData(emptyForm);
+ },
+ onError: (err: any) => {
+ toast({
+ title: 'Error',
+ description: err?.response?.data?.message ?? 'Failed',
+ variant: 'destructive',
});
},
});
- const vehicleOptions = useMemo(
- () => vehicles?.map(v => ({ label: v.registrationNumber || v.id, value: v.id })) || [],
- [vehicles]
- );
+ const vehicleOptions =
+ vehiclesData?.map((v: VehicleType) => ({
+ value: v.id,
+ label: v.plateNumber
+ ? `${v.plateNumber} - ${v.manufacturer} ${v.model}`
+ : v.registrationNumber || v.id,
+ })) || [];
const statusColor = (status: string) => {
const colors: Record = {
@@ -88,66 +117,85 @@ export function MaintenancePage() {
return (
+
+
+
+ Maintenance
+
+
+
-
-
-
- Schedule Maintenance
-
-
-
-
+
-
-
-
- {selectedVehicle && (
-
-
- Upcoming Maintenance
-
-
- {isLoading ? (
- Loading...
- ) : (upcoming || []).length > 0 ? (
-
-
-
- Type
- Description
- Scheduled
- Est. Cost
- Status
-
-
-
- {(upcoming as MaintenanceSchedule[]).map(m => (
-
- {m.maintenanceType}
- {m.description}
- {new Date(m.scheduledDate).toLocaleDateString()}
- ${m.estimatedCost?.toFixed(2) || '—'}
-
- {m.status}
-
-
- ))}
-
-
- ) : (
- No upcoming maintenance
- )}
-
- )}
+
+ {!selectedVehicle ? (
+
+
+ Select a vehicle to view its maintenance schedule
+
+
+ ) : (
+
+
+ Upcoming Maintenance
+
+
+ {isLoading ? (
+ Loading...
+ ) : upcomingList.length > 0 ? (
+
+
+
+ Type
+ Description
+ Scheduled
+ Est. Cost
+ Status
+
+
+
+ {upcomingList.map((m) => (
+
+ {m.maintenanceType}
+ {m.description}
+ {new Date(m.scheduledDate).toLocaleDateString()}
+
+ {m.estimatedCost != null
+ ? `ETB ${Number(m.estimatedCost).toLocaleString('en-US', {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ })}`
+ : '—'}
+
+
+ {m.status}
+
+
+ ))}
+
+
+ ) : (
+ No upcoming maintenance
+ )}
+
+
+ )}
+
setFormData({ ...formData, maintenanceType: v || 'PREVENTIVE' })}
+ onChange={(v) => setFormData({ ...formData, maintenanceType: v || 'PREVENTIVE' })}
/>
setFormData({ ...formData, description: e.currentTarget.value })}
+ onChange={(e) => setFormData({ ...formData, description: e.currentTarget.value })}
/>
- setFormData({ ...formData, scheduledDate: d || new Date() })}
+ onChange={(e) => setFormData({ ...formData, scheduledDate: e.currentTarget.value })}
/>
setFormData({ ...formData, estimatedCost: Number(v) })}
+ onChange={(v) => setFormData({ ...formData, estimatedCost: Number(v) })}
/>
setFormData({ ...formData, serviceProvider: e.currentTarget.value })}
+ onChange={(e) => setFormData({ ...formData, serviceProvider: e.currentTarget.value })}
/>
setFormData({ ...formData, notes: e.currentTarget.value })}
+ onChange={(e) => setFormData({ ...formData, notes: e.currentTarget.value })}
/>
-
-
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx
index a40579027..bdba6dc97 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx
@@ -24,7 +24,6 @@ import {
SimpleGrid,
Stack,
Text,
- TextInput,
ThemeIcon,
Tooltip,
} from "@mantine/core";
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/TrackingPage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/TrackingPage.tsx
index 8230a7446..f60c7f72b 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/TrackingPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/TrackingPage.tsx
@@ -1,6 +1,6 @@
import { useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
-import { Container, Grid, Card, Stack, Group, Select, Text, Badge, Button, Box, Table, ThemeIcon, SimpleGrid } from '@mantine/core';
+import { Container, Grid, Card, Stack, Group, Select, Text, Badge, Button, Box, Table, SimpleGrid } from '@mantine/core';
import { MapPin, Navigation, Radio, Activity } from 'lucide-react';
import Breadcrumbs from '@/components/ui/Breadcrumbs';
import { QUERY_KEYS } from '@/constants/QUERY_KEYS';
@@ -24,8 +24,8 @@ interface GPSLocation {
lastUpdate?: string;
}
-// Mock GPS data for demo
-const generateMockGPS = (index: number): GPSLocation => ({
+// Mock GPS data for demo (no real GPS backend exists — these are simulated values)
+const generateMockGPS = (): GPSLocation => ({
lat: 9.0 + Math.random() * 0.5,
lng: 38.7 + Math.random() * 0.5,
speed: Math.floor(Math.random() * 120),
@@ -36,7 +36,6 @@ const generateMockGPS = (index: number): GPSLocation => ({
export function TrackingPage() {
const [selectedVehicleId, setSelectedVehicleId] = useState(null);
const [mapCenter] = useState({ lat: 9.0, lng: 38.8 });
- const mapZoom = 10;
const { data: vehicles = [] } = useQuery({
queryKey: QUERY_KEYS.VEHICLES.list(),
@@ -48,9 +47,9 @@ export function TrackingPage() {
// Generate mock GPS data for each vehicle
const vehiclesWithGPS = useMemo(() => {
- return (vehicles as Vehicle[]).map((v, idx) => ({
+ return (vehicles as Vehicle[]).map((v) => ({
...v,
- gps: generateMockGPS(idx),
+ gps: generateMockGPS(),
}));
}, [vehicles]);
@@ -84,9 +83,14 @@ export function TrackingPage() {
-
- Real-Time Vehicle Tracking
-
+
+
+ Real-Time Vehicle Tracking
+
+
+ Simulated GPS
+
+
Monitor vehicle locations, speed, and status
@@ -109,17 +113,18 @@ export function TrackingPage() {
-
+
+
{/* Grid background */}
+
+
+ Simulated map — coordinates, speed, and heading are demo values, not live GPS.
+
@@ -314,7 +323,7 @@ export function TrackingPage() {
Tracked Vehicles ({trackableVehicles.length})
-
+
{trackableVehicles.map(v => (
{
: "";
return (
-
+
navigate("/dashboard/vehicles")} aria-label="Back">
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts b/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts
index 0e7c9934d..3ac7f2458 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts
@@ -53,7 +53,7 @@ export interface FleetFormFieldDef extends FormFieldDef {
}
export interface FleetListFilterDef {
- key: "status" | "currentYardId" | "wagonTypeId" | "trainId";
+ key: "status" | "availability" | "currentYardId" | "wagonTypeId" | "trainId";
label: string;
options?: Array<{ value: string; label: string }>;
allLabel?: string;
diff --git a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/CargoTypesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/CargoTypesPage.tsx
index 2568c76f7..d44d0cffb 100644
--- a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/CargoTypesPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/CargoTypesPage.tsx
@@ -19,7 +19,6 @@ import {
import {
Boxes,
ChevronRight,
- FileText,
Home,
Layers,
Package,
diff --git a/apps/edr-freight-web/backoffice/src/services/bookings.service.ts b/apps/edr-freight-web/backoffice/src/services/bookings.service.ts
index fe788b2aa..6a4c0cb1c 100644
--- a/apps/edr-freight-web/backoffice/src/services/bookings.service.ts
+++ b/apps/edr-freight-web/backoffice/src/services/bookings.service.ts
@@ -175,11 +175,6 @@ export const bookingsService = {
},
// ── Document clearance (GL workflow) ──
- getClearance: async (id: string): Promise => {
- const response = await client.get(`/bookings/${id}/clearance`);
- return unwrap(response.data) as Freight.ClearanceView;
- },
-
reviewClearanceDocument: (
id: string,
payload: { fileKey: string; status: "APPROVED" | "QUERIED"; note?: string },
diff --git a/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts b/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts
index fd34b5e62..6b48ed801 100644
--- a/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts
+++ b/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts
@@ -35,6 +35,9 @@ export interface Vehicle {
powerPlateNo?: string | null;
trailerPlateNo?: string | null;
locationId?: string | null;
+ /** Odometer-derived distances (API sends numeric strings; coerce with Number). */
+ estimatedDistanceKm?: number | null;
+ actualDistanceKm?: number | null;
createdAt: string;
updatedAt: string;
}