mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
fix
This commit is contained in:
@@ -33,7 +33,6 @@ export interface ContainerAllocationTableProps {
|
||||
* Displays containers with type/qty, vehicle dropdown per row, and save action.
|
||||
*/
|
||||
export function ContainerAllocationTable({
|
||||
bookingId,
|
||||
containers,
|
||||
onSave,
|
||||
}: ContainerAllocationTableProps) {
|
||||
@@ -43,7 +42,10 @@ export function ContainerAllocationTable({
|
||||
|
||||
const { data: vehicles = [], isLoading: vehiclesLoading } = useQuery({
|
||||
queryKey: ["vehicles", "free"],
|
||||
queryFn: () => vehiclesService.getAll({ status: "ACTIVE", availability: "FREE" }),
|
||||
queryFn: async () => {
|
||||
const res = await vehiclesService.getAll({ status: "ACTIVE", availability: "FREE" });
|
||||
return res.data ?? [];
|
||||
},
|
||||
});
|
||||
|
||||
const vehicleOptions = useMemo(
|
||||
@@ -85,13 +87,12 @@ export function ContainerAllocationTable({
|
||||
});
|
||||
|
||||
const allocatedCount = Object.values(allocations).filter(Boolean).length;
|
||||
const allAllocated = allocatedCount === containers.length;
|
||||
|
||||
if (vehiclesLoading) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" p="xl">
|
||||
<Group justify="center" p="xl">
|
||||
<Loader size="sm" />
|
||||
</Box>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export function TransportDocumentCard({ bookingId }: { bookingId: string }) {
|
||||
if (!file) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await contractsService.uploadTransportDocument(bookingId, file);
|
||||
await contractsService.uploadTransportDocument(bookingId, { transportDocument: file });
|
||||
toast.success("Transport document uploaded");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Upload failed");
|
||||
|
||||
@@ -282,7 +282,6 @@ export const URL_CONSTANTS = {
|
||||
`/train-scheduling/schedules/${id}/run-allocation`,
|
||||
DOC_REVIEW_COMPLETE: (id: string) =>
|
||||
`/train-scheduling/schedules/${id}/doc-review-complete`,
|
||||
RUN_ALLOCATION: (id: string) => `/train-scheduling/schedules/${id}/run-allocation`,
|
||||
ASSIGN_UNASSIGNED_BOOKING: (id: string) =>
|
||||
`/train-scheduling/schedules/${id}/assign-unassigned-booking`,
|
||||
BOOKING_WINDOW: (id: string) =>
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
type BookingDetailView,
|
||||
} from "@/components/bookings/detail";
|
||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||
import ContainerAllocationTable from "@/components/ContainerAllocationTable";
|
||||
import { api } from "@/services/api";
|
||||
import { ContainerAllocationTable } from "@/components/ContainerAllocationTable";
|
||||
import { api } from "@/auth/http";
|
||||
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
|
||||
|
||||
const BookingDetailPage = () => {
|
||||
@@ -160,9 +160,9 @@ const BookingDetailPage = () => {
|
||||
type: c.containerType?.label ?? "Unknown",
|
||||
qty: c.quantity,
|
||||
}))}
|
||||
onSave={(allocations) =>
|
||||
allocateMutation.mutateAsync({ allocations })
|
||||
}
|
||||
onSave={async (allocations) => {
|
||||
await allocateMutation.mutateAsync({ allocations });
|
||||
}}
|
||||
/>
|
||||
<BookingApprovalCard
|
||||
steps={approvalSteps}
|
||||
|
||||
@@ -185,7 +185,7 @@ export default function GlClearanceDetailPage() {
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="workflow">
|
||||
<Grid gutter="lg">
|
||||
<Grid>
|
||||
<Grid.Col span={{ base: 12, lg: 7 }}>
|
||||
{data.kind === "booking" ? (
|
||||
<ClearanceReviewSection bookingId={id!} hideSummary readOnly />
|
||||
|
||||
@@ -70,7 +70,7 @@ const DriverDetailPage = () => {
|
||||
driver?.licenseExpiryDate && new Date(driver.licenseExpiryDate) < new Date();
|
||||
|
||||
return (
|
||||
<Container size="lg" py="md">
|
||||
<Container size="xl" py="xl" px="lg">
|
||||
<Group gap="sm" mb="md" wrap="nowrap">
|
||||
<ActionIcon variant="subtle" onClick={() => navigate("/dashboard/drivers")} aria-label="Back">
|
||||
<ArrowLeft size={18} />
|
||||
|
||||
@@ -1,26 +1,10 @@
|
||||
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 { Card, Stack, Group, Grid, Select, Text, RingProgress, Container, Title } from '@mantine/core';
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs';
|
||||
import { QUERY_KEYS } from '@/constants/QUERY_KEYS';
|
||||
import { api } from '@/services/api';
|
||||
import { api } from '@/auth/http';
|
||||
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;
|
||||
@@ -31,6 +15,9 @@ interface CombinedReport {
|
||||
maintenancePercentage: number;
|
||||
}
|
||||
|
||||
const etb = (n: number) =>
|
||||
'ETB ' + Number(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
|
||||
export function FinancialReportsPage() {
|
||||
const [selectedVehicle, setSelectedVehicle] = useState<string | null>(null);
|
||||
const [months, setMonths] = useState('12');
|
||||
@@ -45,13 +32,21 @@ export function FinancialReportsPage() {
|
||||
|
||||
const { data: fuelStats } = useQuery({
|
||||
queryKey: QUERY_KEYS.FUEL.stats(selectedVehicle || ''),
|
||||
queryFn: () => selectedVehicle ? api.get(`/fuel/stats/${selectedVehicle}?months=${months}`) : Promise.resolve(null),
|
||||
queryFn: async () => {
|
||||
if (!selectedVehicle) return Promise.resolve(null);
|
||||
const res = await api.get(`/fuel/stats/${selectedVehicle}?months=${months}`);
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!selectedVehicle,
|
||||
});
|
||||
|
||||
const { data: maintenanceStats } = useQuery({
|
||||
queryKey: QUERY_KEYS.MAINTENANCE.stats(selectedVehicle || ''),
|
||||
queryFn: () => selectedVehicle ? api.get(`/maintenance/stats/${selectedVehicle}`) : Promise.resolve(null),
|
||||
queryFn: async () => {
|
||||
if (!selectedVehicle) return Promise.resolve(null);
|
||||
const res = await api.get(`/maintenance/stats/${selectedVehicle}`);
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!selectedVehicle,
|
||||
});
|
||||
|
||||
@@ -60,11 +55,11 @@ export function FinancialReportsPage() {
|
||||
[vehicles]
|
||||
);
|
||||
|
||||
const report = useMemo(() => {
|
||||
if (!fuelStats || !maintenanceStats) return null;
|
||||
const report = useMemo<CombinedReport | null>(() => {
|
||||
if (!fuelStats && !maintenanceStats) return null;
|
||||
|
||||
const fuelCost = Number(fuelStats.totalCost) || 0;
|
||||
const maintenanceCost = Number(maintenanceStats.totalCost) || 0;
|
||||
const fuelCost = Number(fuelStats?.totalCost ?? 0) || 0;
|
||||
const maintenanceCost = Number(maintenanceStats?.totalCost ?? 0) || 0;
|
||||
const total = fuelCost + maintenanceCost;
|
||||
|
||||
return {
|
||||
@@ -93,6 +88,9 @@ export function FinancialReportsPage() {
|
||||
return (
|
||||
<Container size="xl" py="xl" px="lg">
|
||||
<Stack gap="md">
|
||||
<Breadcrumbs items={[{ label: 'Fleet' }, { label: 'Financial Reports' }]} />
|
||||
<Title order={1}>Financial Reports</Title>
|
||||
|
||||
<Card>
|
||||
<Card.Section p="md" withBorder>
|
||||
<Text fw={500}>Fleet Financial Analysis</Text>
|
||||
@@ -126,13 +124,13 @@ export function FinancialReportsPage() {
|
||||
<>
|
||||
<Grid>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<StatCard label="Total Operating Cost" value={`$${report.totalOperatingCost.toFixed(2)}`} />
|
||||
<StatCard label="Total Operating Cost" value={etb(report.totalOperatingCost)} />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<StatCard label="Fuel Cost" value={`$${report.fuelCost.toFixed(2)}`} />
|
||||
<StatCard label="Fuel Cost" value={etb(report.fuelCost)} />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<StatCard label="Maintenance Cost" value={`$${report.maintenanceCost.toFixed(2)}`} />
|
||||
<StatCard label="Maintenance Cost" value={etb(report.maintenanceCost)} />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<Card withBorder>
|
||||
@@ -141,7 +139,7 @@ export function FinancialReportsPage() {
|
||||
Monthly Avg
|
||||
</Text>
|
||||
<Text fw={700} size="lg">
|
||||
${(report.totalOperatingCost / parseInt(months)).toFixed(2)}
|
||||
{etb(report.totalOperatingCost / parseInt(months))}
|
||||
</Text>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
@@ -166,7 +164,7 @@ export function FinancialReportsPage() {
|
||||
<RingProgress
|
||||
sections={[{ value: report.fuelPercentage, color: 'edr-accent' }]}
|
||||
label={
|
||||
<Text size="xs" align="center">
|
||||
<Text size="xs" ta="center">
|
||||
{report.fuelPercentage}%
|
||||
</Text>
|
||||
}
|
||||
@@ -184,7 +182,7 @@ export function FinancialReportsPage() {
|
||||
<RingProgress
|
||||
sections={[{ value: report.maintenancePercentage, color: 'edr-red' }]}
|
||||
label={
|
||||
<Text size="xs" align="center">
|
||||
<Text size="xs" ta="center">
|
||||
{report.maintenancePercentage}%
|
||||
</Text>
|
||||
}
|
||||
@@ -228,7 +226,7 @@ export function FinancialReportsPage() {
|
||||
<Text size="sm" c="dimmed">
|
||||
Avg Maintenance Cost
|
||||
</Text>
|
||||
<Text fw={500}>${maintenanceStats?.averageCostPerMaintenance?.toFixed(2) || '0.00'}</Text>
|
||||
<Text fw={500}>{etb(maintenanceStats?.averageCostPerMaintenance ?? 0)}</Text>
|
||||
</div>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card, Stack, Group, Grid, Text, ThemeIcon, Progress, Badge, Table, RingProgress, Container, Title, Box, Tabs, Button } from '@mantine/core';
|
||||
import { Truck, Fuel, Wrench, TrendingUp, AlertCircle, Users, User, MapPin, Calendar, BarChart3 } 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, AlertCircle, Users, User, MapPin } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs';
|
||||
import { QUERY_KEYS } from '@/constants/QUERY_KEYS';
|
||||
import { api } from '@/auth/http';
|
||||
@@ -39,7 +40,20 @@ interface FleetMetrics {
|
||||
assignedDrivers: number;
|
||||
}
|
||||
|
||||
const StatCard = ({ icon: Icon, label, value, color = 'edr-green', change }: any) => (
|
||||
/** ETB money, whole-birr for dashboard headlines. */
|
||||
const etb = (n: number) => `ETB ${(Number(n) || 0).toLocaleString('en-US', { maximumFractionDigits: 0 })}`;
|
||||
/** Safe percentage — 0 when the denominator is 0 (empty fleet). */
|
||||
const pct = (n: number, d: number) => (d > 0 ? (n / d) * 100 : 0);
|
||||
|
||||
interface StatCardProps {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
value: string | number;
|
||||
color?: string;
|
||||
change?: number;
|
||||
}
|
||||
|
||||
const StatCard = ({ icon: Icon, label, value, color = 'edr-green', change }: StatCardProps) => (
|
||||
<Card withBorder p="lg" style={{ borderTop: `3px solid ${freightBrand.primary}` }}>
|
||||
<Group justify="space-between" mb="sm">
|
||||
<ThemeIcon size="xl" radius="md" color={color} variant="light">
|
||||
@@ -111,8 +125,8 @@ export function FleetDashboard() {
|
||||
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;
|
||||
const fuelTotal = Number(fuelStats?.totalCost) || 0;
|
||||
const maintenanceTotal = Number(maintenanceStats?.totalCost) || 0;
|
||||
|
||||
return {
|
||||
totalVehicles,
|
||||
@@ -152,10 +166,10 @@ export function FleetDashboard() {
|
||||
<StatCard icon={Users} label="Total Drivers" value={metrics.totalDrivers} color="edr-blue" />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<StatCard icon={Fuel} label="Fuel Spend" value={`$${metrics.totalFuelSpend.toFixed(0)}`} color="edr-accent" />
|
||||
<StatCard icon={Fuel} label="Fuel Spend" value={etb(metrics.totalFuelSpend)} color="edr-accent" />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<StatCard icon={Wrench} label="Maintenance" value={`$${metrics.totalMaintenanceSpend.toFixed(0)}`} color="edr-red" />
|
||||
<StatCard icon={Wrench} label="Maintenance" value={etb(metrics.totalMaintenanceSpend)} color="edr-red" />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
@@ -173,7 +187,7 @@ export function FleetDashboard() {
|
||||
<Text size="sm">Active Vehicles</Text>
|
||||
<Text fw={700}>{metrics.activeVehicles} / {metrics.totalVehicles}</Text>
|
||||
</Group>
|
||||
<Progress value={(metrics.activeVehicles / metrics.totalVehicles) * 100} color="edr-green" />
|
||||
<Progress value={pct(metrics.activeVehicles, metrics.totalVehicles)} color="edr-green" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -189,7 +203,7 @@ export function FleetDashboard() {
|
||||
<Text size="sm">Idle / Under Maintenance</Text>
|
||||
<Text fw={700}>{metrics.totalVehicles - metrics.activeVehicles}</Text>
|
||||
</Group>
|
||||
<Progress value={((metrics.totalVehicles - metrics.activeVehicles) / metrics.totalVehicles) * 100} color="edr-amber-soft" />
|
||||
<Progress value={pct(metrics.totalVehicles - metrics.activeVehicles, metrics.totalVehicles)} color="edr-amber-soft" />
|
||||
</div>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
@@ -212,7 +226,7 @@ export function FleetDashboard() {
|
||||
label={
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text fw={700} size="sm">
|
||||
${operatingCost.toFixed(0)}
|
||||
{etb(operatingCost)}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Total Cost
|
||||
@@ -325,13 +339,12 @@ export function FleetDashboard() {
|
||||
</Table.Td>
|
||||
<Table.Td>{d.licenseNumber || 'N/A'}</Table.Td>
|
||||
<Table.Td>
|
||||
<Stack gap={0} size="xs">
|
||||
<Stack gap={0}>
|
||||
{d.phone && (
|
||||
<Text size="xs">
|
||||
<Group gap={4} inline>
|
||||
<MapPin size={12} /> {d.phone}
|
||||
</Group>
|
||||
</Text>
|
||||
<Group gap={4}>
|
||||
<MapPin size={12} />
|
||||
<Text size="xs">{d.phone}</Text>
|
||||
</Group>
|
||||
)}
|
||||
{d.email && <Text size="xs">{d.email}</Text>}
|
||||
</Stack>
|
||||
|
||||
@@ -357,7 +357,7 @@ const FleetResourcePage = () => {
|
||||
const itemLabel = config.label.toLowerCase();
|
||||
|
||||
return (
|
||||
<Container size="xxl" py="lg">
|
||||
<Container size="xxl" py="lg" px="lg">
|
||||
<Breadcrumbs items={[{ label: config.label }]} />
|
||||
|
||||
<Stack gap="lg" mt="sm">
|
||||
|
||||
@@ -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 (
|
||||
<Container size="xl" py="xl" px="lg">
|
||||
<Breadcrumbs items={[{ label: "Fleet" }, { label: "Fuel Management" }, { label: "Record Purchase" }]} />
|
||||
@@ -147,10 +157,7 @@ export default function FuelPurchasePage() {
|
||||
Total Liters
|
||||
</Text>
|
||||
<Text fw={700} size="lg">
|
||||
{purchasesData
|
||||
.reduce((sum: number, p: FuelPurchase) => sum + Number(p.liters), 0)
|
||||
.toFixed(2)}{" "}
|
||||
L
|
||||
{totalLiters.toFixed(2)} L
|
||||
</Text>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
@@ -160,9 +167,7 @@ export default function FuelPurchasePage() {
|
||||
Total Cost
|
||||
</Text>
|
||||
<Text fw={700} size="lg">
|
||||
ETB {purchasesData
|
||||
.reduce((sum: number, p: FuelPurchase) => sum + Number(p.totalCost), 0)
|
||||
.toLocaleString("en-US", { maximumFractionDigits: 2 })}
|
||||
ETB {totalPurchaseCost.toLocaleString("en-US", { maximumFractionDigits: 2 })}
|
||||
</Text>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
@@ -172,11 +177,7 @@ export default function FuelPurchasePage() {
|
||||
Avg Price/L
|
||||
</Text>
|
||||
<Text fw={700} size="lg">
|
||||
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)}
|
||||
</Text>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
@@ -197,6 +198,23 @@ export default function FuelPurchasePage() {
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{isLoadingPurchases ? (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={7}>
|
||||
<Group justify="center" py="md">
|
||||
<Loader size="sm" />
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
) : purchasesData.length === 0 ? (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={7}>
|
||||
<Text c="dimmed" ta="center" py="md">
|
||||
No fuel purchases recorded yet.
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
) : null}
|
||||
{(purchasesData as FuelPurchase[])?.map((purchase) => (
|
||||
<Table.Tr key={purchase.id}>
|
||||
<Table.Td>{(purchase as any).vehicle?.registrationNumber || (purchase as any).vehicle?.plateNumber || purchase.vehicleId}</Table.Td>
|
||||
|
||||
@@ -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<string>("");
|
||||
const [monthsBack, setMonthsBack] = useState<string>("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
|
||||
</Text>
|
||||
<Text fw={700} size="lg">
|
||||
{statsData.totalPurchases}
|
||||
{Number(statsData.totalPurchases) || 0}
|
||||
</Text>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
@@ -112,7 +103,7 @@ export default function FuelStatsPage() {
|
||||
Total Fuel
|
||||
</Text>
|
||||
<Text fw={700} size="lg">
|
||||
{statsData.totalLiters.toFixed(2)} L
|
||||
{(Number(statsData.totalLiters) || 0).toFixed(2)} L
|
||||
</Text>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
@@ -122,7 +113,7 @@ export default function FuelStatsPage() {
|
||||
Total Cost
|
||||
</Text>
|
||||
<Text fw={700} size="lg">
|
||||
ETB {statsData.totalCost.toLocaleString("en-US", { maximumFractionDigits: 2 })}
|
||||
ETB {(Number(statsData.totalCost) || 0).toLocaleString("en-US", { maximumFractionDigits: 2 })}
|
||||
</Text>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
@@ -132,7 +123,7 @@ export default function FuelStatsPage() {
|
||||
Avg Price/L
|
||||
</Text>
|
||||
<Text fw={700} size="lg">
|
||||
ETB {statsData.averagePricePerLiter.toFixed(2)}
|
||||
ETB {(Number(statsData.averagePricePerLiter) || 0).toFixed(2)}
|
||||
</Text>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
@@ -172,15 +163,15 @@ export default function FuelStatsPage() {
|
||||
<Text size="sm">
|
||||
{selectedVehicle?.plateNumber} consumed{" "}
|
||||
<Text fw={700} span>
|
||||
{statsData.totalLiters.toFixed(2)} liters
|
||||
{(Number(statsData.totalLiters) || 0).toFixed(2)} liters
|
||||
</Text>{" "}
|
||||
over the last {monthsBack} months, costing{" "}
|
||||
<Text fw={700} span>
|
||||
ETB {statsData.totalCost.toLocaleString("en-US", { maximumFractionDigits: 2 })}
|
||||
ETB {(Number(statsData.totalCost) || 0).toLocaleString("en-US", { maximumFractionDigits: 2 })}
|
||||
</Text>
|
||||
. Average fuel price was{" "}
|
||||
<Text fw={700} span>
|
||||
ETB {statsData.averagePricePerLiter.toFixed(2)} per liter
|
||||
ETB {(Number(statsData.averagePricePerLiter) || 0).toFixed(2)} per liter
|
||||
</Text>
|
||||
.
|
||||
</Text>
|
||||
@@ -188,6 +179,12 @@ export default function FuelStatsPage() {
|
||||
</Stack>
|
||||
</Card>
|
||||
</>
|
||||
) : selectedVehicleId && isStatsFetching ? (
|
||||
<Card withBorder padding="lg">
|
||||
<Group justify="center">
|
||||
<Loader />
|
||||
</Group>
|
||||
</Card>
|
||||
) : (
|
||||
<Card withBorder padding="lg">
|
||||
<Text c="dimmed" ta="center">
|
||||
|
||||
@@ -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<string | null>(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<string, string> = {
|
||||
@@ -88,66 +117,85 @@ export function MaintenancePage() {
|
||||
|
||||
return (
|
||||
<Container size="xl" py="xl" px="lg">
|
||||
<Breadcrumbs items={[{ label: 'Fleet' }, { label: 'Maintenance' }]} />
|
||||
|
||||
<Group justify="space-between" mb="lg">
|
||||
<Title order={1}>Maintenance</Title>
|
||||
<Button
|
||||
onClick={() => setOpenScheduleModal(true)}
|
||||
color="edr-green"
|
||||
leftSection={<Plus size={16} />}
|
||||
disabled={!selectedVehicle}
|
||||
>
|
||||
New Schedule
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Stack gap="md">
|
||||
<Card>
|
||||
<Card.Section p="md" withBorder>
|
||||
<Group justify="space-between">
|
||||
<Text fw={500}>Schedule Maintenance</Text>
|
||||
<Button onClick={() => setOpenScheduleModal(true)} color="edr-green" leftSection={<Plus size={16} />}>
|
||||
New Schedule
|
||||
</Button>
|
||||
</Group>
|
||||
</Card.Section>
|
||||
<Card.Section p="md">
|
||||
<Card withBorder padding="md">
|
||||
<Select
|
||||
label="Select Vehicle"
|
||||
placeholder="Pick a vehicle"
|
||||
data={vehicleOptions}
|
||||
value={selectedVehicle}
|
||||
onChange={setSelectedVehicle}
|
||||
searchable
|
||||
/>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
|
||||
{selectedVehicle && (
|
||||
<Card>
|
||||
<Card.Section p="md" withBorder>
|
||||
<Text fw={500}>Upcoming Maintenance</Text>
|
||||
</Card.Section>
|
||||
<Card.Section p="md">
|
||||
{isLoading ? (
|
||||
<Text>Loading...</Text>
|
||||
) : (upcoming || []).length > 0 ? (
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Type</Table.Th>
|
||||
<Table.Th>Description</Table.Th>
|
||||
<Table.Th>Scheduled</Table.Th>
|
||||
<Table.Th>Est. Cost</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{(upcoming as MaintenanceSchedule[]).map(m => (
|
||||
<Table.Tr key={m.id}>
|
||||
<Table.Td>{m.maintenanceType}</Table.Td>
|
||||
<Table.Td>{m.description}</Table.Td>
|
||||
<Table.Td>{new Date(m.scheduledDate).toLocaleDateString()}</Table.Td>
|
||||
<Table.Td>${m.estimatedCost?.toFixed(2) || '—'}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={statusColor(m.status)}>{m.status}</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<Text c="dimmed">No upcoming maintenance</Text>
|
||||
)}
|
||||
</Card.Section>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!selectedVehicle ? (
|
||||
<Card withBorder padding="lg">
|
||||
<Text c="dimmed" ta="center">
|
||||
Select a vehicle to view its maintenance schedule
|
||||
</Text>
|
||||
</Card>
|
||||
) : (
|
||||
<Card withBorder>
|
||||
<Card.Section p="md" withBorder>
|
||||
<Text fw={500}>Upcoming Maintenance</Text>
|
||||
</Card.Section>
|
||||
<Card.Section p="md">
|
||||
{isLoading ? (
|
||||
<Text>Loading...</Text>
|
||||
) : upcomingList.length > 0 ? (
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Type</Table.Th>
|
||||
<Table.Th>Description</Table.Th>
|
||||
<Table.Th>Scheduled</Table.Th>
|
||||
<Table.Th>Est. Cost</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{upcomingList.map((m) => (
|
||||
<Table.Tr key={m.id}>
|
||||
<Table.Td>{m.maintenanceType}</Table.Td>
|
||||
<Table.Td>{m.description}</Table.Td>
|
||||
<Table.Td>{new Date(m.scheduledDate).toLocaleDateString()}</Table.Td>
|
||||
<Table.Td>
|
||||
{m.estimatedCost != null
|
||||
? `ETB ${Number(m.estimatedCost).toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})}`
|
||||
: '—'}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={statusColor(m.status)}>{m.status}</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<Text c="dimmed">No upcoming maintenance</Text>
|
||||
)}
|
||||
</Card.Section>
|
||||
</Card>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Modal
|
||||
opened={openScheduleModal}
|
||||
@@ -160,47 +208,52 @@ export function MaintenancePage() {
|
||||
label="Type"
|
||||
data={['PREVENTIVE', 'CORRECTIVE', 'INSPECTION', 'REPAIR']}
|
||||
value={formData.maintenanceType}
|
||||
onChange={v => setFormData({ ...formData, maintenanceType: v || 'PREVENTIVE' })}
|
||||
onChange={(v) => setFormData({ ...formData, maintenanceType: v || 'PREVENTIVE' })}
|
||||
/>
|
||||
<TextInput
|
||||
label="Description"
|
||||
placeholder="What needs to be done?"
|
||||
value={formData.description}
|
||||
onChange={e => setFormData({ ...formData, description: e.currentTarget.value })}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.currentTarget.value })}
|
||||
/>
|
||||
<DateInput
|
||||
<TextInput
|
||||
label="Scheduled Date"
|
||||
type="date"
|
||||
value={formData.scheduledDate}
|
||||
onChange={d => setFormData({ ...formData, scheduledDate: d || new Date() })}
|
||||
onChange={(e) => setFormData({ ...formData, scheduledDate: e.currentTarget.value })}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Estimated Cost"
|
||||
min={0}
|
||||
value={formData.estimatedCost}
|
||||
onChange={v => setFormData({ ...formData, estimatedCost: Number(v) })}
|
||||
onChange={(v) => setFormData({ ...formData, estimatedCost: Number(v) })}
|
||||
/>
|
||||
<TextInput
|
||||
label="Service Provider"
|
||||
placeholder="e.g., John's Auto Repair"
|
||||
value={formData.serviceProvider}
|
||||
onChange={e => setFormData({ ...formData, serviceProvider: e.currentTarget.value })}
|
||||
onChange={(e) => setFormData({ ...formData, serviceProvider: e.currentTarget.value })}
|
||||
/>
|
||||
<TextInput
|
||||
label="Notes"
|
||||
placeholder="Additional notes"
|
||||
value={formData.notes}
|
||||
onChange={e => setFormData({ ...formData, notes: e.currentTarget.value })}
|
||||
onChange={(e) => setFormData({ ...formData, notes: e.currentTarget.value })}
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="light" onClick={() => setOpenScheduleModal(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => scheduleMutation.mutate()} loading={scheduleMutation.isPending}>
|
||||
<Button
|
||||
onClick={() => scheduleMutation.mutate()}
|
||||
loading={scheduleMutation.isPending}
|
||||
disabled={!selectedVehicle}
|
||||
>
|
||||
Schedule
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
|
||||
@@ -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<string | null>(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() {
|
||||
<Stack gap="xl">
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Text fw={700} size="xl">
|
||||
Real-Time Vehicle Tracking
|
||||
</Text>
|
||||
<Group gap="xs" align="center">
|
||||
<Text fw={700} size="xl">
|
||||
Real-Time Vehicle Tracking
|
||||
</Text>
|
||||
<Badge color="yellow" variant="light">
|
||||
Simulated GPS
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text c="dimmed" size="sm">
|
||||
Monitor vehicle locations, speed, and status
|
||||
</Text>
|
||||
@@ -109,17 +113,18 @@ export function TrackingPage() {
|
||||
</Card.Section>
|
||||
|
||||
<Card.Section p="md">
|
||||
<Box
|
||||
pos="relative"
|
||||
style={{
|
||||
width: mapWidth,
|
||||
height: mapHeight,
|
||||
backgroundColor: '#f0f8f7',
|
||||
border: `2px solid ${freightBrand.primary}`,
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Box style={{ overflowX: 'auto', maxWidth: '100%' }}>
|
||||
<Box
|
||||
pos="relative"
|
||||
style={{
|
||||
width: mapWidth,
|
||||
height: mapHeight,
|
||||
backgroundColor: '#f0f8f7',
|
||||
border: `2px solid ${freightBrand.primary}`,
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Grid background */}
|
||||
<svg
|
||||
width={mapWidth}
|
||||
@@ -199,7 +204,11 @@ export function TrackingPage() {
|
||||
📍 Addis Ababa, Ethiopia
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Text size="xs" c="dimmed" mt="xs">
|
||||
Simulated map — coordinates, speed, and heading are demo values, not live GPS.
|
||||
</Text>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
@@ -314,7 +323,7 @@ export function TrackingPage() {
|
||||
<Stack gap="md">
|
||||
<Text fw={500}>Tracked Vehicles ({trackableVehicles.length})</Text>
|
||||
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
||||
<Table size="sm">
|
||||
<Table>
|
||||
<Table.Tbody>
|
||||
{trackableVehicles.map(v => (
|
||||
<Table.Tr
|
||||
|
||||
@@ -100,7 +100,7 @@ const VehicleDetailPage = () => {
|
||||
: "";
|
||||
|
||||
return (
|
||||
<Container size="lg" py="md">
|
||||
<Container size="xl" py="xl" px="lg">
|
||||
<Group gap="sm" mb="md" wrap="nowrap">
|
||||
<ActionIcon variant="subtle" onClick={() => navigate("/dashboard/vehicles")} aria-label="Back">
|
||||
<ArrowLeft size={18} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
import {
|
||||
Boxes,
|
||||
ChevronRight,
|
||||
FileText,
|
||||
Home,
|
||||
Layers,
|
||||
Package,
|
||||
|
||||
@@ -175,11 +175,6 @@ export const bookingsService = {
|
||||
},
|
||||
|
||||
// ── Document clearance (GL workflow) ──
|
||||
getClearance: async (id: string): Promise<Freight.ClearanceView> => {
|
||||
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 },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user