From fd1646e4d515c62bcf249b1e7d35b32293fb2366 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Thu, 30 Jul 2026 12:14:49 +0000 Subject: [PATCH] feat: repeat truck column labels inside each expanded booking Extract the truck column list to TRUCK_COLUMNS and drive both the table head and a per-booking label row from it, so an expanded booking's rows are labelled without scrolling back to the head. --- .../src/components/warehouses/options.ts | 40 ++++++ .../src/pages/operations/LastMilePage.tsx | 35 +---- .../src/pages/warehouses/ImportTrucksPage.tsx | 123 ++++++++++++++++-- 3 files changed, 151 insertions(+), 47 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/options.ts b/apps/edr-freight-web/backoffice/src/components/warehouses/options.ts index 958e2d7f7..8b12d33e6 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/options.ts +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/options.ts @@ -4,7 +4,9 @@ import { WAREHOUSE_ZONE_TYPES, WAREHOUSE_STATUSES, INVENTORY_STATUSES, + type ImportUnloadedItem, type Warehouse, + type WarehouseInventoryItem, type WarehouseYard, } from '@/types/warehouse'; @@ -120,6 +122,44 @@ export const extractErrorMessage = (error: unknown, fallback = 'Something went w return Array.isArray(rawMessage) ? rawMessage.join(', ') : rawMessage ? String(rawMessage) : fallback; }; +/** + * An unloaded-queue row seen as the inventory item `ReleaseOrderModal` expects. + * Both truck-arrival openers (last mile, import trucks) work off queue rows. + */ +export const toReleaseInventoryItem = (row: ImportUnloadedItem): WarehouseInventoryItem => + ({ + id: row.id, + bookingId: row.bookingId, + quantity: 1, + weight: Number(row.weight) || 0, + grnNumber: row.grnNumber, + status: row.currentStatus, + arrivedAt: row.arrivalTime, + unloadedAt: row.arrivalTime, + inspectionStatus: row.inspectionStatus, + releaseDate: row.releaseDate, + releaseOrderReference: row.releaseOrderReference, + handoverDocumentReference: row.handoverDocumentReference, + handoverDocumentDate: row.handoverDocumentDate, + deliveredAt: row.deliveredAt, + // Carries the saved [Exit Inspection] block so truck-leaving prefills the + // details captured at arrival (plate, driver, tare, gate-in). + notes: row.notes, + booking: row.bookingId + ? { + id: row.bookingId, + reference: row.bookingReference ?? row.bookingId, + tradeDirection: 'IMPORT', + lastMileDeliveryAddress: row.lastMileRequested ? row.pickupOption : null, + customerTruckPlateNumber: row.customerTruckPlateNumber, + customerTruckDriverName: row.customerTruckDriverName, + customerTruckType: row.customerTruckType, + customerTruckContainerNumber: row.customerTruckContainerNumber, + customerTruckAssignedAt: row.customerTruckAssignedAt, + } + : null, + }) as unknown as WarehouseInventoryItem; + /** * Error extractor for blob-download requests. When `responseType: 'blob'`, axios * delivers the JSON error body as a Blob, so `extractErrorMessage` can't read diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx index 4145beb31..33e9bbed8 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -57,6 +57,7 @@ import { import { vehiclesService } from "@/services/vehicles.service"; import { driversService, type Driver } from "@/services/drivers.service"; import { ReleaseOrderModal, type ReleaseOrderTruckPrefill } from "@/components/warehouses/ReleaseOrderModal"; +import { toReleaseInventoryItem } from "@/components/warehouses/options"; import { EdrTruckExitPapersModal } from "./EdrTruckExitPapersModal"; import { TruckDetentionModal } from "@/components/operations/TruckDetentionModal"; import { ProofOfDeliveryModal } from "@/components/operations/ProofOfDeliveryModal"; @@ -299,40 +300,6 @@ const requestedDate = (r: LastMileRecord) => { const serviceTypeName = (r: LastMileRecord) => r.booking?.serviceType?.label ?? r.booking?.serviceType?.name ?? "—"; -const toReleaseInventoryItem = (row: ImportUnloadedItem): WarehouseInventoryItem => - ({ - id: row.id, - bookingId: row.bookingId, - quantity: 1, - weight: Number(row.weight) || 0, - grnNumber: row.grnNumber, - status: row.currentStatus, - arrivedAt: row.arrivalTime, - unloadedAt: row.arrivalTime, - inspectionStatus: row.inspectionStatus, - releaseDate: row.releaseDate, - releaseOrderReference: row.releaseOrderReference, - handoverDocumentReference: row.handoverDocumentReference, - handoverDocumentDate: row.handoverDocumentDate, - deliveredAt: row.deliveredAt, - // Carries the saved [Exit Inspection] block so truck-leaving prefills the - // details captured at arrival (plate, driver, tare, gate-in). - notes: row.notes, - booking: row.bookingId - ? { - id: row.bookingId, - reference: row.bookingReference ?? row.bookingId, - tradeDirection: "IMPORT", - lastMileDeliveryAddress: row.lastMileRequested ? row.pickupOption : null, - customerTruckPlateNumber: row.customerTruckPlateNumber, - customerTruckDriverName: row.customerTruckDriverName, - customerTruckType: row.customerTruckType, - customerTruckContainerNumber: row.customerTruckContainerNumber, - customerTruckAssignedAt: row.customerTruckAssignedAt, - } - : null, - }) as unknown as WarehouseInventoryItem; - const releasePrefillFromLastMile = ( record: LastMileRecord, row?: ImportUnloadedItem | null, diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/ImportTrucksPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/ImportTrucksPage.tsx index 1686d0106..8072b67d5 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/ImportTrucksPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/ImportTrucksPage.tsx @@ -1,5 +1,5 @@ import { Fragment, useMemo, useState } from "react"; -import { useQueries, useQuery } from "@tanstack/react-query"; +import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; import { ActionIcon, Alert, @@ -18,6 +18,7 @@ import { FileText, MoreHorizontal, Receipt, + Truck, } from "lucide-react"; import type { Freight } from "@edr/types"; @@ -27,14 +28,22 @@ import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter"; import { InspectionReportModal } from "@/components/warehouses/InspectionReportModal"; import { TruckDetentionModal } from "@/components/operations/TruckDetentionModal"; import { WarehouseGateTimesModal } from "@/components/operations/WarehouseGateTimesModal"; -import { extractDownloadErrorMessage, formatNumber } from "@/components/warehouses/options"; +import { + ReleaseOrderModal, + type ReleaseOrderTruckPrefill, +} from "@/components/warehouses/ReleaseOrderModal"; +import { + extractDownloadErrorMessage, + formatNumber, + toReleaseInventoryItem, +} from "@/components/warehouses/options"; import { openPdfBlob } from "@/components/warehouses/pdf"; import { useListControls } from "@/hooks/useListControls"; import { useToast } from "@/hooks/use-toast"; import { api } from "@/services/api"; import { lastMileService } from "@/services/last-mile.service"; import { warehouseService, type LastMileArrivalTruck } from "@/services/warehouse.service"; -import type { ImportUnloadedItem } from "@/types/warehouse"; +import type { ImportUnloadedItem, WarehouseInventoryItem } from "@/types/warehouse"; /** * Import trucks — the unloaded queue seen truck-first instead of item-first. @@ -53,6 +62,20 @@ import type { ImportUnloadedItem } from "@/types/warehouse"; const COLS = 11; +/** Truck columns — the table head, and repeated inside each expanded booking. */ +const TRUCK_COLUMNS = [ + "Plate", + "Type", + "Containers", + "Truck Arrival", + "Truck Leaving", + "Weight", + "Demurrage", + "Storage", + "Detention", + "Actions", +] as const; + const money = (amount: number, currency: string) => `${Number(amount).toLocaleString(undefined, { maximumFractionDigits: 2 })} ${currency === "ETB" ? "ETB" : currency}`; @@ -109,6 +132,8 @@ interface TruckRow { vehicleId: string | null; arrivedAt: string | null; departedAt: string | null; + /** Identity handed to the release modal so it opens on THIS truck. */ + prefill: ReleaseOrderTruckPrefill; } /** @@ -117,10 +142,17 @@ interface TruckRow { */ function TruckRows({ group }: { group: BookingGroup }) { const { toast } = useToast(); + const queryClient = useQueryClient(); const [busy, setBusy] = useState(false); const [inspectId, setInspectId] = useState(null); const [detentionOpen, setDetentionOpen] = useState(false); const [gateTimesOpen, setGateTimesOpen] = useState(false); + // The truck whose arrival/exit weighing is open — kept in state so the prefill + // object stays referentially stable while the modal is up. + const [release, setRelease] = useState<{ + item: WarehouseInventoryItem; + prefill: ReleaseOrderTruckPrefill; + } | null>(null); const edrQuery = useQuery({ queryKey: ["booking-edr-trucks", group.bookingId], @@ -194,6 +226,15 @@ function TruckRows({ group }: { group: BookingGroup }) { vehicleId: t.vehicleId, arrivedAt: t.arrivedAt, departedAt: t.departedAt, + prefill: { + truckPlateNumber: t.truckPlateNumber, + trailerPlateNumber: t.trailerPlateNumber, + driverName: t.driverName, + driverLicense: t.driverLicense, + driverPhone: t.driverPhone, + truckType: t.truckType, + containerNumber: t.containerNumber, + }, ...costsFor(containers), }; }; @@ -208,11 +249,39 @@ function TruckRows({ group }: { group: BookingGroup }) { vehicleId: null, arrivedAt: t.arrivedAt ?? null, departedAt: t.departedAt ?? null, + prefill: { + truckPlateNumber: t.plateNumber, + driverName: t.driverName, + truckType: t.truckType, + containerNumber: containers.join(", "), + }, ...costsFor(containers), }; }; const trucks: TruckRow[] = isEdr ? edrTrucks.map(fromEdr) : customerTrucks.map(fromCustomer); + /** + * Arrival and leaving are one form per truck: the modal picks the step from + * that plate's own saved weighing block, so both menu items open it the same + * way. The booking-level opener (inventory workbench) stays as it was. + */ + const openRelease = (t: TruckRow) => { + const row = group.rows.find((r) => r.id === t.inventoryIds[0]) ?? group.rows[0]; + if (!row) return; + setRelease({ item: toReleaseInventoryItem(row), prefill: t.prefill }); + }; + + const closeRelease = () => { + setRelease(null); + void queryClient.invalidateQueries({ queryKey: ["booking-edr-trucks", group.bookingId] }); + void queryClient.invalidateQueries({ queryKey: ["booking-customer-trucks", group.bookingId] }); + // The saved weighing block lives in the inventory row's notes — refetch the + // queue or the next open would still show the truck as never arrived. + void queryClient.invalidateQueries({ + queryKey: api.warehouses.importUnloadedQueue.queryOptions({}).queryKey, + }); + }; + const openDocument = async ( kind: "release" | "handover", inventoryId: string, @@ -267,6 +336,18 @@ function TruckRows({ group }: { group: BookingGroup }) { return ( <> + {/* The booking row sits between the head and these trucks, so repeat the + column labels — otherwise an expanded booking reads as unlabelled. */} + + + {TRUCK_COLUMNS.map((label) => ( + + + {label} + + + ))} + {trucks.map((t, idx) => { const detention = t.vehicleId ? detentionByVehicle.get(t.vehicleId) : undefined; const primaryId = t.inventoryIds[0] ?? null; @@ -337,6 +418,21 @@ function TruckRows({ group }: { group: BookingGroup }) { + } + disabled={!primaryId} + onClick={() => openRelease(t)} + > + Truck Arrival + + } + disabled={!primaryId} + onClick={() => openRelease(t)} + > + Truck Leaving + + } disabled={!primaryId} @@ -388,6 +484,12 @@ function TruckRows({ group }: { group: BookingGroup }) { onClose={() => setInspectId(null)} inventoryId={inspectId} /> + {isEdr && ( <> - Plate - Type - Containers - Truck Arrival - Truck Leaving - Weight - Demurrage - Storage - Detention - Actions + {TRUCK_COLUMNS.map((label) => ( + + {label} + + ))}