mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string | null>(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. */}
|
||||
<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) => {
|
||||
const detention = t.vehicleId ? detentionByVehicle.get(t.vehicleId) : undefined;
|
||||
const primaryId = t.inventoryIds[0] ?? null;
|
||||
@@ -337,6 +418,21 @@ function TruckRows({ group }: { group: BookingGroup }) {
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<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
|
||||
leftSection={<FileText size={14} />}
|
||||
disabled={!primaryId}
|
||||
@@ -388,6 +484,12 @@ function TruckRows({ group }: { group: BookingGroup }) {
|
||||
onClose={() => setInspectId(null)}
|
||||
inventoryId={inspectId}
|
||||
/>
|
||||
<ReleaseOrderModal
|
||||
opened={Boolean(release)}
|
||||
onClose={closeRelease}
|
||||
item={release?.item ?? null}
|
||||
truckPrefill={release?.prefill ?? null}
|
||||
/>
|
||||
{isEdr && (
|
||||
<>
|
||||
<TruckDetentionModal
|
||||
@@ -459,16 +561,11 @@ export default function ImportTrucksPage() {
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th w={40} />
|
||||
<Table.Th>Plate</Table.Th>
|
||||
<Table.Th>Type</Table.Th>
|
||||
<Table.Th>Containers</Table.Th>
|
||||
<Table.Th>Truck Arrival</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>
|
||||
{TRUCK_COLUMNS.map((label) => (
|
||||
<Table.Th key={label} ta={label === "Actions" ? "right" : undefined}>
|
||||
{label}
|
||||
</Table.Th>
|
||||
))}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
|
||||
Reference in New Issue
Block a user