Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/fleet/MaintenancePage.tsx
natib21 9d3efe7f15 fix
2026-07-04 03:47:36 +00:00

260 lines
8.1 KiB
TypeScript

import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
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 '@/auth/http';
import { vehiclesService, type Vehicle as VehicleType } 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;
}
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(emptyForm);
const { data: vehiclesData } = useQuery({
queryKey: QUERY_KEYS.VEHICLES.list(),
queryFn: async () => {
const res = await vehiclesService.getAll({ limit: 1000 });
return res.data || [];
},
});
const { data: upcoming, isLoading } = useQuery({
queryKey: QUERY_KEYS.MAINTENANCE.upcoming(selectedVehicle || ''),
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;
const res = await api.post('/maintenance/schedules', {
vehicleId: selectedVehicle,
...formData,
});
return res.data;
},
onSuccess: () => {
toast({ title: 'Maintenance scheduled' });
queryClient.invalidateQueries({
queryKey: QUERY_KEYS.MAINTENANCE.upcoming(selectedVehicle || ''),
});
setOpenScheduleModal(false);
setFormData(emptyForm);
},
onError: (err: any) => {
toast({
title: 'Error',
description: err?.response?.data?.message ?? 'Failed',
variant: 'destructive',
});
},
});
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> = {
SCHEDULED: 'edr-blue',
IN_PROGRESS: 'edr-amber-soft',
COMPLETED: 'edr-green',
OVERDUE: 'edr-red',
};
return colors[status] || 'edr-slate';
};
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 withBorder padding="md">
<Select
label="Select Vehicle"
placeholder="Pick a vehicle"
data={vehicleOptions}
value={selectedVehicle}
onChange={setSelectedVehicle}
searchable
/>
</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}
onClose={() => setOpenScheduleModal(false)}
title="Schedule Maintenance"
size="md"
>
<Stack gap="md">
<Select
label="Type"
data={['PREVENTIVE', 'CORRECTIVE', 'INSPECTION', 'REPAIR']}
value={formData.maintenanceType}
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 })}
/>
<TextInput
label="Scheduled Date"
type="date"
value={formData.scheduledDate}
onChange={(e) => setFormData({ ...formData, scheduledDate: e.currentTarget.value })}
/>
<NumberInput
label="Estimated Cost"
min={0}
value={formData.estimatedCost}
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 })}
/>
<TextInput
label="Notes"
placeholder="Additional notes"
value={formData.notes}
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}
disabled={!selectedVehicle}
>
Schedule
</Button>
</Group>
</Stack>
</Modal>
</Container>
);
}