mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 10:10:57 +00:00
Merge branch 'dev' into freight/feat/fixes-v1
This commit is contained in:
@@ -38,6 +38,7 @@ import {
|
||||
MapPin,
|
||||
Package,
|
||||
Receipt,
|
||||
Repeat,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import type { Freight } from "@edr/types";
|
||||
@@ -192,9 +193,19 @@ export default function GlCreateBookingForm() {
|
||||
const [notes, setNotes] = useState("");
|
||||
const [containerLines, setContainerLines] = useState<ContainerLineDraft[]>([]);
|
||||
const [bulkLines, setBulkLines] = useState<BulkLineDraft[]>([]);
|
||||
const [withReturn, setWithReturn] = useState(false);
|
||||
const [prefilled, setPrefilled] = useState(false);
|
||||
const [priceOpen, setPriceOpen] = useState(false);
|
||||
const seededRef = useRef(false);
|
||||
const returnSeededRef = useRef(false);
|
||||
|
||||
// Seed the equipment-return toggle from the contract exactly once (also when
|
||||
// the form is prefilled from a shipment request); GL can flip it per shipment.
|
||||
useEffect(() => {
|
||||
if (!contract || returnSeededRef.current) return;
|
||||
returnSeededRef.current = true;
|
||||
setWithReturn(contract.equipmentReturn === "WITH_RETURN");
|
||||
}, [contract]);
|
||||
|
||||
const isContainer = contract?.freightType === "CONTAINER";
|
||||
const routes = useMemo(
|
||||
@@ -527,6 +538,10 @@ export default function GlCreateBookingForm() {
|
||||
scheduledDate,
|
||||
...(contractRouteId ? { contractRouteId } : {}),
|
||||
...(notes.trim() ? { notes: notes.trim() } : {}),
|
||||
// Equipment return is a container concern — bulk keeps the contract default.
|
||||
...(isContainer
|
||||
? { equipmentReturn: withReturn ? "WITH_RETURN" : "WITHOUT_RETURN" }
|
||||
: {}),
|
||||
};
|
||||
|
||||
if (isContainer) {
|
||||
@@ -1091,6 +1106,67 @@ export default function GlCreateBookingForm() {
|
||||
</StepCard>
|
||||
)}
|
||||
|
||||
{isContainer ? (
|
||||
<StepCard>
|
||||
<StepHeader
|
||||
icon={<Repeat size={22} />}
|
||||
title="Equipment Return"
|
||||
description="Choose whether the empty container(s) come back to EDR after unloading."
|
||||
/>
|
||||
<Paper
|
||||
withBorder
|
||||
radius="md"
|
||||
p="md"
|
||||
style={{
|
||||
borderColor: withReturn ? "#CDEBDD" : "#E6ECF2",
|
||||
background: withReturn ? "#F6FBF8" : "white",
|
||||
cursor: "pointer",
|
||||
transition: "border-color 150ms ease, background 150ms ease",
|
||||
}}
|
||||
onClick={() => setWithReturn((v) => !v)}
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap" gap="md">
|
||||
<Group gap={13} wrap="nowrap" align="flex-start">
|
||||
<Box
|
||||
style={{
|
||||
width: 38,
|
||||
height: 38,
|
||||
flexShrink: 0,
|
||||
borderRadius: 11,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: withReturn ? "#ECF6F1" : "#F1F4F7",
|
||||
color: withReturn ? "#0A6F4D" : "#6B7C8E",
|
||||
}}
|
||||
>
|
||||
<Repeat size={18} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz={14} fw={700}>
|
||||
With return
|
||||
</Text>
|
||||
<Text fz={12} c="dimmed" style={{ lineHeight: 1.4 }}>
|
||||
{withReturn
|
||||
? "Container(s) returned to EDR after unloading."
|
||||
: "Container(s) retained by the customer after delivery."}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
<Switch
|
||||
size="md"
|
||||
color="edr-green"
|
||||
aria-label="With return"
|
||||
checked={withReturn}
|
||||
onChange={(e) => setWithReturn(e.currentTarget.checked)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
</Group>
|
||||
</Paper>
|
||||
</StepCard>
|
||||
) : null}
|
||||
|
||||
<StepCard>
|
||||
<StepHeader
|
||||
icon={<CalendarDays size={22} />}
|
||||
|
||||
@@ -51,6 +51,7 @@ import { warehouseService } from '@/services/warehouse.service';
|
||||
import type {
|
||||
EligibleBooking,
|
||||
InventoryInquiryFilter,
|
||||
InventoryStatus,
|
||||
InventoryInquiryResult,
|
||||
ImportTrain,
|
||||
ImportTrainItem,
|
||||
@@ -63,6 +64,7 @@ import type {
|
||||
WarehouseYard,
|
||||
WarehouseZone,
|
||||
} from '@/types/warehouse';
|
||||
import { InventoryStatusBadge } from './badges';
|
||||
import { BookingSelect } from './BookingSelect';
|
||||
import { DeliverInventoryModal } from './DeliverInventoryModal';
|
||||
import { ContainerItemsModal } from './ContainerItemsModal';
|
||||
@@ -253,6 +255,127 @@ const toTruckEntrancePayload = (form: TruckEntranceFormState): TruckEntrancePayl
|
||||
warehouseManagerName: form.warehouseManagerName.trim() || undefined,
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
const SUB_STAGE_COLOR: Record<string, string> = {
|
||||
PENDING: 'gray',
|
||||
RECEIVED: 'blue',
|
||||
GRN: 'teal',
|
||||
ASSIGNED: 'indigo',
|
||||
LOADED: 'grape',
|
||||
LEFT: 'orange',
|
||||
DELIVERED: 'green',
|
||||
};
|
||||
|
||||
/**
|
||||
* Expanded booking row: the booking's containers / bulk items with their
|
||||
* lifecycle stage. Shares the ['container-items', bookingId] cache with
|
||||
* ContainerItemsModal, so expanding after using the modal is instant.
|
||||
*/
|
||||
function BookingItemsExpansion({
|
||||
bookingId,
|
||||
colSpan,
|
||||
bulkFallback,
|
||||
}: {
|
||||
bookingId: string | null;
|
||||
colSpan: number;
|
||||
bulkFallback?: string;
|
||||
}) {
|
||||
const { data: items = [], isLoading } = useQuery({
|
||||
queryKey: ['container-items', bookingId],
|
||||
queryFn: () => warehouseService.getContainerItems(bookingId as string),
|
||||
enabled: Boolean(bookingId),
|
||||
});
|
||||
|
||||
return (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={colSpan} bg="var(--mantine-color-gray-0)">
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="sm">
|
||||
<Loader size="xs" />
|
||||
</Group>
|
||||
) : items.length === 0 ? (
|
||||
<Text size="xs" c="dimmed" py={6}>
|
||||
{bulkFallback ?? 'No container units recorded on this booking.'}
|
||||
</Text>
|
||||
) : (
|
||||
<Table verticalSpacing={4} fz="xs" withTableBorder>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Container #</Table.Th>
|
||||
<Table.Th>Goods</Table.Th>
|
||||
<Table.Th>Stage</Table.Th>
|
||||
<Table.Th>Truck</Table.Th>
|
||||
<Table.Th>GRN</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{items.map((i) => (
|
||||
<Table.Tr key={i.containerNumber}>
|
||||
<Table.Td>
|
||||
<Text size="xs" fw={600}>{i.containerNumber}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{i.goods ?? '—'}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="xs" variant="light" color={SUB_STAGE_COLOR[i.stage] ?? 'gray'}>
|
||||
{i.stage}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{i.truckPlate ?? '—'}</Table.Td>
|
||||
<Table.Td>{i.grnNumber ?? '—'}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
}
|
||||
|
||||
type ConfirmAction = { title: string; message: string; confirmLabel: string; run: () => void };
|
||||
|
||||
/** One-click bulk actions are irreversible — make the click deliberate. */
|
||||
function ConfirmActionModal({
|
||||
action,
|
||||
onClose,
|
||||
}: {
|
||||
action: ConfirmAction | null;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Modal opened={Boolean(action)} onClose={onClose} title={action?.title ?? ''} centered size="sm">
|
||||
<Stack gap="md">
|
||||
<Text size="sm">{action?.message}</Text>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
action?.run();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{action?.confirmLabel}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
/** "3 skipped — Booking not PAID" instead of a bare count. */
|
||||
const skippedSummary = (
|
||||
skippedCount: number,
|
||||
results: Array<{ reason?: string; message?: string }>,
|
||||
): string | undefined => {
|
||||
if (!skippedCount) return undefined;
|
||||
const reason = results.find((x) => x.reason || x.message);
|
||||
return `${skippedCount} skipped${reason ? ` — ${reason.reason ?? reason.message}` : ''}`;
|
||||
};
|
||||
|
||||
const commonNonEmptyValue = (values: Array<string | null | undefined>) => {
|
||||
const unique = [...new Set(values.map((value) => value?.trim()).filter(Boolean))] as string[];
|
||||
return unique.length === 1 ? unique[0] : '';
|
||||
@@ -860,7 +983,7 @@ function EligibleTab({
|
||||
});
|
||||
toast({
|
||||
title: `${r.receivedCount} received at ${direction === 'EXPORT' ? 'facility' : 'warehouse'}`,
|
||||
description: r.results.find((item) => item.grnNumber)?.grnNumber ?? (r.skippedCount ? `${r.skippedCount} skipped` : undefined),
|
||||
description: r.results.find((item) => item.grnNumber)?.grnNumber ?? skippedSummary(r.skippedCount, r.results),
|
||||
});
|
||||
const firstGrn = r.results.find((item) => item.inventoryId && item.grnNumber);
|
||||
if (focusedBookingId && firstGrn?.inventoryId && firstGrn.grnNumber) {
|
||||
@@ -1030,7 +1153,7 @@ function EligibleTab({
|
||||
: `No eligible PAID ${direction.toLowerCase()} bookings to receive.`}
|
||||
</Text>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={1700}>
|
||||
<Table.ScrollContainer minWidth={1350}>
|
||||
<Table highlightOnHover verticalSpacing="xs" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
@@ -1043,8 +1166,6 @@ function EligibleTab({
|
||||
/>
|
||||
</Table.Th>
|
||||
<Table.Th>Booking Ref</Table.Th>
|
||||
<Table.Th>Booking ID</Table.Th>
|
||||
<Table.Th>Customer ID</Table.Th>
|
||||
<Table.Th>Customer Name</Table.Th>
|
||||
<Table.Th>Origin</Table.Th>
|
||||
<Table.Th>Destination</Table.Th>
|
||||
@@ -1077,12 +1198,6 @@ function EligibleTab({
|
||||
{r.reference}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{r.id.slice(0, 8)}…</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{r.customerId ? `${r.customerId.slice(0, 8)}…` : '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{r.customer ?? '—'}</Table.Td>
|
||||
<Table.Td>{r.origin ?? '—'}</Table.Td>
|
||||
<Table.Td>{r.destination ?? '—'}</Table.Td>
|
||||
@@ -1256,6 +1371,8 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
|
||||
api.warehouses.bulkMarkInspected.mutationOptions(),
|
||||
);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [confirmAction, setConfirmAction] = useState<ConfirmAction | null>(null);
|
||||
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||
const [inspectId, setInspectId] = useState<string | null>(null);
|
||||
|
||||
const pendingRows = rows.filter((r) => r.inspectionStatus !== 'PASSED');
|
||||
@@ -1279,7 +1396,7 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
|
||||
const r = await inspectMutation.mutateAsync({ inventoryIds: [...selected] });
|
||||
toast({
|
||||
title: `${r.inspectedCount} marked inspected`,
|
||||
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
||||
description: skippedSummary(r.skippedCount, r.results),
|
||||
});
|
||||
setSelected(new Set());
|
||||
onChanged?.();
|
||||
@@ -1300,7 +1417,14 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
|
||||
leftSection={<ClipboardCheck size={14} />}
|
||||
disabled={selected.size === 0}
|
||||
loading={inspectMutation.isPending}
|
||||
onClick={markInspected}
|
||||
onClick={() =>
|
||||
setConfirmAction({
|
||||
title: 'Mark inspected',
|
||||
message: `Mark ${selected.size} selected item(s) as inspection PASSED?`,
|
||||
confirmLabel: `Mark ${selected.size} inspected`,
|
||||
run: markInspected,
|
||||
})
|
||||
}
|
||||
>
|
||||
Mark Selected as Inspected
|
||||
</Button>
|
||||
@@ -1315,10 +1439,11 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
|
||||
No received export items awaiting inspection.
|
||||
</Text>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={1700}>
|
||||
<Table.ScrollContainer minWidth={1350}>
|
||||
<Table highlightOnHover verticalSpacing="xs" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th w={34} />
|
||||
<Table.Th w={40}>
|
||||
<Checkbox
|
||||
aria-label="Select all"
|
||||
@@ -1329,8 +1454,6 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
|
||||
</Table.Th>
|
||||
<Table.Th>Booking Ref</Table.Th>
|
||||
<Table.Th>GRN</Table.Th>
|
||||
<Table.Th>Booking ID</Table.Th>
|
||||
<Table.Th>Customer ID</Table.Th>
|
||||
<Table.Th>Customer Name</Table.Th>
|
||||
<Table.Th>Container / Cargo Items</Table.Th>
|
||||
<Table.Th>Cargo Type</Table.Th>
|
||||
@@ -1345,7 +1468,18 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
|
||||
{rows.map((r: ReadyToLoadRow) => {
|
||||
const selectable = r.inspectionStatus !== 'PASSED';
|
||||
return (
|
||||
<Table.Tr key={r.id}>
|
||||
<Fragment key={r.id}>
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label="Show containers"
|
||||
onClick={() => setExpandedRow(expandedRow === r.id ? null : r.id)}
|
||||
>
|
||||
{expandedRow === r.id ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Checkbox
|
||||
aria-label={`Select ${r.bookingReference ?? r.id}`}
|
||||
@@ -1355,20 +1489,11 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Stack gap={2}>
|
||||
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
|
||||
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
|
||||
</Stack>
|
||||
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{r.bookingId ? `${r.bookingId.slice(0, 8)}…` : '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{r.customerId ? `${r.customerId.slice(0, 8)}…` : '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{r.customerName ?? '—'}</Table.Td>
|
||||
<Table.Td>{r.containerNumber ?? '—'}</Table.Td>
|
||||
<Table.Td>{r.cargoType ?? '—'}</Table.Td>
|
||||
@@ -1382,9 +1507,7 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="blue" variant="light" size="sm">
|
||||
{r.status}
|
||||
</Badge>
|
||||
<InventoryStatusBadge status={r.status as InventoryStatus} />
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Button size="compact-xs" variant="light" color="orange" onClick={() => setInspectId(r.id)}>
|
||||
@@ -1392,6 +1515,14 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{expandedRow === r.id && (
|
||||
<BookingItemsExpansion
|
||||
bookingId={r.bookingId}
|
||||
colSpan={18}
|
||||
bulkFallback={r.cargoType ? `Bulk cargo — ${r.cargoType}, ${formatNumber(Number(r.weight))} t` : undefined}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
@@ -1404,6 +1535,7 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
|
||||
opened={Boolean(inspectId)}
|
||||
onClose={() => setInspectId(null)}
|
||||
/>
|
||||
<ConfirmActionModal action={confirmAction} onClose={() => setConfirmAction(null)} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1414,27 +1546,48 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
const { data: rows = [], isLoading } = useQuery(
|
||||
api.warehouses.readyToLoadExport.queryOptions({ enabled }),
|
||||
);
|
||||
const loadPassed = useMutation(api.warehouses.loadPassedExport.mutationOptions());
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const qc = useQueryClient();
|
||||
const [trainPickerOpen, setTrainPickerOpen] = useState(false);
|
||||
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||
const [targetScheduleId, setTargetScheduleId] = useState<string | null>(null);
|
||||
// Loading is always onto a SPECIFIC pre-dispatch train. No train -> no auto-load.
|
||||
const { data: trains = [], isLoading: trainsLoading } = useQuery({
|
||||
queryKey: ['warehouse-inventory', 'loadable-trains'],
|
||||
queryFn: () => warehouseService.getLoadableTrains(),
|
||||
enabled: enabled && trainPickerOpen,
|
||||
});
|
||||
const loadOntoTrain = useMutation({
|
||||
mutationFn: async (scheduleId: string) => {
|
||||
const items = await warehouseService.getTrainLoadableItems(scheduleId);
|
||||
const loadableIds = items.filter((i) => i.loadable).map((i) => i.id);
|
||||
if (!loadableIds.length) {
|
||||
throw new Error('No ready items with an allocated wagon on this train');
|
||||
}
|
||||
return warehouseService.loadItemsOntoTrain(scheduleId, loadableIds);
|
||||
},
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: ['warehouse-inventory'] });
|
||||
void qc.invalidateQueries({ queryKey: ['warehouse-loadings'] });
|
||||
},
|
||||
});
|
||||
|
||||
const allSelected = rows.length > 0 && selected.size === rows.length;
|
||||
const someSelected = selected.size > 0 && !allSelected;
|
||||
const toggleAll = () => setSelected(allSelected ? new Set() : new Set(rows.map((r) => r.id)));
|
||||
const toggleOne = (id: string) =>
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.has(id) ? next.delete(id) : next.add(id);
|
||||
return next;
|
||||
});
|
||||
|
||||
const autoLoad = async () => {
|
||||
const confirmLoad = async () => {
|
||||
if (!targetScheduleId) {
|
||||
toast({ variant: 'destructive', title: 'Select a train to load onto' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const r = await loadPassed.mutateAsync(undefined);
|
||||
const r = await loadOntoTrain.mutateAsync(targetScheduleId);
|
||||
const train = trains.find((t) => t.scheduleId === targetScheduleId);
|
||||
toast({
|
||||
title: `${r.loadedCount} items loaded`,
|
||||
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
||||
title: `${r.loadedCount} item(s) loaded onto train ${train?.trainNumber ?? ''}`.trim(),
|
||||
description: r.skippedCount
|
||||
? `${r.skippedCount} skipped — ${r.results.find((x) => x.reason)?.reason ?? 'see results'}`
|
||||
: undefined,
|
||||
});
|
||||
setSelected(new Set());
|
||||
setTrainPickerOpen(false);
|
||||
setTargetScheduleId(null);
|
||||
onChanged?.();
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Load failed', description: extractErrorMessage(error) });
|
||||
@@ -1452,14 +1605,58 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
variant="filled"
|
||||
color="teal"
|
||||
leftSection={<Truck size={14} />}
|
||||
loading={loadPassed.isPending}
|
||||
disabled={rows.length === 0}
|
||||
onClick={autoLoad}
|
||||
onClick={() => setTrainPickerOpen(true)}
|
||||
>
|
||||
Auto Load Ready Items
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Modal
|
||||
opened={trainPickerOpen}
|
||||
onClose={() => setTrainPickerOpen(false)}
|
||||
title="Load ready items onto a train"
|
||||
centered
|
||||
size="lg"
|
||||
>
|
||||
<Stack gap="md">
|
||||
{trainsLoading ? (
|
||||
<Group justify="center" py="md"><Loader size="sm" /></Group>
|
||||
) : trains.length === 0 ? (
|
||||
<Alert color="yellow" variant="light" icon={<Info size={16} />}>
|
||||
No train available. Auto-loading needs a scheduled (not yet dispatched) train with
|
||||
these bookings assigned — schedule the train and allocate wagons first.
|
||||
</Alert>
|
||||
) : (
|
||||
<Select
|
||||
label="Available trains"
|
||||
placeholder="Select the train to load onto"
|
||||
data={trains.map((t) => ({
|
||||
value: t.scheduleId,
|
||||
label: `${t.trainNumber ?? t.scheduleId.slice(0, 8)} · ${t.origin ?? '?'} → ${t.destination ?? '?'} · dep ${t.departureTime ? formatDate(t.departureTime) : '—'} · ${t.readyCount} ready`,
|
||||
}))}
|
||||
value={targetScheduleId}
|
||||
onChange={setTargetScheduleId}
|
||||
searchable
|
||||
/>
|
||||
)}
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={() => setTrainPickerOpen(false)} disabled={loadOntoTrain.isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="teal"
|
||||
leftSection={<Truck size={14} />}
|
||||
loading={loadOntoTrain.isPending}
|
||||
disabled={!targetScheduleId}
|
||||
onClick={confirmLoad}
|
||||
>
|
||||
Load onto this train
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="lg">
|
||||
<Loader size="sm" />
|
||||
@@ -1469,22 +1666,13 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
No EXPORT items with inspection PASSED waiting to be loaded.
|
||||
</Text>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={1600}>
|
||||
<Table.ScrollContainer minWidth={1200}>
|
||||
<Table highlightOnHover verticalSpacing="xs" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th w={40}>
|
||||
<Checkbox
|
||||
aria-label="Select all"
|
||||
checked={allSelected}
|
||||
indeterminate={someSelected}
|
||||
onChange={toggleAll}
|
||||
/>
|
||||
</Table.Th>
|
||||
<Table.Th w={34} />
|
||||
<Table.Th>Booking Ref</Table.Th>
|
||||
<Table.Th>GRN</Table.Th>
|
||||
<Table.Th>Booking ID</Table.Th>
|
||||
<Table.Th>Customer ID</Table.Th>
|
||||
<Table.Th>Customer Name</Table.Th>
|
||||
<Table.Th>Container #</Table.Th>
|
||||
<Table.Th>Cargo Type</Table.Th>
|
||||
@@ -1496,29 +1684,24 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rows.map((r: ReadyToLoadRow) => (
|
||||
<Table.Tr key={r.id}>
|
||||
<Fragment key={r.id}>
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<Checkbox
|
||||
aria-label={`Select ${r.bookingReference ?? r.id}`}
|
||||
checked={selected.has(r.id)}
|
||||
onChange={() => toggleOne(r.id)}
|
||||
/>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label="Show containers"
|
||||
onClick={() => setExpandedRow(expandedRow === r.id ? null : r.id)}
|
||||
>
|
||||
{expandedRow === r.id ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Stack gap={2}>
|
||||
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
|
||||
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
|
||||
</Stack>
|
||||
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{r.bookingId ? `${r.bookingId.slice(0, 8)}…` : '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{r.customerId ? `${r.customerId.slice(0, 8)}…` : '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{r.customerName ?? '—'}</Table.Td>
|
||||
<Table.Td>{r.containerNumber ?? '—'}</Table.Td>
|
||||
<Table.Td>{r.cargoType ?? '—'}</Table.Td>
|
||||
@@ -1532,11 +1715,17 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="teal" variant="light" size="sm">
|
||||
{r.status}
|
||||
</Badge>
|
||||
<InventoryStatusBadge status={r.status as InventoryStatus} />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{expandedRow === r.id && (
|
||||
<BookingItemsExpansion
|
||||
bookingId={r.bookingId}
|
||||
colSpan={11}
|
||||
bulkFallback={r.cargoType ? `Bulk cargo — ${r.cargoType}, ${formatNumber(Number(r.weight))} t` : undefined}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
@@ -1563,6 +1752,8 @@ function LoadedExportTab({
|
||||
const { data: rows = [], isLoading } = useQuery(
|
||||
api.warehouses.loadedExport.queryOptions({ enabled }),
|
||||
);
|
||||
const [confirmAction, setConfirmAction] = useState<ConfirmAction | null>(null);
|
||||
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||
const bulkDispatch = useMutation(
|
||||
api.warehouses.bulkDispatchExport.mutationOptions(),
|
||||
);
|
||||
@@ -1587,7 +1778,7 @@ function LoadedExportTab({
|
||||
const r = await bulkDispatch.mutateAsync(inventoryIds);
|
||||
toast({
|
||||
title: `${r.dispatchedCount} dispatched`,
|
||||
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
||||
description: skippedSummary(r.skippedCount, r.results),
|
||||
});
|
||||
setSelected(new Set());
|
||||
onChanged?.();
|
||||
@@ -1617,7 +1808,14 @@ function LoadedExportTab({
|
||||
variant="default"
|
||||
disabled={rows.length === 0}
|
||||
loading={bulkDispatch.isPending}
|
||||
onClick={() => dispatch(rows.map((r) => r.id))}
|
||||
onClick={() =>
|
||||
setConfirmAction({
|
||||
title: 'Dispatch all',
|
||||
message: `Dispatch all ${rows.length} loaded item(s)? They leave warehouse inventory for the train.`,
|
||||
confirmLabel: `Dispatch ${rows.length}`,
|
||||
run: () => dispatch(rows.map((r) => r.id)),
|
||||
})
|
||||
}
|
||||
>
|
||||
Dispatch All
|
||||
</Button>
|
||||
@@ -1627,7 +1825,14 @@ function LoadedExportTab({
|
||||
leftSection={<Truck size={14} />}
|
||||
disabled={selected.size === 0}
|
||||
loading={bulkDispatch.isPending}
|
||||
onClick={() => dispatch([...selected])}
|
||||
onClick={() =>
|
||||
setConfirmAction({
|
||||
title: 'Dispatch selected',
|
||||
message: `Dispatch ${selected.size} selected item(s)? They leave warehouse inventory for the train.`,
|
||||
confirmLabel: `Dispatch ${selected.size}`,
|
||||
run: () => dispatch([...selected]),
|
||||
})
|
||||
}
|
||||
>
|
||||
Dispatch Selected
|
||||
</Button>
|
||||
@@ -1644,7 +1849,7 @@ function LoadedExportTab({
|
||||
No LOADED export items {dispatchable ? 'waiting to dispatch' : 'yet'}.
|
||||
</Text>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={1600}>
|
||||
<Table.ScrollContainer minWidth={1200}>
|
||||
<Table highlightOnHover verticalSpacing="xs" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
@@ -1658,10 +1863,9 @@ function LoadedExportTab({
|
||||
/>
|
||||
</Table.Th>
|
||||
)}
|
||||
<Table.Th w={34} />
|
||||
<Table.Th>Booking Ref</Table.Th>
|
||||
<Table.Th>GRN</Table.Th>
|
||||
<Table.Th>Booking ID</Table.Th>
|
||||
<Table.Th>Customer ID</Table.Th>
|
||||
<Table.Th>Customer Name</Table.Th>
|
||||
<Table.Th>Container #</Table.Th>
|
||||
<Table.Th>Cargo Type</Table.Th>
|
||||
@@ -1672,7 +1876,8 @@ function LoadedExportTab({
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rows.map((r: ReadyToLoadRow) => (
|
||||
<Table.Tr key={r.id}>
|
||||
<Fragment key={r.id}>
|
||||
<Table.Tr>
|
||||
{dispatchable && (
|
||||
<Table.Td>
|
||||
<Checkbox
|
||||
@@ -1683,20 +1888,21 @@ function LoadedExportTab({
|
||||
</Table.Td>
|
||||
)}
|
||||
<Table.Td>
|
||||
<Stack gap={2}>
|
||||
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
|
||||
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
|
||||
</Stack>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label="Show containers"
|
||||
onClick={() => setExpandedRow(expandedRow === r.id ? null : r.id)}
|
||||
>
|
||||
{expandedRow === r.id ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{r.bookingId ? `${r.bookingId.slice(0, 8)}…` : '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{r.customerId ? `${r.customerId.slice(0, 8)}…` : '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{r.customerName ?? '—'}</Table.Td>
|
||||
<Table.Td>{r.containerNumber ?? '—'}</Table.Td>
|
||||
<Table.Td>{r.cargoType ?? '—'}</Table.Td>
|
||||
@@ -1705,16 +1911,23 @@ function LoadedExportTab({
|
||||
{r.origin || r.destination ? `${r.origin ?? '?'} → ${r.destination ?? '?'}` : '—'}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="blue" variant="light" size="sm">
|
||||
{r.status}
|
||||
</Badge>
|
||||
<InventoryStatusBadge status={r.status as InventoryStatus} />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{expandedRow === r.id && (
|
||||
<BookingItemsExpansion
|
||||
bookingId={r.bookingId}
|
||||
colSpan={11}
|
||||
bulkFallback={r.cargoType ? `Bulk cargo — ${r.cargoType}, ${formatNumber(Number(r.weight))} t` : undefined}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
<ConfirmActionModal action={confirmAction} onClose={() => setConfirmAction(null)} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1816,9 +2029,7 @@ function ImportTrainDetailTable({
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Wagon</Table.Th>
|
||||
<Table.Th>Booking ID</Table.Th>
|
||||
<Table.Th>Booking Ref</Table.Th>
|
||||
<Table.Th>Customer ID</Table.Th>
|
||||
<Table.Th>Customer Name</Table.Th>
|
||||
<Table.Th>Container #</Table.Th>
|
||||
<Table.Th>Cargo Type</Table.Th>
|
||||
@@ -1852,15 +2063,9 @@ function ImportTrainDetailTable({
|
||||
{it.sequenceNo ? `#${it.sequenceNo}` : '-'} {it.wagonNumber ?? ''}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{it.bookingId.slice(0, 8)}…</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" fw={600}>{it.bookingReference ?? '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{it.customerId ? `${it.customerId.slice(0, 8)}…` : '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{it.customerName ?? '—'}</Table.Td>
|
||||
<Table.Td>{it.containerNumber ?? '—'}</Table.Td>
|
||||
<Table.Td>{it.cargoType ?? '—'}</Table.Td>
|
||||
@@ -1950,6 +2155,7 @@ function ImportArriveQueueTab({
|
||||
api.warehouses.autoUnloadArrivedBookings.mutationOptions(),
|
||||
);
|
||||
const [openId, setOpenId] = useState<string | null>(null);
|
||||
const [confirmAction, setConfirmAction] = useState<ConfirmAction | null>(null);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [assignmentsBySchedule, setAssignmentsBySchedule] = useState<
|
||||
Record<string, Record<string, ImportUnloadAssignmentDraft>>
|
||||
@@ -1991,7 +2197,7 @@ function ImportArriveQueueTab({
|
||||
const alreadyUnloaded = r.unloadedCount === 0 && r.skippedCount > 0 && r.failedCount === 0;
|
||||
const firstReason = r.results.find((item) => item.reason)?.reason;
|
||||
const extra = [
|
||||
r.skippedCount ? `${r.skippedCount} skipped` : '',
|
||||
skippedSummary(r.skippedCount, r.results) ?? '',
|
||||
r.failedCount ? `${r.failedCount} failed` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -2087,7 +2293,14 @@ function ImportArriveQueueTab({
|
||||
leftSection={<Truck size={14} />}
|
||||
loading={busyId === t.scheduleId}
|
||||
disabled={fullyUnloaded || t.totalBookings === 0 || !readyBySchedule[t.scheduleId] || warehousesLoading}
|
||||
onClick={() => autoUnload(t)}
|
||||
onClick={() =>
|
||||
setConfirmAction({
|
||||
title: 'Auto unload train',
|
||||
message: `Unload all arrived bookings from train ${t.trainNumber ?? t.scheduleId.slice(0, 8)} into their assigned warehouse locations?`,
|
||||
confirmLabel: 'Unload train',
|
||||
run: () => autoUnload(t),
|
||||
})
|
||||
}
|
||||
>
|
||||
{fullyUnloaded ? 'Already Unloaded' : 'Auto Unload Arrived Bookings'}
|
||||
</Button>
|
||||
@@ -2126,6 +2339,7 @@ function ImportArriveQueueTab({
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
<ConfirmActionModal action={confirmAction} onClose={() => setConfirmAction(null)} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -2146,6 +2360,8 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
);
|
||||
const readyMutation = useMutation(api.warehouses.markReadyForPickup.mutationOptions());
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [confirmAction, setConfirmAction] = useState<ConfirmAction | null>(null);
|
||||
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||
const [inspectId, setInspectId] = useState<string | null>(null);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [viewItem, setViewItem] = useState<WarehouseInventoryItem | null>(null);
|
||||
@@ -2177,7 +2393,7 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
const r = await inspectMutation.mutateAsync({ inventoryIds: [...selected] });
|
||||
toast({
|
||||
title: `${r.inspectedCount} marked inspected`,
|
||||
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
||||
description: skippedSummary(r.skippedCount, r.results),
|
||||
});
|
||||
setSelected(new Set());
|
||||
void qc.invalidateQueries({ queryKey: ['warehouse-inventory'] });
|
||||
@@ -2281,7 +2497,14 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
leftSection={<ClipboardCheck size={14} />}
|
||||
disabled={selected.size === 0}
|
||||
loading={inspectMutation.isPending}
|
||||
onClick={markInspected}
|
||||
onClick={() =>
|
||||
setConfirmAction({
|
||||
title: 'Mark inspected',
|
||||
message: `Mark ${selected.size} selected item(s) as inspection PASSED? Passed import items become ready for pickup.`,
|
||||
confirmLabel: `Mark ${selected.size} inspected`,
|
||||
run: markInspected,
|
||||
})
|
||||
}
|
||||
>
|
||||
Mark Selected as Inspected
|
||||
</Button>
|
||||
@@ -2297,10 +2520,11 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
No unloaded import items. Items appear here after Auto Unload on an arrived train.
|
||||
</Text>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={2000}>
|
||||
<Table.ScrollContainer minWidth={1650}>
|
||||
<Table highlightOnHover verticalSpacing="xs" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th w={34} />
|
||||
<Table.Th w={40}>
|
||||
<Checkbox
|
||||
aria-label="Select all"
|
||||
@@ -2309,10 +2533,8 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
onChange={() => (allSelected ? unselectAll() : selectAll())}
|
||||
/>
|
||||
</Table.Th>
|
||||
<Table.Th>Booking ID</Table.Th>
|
||||
<Table.Th>Booking Ref</Table.Th>
|
||||
<Table.Th>GRN</Table.Th>
|
||||
<Table.Th>Customer ID</Table.Th>
|
||||
<Table.Th>Customer Name</Table.Th>
|
||||
<Table.Th>Arrival Time</Table.Th>
|
||||
<Table.Th>Container #</Table.Th>
|
||||
@@ -2328,7 +2550,18 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rows.map((r: ImportUnloadedItem) => (
|
||||
<Table.Tr key={r.id}>
|
||||
<Fragment key={r.id}>
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label="Show containers"
|
||||
onClick={() => setExpandedRow(expandedRow === r.id ? null : r.id)}
|
||||
>
|
||||
{expandedRow === r.id ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Checkbox
|
||||
aria-label={`Select ${r.bookingReference ?? r.id}`}
|
||||
@@ -2337,20 +2570,11 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{r.bookingId ? `${r.bookingId.slice(0, 8)}…` : '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Stack gap={2}>
|
||||
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
|
||||
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
|
||||
</Stack>
|
||||
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{r.customerId ? `${r.customerId.slice(0, 8)}…` : '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{r.customerName ?? '—'}</Table.Td>
|
||||
<Table.Td>{formatDate(r.arrivalTime)}</Table.Td>
|
||||
<Table.Td>{r.containerNumber ?? '—'}</Table.Td>
|
||||
@@ -2369,7 +2593,7 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="indigo" variant="light" size="sm">{r.currentStatus}</Badge>
|
||||
<InventoryStatusBadge status={r.currentStatus as InventoryStatus} />
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||
@@ -2463,6 +2687,14 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{expandedRow === r.id && (
|
||||
<BookingItemsExpansion
|
||||
bookingId={r.bookingId}
|
||||
colSpan={16}
|
||||
bulkFallback={r.cargoType ? `Bulk cargo — ${r.cargoType}, ${formatNumber(Number(r.weight))} t` : undefined}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
@@ -2491,6 +2723,7 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
bookingId={containerItemsItem?.booking?.id ?? null}
|
||||
bookingReference={containerItemsItem?.booking?.reference ?? null}
|
||||
/>
|
||||
<ConfirmActionModal action={confirmAction} onClose={() => setConfirmAction(null)} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,7 +112,9 @@ const parseInspectionNote = (notes: string | null | undefined) => {
|
||||
export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: ReleaseOrderModalProps) {
|
||||
const { toast } = useToast();
|
||||
const releaseMutation = useMutation(api.warehouses.release.mutationOptions());
|
||||
const bookingId = item?.booking?.id;
|
||||
// Some openers (inventory workbench) supply bookingId without the booking
|
||||
// relation — fall back to it, or the truck/container-weight queries never run.
|
||||
const bookingId = item?.booking?.id ?? item?.bookingId ?? undefined;
|
||||
// Customer self-haul trucks assigned to this booking via the portal.
|
||||
const { data: customerTrucks = [] } = useQuery({
|
||||
queryKey: ['release-customer-trucks', bookingId],
|
||||
@@ -200,8 +202,11 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
||||
];
|
||||
// Only trucks actually assigned to THIS booking (last-mile prefill or customer
|
||||
// portal) are selectable. No global fleet list — if nothing is assigned, the
|
||||
// operator types the plate manually in the field below.
|
||||
const truckSelectOptions = assignedTruckOptions;
|
||||
// operator types the plate manually in the field below. Deduped by plate:
|
||||
// duplicate option values crash Mantine's Select.
|
||||
const truckSelectOptions = [
|
||||
...new Map(assignedTruckOptions.map((t) => [t.value, t])).values(),
|
||||
];
|
||||
// Neither a last-mile truck nor a customer truck has been assigned yet.
|
||||
const noTruckAssigned = assignedTruckOptions.length === 0 && !isCustomerAssignedTruck;
|
||||
const isTruckIdentityLocked = isEntranceLocked || isCustomerAssignedTruck || hasLastMileTruckPrefill;
|
||||
@@ -214,10 +219,19 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
||||
const containerWeightByNumber = new Map(
|
||||
containerWeights.map((c) => [c.containerNumber.toUpperCase(), Number(c.weightTons) || 0]),
|
||||
);
|
||||
const containerSelectData = containerWeights.map((c) => ({
|
||||
value: c.containerNumber,
|
||||
label: `${c.containerNumber} · ${(Number(c.weightTons) || 0).toLocaleString()} t`,
|
||||
}));
|
||||
// Mantine Selects throw on duplicate option values — legacy bookings can carry
|
||||
// the same container number on two lines, so dedupe defensively.
|
||||
const containerSelectData = [
|
||||
...new Map(
|
||||
containerWeights.map((c) => [
|
||||
c.containerNumber,
|
||||
{
|
||||
value: c.containerNumber,
|
||||
label: `${c.containerNumber} · ${(Number(c.weightTons) || 0).toLocaleString()} t`,
|
||||
},
|
||||
]),
|
||||
).values(),
|
||||
];
|
||||
const selectedContainerNumbers = containerNumbers.map((n) => n.trim()).filter(Boolean);
|
||||
const selectedCargoWeight = Number(
|
||||
selectedContainerNumbers
|
||||
|
||||
Reference in New Issue
Block a user