import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query, } from '@nestjs/common'; import { CurrentUser } from '@edr/api-common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import type { AuthUserPayload } from '../../common/resolve-auth-user-id'; import { resolveAuthUserId } from '../../common/resolve-auth-user-id'; import { TrainSchedulingManage, TrainSchedulingView } from '../../common/booking-guards'; import { AssignBookingsDto } from './dto/assign-bookings.dto'; import { AssignUnassignedBookingDto } from './dto/assign-unassigned-booking.dto'; import { CreateContainerTrainScheduleDto } from './dto/create-container-train-schedule.dto'; import { GetEligibleBookingsDto } from './dto/get-eligible-bookings.dto'; import { GetEligibleBulkBookingsDto } from './dto/get-eligible-bulk-bookings.dto'; import { GetEligibleContainerBookingsDto } from './dto/get-eligible-container-bookings.dto'; import { PinWagonsDto } from './dto/pin-wagons.dto'; import { UpdateContainerItemDto } from './dto/update-container-item.dto'; import { PreviewBulkTrainScheduleDto } from './dto/preview-bulk-train-schedule.dto'; import { PreviewContainerTrainScheduleDto } from './dto/preview-container-train-schedule.dto'; import { PreviewTrainScheduleDto } from './dto/preview-train-schedule.dto'; import { RecordCheckpointDto } from './dto/record-checkpoint.dto'; import { AvailableLocomotivesQueryDto } from './dto/available-locomotives-query.dto'; import { BookableSchedulesQueryDto } from './dto/bookable-schedules-query.dto'; import { UpdateTrainSchedulingGlobalRulesDto } from './dto/update-train-scheduling-global-rules.dto'; import { TrainSchedulingService } from './train-scheduling.service'; import { BookingBatchService } from './booking-batch.service'; @ApiTags('train-scheduling') @ApiBearerAuth() @Controller('train-scheduling') export class TrainSchedulingController { constructor( private readonly trainSchedulingService: TrainSchedulingService, private readonly bookingBatchService: BookingBatchService, ) {} @Get('global-rules') @TrainSchedulingView() @ApiOperation({ summary: 'Get global train scheduling rules (singleton)' }) getGlobalRules() { return this.trainSchedulingService.getTrainSchedulingGlobalRules(); } @Patch('global-rules') @TrainSchedulingManage() @ApiOperation({ summary: 'Update global train scheduling rules (singleton)' }) updateGlobalRules(@Body() dto: UpdateTrainSchedulingGlobalRulesDto) { return this.trainSchedulingService.updateTrainSchedulingGlobalRules(dto); } @Get('eligible-bookings') @TrainSchedulingView() @ApiOperation({ summary: 'List eligible bookings (container and/or bulk)' }) getEligibleBookings(@Query() query: GetEligibleBookingsDto) { return this.trainSchedulingService.getEligibleBookings(query); } @Get('batch-board') @TrainSchedulingView() @ApiOperation({ summary: 'Batch monitoring board: schedules with bookings grouped by state' }) getBatchBoard() { return this.bookingBatchService.getBatchBoard(); } @Get('batch-board/:scheduleId') @TrainSchedulingView() @ApiOperation({ summary: 'Batch board detail for one schedule with EAT 3h windows' }) getBatchBoardDetail(@Param('scheduleId', ParseUUIDPipe) scheduleId: string) { return this.bookingBatchService.getBatchBoardDetail(scheduleId); } @Get('available-locomotives') @TrainSchedulingView() @ApiOperation({ summary: 'List AVAILABLE locomotives at the route origin yard', }) getAvailableLocomotives(@Query() query: AvailableLocomotivesQueryDto) { return this.trainSchedulingService.getAvailableLocomotivesForRoute(query.routeId); } @Get('bookable-schedules') @TrainSchedulingView() @ApiOperation({ summary: 'OPEN same-route schedules a new booking can target' }) getBookableSchedules(@Query() query: BookableSchedulesQueryDto) { return this.trainSchedulingService.getBookableSchedules( query.originYardId, query.destinationYardId, ); } @Get('container/eligible-bookings') @TrainSchedulingView() @ApiOperation({ summary: 'List eligible container bookings' }) getEligibleContainerBookings(@Query() query: GetEligibleContainerBookingsDto) { return this.trainSchedulingService.getEligibleContainerBookings(query); } @Get('bulk/eligible-bookings') @TrainSchedulingView() @ApiOperation({ summary: 'List eligible bulk bookings' }) getEligibleBulkBookings(@Query() query: GetEligibleBulkBookingsDto) { return this.trainSchedulingService.getEligibleBulkBookings(query); } @Post('preview') @TrainSchedulingView() @ApiOperation({ summary: 'Preview a mixed-capable train schedule' }) previewTrainSchedule(@Body() dto: PreviewTrainScheduleDto) { return this.trainSchedulingService.previewTrainSchedule(dto); } @Post('container/preview') @TrainSchedulingView() @ApiOperation({ summary: 'Preview a container train schedule' }) previewContainerTrainSchedule(@Body() dto: PreviewContainerTrainScheduleDto) { return this.trainSchedulingService.previewContainerTrainSchedule(dto); } @Post('bulk/preview') @TrainSchedulingView() @ApiOperation({ summary: 'Preview a bulk train schedule' }) previewBulkTrainSchedule(@Body() dto: PreviewBulkTrainScheduleDto) { return this.trainSchedulingService.previewBulkTrainSchedule(dto); } @Post('container/schedules') @TrainSchedulingManage() @ApiOperation({ summary: 'Create a container train schedule' }) createContainerTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) { return this.trainSchedulingService.createContainerTrainSchedule(dto); } @Post('bulk/schedules') @TrainSchedulingManage() @ApiOperation({ summary: 'Create a bulk train schedule' }) createBulkTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) { return this.trainSchedulingService.createContainerTrainSchedule(dto); } @Post('schedules/:id/assign-bookings') @TrainSchedulingManage() @ApiOperation({ summary: 'Assign bookings to a train schedule (mixed-capable)' }) assignBookings( @Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignBookingsDto, ) { return this.trainSchedulingService.assignBookingsToSchedule(id, dto); } @Post('container/schedules/:id/assign-bookings') @TrainSchedulingManage() @ApiOperation({ summary: 'Assign container bookings to a train schedule' }) assignContainerBookings( @Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignBookingsDto, ) { return this.trainSchedulingService.assignBookingsToSchedule(id, dto, 'CONTAINER'); } @Post('bulk/schedules/:id/assign-bookings') @TrainSchedulingManage() @ApiOperation({ summary: 'Assign bulk bookings to a train schedule' }) assignBulkBookings( @Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignBookingsDto, ) { return this.trainSchedulingService.assignBookingsToSchedule(id, dto, 'BULK'); } @Delete('schedules/:id/bookings/:bookingId') @TrainSchedulingManage() @ApiOperation({ summary: 'Unassign a booking from a train schedule' }) unassignBooking( @Param('id', ParseUUIDPipe) id: string, @Param('bookingId', ParseUUIDPipe) bookingId: string, @CurrentUser() user: AuthUserPayload, ) { return this.trainSchedulingService.unassignBooking(id, bookingId, resolveAuthUserId(user)); } @Delete('schedules/:id/wagons/:trainSetWagonId') @TrainSchedulingManage() @ApiOperation({ summary: 'Remove an empty wagon slot from a train' }) removeWagonSlot( @Param('id', ParseUUIDPipe) id: string, @Param('trainSetWagonId', ParseUUIDPipe) trainSetWagonId: string, ) { return this.trainSchedulingService.removeTrainSetWagonSlot(id, trainSetWagonId); } @Patch('schedules/:id/container-items/:itemId') @TrainSchedulingManage() @ApiOperation({ summary: 'Update a container number on a wagon slot' }) updateContainerItem( @Param('id', ParseUUIDPipe) id: string, @Param('itemId', ParseUUIDPipe) itemId: string, @Body() dto: UpdateContainerItemDto, ) { return this.trainSchedulingService.updateContainerItem(id, itemId, dto); } @Get('schedules/:id/unassigned-bookings') @TrainSchedulingView() @ApiOperation({ summary: 'Get unassigned bookings for a schedule' }) getUnassignedBookings(@Param('id', ParseUUIDPipe) id: string) { return this.trainSchedulingService.getUnassignedBookings(id); } @Post('schedules/:id/assign-unassigned-booking') @TrainSchedulingManage() @ApiOperation({ summary: 'Assign one linked unallocated booking to wagons (preserves existing assignments)', }) assignUnassignedBooking( @Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignUnassignedBookingDto, ) { return this.trainSchedulingService.assignUnassignedBookingToWagons(id, dto.bookingId); } @Get('schedules/:id/composition-removals') @TrainSchedulingView() @ApiOperation({ summary: 'Get removal log for a schedule' }) getCompositionRemovals(@Param('id', ParseUUIDPipe) id: string) { return this.trainSchedulingService.getCompositionRemovals(id); } @Post('schedules/:id/pin-wagons') @TrainSchedulingManage() @ApiOperation({ summary: 'Pin physical wagons to train set slots' }) pinWagons(@Param('id', ParseUUIDPipe) id: string, @Body() dto: PinWagonsDto) { return this.trainSchedulingService.pinWagons(id, dto); } @Post('schedules/:id/finalize') @TrainSchedulingManage() @ApiOperation({ summary: 'Finalize a draft train schedule' }) finalizeSchedule(@Param('id', ParseUUIDPipe) id: string) { return this.trainSchedulingService.finalizeSchedule(id); } @Post('schedules/:id/dispatch') @TrainSchedulingManage() @ApiOperation({ summary: 'Dispatch a scheduled train' }) dispatchSchedule(@Param('id', ParseUUIDPipe) id: string) { return this.trainSchedulingService.dispatchSchedule(id); } // ---- batch / booking-window staff actions ---- @Post('schedules/:id/run-batch') @TrainSchedulingManage() @ApiOperation({ summary: 'Manually run the batch fill for a schedule' }) async runBatch(@Param('id', ParseUUIDPipe) id: string) { await this.bookingBatchService.fillSchedule(id); return this.bookingBatchService.getBatchBoardDetail(id); } @Post('schedules/:id/run-allocation') @TrainSchedulingManage() @ApiOperation({ summary: 'Run wagon-level allocation for all eligible linked bookings' }) async runAllocation(@Param('id', ParseUUIDPipe) id: string) { return this.bookingBatchService.runWagonAllocation(id); } @Patch('schedules/:id/booking-window') @TrainSchedulingManage() @ApiOperation({ summary: 'Open or close a schedule booking window' }) async setBookingWindow( @Param('id', ParseUUIDPipe) id: string, @Body('status') status: 'OPEN' | 'CLOSED', ) { await this.trainSchedulingService.setBookingWindow(id, status === 'CLOSED' ? 'CLOSED' : 'OPEN'); return this.trainSchedulingService.getContainerTrainScheduleById(id); } @Post('bookings/:bookingId/mark-paid') @TrainSchedulingManage() @ApiOperation({ summary: 'Staff: mark a reserved booking paid and allocate it now' }) async markBookingPaid(@Param('bookingId', ParseUUIDPipe) bookingId: string) { await this.bookingBatchService.markPaid(bookingId); return { ok: true }; } @Post('bookings/:bookingId/expire') @TrainSchedulingManage() @ApiOperation({ summary: 'Staff: expire a reservation and free its capacity' }) async expireBooking(@Param('bookingId', ParseUUIDPipe) bookingId: string) { await this.bookingBatchService.expireReservation(bookingId); return { ok: true }; } @Post('bookings/:bookingId/move-schedule') @TrainSchedulingManage() @ApiOperation({ summary: 'Re-point a booking to another OPEN same-route schedule' }) async moveBookingSchedule( @Param('bookingId', ParseUUIDPipe) bookingId: string, @Body('trainScheduleId', ParseUUIDPipe) trainScheduleId: string, ) { await this.bookingBatchService.moveToSchedule(bookingId, trainScheduleId); return { ok: true }; } @Get('schedules/:id/checkpoints') @TrainSchedulingView() @ApiOperation({ summary: 'Get the tracking corridor + logged checkpoints for a train' }) getScheduleCheckpoints(@Param('id', ParseUUIDPipe) id: string) { return this.trainSchedulingService.getScheduleCheckpoints(id); } @Post('schedules/:id/checkpoints') @TrainSchedulingManage() @ApiOperation({ summary: 'Log the train passing a station (final station triggers arrival)' }) recordCheckpoint( @Param('id', ParseUUIDPipe) id: string, @Body() dto: RecordCheckpointDto, ) { return this.trainSchedulingService.recordCheckpoint(id, dto); } @Post('schedules/:id/arrive') @TrainSchedulingManage() @ApiOperation({ summary: 'Mark a dispatched train arrived (move assets to destination yard, free assets)' }) arriveSchedule(@Param('id', ParseUUIDPipe) id: string) { return this.trainSchedulingService.arriveSchedule(id); } @Get('container/schedules') @TrainSchedulingView() @ApiOperation({ summary: 'List container train schedules' }) getContainerTrainSchedules() { return this.trainSchedulingService.getContainerTrainSchedules(); } @Get('bulk/schedules') @TrainSchedulingView() @ApiOperation({ summary: 'List bulk train schedules' }) getBulkTrainSchedules() { return this.trainSchedulingService.getContainerTrainSchedules(); } @Get('container/schedules/:id') @TrainSchedulingView() @ApiOperation({ summary: 'Get container train schedule detail' }) getContainerTrainScheduleById(@Param('id', ParseUUIDPipe) id: string) { return this.trainSchedulingService.getContainerTrainScheduleById(id); } @Get('bulk/schedules/:id') @TrainSchedulingView() @ApiOperation({ summary: 'Get bulk train schedule detail' }) getBulkTrainScheduleById(@Param('id', ParseUUIDPipe) id: string) { return this.trainSchedulingService.getContainerTrainScheduleById(id); } @Post('container/schedules/:id/cancel') @TrainSchedulingManage() @ApiOperation({ summary: 'Cancel container train schedule' }) cancelTrainSchedule(@Param('id', ParseUUIDPipe) id: string) { return this.trainSchedulingService.cancelTrainSchedule(id); } @Post('bulk/schedules/:id/cancel') @TrainSchedulingManage() @ApiOperation({ summary: 'Cancel bulk train schedule' }) cancelBulkTrainSchedule(@Param('id', ParseUUIDPipe) id: string) { return this.trainSchedulingService.cancelTrainSchedule(id); } }