Merge pull request #1022 from Tria-plc/edrmiles

feat: repeat truck column labels inside each expanded booking
This commit is contained in:
Hagernesh Tadesse
2026-07-30 15:17:13 +03:00
committed by GitHub
3 changed files with 151 additions and 47 deletions

View File

@@ -4,7 +4,9 @@ import {
WAREHOUSE_ZONE_TYPES, WAREHOUSE_ZONE_TYPES,
WAREHOUSE_STATUSES, WAREHOUSE_STATUSES,
INVENTORY_STATUSES, INVENTORY_STATUSES,
type ImportUnloadedItem,
type Warehouse, type Warehouse,
type WarehouseInventoryItem,
type WarehouseYard, type WarehouseYard,
} from '@/types/warehouse'; } 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; 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 * Error extractor for blob-download requests. When `responseType: 'blob'`, axios
* delivers the JSON error body as a Blob, so `extractErrorMessage` can't read * delivers the JSON error body as a Blob, so `extractErrorMessage` can't read

View File

@@ -57,6 +57,7 @@ import {
import { vehiclesService } from "@/services/vehicles.service"; import { vehiclesService } from "@/services/vehicles.service";
import { driversService, type Driver } from "@/services/drivers.service"; import { driversService, type Driver } from "@/services/drivers.service";
import { ReleaseOrderModal, type ReleaseOrderTruckPrefill } from "@/components/warehouses/ReleaseOrderModal"; import { ReleaseOrderModal, type ReleaseOrderTruckPrefill } from "@/components/warehouses/ReleaseOrderModal";
import { toReleaseInventoryItem } from "@/components/warehouses/options";
import { EdrTruckExitPapersModal } from "./EdrTruckExitPapersModal"; import { EdrTruckExitPapersModal } from "./EdrTruckExitPapersModal";
import { TruckDetentionModal } from "@/components/operations/TruckDetentionModal"; import { TruckDetentionModal } from "@/components/operations/TruckDetentionModal";
import { ProofOfDeliveryModal } from "@/components/operations/ProofOfDeliveryModal"; import { ProofOfDeliveryModal } from "@/components/operations/ProofOfDeliveryModal";
@@ -299,40 +300,6 @@ const requestedDate = (r: LastMileRecord) => {
const serviceTypeName = (r: LastMileRecord) => const serviceTypeName = (r: LastMileRecord) =>
r.booking?.serviceType?.label ?? r.booking?.serviceType?.name ?? "—"; 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 = ( const releasePrefillFromLastMile = (
record: LastMileRecord, record: LastMileRecord,
row?: ImportUnloadedItem | null, row?: ImportUnloadedItem | null,

View File

@@ -1,5 +1,5 @@
import { Fragment, useMemo, useState } from "react"; import { Fragment, useMemo, useState } from "react";
import { useQueries, useQuery } from "@tanstack/react-query"; import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
import { import {
ActionIcon, ActionIcon,
Alert, Alert,
@@ -18,6 +18,7 @@ import {
FileText, FileText,
MoreHorizontal, MoreHorizontal,
Receipt, Receipt,
Truck,
} from "lucide-react"; } from "lucide-react";
import type { Freight } from "@edr/types"; import type { Freight } from "@edr/types";
@@ -27,14 +28,22 @@ import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
import { InspectionReportModal } from "@/components/warehouses/InspectionReportModal"; import { InspectionReportModal } from "@/components/warehouses/InspectionReportModal";
import { TruckDetentionModal } from "@/components/operations/TruckDetentionModal"; import { TruckDetentionModal } from "@/components/operations/TruckDetentionModal";
import { WarehouseGateTimesModal } from "@/components/operations/WarehouseGateTimesModal"; 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 { openPdfBlob } from "@/components/warehouses/pdf";
import { useListControls } from "@/hooks/useListControls"; import { useListControls } from "@/hooks/useListControls";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { api } from "@/services/api"; import { api } from "@/services/api";
import { lastMileService } from "@/services/last-mile.service"; import { lastMileService } from "@/services/last-mile.service";
import { warehouseService, type LastMileArrivalTruck } from "@/services/warehouse.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. * 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; 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) => const money = (amount: number, currency: string) =>
`${Number(amount).toLocaleString(undefined, { maximumFractionDigits: 2 })} ${currency === "ETB" ? "ETB" : currency}`; `${Number(amount).toLocaleString(undefined, { maximumFractionDigits: 2 })} ${currency === "ETB" ? "ETB" : currency}`;
@@ -109,6 +132,8 @@ interface TruckRow {
vehicleId: string | null; vehicleId: string | null;
arrivedAt: string | null; arrivedAt: string | null;
departedAt: 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 }) { function TruckRows({ group }: { group: BookingGroup }) {
const { toast } = useToast(); const { toast } = useToast();
const queryClient = useQueryClient();
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [inspectId, setInspectId] = useState<string | null>(null); const [inspectId, setInspectId] = useState<string | null>(null);
const [detentionOpen, setDetentionOpen] = useState(false); const [detentionOpen, setDetentionOpen] = useState(false);
const [gateTimesOpen, setGateTimesOpen] = 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({ const edrQuery = useQuery({
queryKey: ["booking-edr-trucks", group.bookingId], queryKey: ["booking-edr-trucks", group.bookingId],
@@ -194,6 +226,15 @@ function TruckRows({ group }: { group: BookingGroup }) {
vehicleId: t.vehicleId, vehicleId: t.vehicleId,
arrivedAt: t.arrivedAt, arrivedAt: t.arrivedAt,
departedAt: t.departedAt, 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), ...costsFor(containers),
}; };
}; };
@@ -208,11 +249,39 @@ function TruckRows({ group }: { group: BookingGroup }) {
vehicleId: null, vehicleId: null,
arrivedAt: t.arrivedAt ?? null, arrivedAt: t.arrivedAt ?? null,
departedAt: t.departedAt ?? null, departedAt: t.departedAt ?? null,
prefill: {
truckPlateNumber: t.plateNumber,
driverName: t.driverName,
truckType: t.truckType,
containerNumber: containers.join(", "),
},
...costsFor(containers), ...costsFor(containers),
}; };
}; };
const trucks: TruckRow[] = isEdr ? edrTrucks.map(fromEdr) : customerTrucks.map(fromCustomer); 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 ( const openDocument = async (
kind: "release" | "handover", kind: "release" | "handover",
inventoryId: string, inventoryId: string,
@@ -267,6 +336,18 @@ function TruckRows({ group }: { group: BookingGroup }) {
return ( return (
<> <>
{/* The booking row sits between the head and these trucks, so repeat the
column labels — otherwise an expanded booking reads as unlabelled. */}
<Table.Tr bg="var(--mantine-color-gray-1)">
<Table.Td />
{TRUCK_COLUMNS.map((label) => (
<Table.Td key={label} ta={label === "Actions" ? "right" : undefined}>
<Text size="xs" fw={700} c="dimmed">
{label}
</Text>
</Table.Td>
))}
</Table.Tr>
{trucks.map((t, idx) => { {trucks.map((t, idx) => {
const detention = t.vehicleId ? detentionByVehicle.get(t.vehicleId) : undefined; const detention = t.vehicleId ? detentionByVehicle.get(t.vehicleId) : undefined;
const primaryId = t.inventoryIds[0] ?? null; const primaryId = t.inventoryIds[0] ?? null;
@@ -337,6 +418,21 @@ function TruckRows({ group }: { group: BookingGroup }) {
</ActionIcon> </ActionIcon>
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Item
leftSection={<Truck size={14} />}
disabled={!primaryId}
onClick={() => openRelease(t)}
>
Truck Arrival
</Menu.Item>
<Menu.Item
leftSection={<Truck size={14} />}
disabled={!primaryId}
onClick={() => openRelease(t)}
>
Truck Leaving
</Menu.Item>
<Menu.Divider />
<Menu.Item <Menu.Item
leftSection={<FileText size={14} />} leftSection={<FileText size={14} />}
disabled={!primaryId} disabled={!primaryId}
@@ -388,6 +484,12 @@ function TruckRows({ group }: { group: BookingGroup }) {
onClose={() => setInspectId(null)} onClose={() => setInspectId(null)}
inventoryId={inspectId} inventoryId={inspectId}
/> />
<ReleaseOrderModal
opened={Boolean(release)}
onClose={closeRelease}
item={release?.item ?? null}
truckPrefill={release?.prefill ?? null}
/>
{isEdr && ( {isEdr && (
<> <>
<TruckDetentionModal <TruckDetentionModal
@@ -459,16 +561,11 @@ export default function ImportTrucksPage() {
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
<Table.Th w={40} /> <Table.Th w={40} />
<Table.Th>Plate</Table.Th> {TRUCK_COLUMNS.map((label) => (
<Table.Th>Type</Table.Th> <Table.Th key={label} ta={label === "Actions" ? "right" : undefined}>
<Table.Th>Containers</Table.Th> {label}
<Table.Th>Truck Arrival</Table.Th> </Table.Th>
<Table.Th>Truck Leaving</Table.Th> ))}
<Table.Th>Weight</Table.Th>
<Table.Th>Demurrage</Table.Th>
<Table.Th>Storage</Table.Th>
<Table.Th>Detention</Table.Th>
<Table.Th ta="right">Actions</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>