diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts index c96c54f9b..2fb9b3032 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts @@ -92,6 +92,7 @@ interface CarriageAcceptanceWagonRow { interface CarriageAcceptanceReceivedRow { allocatedWeightTons: string | null; containerNumbers: string | null; + sealNumbers?: string | null; } const URGENT_PRIORITY_THRESHOLD = 1000; @@ -291,15 +292,19 @@ export class BookingsService { if (pendingWagons) { // Direct truck-to-train cargo never enters the warehouse, so there is no // GRN'd inventory to build the sheet from. Choosing direct handover is - // itself the acceptance, so the sheet issues off the booking's own - // containers (or its VGM weight when the cargo is bulk). + // itself the acceptance, so the sheet issues off the containers the + // customer declared on the booking — freight.containers only gains rows at + // allocation, by which point the wagon query above already serves. const receivedLines: CarriageAcceptanceReceivedRow[] = isDirectExport ? await this.dataSource.query( `SELECT NULL::numeric AS "allocatedWeightTons", - c.container_number AS "containerNumbers" - FROM freight.containers c - WHERE c.booking_id = $1 AND c.deleted_at IS NULL - ORDER BY c.container_number`, + unit.container_number AS "containerNumbers", + unit.seal_number AS "sealNumbers" + FROM freight.booking_container_units unit + JOIN freight.booking_container line + ON line.id = unit.booking_container_id AND line.deleted_at IS NULL + WHERE line.booking_id = $1 AND unit.deleted_at IS NULL + ORDER BY unit.container_number`, [bookingId], ) : booking.tradeDirection === 'EXPORT' @@ -319,11 +324,13 @@ export class BookingsService { ) : []; // Bulk direct cargo has no containers — one line carrying the booking's - // declared weight still makes a valid sheet. + // declared weight still makes a valid sheet. bulkTotalWeightTons only + // holds the real tonnage for PER_ITEM break-bulk; everywhere else (PER_TON + // bulk and every container booking) the VGM column is the weight. if (isDirectExport && receivedLines.length === 0) { + const totalWeight = booking.bulkTotalWeightTons ?? booking.cargoTotalWeightVgm; receivedLines.push({ - allocatedWeightTons: - booking.bulkTotalWeightTons == null ? null : String(booking.bulkTotalWeightTons), + allocatedWeightTons: totalWeight == null ? null : String(totalWeight), containerNumbers: null, }); } @@ -347,7 +354,7 @@ export class BookingsService { marshalledAt: null, arrivalAt: null, containerNumbers: row.containerNumbers, - sealNumbers: null, + sealNumbers: row.sealNumbers ?? null, })); } diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts index 8668b4f75..bf6ab5069 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts @@ -95,6 +95,20 @@ export class BookingJourneyService { await manager .getRepository(TrainScheduleBooking) .update({ trainScheduleId: scheduleId, bookingId }, { loadingStatus: 'LOADED' }); + // Warehouse cargo may be loaded either from the warehouse Load-to-Train + // queue or from the schedule itself. Loading here must move its inventory + // too, otherwise the goods read as still sitting in the shed while the + // train leaves with them. No-ops for direct truck-to-train (no inventory). + // ponytail: no WarehouseLoading record on this path — those are only read + // back as per-inventory loading history, never billed. Create them here if + // that history ever has to be complete. + await manager.query( + `UPDATE freight.warehouse_inventory + SET status = 'LOADED', loaded_at = COALESCE(loaded_at, $2), updated_at = NOW() + WHERE booking_id = $1 AND deleted_at IS NULL + AND status NOT IN ('LOADED', 'DISPATCHED')`, + [bookingId, now], + ); // The facility handed the cargo over — raise its GRN. No-ops for yards // without a facility (import/export terminals), which keep their own flow. await this.facilityHandling.recordHandling(manager, { diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index 4f13ec6e0..271d38f61 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -92,7 +92,6 @@ import TrainDetailPage from "./pages/trains/TrainDetailPage"; import TrainBuilderDetailPage from "./pages/trainBuilder/TrainBuilderDetailPage"; import TrainBuilderListPage from "./pages/trainBuilder/TrainBuilderListPage"; import ArrivalQueuePage from "./pages/warehouses/ArrivalQueuePage"; -import EDRLastMileReturnsPage from "./pages/warehouses/EDRLastMileReturnsPage"; import ContainerReturnsPage from "./pages/warehouses/ContainerReturnsPage"; import DispatchQueuePage from "./pages/warehouses/DispatchQueuePage"; import ExportDjiboutiUnloadingQueuePage from "./pages/warehouses/ExportDjiboutiUnloadingQueuePage"; @@ -506,7 +505,6 @@ const App = () => { } /> } /> } /> - } /> } /> } /> } /> diff --git a/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx b/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx index e0bf818ff..cfbc50528 100644 --- a/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx +++ b/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx @@ -344,12 +344,6 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] icon: , permission: FREIGHT_PERMS.warehouseInventory.view, }, - { - label: "EDR Last Mile Returns", - href: "/dashboard/edr-last-mile-returns", - icon: , - permission: FREIGHT_PERMS.warehouseInventory.view, - }, { label: "Container Returns", href: "/dashboard/container-returns", diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/EDRLastMileReturnsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/EDRLastMileReturnsPage.tsx deleted file mode 100644 index 4c38841f8..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/EDRLastMileReturnsPage.tsx +++ /dev/null @@ -1,406 +0,0 @@ -import { Fragment, useMemo, useState } from "react"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { - ActionIcon, - Alert, - Badge, - Button, - Group, - Loader, - Modal, - Stack, - Table, - Text, - TextInput, - Textarea, - Select, - Checkbox, -} from "@mantine/core"; -import { ChevronDown, ChevronRight } from "lucide-react"; - -import { PageContainer, PageHeader } from "@/components/page"; -import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter"; -import { useListControls } from "@/hooks/useListControls"; -import { useToast } from "@/hooks/use-toast"; -import { api } from "@/services/api"; -import { warehouseService } from "@/services/warehouse.service"; -import { importOperationsService } from "@/services/importOperations.service"; - -interface ReturnContainer { - containerNumber: string; - size: string | null; - type: string | null; - selected: boolean; -} - -interface TruckReturn { - key: string; - plate: string; - companyName: string | null; - bookingRef: string; - bookingId: string; - customerId: string | null; - containers: ReturnContainer[]; -} - - -export default function EDRLastMileReturnsPage() { - const { toast } = useToast(); - const qc = useQueryClient(); - const [expanded, setExpanded] = useState(null); - const [returnModalOpen, setReturnModalOpen] = useState(false); - const [activeKey, setActiveKey] = useState(null); - - const { data: unloadedQueue = [], isLoading: queueLoading } = useQuery({ - queryKey: ["import-unloaded-queue"], - queryFn: async () => { - const response = await api.warehouses.importUnloadedQueue.call(); - return response ?? []; - }, - }); - - const bookingIds = unloadedQueue.map((item) => item.bookingId).filter(Boolean) as string[]; - const truckReturnsQuery = useQuery({ - queryKey: ["edr-last-mile-returns", bookingIds], - queryFn: async () => { - const grouped = new Map(); - - for (const item of unloadedQueue) { - if (!item.bookingId) continue; - - const edrTrucks = await warehouseService.getLastMileTrucks(item.bookingId).catch(() => []); - for (const truck of edrTrucks) { - const inventory = await api.warehouses.listInventory.call({ filter: { bookingId: item.bookingId } }).catch(() => []); - - const returnContainers = inventory - .filter((inv: any) => inv.isReturn) - .map((inv: any) => ({ - containerNumber: inv.containerNumber || "—", - size: inv.containerSize || null, - type: inv.containerType || null, - selected: false, - })); - - if (returnContainers.length > 0) { - const key = `${item.bookingId}-${truck.vehicleId}`; - grouped.set(key, { - key, - plate: [truck.truckPlateNumber, truck.trailerPlateNumber].filter(Boolean).join(" + ") || "—", - companyName: item.customerName ?? null, - bookingRef: item.bookingReference ?? item.bookingId, - bookingId: item.bookingId, - customerId: item.customerId || null, - containers: returnContainers, - }); - } - } - } - - return Array.from(grouped.values()); - }, - enabled: bookingIds.length > 0 && !queueLoading, - }); - - const trucksWithReturns = useMemo(() => truckReturnsQuery.data ?? [], [truckReturnsQuery.data]); - const controls = useListControls(trucksWithReturns, { - searchKeys: ["plate", "companyName", "bookingRef"], - }); - - const createReturnsMutation = useMutation({ - mutationFn: async (payload: { trucks: Array<{ bookingId: string; customerId: string | null; containers: Array<{ containerNumber: string; returnDate: string; facility: string; yard?: string; zone?: string; condition?: string; handoverNote?: string }> }> }) => { - const results = []; - for (const truck of payload.trucks) { - for (const container of truck.containers) { - const result = await importOperationsService.createEmptyReturn({ - containerNumber: container.containerNumber, - returnDate: new Date(container.returnDate).toISOString(), - bookingId: truck.bookingId, - customerId: truck.customerId ?? undefined, - facility: container.facility, - yard: container.yard, - zone: container.zone, - condition: container.condition, - handoverNote: container.handoverNote, - }); - results.push(result); - } - } - return results; - }, - onSuccess: () => { - toast({ title: "Empty container returns recorded" }); - qc.invalidateQueries({ queryKey: ["edr-last-mile-returns", bookingIds] }); - setReturnModalOpen(false); - setActiveKey(null); - }, - onError: (error: any) => { - toast({ - variant: "destructive", - title: "Failed to record returns", - description: error?.response?.data?.message || error?.message, - }); - }, - }); - - const activeTruck = activeKey ? trucksWithReturns.find(t => t.key === activeKey) ?? null : null; - - if (queueLoading || truckReturnsQuery.isLoading) { - return ( - - - - - - ); - } - - return ( - - - - {trucksWithReturns.length === 0 ? ( - No EDR trucks with return containers found. - ) : ( - <> - - - - - - Plate - Company - Booking Ref - Return Containers - Actions - - - - {controls.pagedRows.map((truck) => { - const isOpen = expanded === truck.key; - return ( - - - - setExpanded(isOpen ? null : truck.key)} - > - {isOpen ? : } - - - - {truck.plate} - - {truck.companyName ?? "—"} - {truck.bookingRef} - - {truck.containers.length} container{truck.containers.length !== 1 ? "s" : ""} - - - - - - {isOpen && ( - - -
- - - - - - Container - Size - Type - - - - {truck.containers.map((container, idx) => ( - - - - - {container.containerNumber} - {container.size ?? "—"} - {container.type ?? "—"} - - ))} - -
- - - )} - - ); - })} - - -
- - - )} - - setReturnModalOpen(false)} - truck={activeTruck} - onSubmit={(payload) => createReturnsMutation.mutate(payload)} - loading={createReturnsMutation.isPending} - /> -
- ); -} - -interface EmptyContainerReturnModalProps { - opened: boolean; - onClose: () => void; - truck: TruckReturn | null; - onSubmit: (payload: any) => void; - loading: boolean; -} - -function EmptyContainerReturnModal({ opened, onClose, truck, onSubmit, loading }: EmptyContainerReturnModalProps) { - const [selectedContainers, setSelectedContainers] = useState([]); - const [returnDate, setReturnDate] = useState(new Date().toISOString().split("T")[0]); - const [warehouse, setWarehouse] = useState(null); - const [condition, setCondition] = useState(""); - const [handoverNote, setHandoverNote] = useState(""); - - const { data: warehousesResponse } = useQuery({ - queryKey: ["warehouses-list"], - queryFn: async () => { - return await warehouseService.list({}); - }, - }); - - const warehouses = (warehousesResponse as any)?.data ?? warehousesResponse ?? []; - const warehouseOptions = Array.isArray(warehouses) ? warehouses.map((wh: any) => ({ - value: wh.id, - label: `${wh.name} ${wh.code ? `(${wh.code})` : ""}`, - })) : []; - - const selectedWarehouse = warehouse && Array.isArray(warehouses) ? warehouses.find((wh: any) => wh.id === warehouse) : null; - - const handleSubmit = () => { - if (!truck || !selectedContainers.length || !warehouse) return; - - const containers = truck.containers - .filter((c) => selectedContainers.includes(c.containerNumber)) - .map((c) => ({ - containerNumber: c.containerNumber, - returnDate, - facility: selectedWarehouse?.name || warehouse, - yard: selectedWarehouse?.code || undefined, - zone: undefined, - condition: condition || undefined, - handoverNote: handoverNote || undefined, - })); - - onSubmit({ - trucks: [{ - bookingId: truck.bookingId, - customerId: truck.customerId, - containers, - }], - }); - }; - - return ( - - {truck && ( - - - {truck.plate} - {truck.bookingRef} - - -
- Select containers to return: - - {truck.containers.map((container) => ( - { - if (e.currentTarget.checked) { - setSelectedContainers([...selectedContainers, container.containerNumber]); - } else { - setSelectedContainers(selectedContainers.filter(c => c !== container.containerNumber)); - } - }} - /> - ))} - -
- -