Files
edr-platform/apps/edr-freight-web/backoffice/src/components/warehouses/TruckDispatchModal.tsx
2026-07-06 13:16:34 +00:00

148 lines
5.8 KiB
TypeScript

import { Alert, Badge, Button, Group, Loader, Modal, MultiSelect, Stack, Text } from '@mantine/core';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { FileText, Truck } from 'lucide-react';
import { useState } from 'react';
import { useToast } from '@/hooks/use-toast';
import { warehouseService } from '@/services/warehouse.service';
import { extractErrorMessage } from './options';
import { openPdfBlob } from './pdf';
interface TruckDispatchModalProps {
opened: boolean;
onClose: () => void;
bookingId: string | null;
bookingReference?: string | null;
}
/**
* Truck_dispatch: after a self-haul truck arrives, staff select which of the
* booking's containers ride each truck. The loaded set drives the truck's gross
* weight; the truck is weighed for real on departure.
*/
export function TruckDispatchModal({ opened, onClose, bookingId, bookingReference }: TruckDispatchModalProps) {
const { toast } = useToast();
const queryClient = useQueryClient();
const [selectedByTruck, setSelectedByTruck] = useState<Record<string, string[]>>({});
const trucksKey = ['td-customer-trucks', bookingId];
const loadableKey = ['td-loadable', bookingId];
const { data: trucks = [], isLoading: trucksLoading } = useQuery({
queryKey: trucksKey,
queryFn: () => warehouseService.getCustomerTrucks(bookingId as string),
enabled: opened && Boolean(bookingId),
});
const { data: loadable = [], isLoading: loadableLoading } = useQuery({
queryKey: loadableKey,
queryFn: () => warehouseService.getLoadableContainers(bookingId as string),
enabled: opened && Boolean(bookingId),
});
const loadMutation = useMutation({
mutationFn: ({ assignmentId, containerNumbers }: { assignmentId: string; containerNumbers: string[] }) =>
warehouseService.loadTruck(bookingId as string, assignmentId, containerNumbers),
onSuccess: (_res, vars) => {
queryClient.invalidateQueries({ queryKey: trucksKey });
queryClient.invalidateQueries({ queryKey: loadableKey });
setSelectedByTruck((s) => ({ ...s, [vars.assignmentId]: [] }));
toast({ title: 'Truck loaded' });
},
onError: (e) => toast({ variant: 'destructive', title: 'Load failed', description: extractErrorMessage(e) }),
});
const openTruckExitPaper = async (assignmentId: string, plate: string) => {
try {
const res = await warehouseService.downloadTruckExitPaper(assignmentId);
openPdfBlob(res.data, `exit-${plate}.pdf`);
} catch (e) {
toast({ variant: 'destructive', title: 'Exit paper not ready', description: extractErrorMessage(e) });
}
};
return (
<Modal
opened={opened}
onClose={onClose}
centered
size="lg"
title={
<Group gap={8}>
<Truck size={18} />
<Text fw={700}>Truck_dispatch load containers {bookingReference ? `· ${bookingReference}` : ''}</Text>
</Group>
}
>
{trucksLoading || loadableLoading ? (
<Group justify="center" py="lg">
<Loader />
</Group>
) : trucks.length === 0 ? (
<Alert color="orange" variant="light">
No customer truck is assigned to this booking yet.
</Alert>
) : (
<Stack gap="md">
{trucks.map((t) => {
const alreadyLoaded = (t.containers ?? []).map((c) => c.containerNumber);
// Options = still-loadable + this truck's own already-loaded (so they stay visible).
const options = Array.from(new Set([...loadable, ...alreadyLoaded]));
const selected = selectedByTruck[t.id] ?? alreadyLoaded;
const departed = Boolean(t.arrivedAt) && Boolean((t as { departedAt?: string }).departedAt);
return (
<Stack
key={t.id}
gap={8}
style={{ border: '1px solid #EEF2F6', borderRadius: 12, padding: 14 }}
>
<Group justify="space-between">
<Text fw={700}>{t.plateNumber}</Text>
<Group gap={6}>
<Text size="sm" c="dimmed">{t.driverName} · {t.truckType}</Text>
{t.arrivedAt ? <Badge color="green" variant="light">Arrived</Badge> : <Badge color="orange" variant="light">Not arrived</Badge>}
</Group>
</Group>
<MultiSelect
label="Containers on this truck"
placeholder="Select containers"
data={options}
value={selected}
onChange={(v) => setSelectedByTruck((s) => ({ ...s, [t.id]: v }))}
searchable
disabled={departed || !t.arrivedAt}
nothingFoundMessage="No loadable containers"
/>
<Group justify="flex-end" gap="xs">
<Button
size="xs"
variant="light"
color="orange"
leftSection={<FileText size={14} />}
onClick={() => openTruckExitPaper(t.id, t.plateNumber)}
>
Exit Paper
</Button>
<Button
size="xs"
color="edr-green"
disabled={departed || !t.arrivedAt || (selectedByTruck[t.id] ?? alreadyLoaded).length === 0}
loading={loadMutation.isPending}
onClick={() =>
loadMutation.mutate({
assignmentId: t.id,
containerNumbers: selectedByTruck[t.id] ?? alreadyLoaded,
})
}
>
Load truck
</Button>
</Group>
</Stack>
);
})}
</Stack>
)}
</Modal>
);
}