Files
edr-platform/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts
Hagernesh 0c0de284b0 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>
2026-07-20 12:54:10 +00:00

556 lines
23 KiB
TypeScript

import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query, Request, Res } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import type { Response } from 'express';
import { CurrentUser } from '@edr/api-common';
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
import { actorLabel } from './current-actor.util';
import { BookingStaff } from '../../common/booking-guards';
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
import { BulkReceiveDto } from './dto/bulk-receive.dto';
import { BulkInspectDto } from './dto/bulk-inspect.dto';
import { DeliverInventoryDto } from './dto/deliver-inventory.dto';
import { FilterWarehouseInventoryDto } from './dto/filter-inventory.dto';
import { InquiryWarehouseInventoryDto } from './dto/inquiry-inventory.dto';
import { LoadInventoryDto } from './dto/load-inventory.dto';
import { MoveInventoryDto } from './dto/move-inventory.dto';
import { StoreInventoryDto } from './dto/store-inventory.dto';
import { ReceiveWarehouseInventoryDto } from './dto/receive-inventory.dto';
import { ApproveDeliveryDto } from './dto/approve-delivery.dto';
import { ReleaseOrderDto } from './dto/release-order.dto';
import { ReserveInventoryDto } from './dto/reserve-inventory.dto';
import { UnloadBookingDto } from './dto/unload-booking.dto';
import { SchedulingReadFacade } from './scheduling-read.facade';
import { WarehouseInventoryService } from './warehouse-inventory.service';
import { HandoverService } from './handover.service';
@ApiTags('warehouse-inventory')
@ApiBearerAuth()
@Controller('warehouse-inventory')
export class WarehouseInventoryController {
constructor(
private readonly inventoryService: WarehouseInventoryService,
private readonly scheduling: SchedulingReadFacade,
private readonly handoverService: HandoverService,
) {}
@Get()
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'List warehouse inventory' })
findAll(@Query() filter: FilterWarehouseInventoryDto) {
return this.inventoryService.findAll(filter);
}
@Get('ready-for-loading')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'List inventory ready for loading' })
findReadyForLoading(@Query() filter: FilterWarehouseInventoryDto) {
return this.inventoryService.findReadyForLoading(filter);
}
@Get('inquiry')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Locate any item inside the warehouse' })
inquiry(@Query() filter: InquiryWarehouseInventoryDto) {
return this.inventoryService.inquiry(filter);
}
@Get('arrival-queue')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Arrived bookings awaiting unload / inspection' })
arrivalQueue() {
return this.inventoryService.arrivalQueue();
}
@Get('ops-stats')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'At-a-glance warehouse ops counters for the KPI strip' })
opsStats() {
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' })
zoneOccupancy(@Query('yardId') yardId?: string) {
return this.inventoryService.zoneOccupancy(yardId);
}
@Get('throughput')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Received-vs-dispatched throughput time series (week/month/year)' })
throughput(@Query('granularity') granularity?: string) {
const g = granularity === 'week' || granularity === 'year' ? granularity : 'month';
return this.inventoryService.throughput(g);
}
@Get('dwell-stats')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Dwell time of in-warehouse items: average + aging buckets' })
dwellStats() {
return this.inventoryService.dwellStats();
}
@Get('cycle-stats')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Average stage cycle times over recently dispatched items' })
cycleStats() {
return this.inventoryService.cycleStats();
}
@Get('gate-stats')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Gate/dock throughput: cleared today, turnaround, hourly clearances' })
gateStats() {
return this.inventoryService.gateStats();
}
@Post('auto-unload-arrived')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.unload)
@ApiOperation({ summary: 'Bulk auto-unload all arrived bookings into the warehouse' })
autoUnloadArrived() {
return this.inventoryService.autoUnloadArrived();
}
@Post('auto-load-ready')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.load)
@ApiOperation({ summary: 'Auto-load READY_FOR_LOADING inventory with PAID bookings' })
autoLoadReady() {
return this.inventoryService.autoLoadReady();
}
@Get('eligible-bookings')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'PAID bookings not yet received, classified IMPORT/EXPORT by route; omit direction for all' })
eligibleBookings(@Query('direction') direction?: string) {
const dir = direction === 'IMPORT' || direction === 'EXPORT' ? direction : undefined;
return this.inventoryService.eligibleBookings(dir);
}
@Post('receive-bulk')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.receive)
@ApiOperation({ summary: 'Bulk-receive selected eligible PAID bookings into a location' })
receiveBulk(@Body() dto: BulkReceiveDto, @CurrentUser() user: TCurrentUser) {
dto.performedBy = actorLabel(user) ?? dto.performedBy;
return this.inventoryService.bulkReceive(dto);
}
@Get('ready-to-load-export')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'EXPORT inventory that passed inspection and is READY_FOR_LOADING' })
readyToLoadExport() {
return this.inventoryService.readyToLoadExport();
}
@Get('received-export')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'EXPORT inventory that has been received and is awaiting inspection' })
receivedExport() {
return this.inventoryService.receivedExport();
}
@Get('loaded-export')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'EXPORT inventory that is LOADED and queued for dispatch' })
loadedExport() {
return this.inventoryService.loadedExport();
}
@Get('loadable-trains')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'EXPORT trains (pre-dispatch) with inventory waiting to be loaded' })
loadableTrains() {
return this.inventoryService.loadableTrains();
}
@Get('train/:scheduleId/loadable-items')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Container/cargo inventory assigned to a train, with allocated wagons' })
trainLoadableItems(@Param('scheduleId', ParseUUIDPipe) scheduleId: string) {
return this.inventoryService.trainLoadableItems(scheduleId);
}
@Post('train/:scheduleId/load')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.load)
@ApiOperation({ summary: 'Load selected inventory items onto their allocated wagons for a train' })
loadItemsOntoTrain(
@Param('scheduleId', ParseUUIDPipe) scheduleId: string,
@Body() dto: { inventoryIds: string[]; performedBy?: string },
@CurrentUser() user: TCurrentUser,
) {
return this.inventoryService.loadItemsOntoTrain(scheduleId, dto.inventoryIds ?? [], actorLabel(user) ?? dto.performedBy);
}
@Post('bulk-dispatch-export')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.dispatch)
@ApiOperation({ summary: 'Bulk-dispatch loaded EXPORT inventory (LOADED → DISPATCHED)' })
bulkDispatchExport(@Body() dto: { inventoryIds: string[]; performedBy?: string }, @CurrentUser() user: TCurrentUser) {
return this.inventoryService.bulkDispatchExport(dto.inventoryIds ?? [], actorLabel(user) ?? dto.performedBy);
}
@Post('bulk-mark-inspected')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.inspect)
@ApiOperation({ summary: 'Bulk mark received inventory inspection PASSED (EXPORT → READY_FOR_LOADING)' })
bulkMarkInspected(@Body() dto: BulkInspectDto) {
return this.inventoryService.bulkMarkInspected(dto);
}
@Post('bookings/:bookingId/unload')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.unload)
@ApiOperation({ summary: 'Unload a single arrived booking into a location' })
unloadBooking(
@Param('bookingId', ParseUUIDPipe) bookingId: string,
@Body() dto: UnloadBookingDto,
) {
return this.inventoryService.unloadBooking(bookingId, dto);
}
@Post(':id/gate-clearance')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.gatePass)
@ApiOperation({ summary: 'Final terminal release / gate clearance (blocked while fees unpaid)' })
gateClearance(
@Param('id', ParseUUIDPipe) id: string,
@Body('performedBy') performedBy: string | undefined,
@CurrentUser() user: TCurrentUser,
) {
return this.inventoryService.gateClearance(id, actorLabel(user) ?? performedBy);
}
@Get('import/arrive-queue')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Arrived IMPORT train schedules (route-derived), read-only from scheduling' })
importArriveQueue() {
return this.scheduling.importArriveQueue();
}
@Get('import/trains/:scheduleId/items')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Assigned bookings/items for an arrived import train (read-only)' })
importTrainDetail(@Param('scheduleId', ParseUUIDPipe) scheduleId: string) {
return this.scheduling.importTrainDetail(scheduleId);
}
@Post('import/auto-unload-arrived-bookings')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.unload)
@ApiOperation({ summary: 'Unload all eligible assigned bookings of an ARRIVED import train (→ UNLOADED)' })
autoUnloadArrivedBookings(@Body() dto: {
scheduleId: string;
warehouseId?: string;
performedBy?: string;
assignments?: { bookingId: string; warehouseId: string; yardId: string; zoneId: string }[];
}, @CurrentUser() user: TCurrentUser) {
return this.inventoryService.autoUnloadArrivedBookings(
dto.scheduleId,
actorLabel(user) ?? dto.performedBy,
dto.warehouseId,
dto.assignments,
);
}
@Get('import/unloaded-queue')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'IMPORT inventory in the Unloaded Queue (UNLOADED / destination inspection)' })
importUnloadedQueue() {
return this.inventoryService.importUnloadedQueue();
}
@Get('export/djibouti-arrival-queue')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Arrived EXPORT train schedules at Djibouti-side ports, ready for unloading' })
exportDjiboutiArrivalQueue(
@Query('scheduleId') scheduleId?: string,
@Query('destination') destination?: string,
@Query('status') status?: string,
@Query('dateFrom') dateFrom?: string,
@Query('dateTo') dateTo?: string,
) {
return this.scheduling.exportDjiboutiArrivalQueue({
scheduleId,
destination,
status,
dateFrom,
dateTo,
});
}
@Get('export/djibouti-trains/:scheduleId/items')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Assigned export bookings/items for an arrived Djibouti-side train' })
exportDjiboutiTrainDetail(@Param('scheduleId', ParseUUIDPipe) scheduleId: string) {
return this.scheduling.exportDjiboutiTrainDetail(scheduleId);
}
@Post('export/auto-unload-at-djibouti')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.unload)
@ApiOperation({ summary: 'Unload all eligible export items assigned to an arrived Djibouti-side train' })
autoUnloadExportAtDjibouti(@Body() dto: { scheduleId: string; performedBy?: string }, @CurrentUser() user: TCurrentUser) {
return this.inventoryService.autoUnloadExportAtDjibouti(dto.scheduleId, actorLabel(user) ?? dto.performedBy);
}
@Get('import/pickup-ready-queue')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'IMPORT inventory that is PICKUP_READY (READY_FOR_PICKUP) awaiting pickup/dispatch' })
importPickupReadyQueue() {
return this.inventoryService.importPickupReadyQueue();
}
@Get('loadable-wagons')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'List wagons usable for loading (read-only from scheduling)' })
loadableWagons() {
return this.scheduling.listLoadableWagons();
}
@Get('booking/:bookingId/schedule')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Read-only schedule + wagon + departure status for a booking' })
bookingSchedule(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
return this.scheduling.getBookingSchedule(bookingId);
}
@Post('receive')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.receive)
@ApiOperation({ summary: 'Receive inventory at a warehouse location' })
receive(@Body() dto: ReceiveWarehouseInventoryDto, @CurrentUser() user: TCurrentUser) {
dto.performedBy = actorLabel(user) ?? dto.performedBy;
return this.inventoryService.receive(dto);
}
@Post('reserve')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.move)
@ApiOperation({ summary: 'Reserve stored inventory for a PAID booking' })
reserve(@Body() dto: ReserveInventoryDto, @CurrentUser() user: TCurrentUser) {
dto.performedBy = actorLabel(user) ?? dto.performedBy;
return this.inventoryService.reserve(dto);
}
@Get(':id/movements')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Inventory movement history' })
movements(@Param('id', ParseUUIDPipe) id: string) {
return this.inventoryService.findMovements(id);
}
@Get(':id/activity')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Inventory activity log' })
activity(@Param('id', ParseUUIDPipe) id: string) {
return this.inventoryService.findActivity(id);
}
@Get(':id/loadings')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Loading records for an inventory item' })
loadings(@Param('id', ParseUUIDPipe) id: string) {
return this.inventoryService.findLoadingsByInventory(id);
}
@Post(':id/move')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.move)
@ApiOperation({ summary: 'Move inventory to another warehouse/yard/zone' })
move(@Param('id', ParseUUIDPipe) id: string, @Body() dto: MoveInventoryDto) {
return this.inventoryService.move(id, dto);
}
@Post(':id/store')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.move)
@ApiOperation({ summary: 'Mark received inventory as STORED (optional explicit warehouse/yard/zone)' })
store(@Param('id', ParseUUIDPipe) id: string, @Body() dto: StoreInventoryDto, @CurrentUser() user: TCurrentUser) {
return this.inventoryService.store(id, actorLabel(user) ?? dto.performedBy, dto);
}
@Post(':id/ready-for-loading')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.move)
@ApiOperation({ summary: 'Mark reserved inventory READY_FOR_LOADING' })
readyForLoading(
@Param('id', ParseUUIDPipe) id: string,
@Body('performedBy') performedBy: string | undefined,
@CurrentUser() user: TCurrentUser,
) {
return this.inventoryService.readyForLoading(id, actorLabel(user) ?? performedBy);
}
@Post(':id/load')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.load)
@ApiOperation({ summary: 'Load READY_FOR_LOADING inventory onto a wagon' })
load(@Param('id', ParseUUIDPipe) id: string, @Body() dto: LoadInventoryDto) {
return this.inventoryService.load(id, dto);
}
@Post(':id/ready-for-pickup')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.move)
@ApiOperation({ summary: 'Mark inspected IMPORT inventory READY_FOR_PICKUP' })
readyForPickup(
@Param('id', ParseUUIDPipe) id: string,
@Body('performedBy') performedBy: string | undefined,
@CurrentUser() user: TCurrentUser,
) {
return this.inventoryService.readyForPickup(id, actorLabel(user) ?? performedBy);
}
@Post(':id/release')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.release)
@ApiOperation({ summary: 'Issue a DO / release order for ready-for-pickup inventory' })
release(@Param('id', ParseUUIDPipe) id: string, @Body() dto: ReleaseOrderDto) {
return this.inventoryService.release(id, dto);
}
@Get(':id/release-document')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'View warehouse release / exit paper PDF' })
async releaseDocument(@Param('id', ParseUUIDPipe) id: string, @Res() res: Response) {
const { filename, buffer } = await this.inventoryService.releaseDocument(id);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
res.setHeader('Content-Length', buffer.length);
return res.send(buffer);
}
@Get('customer-truck-exit-paper/:assignmentId')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Per-truck exit paper PDF (containers loaded on one customer truck)' })
async truckExitPaper(
@Param('assignmentId', ParseUUIDPipe) assignmentId: string,
@Res() res: Response,
) {
const { filename, buffer } = await this.inventoryService.truckExitPaper(assignmentId);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
res.setHeader('Content-Length', buffer.length);
return res.send(buffer);
}
@Get('edr-truck-exit-paper/:assignmentId')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'Per-truck exit paper PDF for an EDR last-mile truck' })
async edrTruckExitPaper(
@Param('assignmentId', ParseUUIDPipe) assignmentId: string,
@Res() res: Response,
) {
const { filename, buffer } = await this.inventoryService.edrTruckExitPaper(assignmentId);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
res.setHeader('Content-Length', buffer.length);
return res.send(buffer);
}
@Get(':id/grn-document')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.view)
@ApiOperation({ summary: 'View goods received note PDF' })
async grnDocument(@Param('id', ParseUUIDPipe) id: string, @Res() res: Response) {
const { filename, buffer } = await this.inventoryService.grnDocument(id);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
res.setHeader('Content-Length', buffer.length);
return res.send(buffer);
}
@Get(':id/handover-document')
@ApiOperation({ summary: 'View import goods handover document PDF' })
async handoverDocument(@Param('id', ParseUUIDPipe) id: string, @Res() res: Response) {
const { filename, buffer } = await this.inventoryService.handoverDocument(id);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
res.setHeader('Content-Length', buffer.length);
return res.send(buffer);
}
@Post('bookings/:bookingId/approve-delivery')
@ApiOperation({ summary: "Approve delivery — customer records their full name (signature optional)" })
approveDeliveryForBooking(
@Param('bookingId', ParseUUIDPipe) bookingId: string,
@Body() dto: ApproveDeliveryDto,
@Request() req: { user?: { id?: string; sub?: string } },
@CurrentUser() user: TCurrentUser,
) {
return this.inventoryService.approveDeliveryForBooking(
bookingId,
user?.id ?? req.user?.id ?? req.user?.sub,
dto.signerName,
);
}
@Get('bookings/:bookingId/handovers')
@ApiOperation({ summary: 'Handover records for a booking (per-booking or per-truck)' })
bookingHandovers(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
return this.handoverService.list(bookingId);
}
@Post('bookings/:bookingId/request-handover-signature')
@ApiOperation({ summary: 'Ask the customer to sign the handover (creates one if none, then notifies)' })
requestHandoverSignature(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
return this.handoverService.requestSignature(bookingId);
}
@Get('bookings/:bookingId/grn-document')
@ApiOperation({ summary: 'View GRN PDF for a booking (customer portal)' })
async bookingGrnDocument(@Param('bookingId', ParseUUIDPipe) bookingId: string, @Res() res: Response) {
const { filename, buffer } = await this.inventoryService.grnDocumentForBooking(bookingId);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
res.setHeader('Content-Length', buffer.length);
return res.send(buffer);
}
@Get('bookings/:bookingId/release-document')
@ApiOperation({ summary: 'View gate-clearance / release-order PDF for a booking (customer portal)' })
async bookingReleaseDocument(@Param('bookingId', ParseUUIDPipe) bookingId: string, @Res() res: Response) {
const { filename, buffer } = await this.inventoryService.releaseDocumentForBooking(bookingId);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
res.setHeader('Content-Length', buffer.length);
return res.send(buffer);
}
@Get('bookings/:bookingId/handover-document')
@ApiOperation({ summary: 'View import goods handover document PDF (resolved by booking)' })
async bookingHandoverDocument(@Param('bookingId', ParseUUIDPipe) bookingId: string, @Res() res: Response) {
const { filename, buffer } = await this.inventoryService.handoverDocumentForBooking(bookingId);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
res.setHeader('Content-Length', buffer.length);
return res.send(buffer);
}
@Get('bookings/:bookingId/container-items')
@ApiOperation({ summary: 'Per-container/bulk items of a booking with lifecycle stage + refs' })
containerItems(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
return this.inventoryService.containerItems(bookingId);
}
@Get('bookings/:bookingId/container-weights')
@ApiOperation({ summary: "A booking's containers + VGM cargo weight (tonnes) for exit weighing" })
containerWeights(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
return this.inventoryService.bookingContainerWeights(bookingId);
}
@Post(':id/deliver')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.deliver)
@ApiOperation({ summary: 'Deliver import goods to the customer + capture proof of delivery' })
deliver(@Param('id', ParseUUIDPipe) id: string, @Body() dto: DeliverInventoryDto, @CurrentUser() user: TCurrentUser) {
dto.performedBy = actorLabel(user) ?? dto.performedBy;
return this.inventoryService.deliver(id, dto);
}
@Patch(':id/dispatch')
@BookingStaff(FREIGHT_PERMS.warehouseInventory.dispatch)
@ApiOperation({ summary: 'Mark loaded inventory DISPATCHED (left the terminal)' })
dispatch(
@Param('id', ParseUUIDPipe) id: string,
@Body('performedBy') performedBy: string | undefined,
@CurrentUser() user: TCurrentUser,
) {
return this.inventoryService.dispatch(id, actorLabel(user) ?? performedBy);
}
}