feat(train-scheduling): add direct truck-to-train load action beside Load

Export bookings whose cargo goes straight from customer truck to wagon
never get a warehouse GRN, so the load gate (assertExportReceivedWithGrn)
rejects them forever unless staff first flip exportHandoverMode on the
booking detail page, then come back and retry Load.

Add a Truck to Train button next to Load in the schedule workspace's
on-train list, shown for EXPORT bookings at the boarding yard. One click
sets exportHandoverMode=DIRECT_TO_TRAIN then loads the booking.
This commit is contained in:
Hagernesh
2026-08-13 08:55:17 +00:00
parent 833e62990e
commit 58cb71a9cf

View File

@@ -29,6 +29,7 @@ import {
// Repeat, // used by the hidden Move (reassign) button
Train,
TrainFront,
Truck,
Weight,
X,
} from "lucide-react";
@@ -37,6 +38,7 @@ import { CountdownTimer } from "@edr/ui-common";
import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
import { api } from "@/services/api";
import { bookingsService } from "@/services/bookings.service";
import { useToast } from "@/hooks/use-toast";
import type {
EligibleContainerBooking,
@@ -411,6 +413,31 @@ export function ScheduleWorkspacePanel({
);
};
// Export cargo that skipped the warehouse (customer truck straight onto the
// wagon) has no GRN and never will — loadBooking's GRN gate would keep
// rejecting it forever. Setting DIRECT_TO_TRAIN tells that gate the carriage
// acceptance sheet is the handover document instead, then loads in one click.
const [truckToTrainPending, setTruckToTrainPending] = useState<string | null>(null);
const doTruckToTrain = (bookingId: string, ref: string) => {
setTruckToTrainPending(bookingId);
bookingsService
.setExportHandoverMode(bookingId, "DIRECT_TO_TRAIN")
.then(() => loadJourney.mutateAsync({ scheduleId: schedule.id, bookingId }))
.then(() => {
toast({ title: `${ref} loaded — direct truck-to-train handover` });
onChanged();
void yardWorkQuery.refetch();
})
.catch((error) =>
toast({
title: "Could not load as direct truck-to-train",
description: apiErrorMessage(error, "Please try again."),
variant: "destructive",
}),
)
.finally(() => setTruckToTrainPending(null));
};
const doUnload = (bookingId: string, ref: string) => {
unloadJourney
.mutateAsync({ scheduleId: schedule.id, bookingId })
@@ -708,6 +735,12 @@ export function ScheduleWorkspacePanel({
const alightHere = trainAtYardId != null && b.destinationYardId === trainAtYardId;
const showLoad = canWork && !riding && !done && (journey?.canLoad ?? false);
const showUnload = canWork && riding && (journey?.canUnload ?? false);
const showTruckToTrain =
canWork &&
!riding &&
!done &&
boardHere &&
b.tradeDirection === "EXPORT";
return (
<BookingCard
key={b.id}
@@ -764,6 +797,24 @@ export function ScheduleWorkspacePanel({
</Button>
</Tooltip>
) : null}
{showTruckToTrain ? (
<Tooltip
label="Customer truck loaded straight onto the wagon — no warehouse receipt, no GRN. Sets direct truck-to-train handover and loads."
withArrow
>
<Button
size="compact-sm"
variant="light"
color="blue"
radius="md"
leftSection={<Truck size={13} />}
loading={truckToTrainPending === b.id}
onClick={() => doTruckToTrain(b.id, ref)}
>
Truck to Train
</Button>
</Tooltip>
) : null}
{showUnload ? (
<Tooltip
label={