fix(portal): Table has no size prop in BulkTruckUploadModal

This commit is contained in:
Hagernesh
2026-07-22 13:34:53 +00:00
parent cd3b2b3873
commit 15540f78e2
4 changed files with 159 additions and 1 deletions

View File

@@ -0,0 +1,76 @@
import { NotificationAudience } from '@edr/types';
import { MaintenanceService } from './maintenance.service';
/**
* The daily due-alert: a SCHEDULED item that crossed its km or date threshold
* gets one BACKOFFICE notification, then is stamped so it isn't repeated.
*/
function makeService(due: Array<Record<string, unknown>>) {
const update = jest.fn();
const notify = jest.fn();
const service = Object.create(MaintenanceService.prototype) as Record<string, unknown>;
service.maintenanceRepository = { getUnnotifiedDue: jest.fn().mockResolvedValue(due) };
service.scheduleRepository = { update };
service.inbox = { notify };
service.logger = { error: jest.fn() };
return { service: service as unknown as MaintenanceService, update, notify };
}
describe('MaintenanceService.sendDueAlerts', () => {
it('reports the km reason when the km threshold was crossed', async () => {
const { service, notify, update } = makeService([
{
id: 'sched-1',
vehicleId: 'v-1',
plateNumber: 'ET-9875',
maintenanceType: 'PREVENTIVE',
description: 'Oil change',
nextDueKm: 50000,
nextDueDate: null,
currentKm: 50200,
},
]);
await service.sendDueAlerts();
expect(notify).toHaveBeenCalledWith(
expect.objectContaining({
audience: NotificationAudience.BACKOFFICE,
title: 'Maintenance due — ET-9875',
body: expect.stringContaining('driven 50200 km (due at 50000 km)'),
}),
);
expect(update).toHaveBeenCalledWith('sched-1', { dueNotifiedAt: expect.any(Date) });
});
it('reports the date reason when only the due date has passed', async () => {
const { service, notify } = makeService([
{
id: 'sched-2',
vehicleId: 'v-2',
plateNumber: 'AA-8642',
maintenanceType: 'INSPECTION',
description: 'Annual inspection',
nextDueKm: null,
nextDueDate: new Date('2026-01-01'),
currentKm: 1000,
},
]);
await service.sendDueAlerts();
expect(notify).toHaveBeenCalledWith(
expect.objectContaining({ body: expect.stringContaining('due 1/1/2026') }),
);
});
it('does nothing when nothing is due', async () => {
const { service, notify, update } = makeService([]);
await service.sendDueAlerts();
expect(notify).not.toHaveBeenCalled();
expect(update).not.toHaveBeenCalled();
});
});

View File

@@ -199,6 +199,7 @@ export const QUERY_KEYS = {
MAINTENANCE: {
ROOT: ["maintenance"] as const,
dueBoard: () => ["maintenance", "due-board"] as const,
schedules: (vehicleId?: string) =>
["maintenance", "schedules", vehicleId ?? "all"] as const,
upcoming: (vehicleId?: string) =>

View File

@@ -35,6 +35,21 @@ interface MaintenanceSchedule {
serviceProvider?: string;
}
interface DueBoardRow {
scheduleId: string;
vehicleId: string;
plateNumber: string;
maintenanceType: string;
description: string;
scheduledDate: string;
nextDueDate: string | null;
nextDueKm: number | null;
currentKm: number | null;
kmRemaining: number | null;
daysRemaining: number | null;
overdue: boolean;
}
const emptyForm = {
maintenanceType: 'PREVENTIVE',
description: '',
@@ -51,6 +66,16 @@ export function MaintenancePage() {
const [openScheduleModal, setOpenScheduleModal] = useState(false);
const [formData, setFormData] = useState(emptyForm);
// Maintenance is driven by time AND km, not a picked-then-scheduled action —
// this is the fleet-wide board of what's actually due, by date or mileage.
const { data: dueBoard, isLoading: dueLoading } = useQuery({
queryKey: QUERY_KEYS.MAINTENANCE.dueBoard(),
queryFn: async () => {
const res = await api.get('/maintenance/due-board');
return (res.data || []) as DueBoardRow[];
},
});
const { data: vehiclesData } = useQuery({
queryKey: QUERY_KEYS.VEHICLES.list(),
queryFn: async () => {
@@ -132,6 +157,62 @@ export function MaintenancePage() {
</Group>
<Stack gap="md">
<Card withBorder>
<Card.Section p="md" withBorder>
<Text fw={500}>Due Board by date and driven km</Text>
</Card.Section>
<Card.Section p="md">
{dueLoading ? (
<Text>Loading</Text>
) : dueBoard && dueBoard.length > 0 ? (
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Vehicle</Table.Th>
<Table.Th>Type</Table.Th>
<Table.Th>Next Due Date</Table.Th>
<Table.Th>Next Due Km</Table.Th>
<Table.Th>Current Km</Table.Th>
<Table.Th>Remaining</Table.Th>
<Table.Th>Status</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{dueBoard.map((row) => (
<Table.Tr
key={row.scheduleId}
onClick={() => setSelectedVehicle(row.vehicleId)}
style={{ cursor: 'pointer' }}
>
<Table.Td>{row.plateNumber}</Table.Td>
<Table.Td>{row.maintenanceType}</Table.Td>
<Table.Td>
{row.nextDueDate ? new Date(row.nextDueDate).toLocaleDateString() : '—'}
</Table.Td>
<Table.Td>{row.nextDueKm ?? '—'}</Table.Td>
<Table.Td>{row.currentKm ?? '—'}</Table.Td>
<Table.Td>
{row.kmRemaining != null
? `${row.kmRemaining} km`
: row.daysRemaining != null
? `${row.daysRemaining} d`
: '—'}
</Table.Td>
<Table.Td>
<Badge color={row.overdue ? 'edr-red' : 'edr-blue'}>
{row.overdue ? 'OVERDUE' : 'SCHEDULED'}
</Badge>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
) : (
<Text c="dimmed">Nothing scheduled fleet-wide</Text>
)}
</Card.Section>
</Card>
<Card withBorder padding="md">
<Select
label="Select Vehicle"

View File

@@ -113,7 +113,7 @@ export function BulkTruckUploadModal({
<Text fw={600} mb="xs">
Preview ({parsed.length} trucks)
</Text>
<Table striped highlightOnHover size="sm">
<Table striped highlightOnHover verticalSpacing="xs">
<Table.Thead>
<Table.Tr>
<Table.Th>Plate Number</Table.Th>