fix(operations): prefill booking context on single assign; enforce per-truck container loads

- single-row Assign vehicle uses the full single-record flow (details + containers)
- release() rejects exit containers not assigned to the departing truck
- weighing modal offers only the selected truck's assigned containers
This commit is contained in:
Hagernesh
2026-07-22 07:03:49 +00:00
parent fd3212f326
commit 19906b273e
7 changed files with 213 additions and 57 deletions

View File

@@ -3011,6 +3011,29 @@ export class WarehouseInventoryService {
);
}
// A truck leaves with the containers ASSIGNED to it — never another
// truck's. Enforced whenever the truck has an assigned load on file
// (customer self-haul or EDR last-mile).
if (dto.containerNumber && dto.truckPlateNumber?.trim()) {
const selectedNumbers = dto.containerNumber
.split(/[,;\n]+/)
.map((n) => n.trim().toUpperCase())
.filter(Boolean);
const assigned = await this.truckAssignedContainers(
item.bookingId,
dto.truckPlateNumber.trim(),
);
if (assigned.length && selectedNumbers.length) {
const foreign = selectedNumbers.filter((n) => !assigned.includes(n));
if (foreign.length) {
throw new BadRequestException(
`Container${foreign.length > 1 ? 's' : ''} ${foreign.join(', ')} ` +
`not assigned to truck ${dto.truckPlateNumber.trim()} — each truck may only carry out its own assigned containers`,
);
}
}
}
// Authoritative weight match: the truck's net (gross tare) must equal the
// total VGM cargo weight of the containers selected as loaded on it.
// Skipped when the operator chose not to weigh (containers only).
@@ -3653,6 +3676,32 @@ export class WarehouseInventoryService {
}));
}
/**
* Container numbers assigned to a truck (by plate) on this booking, from both
* haulage paths: customer self-haul (customer_truck_containers) and EDR
* last-mile (last_mile_vehicle_containers / legacy scalar). Uppercased.
*/
private async truckAssignedContainers(bookingId: string, plate: string): Promise<string[]> {
const rows: Array<{ cn: string | null }> = await this.dataSource.query(
`SELECT UPPER(cc.container_number) AS cn
FROM freight.customer_truck_assignments a
JOIN freight.customer_truck_containers cc
ON cc.assignment_id = a.id AND cc.deleted_at IS NULL
WHERE a.booking_id = $1 AND UPPER(a.plate_number) = UPPER($2) AND a.deleted_at IS NULL
UNION
SELECT UPPER(COALESCE(vc.container_number, va.container_number)) AS cn
FROM freight.last_mile_vehicle_assignments va
JOIN freight.last_mile l ON l.id = va.last_mile_id AND l.deleted_at IS NULL
LEFT JOIN freight.last_mile_vehicle_containers vc
ON vc.assignment_id = va.id AND vc.deleted_at IS NULL
JOIN freight.vehicles v ON v.id = va.vehicle_id
WHERE l.booking_id = $1 AND va.deleted_at IS NULL
AND (UPPER(v.power_plate_no) = UPPER($2) OR UPPER(v.plate_number) = UPPER($2))`,
[bookingId, plate],
);
return rows.map((r) => r.cn).filter((n): n is string => Boolean(n));
}
/**
* The booking's containers with their VGM cargo weight (tonnes), keyed by
* container number. Drives the truck-leaving exit weighing: the selected