From bf5b0b5dc883c735e881727c54af4b95b4d1bc60 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Fri, 3 Jul 2026 14:03:02 +0000 Subject: [PATCH] =?UTF-8?q?approve-delivery=20exit-gate=20fix=20+=20Import?= =?UTF-8?q?=20Loading=20Confirmation=20frontend=20panel=20=E2=80=94=20done?= =?UTF-8?q?=20this=20session,=20not=20yet=20committed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../warehouses/warehouse-inventory.service.ts | 16 ++ .../ImportLoadingConfirmationPanel.tsx | 172 ++++++++++++++++++ .../TrainScheduleV2DetailPage.tsx | 25 +++ 3 files changed, 213 insertions(+) create mode 100644 apps/edr-freight-web/backoffice/src/components/trainScheduling/ImportLoadingConfirmationPanel.tsx diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts index 3b27c7213..ac6b71c89 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts @@ -2033,6 +2033,22 @@ export class WarehouseInventoryService { const isTruckLeaving = dto.grossWeight !== undefined && Boolean(dto.gateOutTime); if (isTruckLeaving) { await this.invoices.assertClearanceAllowed(id); + + if (item.bookingId) { + const [truckInfo]: Array<{ customerTruckAssignedAt: string | null }> = + await this.dataSource.query( + `SELECT customer_truck_assigned_at AS "customerTruckAssignedAt" + FROM freight.bookings + WHERE id = $1 AND deleted_at IS NULL`, + [item.bookingId], + ); + const usesCustomerTruck = Boolean(truckInfo?.customerTruckAssignedAt); + if (usesCustomerTruck && !this.extractCustomerDeliveryApproval(item.notes)) { + throw new BadRequestException( + 'Customer must approve delivery (sign the handover) before the exit paper can be generated', + ); + } + } } const releaseDate = isTruckLeaving ? dto.releaseDate ? new Date(dto.releaseDate) : new Date() diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/ImportLoadingConfirmationPanel.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/ImportLoadingConfirmationPanel.tsx new file mode 100644 index 000000000..ab1d907e6 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/ImportLoadingConfirmationPanel.tsx @@ -0,0 +1,172 @@ +import { useMemo, useState } from "react"; +import { PackageCheck } from "lucide-react"; +import { Badge, Button, Checkbox, Group, Loader, Paper, Stack, Text } from "@mantine/core"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; + +import { api } from "@/services/api"; +import type { + ImportLoadingBooking, + ImportLoadingBookingsResponse, + LoadingStatus, +} from "@/types/trainScheduling"; + +function ImportLoadingBookingRow({ + booking, + selected, + onToggle, +}: { + booking: ImportLoadingBooking; + selected: boolean; + onToggle: () => void; +}) { + return ( + + + + + + + {booking.reference ?? booking.id} + + + {booking.loadingStatus} + + + + {booking.customer ?? "Unknown customer"} + + + {booking.weightTons}T + + + + ); +} + +export function ImportLoadingConfirmationPanel({ + scheduleId, + items, + isLoading, +}: { + scheduleId: string; + items: ImportLoadingBooking[]; + isLoading?: boolean; +}) { + const [selectedIds, setSelectedIds] = useState([]); + const queryClient = useQueryClient(); + + const updateStatus = useMutation< + ImportLoadingBookingsResponse, + Error, + { id: string; bookingIds: string[]; loadingStatus: LoadingStatus } + >({ + ...api.trainScheduling.updateImportLoadingStatus.mutationOptions(), + onSuccess: () => { + setSelectedIds([]); + queryClient.invalidateQueries({ + queryKey: api.trainScheduling.importLoadingBookings.queryKey({ id: scheduleId }), + }); + }, + onError: (error) => { + toast.error(error instanceof Error ? error.message : "Could not update loading status"); + }, + }); + + const toggle = (id: string) => { + setSelectedIds((prev) => + prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id], + ); + }; + + const allIds = useMemo(() => items.map((b) => b.id), [items]); + + if (isLoading) { + return ( + + + + Loading import bookings… + + + ); + } + + if (!items.length) { + return ( + + + No paid import bookings with wagons allocated on this schedule + + + ); + } + + return ( + + + + Import bookings ({items.length}) + + + + + + + + + {items.map((booking) => ( + toggle(booking.id)} + /> + ))} + + + + + + + + ); +} diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx index 9319e6a01..c02eb2c39 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx @@ -42,6 +42,7 @@ import { } from "@/components/trainScheduling/containerPlacement.util"; import { ContainerPlacementGrid } from "@/components/trainScheduling/ContainerPlacementGrid"; import { FleetAvailabilitySummary } from "@/components/trainScheduling/FleetAvailabilitySummary"; +import { ImportLoadingConfirmationPanel } from "@/components/trainScheduling/ImportLoadingConfirmationPanel"; import { RescheduleTrainDialog } from "@/components/trainScheduling/RescheduleTrainDialog"; import { ScheduleBatchPanel } from "@/components/trainScheduling/ScheduleBatchPanel"; import { ScheduleBookingsStep } from "@/components/trainScheduling/ScheduleBookingsStep"; @@ -141,6 +142,13 @@ export default function TrainScheduleV2DetailPage() { }, }); + const importLoadingQuery = useQuery( + api.trainScheduling.importLoadingBookings.queryOptions({ + input: { id: scheduleId ?? "" }, + enabled: Boolean(scheduleId && schedule?.direction === "IMPORT"), + }), + ); + const eligibleFilters = useMemo( () => schedule @@ -951,6 +959,23 @@ export default function TrainScheduleV2DetailPage() { ]} /> + {schedule?.direction === "IMPORT" ? ( + + + Import loading confirmation + + Paid import bookings with a wagon allocated on this schedule. Marking loaded/unloaded + is tracking only — it does not block dispatch. + + + + + ) : null} + {gatepassApplies ? (