mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 01:48:12 +00:00
list trucks + loadable containers → load)
This commit is contained in:
@@ -64,6 +64,7 @@ import { ContractViewDto } from './dto/contract-view.dto';
|
||||
import { CustomerTruckAssignmentDto } from './dto/customer-truck-assignment.dto';
|
||||
import { AddCustomerTruckDto } from './dto/add-customer-truck.dto';
|
||||
import { DepartCustomerTruckDto } from './dto/depart-customer-truck.dto';
|
||||
import { LoadCustomerTruckDto } from './dto/load-customer-truck.dto';
|
||||
import { CustomerTruckService } from './customer-truck.service';
|
||||
import { GenerateGrnDto } from './dto/generate-grn.dto';
|
||||
import { ContainerReceiptService } from './container-receipt.service';
|
||||
@@ -358,6 +359,33 @@ export class BookingsController {
|
||||
return this.customerTruckService.removeTruck(id, assignmentId);
|
||||
}
|
||||
|
||||
@Get(':id/customer-trucks/loadable-containers')
|
||||
@ApiOperation({ summary: 'Booking containers not yet loaded onto a truck' })
|
||||
async loadableContainers(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@CurrentUser() user: TCurrentUser,
|
||||
) {
|
||||
const booking = await this.bookingsService.findById(id);
|
||||
if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
|
||||
await this.bookingsService.assertCustomerCanAccessBooking(user?.id, booking);
|
||||
}
|
||||
return this.customerTruckService.getLoadableContainers(id);
|
||||
}
|
||||
|
||||
@Post(':id/customer-trucks/:assignmentId/load')
|
||||
@ApiOperation({ summary: 'Truck_dispatch: load selected containers onto a truck (staff)' })
|
||||
async loadCustomerTruck(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Param('assignmentId', ParseUUIDPipe) assignmentId: string,
|
||||
@Body() dto: LoadCustomerTruckDto,
|
||||
@CurrentUser() user: TCurrentUser,
|
||||
) {
|
||||
if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
|
||||
throw new ForbiddenException('Only warehouse staff can load a truck');
|
||||
}
|
||||
return this.customerTruckService.loadTruck(id, assignmentId, dto);
|
||||
}
|
||||
|
||||
@Post(':id/customer-trucks/:assignmentId/depart')
|
||||
@ApiOperation({
|
||||
summary: 'Register an import truck leaving: containers loaded + weighed gross (staff)',
|
||||
|
||||
@@ -198,6 +198,87 @@ export class CustomerTruckService {
|
||||
return this.listTrucks(bookingId);
|
||||
}
|
||||
|
||||
/** Booking container numbers not yet loaded onto any truck. */
|
||||
async getLoadableContainers(bookingId: string): Promise<string[]> {
|
||||
const [all, assigned] = await Promise.all([
|
||||
this.bookingContainerNumbers(bookingId),
|
||||
this.assignedContainerNumbers(bookingId),
|
||||
]);
|
||||
const taken = new Set(assigned);
|
||||
return all.filter((n) => !taken.has(n));
|
||||
}
|
||||
|
||||
/**
|
||||
* Truck_dispatch (load): assign the selected containers to a truck after it has
|
||||
* arrived, and set a provisional gross weight from their VGM. The truck is
|
||||
* weighed for real on departure. Locked once the truck has left.
|
||||
*/
|
||||
async loadTruck(
|
||||
bookingId: string,
|
||||
assignmentId: string,
|
||||
dto: { containerNumbers: string[] },
|
||||
): Promise<CustomerTruckAssignment[]> {
|
||||
await this.loadBookingGuard(bookingId);
|
||||
const assignment = await this.assignments.findByIdWithContainers(assignmentId);
|
||||
if (!assignment || assignment.bookingId !== bookingId) {
|
||||
throw new NotFoundException('Truck assignment not found for this booking');
|
||||
}
|
||||
if (assignment.departedAt) {
|
||||
throw new ConflictException('This truck has already left — its load is locked');
|
||||
}
|
||||
|
||||
const requested = (dto.containerNumbers ?? []).map((n) => n.trim().toUpperCase());
|
||||
if (!requested.length) {
|
||||
throw new BadRequestException('Select at least one container to load onto the truck');
|
||||
}
|
||||
const bookingNumbers = await this.bookingContainerNumbers(bookingId);
|
||||
for (const n of requested) {
|
||||
if (!bookingNumbers.includes(n)) {
|
||||
throw new BadRequestException(`Container ${n} is not one of this booking's containers`);
|
||||
}
|
||||
}
|
||||
const elsewhere = await this.assignedContainerNumbersExcept(bookingId, assignmentId);
|
||||
for (const n of requested) {
|
||||
if (elsewhere.includes(n)) {
|
||||
throw new ConflictException(`Container ${n} is already loaded onto another truck`);
|
||||
}
|
||||
}
|
||||
|
||||
const grossKg = await this.vgmKgForContainers(bookingId, requested);
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
await manager.getRepository(CustomerTruckContainer).softDelete({ assignmentId });
|
||||
await manager.getRepository(CustomerTruckContainer).save(
|
||||
requested.map((containerNumber) =>
|
||||
manager.getRepository(CustomerTruckContainer).create({
|
||||
assignmentId,
|
||||
bookingId,
|
||||
containerNumber,
|
||||
}),
|
||||
),
|
||||
);
|
||||
// Provisional gross from the loaded containers' VGM — overridden by the
|
||||
// weighed gross on departure.
|
||||
await manager.getRepository(CustomerTruckAssignment).update(assignmentId, {
|
||||
grossWeightKg: grossKg,
|
||||
});
|
||||
});
|
||||
return this.listTrucks(bookingId);
|
||||
}
|
||||
|
||||
private async vgmKgForContainers(bookingId: string, numbers: string[]): Promise<number> {
|
||||
const [row]: Array<{ kg: string }> = await this.dataSource.query(
|
||||
`SELECT COALESCE(SUM(bcu.vgm_tons), 0) * 1000 AS kg
|
||||
FROM freight.booking_container_units bcu
|
||||
JOIN freight.booking_containers bc
|
||||
ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
|
||||
WHERE bc.booking_id = $1
|
||||
AND bcu.container_number = ANY($2::varchar[])
|
||||
AND bcu.deleted_at IS NULL`,
|
||||
[bookingId, numbers],
|
||||
);
|
||||
return Number(row?.kg ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the truck carrying `containerNumber` as arrived. Called by the warehouse
|
||||
* receive flow. When every truck on the booking has arrived, the booking-level
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ArrayMinSize, ArrayUnique, IsArray, Matches } from 'class-validator';
|
||||
|
||||
/** Containers loaded onto a truck at Truck_dispatch (after arrival, before it leaves). */
|
||||
export class LoadCustomerTruckDto {
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ArrayUnique()
|
||||
@Matches(/^[A-Z]{4}\d{7}$/, {
|
||||
each: true,
|
||||
message: 'each container number must match ISO container format, e.g. ABCD1234567',
|
||||
})
|
||||
containerNumbers!: string[];
|
||||
}
|
||||
Reference in New Issue
Block a user