From 58cb71a9cf6872e6dde8791a5f96d75e5bbec6e8 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Thu, 13 Aug 2026 08:55:17 +0000 Subject: [PATCH] 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. --- .../ScheduleWorkspacePanel.tsx | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/ScheduleWorkspacePanel.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/ScheduleWorkspacePanel.tsx index de954742e..25c20c8db 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/ScheduleWorkspacePanel.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/ScheduleWorkspacePanel.tsx @@ -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(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 ( ) : null} + {showTruckToTrain ? ( + + + + ) : null} {showUnload ? (