mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
260 lines
8.1 KiB
TypeScript
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>
|
|
);
|
|
}
|