Merge pull request #867 from Tria-plc/testfixes

Onsite truck
Fixed
The page now lists every truck assigned to a booking that hasn't departed, both haulage paths, tagged:

On site — arrived, not departed (green)
Inbound — assigned, not yet arrived (grey)
This commit is contained in:
Hagernesh Tadesse
2026-07-21 11:50:15 +03:00
committed by GitHub
3 changed files with 59 additions and 20 deletions

View File

@@ -417,12 +417,16 @@ export class WarehouseInventoryService {
* *
* Covers both haulage paths because the gate does: a customer's own truck and * Covers both haulage paths because the gate does: a customer's own truck and
* an EDR last-mile truck arrive at the same barrier and need the same paper. * an EDR last-mile truck arrive at the same barrier and need the same paper.
* "On site" means arrived and not yet departed. * Includes trucks assigned but not yet arrived, flagged INBOUND, so staff see
* what is coming as well as what is here — an assigned truck only stamps
* `arrived_at` when it reaches the warehouse. A truck drops off the list once
* it departs.
*/ */
async trucksOnSite(): Promise< async trucksOnSite(): Promise<
Array<{ Array<{
source: 'CUSTOMER' | 'EDR'; source: 'CUSTOMER' | 'EDR';
assignmentId: string; assignmentId: string;
status: 'INBOUND' | 'ON_SITE';
plateNumber: string | null; plateNumber: string | null;
driverName: string | null; driverName: string | null;
truckType: string | null; truckType: string | null;
@@ -436,6 +440,7 @@ export class WarehouseInventoryService {
return this.dataSource.query( return this.dataSource.query(
`SELECT 'CUSTOMER' AS "source", `SELECT 'CUSTOMER' AS "source",
a.id AS "assignmentId", a.id AS "assignmentId",
CASE WHEN a.arrived_at IS NULL THEN 'INBOUND' ELSE 'ON_SITE' END AS "status",
a.plate_number AS "plateNumber", a.plate_number AS "plateNumber",
a.driver_name AS "driverName", a.driver_name AS "driverName",
a.truck_type AS "truckType", a.truck_type AS "truckType",
@@ -450,13 +455,13 @@ export class WarehouseInventoryService {
JOIN freight.bookings b ON b.id = a.booking_id AND b.deleted_at IS NULL JOIN freight.bookings b ON b.id = a.booking_id AND b.deleted_at IS NULL
LEFT JOIN freight.companies company ON company.id = b.company_id LEFT JOIN freight.companies company ON company.id = b.company_id
WHERE a.deleted_at IS NULL WHERE a.deleted_at IS NULL
AND a.arrived_at IS NOT NULL
AND a.departed_at IS NULL AND a.departed_at IS NULL
UNION ALL UNION ALL
SELECT 'EDR' AS "source", SELECT 'EDR' AS "source",
va.id AS "assignmentId", va.id AS "assignmentId",
CASE WHEN va.arrived_at IS NULL THEN 'INBOUND' ELSE 'ON_SITE' END AS "status",
COALESCE(v.plate_number, v.power_plate_no) AS "plateNumber", COALESCE(v.plate_number, v.power_plate_no) AS "plateNumber",
NULLIF(TRIM(CONCAT_WS(' ', d.first_name, d.last_name)), '') AS "driverName", NULLIF(TRIM(CONCAT_WS(' ', d.first_name, d.last_name)), '') AS "driverName",
v.vehicle_type AS "truckType", v.vehicle_type AS "truckType",
@@ -474,10 +479,11 @@ export class WarehouseInventoryService {
LEFT JOIN freight.drivers d ON d.id = v.assigned_driver_id LEFT JOIN freight.drivers d ON d.id = v.assigned_driver_id
LEFT JOIN freight.companies company ON company.id = b.company_id LEFT JOIN freight.companies company ON company.id = b.company_id
WHERE va.deleted_at IS NULL WHERE va.deleted_at IS NULL
AND va.arrived_at IS NOT NULL
AND va.departed_at IS NULL AND va.departed_at IS NULL
ORDER BY "arrivedAt" ASC`, -- On-site trucks first, each group oldest-arrival first; inbound trucks
-- (null arrival) sort to the end.
ORDER BY "arrivedAt" ASC NULLS LAST`,
); );
} }

View File

@@ -47,15 +47,16 @@ function Rows({ rows }: { rows: TruckOnSite[] }) {
if (rows.length === 0) { if (rows.length === 0) {
return ( return (
<Alert variant="light" color="gray"> <Alert variant="light" color="gray">
No trucks on site. No trucks assigned or on site.
</Alert> </Alert>
); );
} }
return ( return (
<Table.ScrollContainer minWidth={980}> <Table.ScrollContainer minWidth={1040}>
<Table striped highlightOnHover verticalSpacing="xs"> <Table striped highlightOnHover verticalSpacing="xs">
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
<Table.Th>Status</Table.Th>
<Table.Th>Plate</Table.Th> <Table.Th>Plate</Table.Th>
<Table.Th>Haulage</Table.Th> <Table.Th>Haulage</Table.Th>
<Table.Th>Driver</Table.Th> <Table.Th>Driver</Table.Th>
@@ -69,6 +70,16 @@ function Rows({ rows }: { rows: TruckOnSite[] }) {
<Table.Tbody> <Table.Tbody>
{rows.map((row) => ( {rows.map((row) => (
<Table.Tr key={`${row.source}-${row.assignmentId}`}> <Table.Tr key={`${row.source}-${row.assignmentId}`}>
<Table.Td>
<Badge
size="sm"
radius="sm"
variant={row.status === "ON_SITE" ? "filled" : "light"}
color={row.status === "ON_SITE" ? "edr-green" : "gray"}
>
{row.status === "ON_SITE" ? "On site" : "Inbound"}
</Badge>
</Table.Td>
<Table.Td> <Table.Td>
<Text size="sm" fw={600}> <Text size="sm" fw={600}>
{row.plateNumber ?? "—"} {row.plateNumber ?? "—"}
@@ -101,9 +112,13 @@ function Rows({ rows }: { rows: TruckOnSite[] }) {
<Text size="sm">{row.containers ?? "Bulk"}</Text> <Text size="sm">{row.containers ?? "Bulk"}</Text>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
{isLongDwell(row.arrivedAt) ? ( {row.arrivedAt == null ? (
<Text size="sm" c="dimmed">
</Text>
) : isLongDwell(row.arrivedAt) ? (
<Tooltip <Tooltip
label={`On site over ${LONG_DWELL_HOURS}h — arrived ${new Date(row.arrivedAt as string).toLocaleString()}`} label={`On site over ${LONG_DWELL_HOURS}h — arrived ${new Date(row.arrivedAt).toLocaleString()}`}
withArrow withArrow
> >
<Text size="sm" c="red" fw={600}> <Text size="sm" c="red" fw={600}>
@@ -124,12 +139,14 @@ function Rows({ rows }: { rows: TruckOnSite[] }) {
export default function TrucksOnSitePage() { export default function TrucksOnSitePage() {
const { data: trucks = [], isLoading } = useTrucksOnSite(); const { data: trucks = [], isLoading } = useTrucksOnSite();
const [scope, setScope] = useState<"ALL" | "ON_SITE" | "INBOUND">("ALL");
const [source, setSource] = useState<"ALL" | "CUSTOMER" | "EDR">("ALL"); const [source, setSource] = useState<"ALL" | "CUSTOMER" | "EDR">("ALL");
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const rows = useMemo(() => { const rows = useMemo(() => {
const term = search.trim().toLowerCase(); const term = search.trim().toLowerCase();
return trucks return trucks
.filter((t) => scope === "ALL" || t.status === scope)
.filter((t) => source === "ALL" || t.source === source) .filter((t) => source === "ALL" || t.source === source)
.filter((t) => .filter((t) =>
!term !term
@@ -137,8 +154,10 @@ export default function TrucksOnSitePage() {
: [t.plateNumber, t.driverName, t.bookingReference, t.customerName, t.containers] : [t.plateNumber, t.driverName, t.bookingReference, t.customerName, t.containers]
.some((field) => field?.toLowerCase().includes(term)), .some((field) => field?.toLowerCase().includes(term)),
); );
}, [trucks, source, search]); }, [trucks, scope, source, search]);
const onSiteCount = trucks.filter((t) => t.status === "ON_SITE").length;
const inboundCount = trucks.length - onSiteCount;
const customerCount = trucks.filter((t) => t.source === "CUSTOMER").length; const customerCount = trucks.filter((t) => t.source === "CUSTOMER").length;
const edrCount = trucks.length - customerCount; const edrCount = trucks.length - customerCount;
@@ -146,20 +165,32 @@ export default function TrucksOnSitePage() {
<PageContainer> <PageContainer>
<PageHeader <PageHeader
title="Trucks on site" title="Trucks on site"
subtitle="Arrived at the yard and not yet left — customer self-haul and EDR last-mile." subtitle="Customer self-haul and EDR last-mile trucks — assigned (inbound) or arrived, until they leave the yard."
/> />
<Group justify="space-between" mb="md" wrap="wrap" gap="sm"> <Group justify="space-between" mb="md" wrap="wrap" gap="sm">
<SegmentedControl <Group gap="sm" wrap="wrap">
size="xs" <SegmentedControl
value={source} size="xs"
onChange={(v) => setSource(v as typeof source)} value={scope}
data={[ onChange={(v) => setScope(v as typeof scope)}
{ label: `All (${trucks.length})`, value: "ALL" }, data={[
{ label: `Customer (${customerCount})`, value: "CUSTOMER" }, { label: `All (${trucks.length})`, value: "ALL" },
{ label: `EDR (${edrCount})`, value: "EDR" }, { label: `On site (${onSiteCount})`, value: "ON_SITE" },
]} { label: `Inbound (${inboundCount})`, value: "INBOUND" },
/> ]}
/>
<SegmentedControl
size="xs"
value={source}
onChange={(v) => setSource(v as typeof source)}
data={[
{ label: "All", value: "ALL" },
{ label: `Customer (${customerCount})`, value: "CUSTOMER" },
{ label: `EDR (${edrCount})`, value: "EDR" },
]}
/>
</Group>
<TextInput <TextInput
size="xs" size="xs"
w={280} w={280}

View File

@@ -1128,6 +1128,8 @@ export interface WarehouseOpsStats {
export interface TruckOnSite { export interface TruckOnSite {
source: "CUSTOMER" | "EDR"; source: "CUSTOMER" | "EDR";
assignmentId: string; assignmentId: string;
/** INBOUND = assigned, not yet arrived; ON_SITE = arrived, not yet departed. */
status: "INBOUND" | "ON_SITE";
plateNumber: string | null; plateNumber: string | null;
driverName: string | null; driverName: string | null;
truckType: string | null; truckType: string | null;