mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 12:18:11 +00:00
feat(warehouse,last-mile): truck load size rule, dedup last-mile, driver-required + arrival prefill
Truck loading: - loadTruck enforces max 2 containers / one 40ft (two 20ft) and auto-marks an assigned truck arrived on load; container-items payload + modal expose container size with a client-side selection cap. - Show "#x containers pending assignment" in the portal truck card and the backoffice container modal. Last-mile: - create() is idempotent — return the existing record for a booking instead of inserting a duplicate delivery row (fixed the same booking showing twice in Assign-Mile). - setVehicles/update reject a truck with no assigned driver; the Assign toast now surfaces the reason. - New GET /last-mile/booking/:id/arrival-trucks returns assigned EDR trucks with driver details; ReleaseOrderModal fetches and auto-fills them so an assigned EDR truck no longer reads as "not assigned yet". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,12 @@ export class LastMileController {
|
||||
return this.lastMileService.findById(id);
|
||||
}
|
||||
|
||||
@Get('booking/:bookingId/arrival-trucks')
|
||||
@ApiOperation({ summary: "Assigned EDR last-mile trucks for a booking (arrival/exit weighing prefill)" })
|
||||
arrivalTrucks(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||||
return this.lastMileService.arrivalTrucksForBooking(bookingId);
|
||||
}
|
||||
|
||||
@Post('accept/:reference')
|
||||
@BookingStaff(FREIGHT_PERMS.lastMile.accept)
|
||||
@ApiOperation({ summary: 'Accept a paid booking and create a last-mile leg' })
|
||||
|
||||
@@ -256,7 +256,95 @@ export class LastMileService {
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* The EDR last-mile trucks assigned to a booking, joined with driver details,
|
||||
* shaped for the arrival/exit weighing prefill (plate, driver, type, container).
|
||||
* Returns [] when the booking has no last-mile truck assigned. Lets the
|
||||
* warehouse arrival/load modals surface an assigned EDR truck the same way the
|
||||
* self-haul customer trucks are surfaced.
|
||||
*/
|
||||
async arrivalTrucksForBooking(bookingId: string): Promise<
|
||||
Array<{
|
||||
vehicleId: string;
|
||||
truckPlateNumber: string | null;
|
||||
trailerPlateNumber: string | null;
|
||||
driverName: string | null;
|
||||
driverLicense: string | null;
|
||||
driverPhone: string | null;
|
||||
truckType: string | null;
|
||||
containerNumber: string | null;
|
||||
}>
|
||||
> {
|
||||
const [lm] = await this.lastMileRepository.findAll({
|
||||
where: { bookingId },
|
||||
relations: { vehicle: true, vehicleAssignments: { vehicle: true } },
|
||||
take: 1,
|
||||
});
|
||||
if (!lm) return [];
|
||||
|
||||
// Prefer the multi-truck junction; fall back to the legacy single vehicle.
|
||||
const sources = lm.vehicleAssignments?.length
|
||||
? lm.vehicleAssignments.map((va) => ({
|
||||
vehicle: va.vehicle,
|
||||
containerNumber: va.containerNumber ?? null,
|
||||
}))
|
||||
: lm.vehicle
|
||||
? [{ vehicle: lm.vehicle, containerNumber: null }]
|
||||
: [];
|
||||
|
||||
const out: Array<{
|
||||
vehicleId: string;
|
||||
truckPlateNumber: string | null;
|
||||
trailerPlateNumber: string | null;
|
||||
driverName: string | null;
|
||||
driverLicense: string | null;
|
||||
driverPhone: string | null;
|
||||
truckType: string | null;
|
||||
containerNumber: string | null;
|
||||
}> = [];
|
||||
for (const { vehicle, containerNumber } of sources) {
|
||||
if (!vehicle) continue;
|
||||
let driverName = vehicle.assignedDriverName ?? null;
|
||||
let driverLicense: string | null = null;
|
||||
let driverPhone: string | null = null;
|
||||
if (vehicle.assignedDriverId) {
|
||||
try {
|
||||
const d = await this.driversService.findById(vehicle.assignedDriverId);
|
||||
driverName = driverName || `${d.firstName ?? ''} ${d.lastName ?? ''}`.trim() || null;
|
||||
driverLicense = d.licenseNumber ?? null;
|
||||
driverPhone = d.phoneNumber ?? null;
|
||||
} catch {
|
||||
/* driver lookup is best-effort — plate still prefills */
|
||||
}
|
||||
}
|
||||
out.push({
|
||||
vehicleId: vehicle.id,
|
||||
truckPlateNumber: vehicle.powerPlateNo || vehicle.plateNumber || null,
|
||||
trailerPlateNumber: vehicle.trailerPlateNo || null,
|
||||
driverName,
|
||||
driverLicense,
|
||||
driverPhone,
|
||||
truckType: vehicle.vehicleType || null,
|
||||
containerNumber,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async create(dto: CreateLastMileDto): Promise<LastMile> {
|
||||
// Idempotent: a booking gets exactly one last-mile record. Extra trucks live
|
||||
// inside that record (vehicleAssignments), never as additional rows — so if a
|
||||
// last-mile already exists for this booking, return it instead of inserting a
|
||||
// duplicate delivery row (which is what made the same booking appear twice in
|
||||
// the Assign-Mile list).
|
||||
const [existing] = await this.lastMileRepository.findAll({
|
||||
where: { bookingId: dto.bookingId },
|
||||
take: 1,
|
||||
});
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const record = await this.lastMileRepository.create({
|
||||
bookingId: dto.bookingId,
|
||||
status: dto.status ?? 'READY_TO_TRANSIT',
|
||||
@@ -318,6 +406,17 @@ export class LastMileService {
|
||||
}
|
||||
}
|
||||
|
||||
// A last-mile truck must have a driver before it can be assigned (same rule
|
||||
// as setVehicles) — block driverless single-vehicle (re)assignment too.
|
||||
if (dto.vehicleId && dto.vehicleId !== existing.vehicleId) {
|
||||
const vehicle = await this.vehiclesService.findById(dto.vehicleId);
|
||||
if (!vehicle?.assignedDriverId) {
|
||||
throw new BadRequestException(
|
||||
`Truck ${vehicle?.plateNumber ?? dto.vehicleId} has no assigned driver — assign a driver to the truck before adding it to this last-mile delivery`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const dtoAny = dto as any;
|
||||
const updated = await this.lastMileRepository.update(id, {
|
||||
...(dto.bookingId !== undefined ? { bookingId: dto.bookingId } : {}),
|
||||
@@ -478,6 +577,17 @@ export class LastMileService {
|
||||
)];
|
||||
const added = desired.filter((v) => !junctionSet.has(v));
|
||||
const removed = releaseIds.filter((v) => !desiredSet.has(v));
|
||||
|
||||
// A last-mile truck must have a driver before it can be assigned — a delivery
|
||||
// can't run driverless, and the arrival/exit weighing needs the driver.
|
||||
for (const vehicleId of added) {
|
||||
const vehicle = await this.vehiclesService.findById(vehicleId);
|
||||
if (!vehicle?.assignedDriverId) {
|
||||
throw new BadRequestException(
|
||||
`Truck ${vehicle?.plateNumber ?? vehicleId} has no assigned driver — assign a driver to the truck before adding it to this last-mile delivery`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Vehicles that stay but whose container number changed.
|
||||
const changed = current.filter(
|
||||
(a) =>
|
||||
|
||||
Reference in New Issue
Block a user