From 5c33ea90bdc72013957f51033708984934d62a8e Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Mon, 3 Aug 2026 13:11:02 +0000 Subject: [PATCH] Truck operations inside each bookings --- .../bookings/detail/BookingTrucksPanel.tsx | 255 +++--------------- .../src/pages/warehouses/ImportTrucksPage.tsx | 6 +- 2 files changed, 38 insertions(+), 223 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingTrucksPanel.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingTrucksPanel.tsx index 1a77632ec..4427367cb 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingTrucksPanel.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingTrucksPanel.tsx @@ -1,13 +1,11 @@ import { useMemo, useState } from "react"; import { useQueries, useQuery } from "@tanstack/react-query"; -import { Badge, Button, Center, Group, Loader, SimpleGrid, Stack, Table, Text } from "@mantine/core"; +import { Button, Center, Group, Loader, SimpleGrid, Stack, Table, Text } from "@mantine/core"; import { Coins, Truck } from "lucide-react"; import { api } from "@/services/api"; -import { warehouseService } from "@/services/warehouse.service"; -import { lastMileService } from "@/services/last-mile.service"; import { FeePreviewModal } from "@/components/warehouses/FeePreviewModal"; -import { TruckDetentionModal } from "@/components/operations/TruckDetentionModal"; +import { groupByBooking, TruckRows } from "@/pages/warehouses/ImportTrucksPage"; import { SectionCard } from "./SectionCard"; import { MetricTile } from "./MetricTile"; @@ -15,43 +13,14 @@ import { MetricTile } from "./MetricTile"; const money = (amount: number, currency: string) => `${Number(amount).toLocaleString()} ${currency === "ETB" ? "Birr (ETB)" : currency}`; -const fmt = (iso: string | null | undefined) => (iso ? new Date(iso).toLocaleString() : "—"); - -function inspectionLabel(status: string | null | undefined): { text: string; color: string } { - if (!status) return { text: "Pending", color: "gray" }; - if (status === "PASSED") return { text: "Passed", color: "edr-green" }; - if (status === "FAILED") return { text: "Failed", color: "red" }; - return { text: status, color: "gray" }; -} - -interface TruckRow { - key: string; - plate: string; - driver: string | null; - truckType: string | null; - containers: string[]; - warehouseArrived: string | null; - warehouseDeparted: string | null; - destinationArrived: string | null; - returned: string | null; - detentionOpen: boolean; - detentionDays: number | null; - detentionAmount: number | null; - hasDetentionRule: boolean; - inspection: { text: string; color: string }; -} - /** - * Every truck tied to a booking's last mile — EDR-dispatched or customer - * self-haul (a booking only ever uses one), each with its own warehouse-gate - * and destination-detention clocks, plus the booking's cargo-side cost totals - * (storage/demurrage/double handling — billed per row internally, always - * shown here as one booking-level total). Detention stays EDR-only; customer - * self-haul rows show "—" since EDR only bills detention on its own fleet. + * Cargo costs (booking-level totals) plus the same truck-import block the + * Unloaded Queue's "Import trucks" view uses — Truck Arrival/Leaving, Exit + * paper, Handover, Inspect, Detention times, Warehouse gate times — reused + * as-is so this tab never drifts from that queue's behavior. */ export function BookingTrucksPanel({ bookingId }: { bookingId: string }) { const [feeModalOpen, setFeeModalOpen] = useState(false); - const [detentionModalOpen, setDetentionModalOpen] = useState(false); const inventoryQuery = useQuery( api.warehouses.listInventory.queryOptions({ input: { filter: { bookingId } } }), @@ -59,47 +28,27 @@ export function BookingTrucksPanel({ bookingId }: { bookingId: string }) { const inventoryItems = inventoryQuery.data ?? []; const latestInventory = inventoryItems[0] ?? null; - const edrTrucksQuery = useQuery({ - queryKey: ["booking-edr-trucks", bookingId], - queryFn: () => warehouseService.getLastMileTrucks(bookingId), - }); - const edrTrucks = edrTrucksQuery.data ?? []; - - const customerTrucksQuery = useQuery({ - queryKey: ["booking-customer-trucks", bookingId], - queryFn: () => warehouseService.getCustomerTrucks(bookingId), - enabled: edrTrucksQuery.isSuccess && edrTrucks.length === 0, - }); - const customerTrucks = customerTrucksQuery.data ?? []; - - const mode: "EDR" | "CUSTOMER" | "NONE" = - edrTrucks.length > 0 ? "EDR" : customerTrucks.length > 0 ? "CUSTOMER" : "NONE"; - - const containerItemsQuery = useQuery({ - queryKey: ["booking-container-items-for-trucks", bookingId], - queryFn: () => warehouseService.getContainerItems(bookingId), - }); - const inspectionByContainer = new Map( - (containerItemsQuery.data ?? []).map((c) => [c.containerNumber, c.inspectionStatus]), + // Same query key as the Unloaded Queue page — shares its cache instead of + // refetching the whole queue when it's already loaded elsewhere. + const unloadedQuery = useQuery(api.warehouses.importUnloadedQueue.queryOptions({})); + const bookingRows = useMemo( + () => (unloadedQuery.data ?? []).filter((row) => row.bookingId === bookingId), + [unloadedQuery.data, bookingId], ); - - const lastMileId = edrTrucks[0]?.lastMileId ?? null; - - const detentionPreviewQuery = useQuery({ - queryKey: ["truck-detention-preview-for-trucks-tab", lastMileId], - queryFn: () => lastMileService.truckDetentionPreview(lastMileId as string).then((r) => r.data), - enabled: Boolean(lastMileId), - }); - const detentionPreview = detentionPreviewQuery.data; - const detentionByVehicle = new Map( - (detentionPreview?.groups ?? []).map((g) => [g.vehicleId ?? "", g]), - ); - - const lastMileRecordQuery = useQuery({ - queryKey: ["last-mile-record-for-trucks-tab", lastMileId], - queryFn: () => lastMileService.getById(lastMileId as string).then((r) => r.data), - enabled: Boolean(lastMileId), - }); + const group = useMemo(() => { + const [existing] = groupByBooking(bookingRows); + return ( + existing ?? { + bookingId, + bookingReference: bookingId, + customerName: null, + trainSchedule: null, + status: "NONE", + arrivalTime: null, + rows: [], + } + ); + }, [bookingRows, bookingId]); // Booking-level cost strip: same per-row fee preview the accrual dashboard // and FeePreviewModal already use, summed across every inventory row on @@ -114,61 +63,7 @@ export function BookingTrucksPanel({ bookingId }: { bookingId: string }) { const sumByType = (type: string) => allFees.filter((f) => f.ruleType === type).reduce((sum, f) => sum + Number(f.amount || 0), 0); - const rows: TruckRow[] = useMemo(() => { - if (mode === "EDR") { - return edrTrucks.map((t) => { - const g = detentionByVehicle.get(t.vehicleId); - return { - key: t.vehicleId, - plate: [t.truckPlateNumber, t.trailerPlateNumber].filter(Boolean).join(" + ") || "—", - driver: t.driverName, - truckType: t.truckType, - containers: t.containerNumber ? [t.containerNumber] : [], - warehouseArrived: t.arrivedAt, - warehouseDeparted: t.departedAt, - destinationArrived: g?.startDate ?? null, - returned: g?.endIsOpen ? null : g?.endDate ?? null, - detentionOpen: Boolean(g?.endIsOpen), - detentionDays: g?.chargeableDays ?? null, - detentionAmount: g?.amount ?? null, - hasDetentionRule: Boolean(g?.ruleId), - inspection: inspectionLabel(t.containerNumber ? inspectionByContainer.get(t.containerNumber) : undefined), - }; - }); - } - if (mode === "CUSTOMER") { - return customerTrucks.map((t) => { - const containers = (t.containers ?? []).map((c) => c.containerNumber); - const statuses = new Set(containers.map((cn) => inspectionByContainer.get(cn) ?? null)); - const inspection = - containers.length === 0 - ? inspectionLabel(undefined) - : statuses.size > 1 - ? { text: "Mixed", color: "yellow" } - : inspectionLabel([...statuses][0]); - return { - key: t.id, - plate: t.plateNumber, - driver: t.driverName, - truckType: t.truckType, - containers, - warehouseArrived: t.arrivedAt ?? null, - warehouseDeparted: t.departedAt ?? null, - destinationArrived: null, - returned: null, - detentionOpen: false, - detentionDays: null, - detentionAmount: null, - hasDetentionRule: false, - inspection, - }; - }); - } - return []; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [mode, edrTrucks, customerTrucks, inspectionByContainer, detentionByVehicle]); - - if (inventoryQuery.isLoading || edrTrucksQuery.isLoading) { + if (inventoryQuery.isLoading || unloadedQuery.isLoading) { return (
@@ -201,87 +96,14 @@ export function BookingTrucksPanel({ bookingId }: { bookingId: string }) { - setDetentionModalOpen(true)}> - Detention times - - ) - } - > - {rows.length === 0 ? ( - - No trucks assigned to this booking's last mile yet. - - ) : ( - - - - - Plate - Driver - Type - Container(s) - Wh. arrived - Wh. departed - Dest. arrived - Returned - Detention - Inspection - - - - {rows.map((r) => ( - - {r.plate} - {r.driver ?? "—"} - {r.truckType ?? "—"} - {r.containers.length ? r.containers.join(", ") : "—"} - {fmt(r.warehouseArrived)} - {fmt(r.warehouseDeparted)} - {fmt(r.destinationArrived)} - - {r.detentionOpen ? ( - - still out - - ) : ( - fmt(r.returned) - )} - - - {mode !== "EDR" || r.detentionDays == null ? ( - "—" - ) : ( - <> - {r.detentionDays}d · {money(r.detentionAmount ?? 0, detentionPreview?.currency ?? "USD")} - {!r.hasDetentionRule && ( - - {" "} - · no rule - - )} - - )} - - - - {r.inspection.text} - - - - ))} - -
-
- )} + + + + + + +
+
setFeeModalOpen(false)} inventoryId={latestInventory?.id ?? null} /> - {mode === "EDR" && ( - setDetentionModalOpen(false)} - record={lastMileRecordQuery.data ?? 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 8072b67d5..bf10e86aa 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/ImportTrucksPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/ImportTrucksPage.tsx @@ -82,7 +82,7 @@ const money = (amount: number, currency: string) => const formatTime = (iso: string | null | undefined) => iso ? new Date(iso).toLocaleString() : "—"; -interface BookingGroup { +export interface BookingGroup { bookingId: string; bookingReference: string; customerName: string | null; @@ -93,7 +93,7 @@ interface BookingGroup { } /** One collapsed line per booking; its inventory rows travel with it for the fee lookup. */ -function groupByBooking(items: ImportUnloadedItem[]): BookingGroup[] { +export function groupByBooking(items: ImportUnloadedItem[]): BookingGroup[] { const groups = new Map(); for (const item of items) { if (!item.bookingId) continue; @@ -140,7 +140,7 @@ interface TruckRow { * Haulage mode is decided by which list comes back non-empty — a booking is * either EDR last mile or customer self-haul, never both. */ -function TruckRows({ group }: { group: BookingGroup }) { +export function TruckRows({ group }: { group: BookingGroup }) { const { toast } = useToast(); const queryClient = useQueryClient(); const [busy, setBusy] = useState(false);