mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 22:30:55 +00:00
feat(warehouse): show the trucks in the yard, not just a count of them
Multi-truck self-haul had no list anywhere on the warehouse side. The ops dashboard counted trucks on site and offered no way to open the list, and the inventory table showed a blank plate on exactly the bookings that have several trucks: it read booking.customer_truck_plate_number, which multi-truck self-haul leaves null because plates live in customer_truck_assignments. Booking BK-2026-000033 has a truck and a driver on file and displayed neither. Adds a Trucks on Site page listing every truck that has arrived and not yet departed, across bookings, with plate, driver, booking, customer, containers and dwell time. It covers both haulage paths because the gate does — a customer's own truck and an EDR last-mile truck reach the same barrier — and flags anything sitting over four hours. It lives under Warehouse Management rather than Imports or Exports, since the yard is not per-direction. The inventory queries now read plates and drivers from the assignments and keep the booking columns as the fallback for single-truck bookings written before that table existed. Both new statements were EXPLAIN-validated against the live schema; the plate fix returns the data that was previously null. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,15 @@ export class WarehouseInventoryController {
|
||||
return this.inventoryService.opsStats();
|
||||
}
|
||||
|
||||
@Get('trucks-on-site')
|
||||
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
|
||||
@ApiOperation({
|
||||
summary: 'Trucks currently in the yard (customer self-haul + EDR last-mile)',
|
||||
})
|
||||
trucksOnSite() {
|
||||
return this.inventoryService.trucksOnSite();
|
||||
}
|
||||
|
||||
@Get('zone-occupancy')
|
||||
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
|
||||
@ApiOperation({ summary: 'Live occupancy per zone (rated capacity vs held) — heatmap data' })
|
||||
|
||||
@@ -410,6 +410,77 @@ export class WarehouseInventoryService {
|
||||
* - trucksOnSite: customer trucks arrived but not departed
|
||||
* - itemsAging: in-warehouse items older than 7 days (demurrage risk)
|
||||
*/
|
||||
/**
|
||||
* Every truck currently inside the yard, across all bookings — the list behind
|
||||
* the `trucksOnSite` figure on the ops dashboard, which until now could only
|
||||
* be counted and never opened.
|
||||
*
|
||||
* 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.
|
||||
* "On site" means arrived and not yet departed.
|
||||
*/
|
||||
async trucksOnSite(): Promise<
|
||||
Array<{
|
||||
source: 'CUSTOMER' | 'EDR';
|
||||
assignmentId: string;
|
||||
plateNumber: string | null;
|
||||
driverName: string | null;
|
||||
truckType: string | null;
|
||||
arrivedAt: string | null;
|
||||
bookingId: string;
|
||||
bookingReference: string | null;
|
||||
customerName: string | null;
|
||||
containers: string | null;
|
||||
}>
|
||||
> {
|
||||
return this.dataSource.query(
|
||||
`SELECT 'CUSTOMER' AS "source",
|
||||
a.id AS "assignmentId",
|
||||
a.plate_number AS "plateNumber",
|
||||
a.driver_name AS "driverName",
|
||||
a.truck_type AS "truckType",
|
||||
a.arrived_at AS "arrivedAt",
|
||||
b.id AS "bookingId",
|
||||
b.reference AS "bookingReference",
|
||||
company.name AS "customerName",
|
||||
(SELECT string_agg(c.container_number, ', ' ORDER BY c.container_number)
|
||||
FROM freight.customer_truck_containers c
|
||||
WHERE c.assignment_id = a.id AND c.deleted_at IS NULL) AS "containers"
|
||||
FROM freight.customer_truck_assignments a
|
||||
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
|
||||
WHERE a.deleted_at IS NULL
|
||||
AND a.arrived_at IS NOT NULL
|
||||
AND a.departed_at IS NULL
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT 'EDR' AS "source",
|
||||
va.id AS "assignmentId",
|
||||
COALESCE(v.plate_number, v.power_plate_no) AS "plateNumber",
|
||||
NULLIF(TRIM(CONCAT_WS(' ', d.first_name, d.last_name)), '') AS "driverName",
|
||||
v.vehicle_type AS "truckType",
|
||||
va.arrived_at AS "arrivedAt",
|
||||
b.id AS "bookingId",
|
||||
b.reference AS "bookingReference",
|
||||
company.name AS "customerName",
|
||||
(SELECT string_agg(lvc.container_number, ', ' ORDER BY lvc.container_number)
|
||||
FROM freight.last_mile_vehicle_containers lvc
|
||||
WHERE lvc.assignment_id = va.id AND lvc.deleted_at IS NULL) AS "containers"
|
||||
FROM freight.last_mile_vehicle_assignments va
|
||||
JOIN freight.last_mile lm ON lm.id = va.last_mile_id AND lm.deleted_at IS NULL
|
||||
JOIN freight.bookings b ON b.id = lm.booking_id AND b.deleted_at IS NULL
|
||||
LEFT JOIN freight.vehicles v ON v.id = va.vehicle_id
|
||||
LEFT JOIN freight.drivers d ON d.id = v.assigned_driver_id
|
||||
LEFT JOIN freight.companies company ON company.id = b.company_id
|
||||
WHERE va.deleted_at IS NULL
|
||||
AND va.arrived_at IS NOT NULL
|
||||
AND va.departed_at IS NULL
|
||||
|
||||
ORDER BY "arrivedAt" ASC`,
|
||||
);
|
||||
}
|
||||
|
||||
async opsStats(): Promise<{
|
||||
receivedToday: number;
|
||||
receivedYesterday: number;
|
||||
@@ -1202,8 +1273,18 @@ export class WarehouseInventoryService {
|
||||
driver.phone_number AS "firstMileDriverPhone",
|
||||
driver.license_number AS "firstMileDriverLicenseNumber",
|
||||
v.vehicle_type AS "firstMileTruckType",
|
||||
b.customer_truck_plate_number AS "customerTruckPlateNumber",
|
||||
b.customer_truck_driver_name AS "customerTruckDriverName",
|
||||
-- Multi-truck self-haul writes plates/drivers to
|
||||
-- customer_truck_assignments and leaves the booking columns null,
|
||||
-- so read the assignments first and keep the legacy column as the
|
||||
-- fallback for single-truck bookings written before that table.
|
||||
COALESCE((SELECT string_agg(DISTINCT cta.plate_number, ', ')
|
||||
FROM freight.customer_truck_assignments cta
|
||||
WHERE cta.booking_id = b.id AND cta.deleted_at IS NULL),
|
||||
b.customer_truck_plate_number) AS "customerTruckPlateNumber",
|
||||
COALESCE((SELECT string_agg(DISTINCT cta.driver_name, ', ')
|
||||
FROM freight.customer_truck_assignments cta
|
||||
WHERE cta.booking_id = b.id AND cta.deleted_at IS NULL),
|
||||
b.customer_truck_driver_name) AS "customerTruckDriverName",
|
||||
b.customer_truck_type AS "customerTruckType",
|
||||
b.customer_truck_container_number AS "customerTruckContainerNumber",
|
||||
b.customer_truck_assigned_at AS "customerTruckAssignedAt"
|
||||
@@ -1334,8 +1415,14 @@ export class WarehouseInventoryService {
|
||||
driver.phone_number AS "firstMileDriverPhone",
|
||||
driver.license_number AS "firstMileDriverLicenseNumber",
|
||||
v.vehicle_type AS "firstMileTruckType",
|
||||
b.customer_truck_plate_number AS "customerTruckPlateNumber",
|
||||
b.customer_truck_driver_name AS "customerTruckDriverName",
|
||||
COALESCE((SELECT string_agg(DISTINCT cta.plate_number, ', ')
|
||||
FROM freight.customer_truck_assignments cta
|
||||
WHERE cta.booking_id = b.id AND cta.deleted_at IS NULL),
|
||||
b.customer_truck_plate_number) AS "customerTruckPlateNumber",
|
||||
COALESCE((SELECT string_agg(DISTINCT cta.driver_name, ', ')
|
||||
FROM freight.customer_truck_assignments cta
|
||||
WHERE cta.booking_id = b.id AND cta.deleted_at IS NULL),
|
||||
b.customer_truck_driver_name) AS "customerTruckDriverName",
|
||||
b.customer_truck_type AS "customerTruckType",
|
||||
b.customer_truck_container_number AS "customerTruckContainerNumber",
|
||||
b.customer_truck_assigned_at AS "customerTruckAssignedAt",
|
||||
@@ -1794,8 +1881,18 @@ export class WarehouseInventoryService {
|
||||
THEN 'DOOR_DELIVERY' ELSE 'TERMINAL_PICKUP' END AS "pickupOption",
|
||||
(NULLIF(TRIM(COALESCE(b.last_mile_delivery_address, '')), '') IS NOT NULL
|
||||
OR COALESCE(st.includes_last_mile, false)) AS "lastMileRequested",
|
||||
b.customer_truck_plate_number AS "customerTruckPlateNumber",
|
||||
b.customer_truck_driver_name AS "customerTruckDriverName",
|
||||
-- Multi-truck self-haul writes plates/drivers to
|
||||
-- customer_truck_assignments and leaves the booking columns null,
|
||||
-- so read the assignments first and keep the legacy column as the
|
||||
-- fallback for single-truck bookings written before that table.
|
||||
COALESCE((SELECT string_agg(DISTINCT cta.plate_number, ', ')
|
||||
FROM freight.customer_truck_assignments cta
|
||||
WHERE cta.booking_id = b.id AND cta.deleted_at IS NULL),
|
||||
b.customer_truck_plate_number) AS "customerTruckPlateNumber",
|
||||
COALESCE((SELECT string_agg(DISTINCT cta.driver_name, ', ')
|
||||
FROM freight.customer_truck_assignments cta
|
||||
WHERE cta.booking_id = b.id AND cta.deleted_at IS NULL),
|
||||
b.customer_truck_driver_name) AS "customerTruckDriverName",
|
||||
b.customer_truck_type AS "customerTruckType",
|
||||
b.customer_truck_container_number AS "customerTruckContainerNumber",
|
||||
b.customer_truck_assigned_at AS "customerTruckAssignedAt",
|
||||
|
||||
Reference in New Issue
Block a user