mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
"Auto Load Ready Items" now opens a train picker (pre-dispatch trains with these bookings assigned, via the existing loadable-trains flow). No train available -> no auto-loading, with a clear notice. Loading goes through the existing per-wagon load path, so items without an allocated wagon are skipped with a reason. The train association is stored on the existing warehouse_loadings table (no new table needed): new train_schedule_id column + a note recording train number, origin -> destination, and departure time; wagon_id becomes nullable. The trainless load-passed-export endpoint, its frontend wiring, and the unused useLoadPassedExport hook are removed. Migration 2100000000000 (idempotent) also applied to the dev database. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
393 lines
16 KiB
TypeScript
393 lines
16 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 { 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 { 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()
|
|
@ApiOperation({ summary: 'List warehouse inventory' })
|
|
findAll(@Query() filter: FilterWarehouseInventoryDto) {
|
|
return this.inventoryService.findAll(filter);
|
|
}
|
|
|
|
@Get('ready-for-loading')
|
|
@ApiOperation({ summary: 'List inventory ready for loading' })
|
|
findReadyForLoading(@Query() filter: FilterWarehouseInventoryDto) {
|
|
return this.inventoryService.findReadyForLoading(filter);
|
|
}
|
|
|
|
@Get('inquiry')
|
|
@ApiOperation({ summary: 'Locate any item inside the warehouse' })
|
|
inquiry(@Query() filter: InquiryWarehouseInventoryDto) {
|
|
return this.inventoryService.inquiry(filter);
|
|
}
|
|
|
|
@Get('arrival-queue')
|
|
@ApiOperation({ summary: 'Arrived bookings awaiting unload / inspection' })
|
|
arrivalQueue() {
|
|
return this.inventoryService.arrivalQueue();
|
|
}
|
|
|
|
@Post('auto-unload-arrived')
|
|
@ApiOperation({ summary: 'Bulk auto-unload all arrived bookings into the warehouse' })
|
|
autoUnloadArrived() {
|
|
return this.inventoryService.autoUnloadArrived();
|
|
}
|
|
|
|
@Post('auto-load-ready')
|
|
@ApiOperation({ summary: 'Auto-load READY_FOR_LOADING inventory with PAID bookings' })
|
|
autoLoadReady() {
|
|
return this.inventoryService.autoLoadReady();
|
|
}
|
|
|
|
@Get('eligible-bookings')
|
|
@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')
|
|
@ApiOperation({ summary: 'Bulk-receive selected eligible PAID bookings into a location' })
|
|
receiveBulk(@Body() dto: BulkReceiveDto) {
|
|
return this.inventoryService.bulkReceive(dto);
|
|
}
|
|
|
|
|
|
@Get('ready-to-load-export')
|
|
@ApiOperation({ summary: 'EXPORT inventory that passed inspection and is READY_FOR_LOADING' })
|
|
readyToLoadExport() {
|
|
return this.inventoryService.readyToLoadExport();
|
|
}
|
|
|
|
@Get('received-export')
|
|
@ApiOperation({ summary: 'EXPORT inventory that has been received and is awaiting inspection' })
|
|
receivedExport() {
|
|
return this.inventoryService.receivedExport();
|
|
}
|
|
|
|
@Get('loaded-export')
|
|
@ApiOperation({ summary: 'EXPORT inventory that is LOADED and queued for dispatch' })
|
|
loadedExport() {
|
|
return this.inventoryService.loadedExport();
|
|
}
|
|
|
|
@Get('loadable-trains')
|
|
@ApiOperation({ summary: 'EXPORT trains (pre-dispatch) with inventory waiting to be loaded' })
|
|
loadableTrains() {
|
|
return this.inventoryService.loadableTrains();
|
|
}
|
|
|
|
@Get('train/:scheduleId/loadable-items')
|
|
@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')
|
|
@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 },
|
|
) {
|
|
return this.inventoryService.loadItemsOntoTrain(scheduleId, dto.inventoryIds ?? [], dto.performedBy);
|
|
}
|
|
|
|
@Post('bulk-dispatch-export')
|
|
@ApiOperation({ summary: 'Bulk-dispatch loaded EXPORT inventory (LOADED → DISPATCHED)' })
|
|
bulkDispatchExport(@Body() dto: { inventoryIds: string[]; performedBy?: string }) {
|
|
return this.inventoryService.bulkDispatchExport(dto.inventoryIds ?? [], dto.performedBy);
|
|
}
|
|
|
|
@Post('bulk-mark-inspected')
|
|
@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')
|
|
@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')
|
|
@ApiOperation({ summary: 'Final terminal release / gate clearance (blocked while fees unpaid)' })
|
|
gateClearance(@Param('id', ParseUUIDPipe) id: string, @Body('performedBy') performedBy?: string) {
|
|
return this.inventoryService.gateClearance(id, performedBy);
|
|
}
|
|
|
|
@Get('import/arrive-queue')
|
|
@ApiOperation({ summary: 'Arrived IMPORT train schedules (route-derived), read-only from scheduling' })
|
|
importArriveQueue() {
|
|
return this.scheduling.importArriveQueue();
|
|
}
|
|
|
|
@Get('import/trains/:scheduleId/items')
|
|
@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')
|
|
@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 }[];
|
|
}) {
|
|
return this.inventoryService.autoUnloadArrivedBookings(
|
|
dto.scheduleId,
|
|
dto.performedBy,
|
|
dto.warehouseId,
|
|
dto.assignments,
|
|
);
|
|
}
|
|
|
|
@Get('import/unloaded-queue')
|
|
@ApiOperation({ summary: 'IMPORT inventory in the Unloaded Queue (UNLOADED / destination inspection)' })
|
|
importUnloadedQueue() {
|
|
return this.inventoryService.importUnloadedQueue();
|
|
}
|
|
|
|
@Get('export/djibouti-arrival-queue')
|
|
@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')
|
|
@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')
|
|
@ApiOperation({ summary: 'Unload all eligible export items assigned to an arrived Djibouti-side train' })
|
|
autoUnloadExportAtDjibouti(@Body() dto: { scheduleId: string; performedBy?: string }) {
|
|
return this.inventoryService.autoUnloadExportAtDjibouti(dto.scheduleId, dto.performedBy);
|
|
}
|
|
|
|
@Get('import/pickup-ready-queue')
|
|
@ApiOperation({ summary: 'IMPORT inventory that is PICKUP_READY (READY_FOR_PICKUP) awaiting pickup/dispatch' })
|
|
importPickupReadyQueue() {
|
|
return this.inventoryService.importPickupReadyQueue();
|
|
}
|
|
|
|
@Get('loadable-wagons')
|
|
@ApiOperation({ summary: 'List wagons usable for loading (read-only from scheduling)' })
|
|
loadableWagons() {
|
|
return this.scheduling.listLoadableWagons();
|
|
}
|
|
|
|
@Get('booking/:bookingId/schedule')
|
|
@ApiOperation({ summary: 'Read-only schedule + wagon + departure status for a booking' })
|
|
bookingSchedule(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
|
return this.scheduling.getBookingSchedule(bookingId);
|
|
}
|
|
|
|
@Post('receive')
|
|
@ApiOperation({ summary: 'Receive inventory at a warehouse location' })
|
|
receive(@Body() dto: ReceiveWarehouseInventoryDto) {
|
|
return this.inventoryService.receive(dto);
|
|
}
|
|
|
|
@Post('reserve')
|
|
@ApiOperation({ summary: 'Reserve stored inventory for a PAID booking' })
|
|
reserve(@Body() dto: ReserveInventoryDto) {
|
|
return this.inventoryService.reserve(dto);
|
|
}
|
|
|
|
@Get(':id/movements')
|
|
@ApiOperation({ summary: 'Inventory movement history' })
|
|
movements(@Param('id', ParseUUIDPipe) id: string) {
|
|
return this.inventoryService.findMovements(id);
|
|
}
|
|
|
|
@Get(':id/activity')
|
|
@ApiOperation({ summary: 'Inventory activity log' })
|
|
activity(@Param('id', ParseUUIDPipe) id: string) {
|
|
return this.inventoryService.findActivity(id);
|
|
}
|
|
|
|
@Get(':id/loadings')
|
|
@ApiOperation({ summary: 'Loading records for an inventory item' })
|
|
loadings(@Param('id', ParseUUIDPipe) id: string) {
|
|
return this.inventoryService.findLoadingsByInventory(id);
|
|
}
|
|
|
|
@Post(':id/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')
|
|
@ApiOperation({ summary: 'Mark received inventory as STORED (optional explicit warehouse/yard/zone)' })
|
|
store(@Param('id', ParseUUIDPipe) id: string, @Body() dto: StoreInventoryDto) {
|
|
return this.inventoryService.store(id, dto.performedBy, dto);
|
|
}
|
|
|
|
@Post(':id/ready-for-loading')
|
|
@ApiOperation({ summary: 'Mark reserved inventory READY_FOR_LOADING' })
|
|
readyForLoading(@Param('id', ParseUUIDPipe) id: string, @Body('performedBy') performedBy?: string) {
|
|
return this.inventoryService.readyForLoading(id, performedBy);
|
|
}
|
|
|
|
@Post(':id/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')
|
|
@ApiOperation({ summary: 'Mark inspected IMPORT inventory READY_FOR_PICKUP' })
|
|
readyForPickup(@Param('id', ParseUUIDPipe) id: string, @Body('performedBy') performedBy?: string) {
|
|
return this.inventoryService.readyForPickup(id, performedBy);
|
|
}
|
|
|
|
@Post(':id/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')
|
|
@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')
|
|
@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(':id/grn-document')
|
|
@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 using the current customer's saved signature" })
|
|
approveDeliveryForBooking(
|
|
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
|
@Request() req: { user?: { id?: string; sub?: string } },
|
|
) {
|
|
return this.inventoryService.approveDeliveryForBooking(bookingId, req.user?.id ?? req.user?.sub);
|
|
}
|
|
|
|
@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/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')
|
|
@ApiOperation({ summary: 'Deliver import goods to the customer + capture proof of delivery' })
|
|
deliver(@Param('id', ParseUUIDPipe) id: string, @Body() dto: DeliverInventoryDto) {
|
|
return this.inventoryService.deliver(id, dto);
|
|
}
|
|
|
|
@Patch(':id/dispatch')
|
|
@ApiOperation({ summary: 'Mark loaded inventory DISPATCHED (left the terminal)' })
|
|
dispatch(@Param('id', ParseUUIDPipe) id: string, @Body('performedBy') performedBy?: string) {
|
|
return this.inventoryService.dispatch(id, performedBy);
|
|
}
|
|
}
|