mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +00:00
All fleet pages now use consistent layout: - Container size: xl - Vertical padding: xl Pages updated: - FleetDashboard: size="xl" py="xl" (unchanged) - FuelPurchasePage: lg → xl - FuelStatsPage: lg → xl - MaintenancePage: Added Container wrapper (xl, xl) - FinancialReportsPage: Added Container wrapper (xl, xl) Uniform spacing across all fleet management dashboards. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
207 lines
6.8 KiB
TypeScript
207 lines
6.8 KiB
TypeScript
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, Container } from '@mantine/core';
|
|
import { DateInput } from '@mantine/dates';
|
|
import { Plus } from 'lucide-react';
|
|
import { QUERY_KEYS } from '@/constants/QUERY_KEYS';
|
|
import { api } from '@/services/api';
|
|
import { vehiclesService } from '@/services/vehicles.service';
|
|
import { freightBrand } from '@/theme/freight-brand';
|
|
|
|
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<string | null>(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: async () => {
|
|
const res = await vehiclesService.getAll({ limit: 1000 });
|
|
return res.data || [];
|
|
},
|
|
});
|
|
|
|
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<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">
|
|
<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">
|
|
<Select
|
|
label="Select Vehicle"
|
|
placeholder="Pick a vehicle"
|
|
data={vehicleOptions}
|
|
value={selectedVehicle}
|
|
onChange={setSelectedVehicle}
|
|
/>
|
|
</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>
|
|
)}
|
|
|
|
<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 })}
|
|
/>
|
|
<DateInput
|
|
label="Scheduled Date"
|
|
value={formData.scheduledDate}
|
|
onChange={d => setFormData({ ...formData, scheduledDate: d || new Date() })}
|
|
/>
|
|
<NumberInput
|
|
label="Estimated Cost"
|
|
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}>
|
|
Schedule
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Modal>
|
|
</Stack>
|
|
</Container>
|
|
);
|
|
}
|