mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 14:20:58 +00:00
Gates the previously open support-agent, procurement, compliance, facilities, list-users and trade-access controllers, separates customer from staff routes across bookings, contracts, companies, billing, warehouses, files and train scheduling, and moves billing, overview, reports and the settings controllers onto their own keys instead of the blanket admin key. Drops the demo-permissions module and the untested notification test route.
983 lines
35 KiB
TypeScript
983 lines
35 KiB
TypeScript
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
|
|
import type { Response } from "express";
|
|
import type { AuthUserPayload } from "../../common/resolve-auth-user-id";
|
|
import { UserTradeAccessService } from "../user-trade-access/user-trade-access.service";
|
|
import { resolveAuthUserId } from "../../common/resolve-auth-user-id";
|
|
|
|
import {
|
|
Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query, Res,
|
|
} from "@nestjs/common";
|
|
import { CurrentUser } from "@edr/api-common";
|
|
import {
|
|
BookingDocReviewAlert,
|
|
BookingStaff,
|
|
MixedAudience,
|
|
PortalCustomer,
|
|
TrainSchedulingCancel,
|
|
TrainSchedulingCreate,
|
|
TrainSchedulingReschedule,
|
|
TrainSchedulingRulesManage,
|
|
TrainSchedulingUpdate,
|
|
TrainSchedulingView,
|
|
} from "../../common/booking-guards";
|
|
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
|
import { AcceptIntercityBookingsDto } from "./dto/accept-intercity-bookings.dto";
|
|
import { AssignBookingsDto } from "./dto/assign-bookings.dto";
|
|
import { AssignUnassignedBookingDto } from "./dto/assign-unassigned-booking.dto";
|
|
import { SwitchGovernmentBookingDto } from "./dto/switch-government-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 { MoveWagonLoadDto } from "./dto/move-wagon-load.dto";
|
|
import { UpdateContainerItemDto } from "./dto/update-container-item.dto";
|
|
import { UpdateImportLoadingStatusDto } from "./dto/update-import-loading-status.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 {
|
|
ImportDjiboutiActionDto,
|
|
UploadImportDjiboutiDocumentDto,
|
|
} from "./dto/import-djibouti-operation.dto";
|
|
import { AvailableLocomotivesQueryDto } from "./dto/available-locomotives-query.dto";
|
|
import { AdjustScheduleConsistDto } from "./dto/adjust-schedule-consist.dto";
|
|
import { AvailableTrainsQueryDto } from "./dto/available-trains-query.dto";
|
|
import { BatchBoardQueryDto } from "./dto/batch-board-query.dto";
|
|
import { BookableSchedulesQueryDto } from "./dto/bookable-schedules-query.dto";
|
|
import { ListTrainSchedulesQueryDto } from "./dto/list-train-schedules-query.dto";
|
|
import { AvailableDaysQueryDto } from "./dto/available-days-query.dto";
|
|
import { AvailableDaysForCargoQueryDto } from "./dto/available-days-for-cargo-query.dto";
|
|
import { UpdateTrainSchedulingGlobalRulesDto } from "./dto/update-train-scheduling-global-rules.dto";
|
|
import { UpdateScheduleWindowRuleDto } from "./dto/update-schedule-window-rule.dto";
|
|
import { UpdateScheduleDateDto } from "./dto/update-schedule-date.dto";
|
|
import { MaintenanceRescheduleDto } from "./dto/maintenance-reschedule.dto";
|
|
import { TrainSchedulingService } from "./train-scheduling.service";
|
|
import { BookingBatchService } from "./booking-batch.service";
|
|
import { BookingJourneyService } from "./booking-journey.service";
|
|
import { BookingWindowService } from "./booking-window.service";
|
|
import { IntercityService } from "./intercity.service";
|
|
import { BillingService } from "../billing/billing.service";
|
|
|
|
@ApiTags("train-scheduling")
|
|
@ApiBearerAuth()
|
|
@Controller("train-scheduling")
|
|
export class TrainSchedulingController {
|
|
constructor(
|
|
private readonly trainSchedulingService: TrainSchedulingService,
|
|
private readonly bookingBatchService: BookingBatchService,
|
|
private readonly bookingWindowService: BookingWindowService,
|
|
private readonly intercityService: IntercityService,
|
|
private readonly bookingJourneyService: BookingJourneyService,
|
|
private readonly billingService: BillingService,
|
|
private readonly userTradeAccessService: UserTradeAccessService,
|
|
) { }
|
|
|
|
@Get("my-booking-windows")
|
|
@PortalCustomer()
|
|
@ApiOperation({
|
|
summary:
|
|
"Upcoming/open booking windows announced to the signed-in customer (all window-engine schedules; their own contract lanes carry a Book-now target)",
|
|
})
|
|
async getMyBookingWindows(@CurrentUser() user: AuthUserPayload) {
|
|
// Every customer sees announced windows; companyId (when resolvable) just
|
|
// enriches lanes they hold a contract on so "Book now" can target it.
|
|
const companyId = await this.billingService.resolveCompanyId(
|
|
resolveAuthUserId(user),
|
|
);
|
|
return this.trainSchedulingService.getBookingWindowsForCompany(companyId);
|
|
}
|
|
|
|
@Get("contracts/:contractId/booking-windows")
|
|
@MixedAudience([
|
|
FREIGHT_PERMS.trainScheduling.view,
|
|
FREIGHT_PERMS.contracts.createBooking,
|
|
])
|
|
@ApiOperation({
|
|
summary:
|
|
"Upcoming/open booking windows on a contract's routes — gates the booking form for customer + Ethiopian GL",
|
|
})
|
|
getContractBookingWindows(
|
|
@Param("contractId", ParseUUIDPipe) contractId: string,
|
|
) {
|
|
return this.trainSchedulingService.getBookingWindowsForContract(contractId);
|
|
}
|
|
|
|
@Get("booking-windows")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({
|
|
summary:
|
|
"All announced booking windows across lanes (import cycle + export FCFS), for staff dashboards",
|
|
})
|
|
listBookingWindows() {
|
|
return this.trainSchedulingService.listAllBookingWindows();
|
|
}
|
|
|
|
@Get("global-rules")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({ summary: "Get global train scheduling rules (singleton)" })
|
|
getGlobalRules() {
|
|
return this.trainSchedulingService.getTrainSchedulingGlobalRules();
|
|
}
|
|
|
|
@Patch("global-rules")
|
|
@TrainSchedulingRulesManage()
|
|
@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: paginated import schedules (all statuses) with bookings grouped by state",
|
|
})
|
|
async getBatchBoard(
|
|
@Query() query: BatchBoardQueryDto,
|
|
@CurrentUser() user: AuthUserPayload,
|
|
) {
|
|
// Batch board is IMPORT-only — a user without IMPORT access sees nothing.
|
|
const allowed =
|
|
await this.userTradeAccessService.resolveAllowedDirections(user);
|
|
return this.bookingBatchService.getBatchBoard(query, allowed ?? undefined);
|
|
}
|
|
|
|
@Get("batch-board/:scheduleId")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({
|
|
summary:
|
|
"Batch board detail for one schedule: its booking window, in-window bookings and pending-contract bucket",
|
|
})
|
|
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("available-trains")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({
|
|
summary:
|
|
"List built trains (Train Builder) schedulable on a route, annotated with yard position and future runs",
|
|
})
|
|
getAvailableTrains(@Query() query: AvailableTrainsQueryDto) {
|
|
return this.trainSchedulingService.getAvailableTrainsForRoute(
|
|
query.routeId,
|
|
);
|
|
}
|
|
|
|
@Get("schedules/:id/consist")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({
|
|
summary:
|
|
"Built-train consist snapshot for a schedule: gross weight/length vs locomotive limits (incl. tolerance), trimmable + addable wagons, adjustment history",
|
|
})
|
|
getScheduleConsist(@Param("id", ParseUUIDPipe) id: string) {
|
|
return this.trainSchedulingService.getScheduleConsist(id);
|
|
}
|
|
|
|
@Post("schedules/:id/adjust-consist")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary:
|
|
"Permanently trim free wagons off / couple yard wagons onto the schedule's built train (weight & length limits incl. tolerance enforced, every change logged)",
|
|
})
|
|
adjustScheduleConsist(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: AdjustScheduleConsistDto,
|
|
@CurrentUser() user: AuthUserPayload,
|
|
) {
|
|
return this.trainSchedulingService.adjustScheduleConsist(
|
|
id,
|
|
dto,
|
|
resolveAuthUserId(user),
|
|
);
|
|
}
|
|
|
|
@Get("schedules/:id/history")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({
|
|
summary:
|
|
"Unified change history for a schedule: wagon consist adjustments (add/remove/switch, with the stop they happened at) merged with booking composition removals, newest first",
|
|
})
|
|
getScheduleHistory(@Param("id", ParseUUIDPipe) id: string) {
|
|
return this.trainSchedulingService.getScheduleHistory(id);
|
|
}
|
|
|
|
@Get("bookable-schedules")
|
|
// No staff guard: customers hit this while creating a booking to find OPEN
|
|
// same-route schedules. Do not attach train_scheduling permissions here.
|
|
@ApiOperation({
|
|
summary: "OPEN same-route schedules a new booking can target",
|
|
})
|
|
getBookableSchedules(@Query() query: BookableSchedulesQueryDto) {
|
|
return this.trainSchedulingService.getBookableSchedules(
|
|
query.originYardId,
|
|
query.destinationYardId,
|
|
);
|
|
}
|
|
|
|
@Get("available-days")
|
|
// No staff guard: customers hit this while creating a booking to find which
|
|
// DAYS have a departure on their route. Day-level pooling — no capacity is
|
|
// returned, only the list of bookable days.
|
|
@ApiOperation({
|
|
summary: "Distinct days with an OPEN same-route departure (day-level pool)",
|
|
})
|
|
getAvailableDays(@Query() query: AvailableDaysQueryDto) {
|
|
return this.trainSchedulingService.getAvailableDays(
|
|
query.originYardId,
|
|
query.destinationYardId,
|
|
);
|
|
}
|
|
|
|
@Get("available-days-for-cargo")
|
|
// No staff guard: customers (portal) and GL (backoffice) both hit this while
|
|
// creating a booking to find which DAYS are feasible for THIS cargo — i.e. the
|
|
// route has a train with remaining capacity AND enough matching-type wagons.
|
|
@ApiOperation({
|
|
summary: "Days bookable for a specific cargo (wagon + train capacity aware)",
|
|
})
|
|
getAvailableDaysForCargo(@Query() query: AvailableDaysForCargoQueryDto) {
|
|
return this.trainSchedulingService.getAvailableDaysForCargo({
|
|
originYardId: query.originYardId,
|
|
destinationYardId: query.destinationYardId,
|
|
freightType: query.freightType,
|
|
cargoTypeId: query.cargoTypeId,
|
|
cargoTypeCode: query.cargoTypeCode,
|
|
totalWeightTons: query.totalWeightTons,
|
|
containers: query.containers,
|
|
containerTypeIds: query.containerTypeIds,
|
|
});
|
|
}
|
|
|
|
@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")
|
|
@TrainSchedulingCreate()
|
|
@ApiOperation({ summary: "Create a container train schedule" })
|
|
createContainerTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) {
|
|
return this.trainSchedulingService.createContainerTrainSchedule(dto);
|
|
}
|
|
|
|
@Post("bulk/schedules")
|
|
@TrainSchedulingCreate()
|
|
@ApiOperation({ summary: "Create a bulk train schedule" })
|
|
createBulkTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) {
|
|
return this.trainSchedulingService.createContainerTrainSchedule(dto);
|
|
}
|
|
|
|
@Post("schedules/:id/assign-bookings")
|
|
@TrainSchedulingUpdate()
|
|
@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")
|
|
@TrainSchedulingUpdate()
|
|
@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")
|
|
@TrainSchedulingUpdate()
|
|
@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")
|
|
@TrainSchedulingUpdate()
|
|
@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")
|
|
@TrainSchedulingUpdate()
|
|
@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")
|
|
@TrainSchedulingUpdate()
|
|
@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);
|
|
}
|
|
|
|
@Post("schedules/:id/wagons/:wagonId/move-load")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary:
|
|
"Move a wagon's whole load to another wagon (empty → move/repin, loaded → swap loads)",
|
|
})
|
|
moveWagonLoad(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Param("wagonId", ParseUUIDPipe) wagonId: string,
|
|
@Body() dto: MoveWagonLoadDto,
|
|
) {
|
|
return this.trainSchedulingService.moveWagonLoad(id, wagonId, 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")
|
|
@TrainSchedulingUpdate()
|
|
@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,
|
|
);
|
|
}
|
|
|
|
@Post("schedules/:id/switch-government-booking")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary:
|
|
"Switch out commercial bookings to allocate a government booking in their place",
|
|
})
|
|
switchGovernmentBooking(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: SwitchGovernmentBookingDto,
|
|
@CurrentUser() user: AuthUserPayload,
|
|
) {
|
|
return this.trainSchedulingService.switchGovernmentBooking(
|
|
id,
|
|
dto.governmentBookingId,
|
|
dto.removeBookingIds,
|
|
resolveAuthUserId(user),
|
|
);
|
|
}
|
|
|
|
@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);
|
|
}
|
|
|
|
@Get("schedules/:id/import-loading-bookings")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({
|
|
summary: "List import bookings eligible for loading confirmation on this schedule",
|
|
})
|
|
getImportLoadingBookings(@Param("id", ParseUUIDPipe) id: string) {
|
|
return this.trainSchedulingService.getImportLoadingBookings(id);
|
|
}
|
|
|
|
@Patch("schedules/:id/import-loading-status")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary:
|
|
"Mark import bookings loaded/unloaded on this schedule (tracking only, does not affect dispatch)",
|
|
})
|
|
updateImportLoadingStatus(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: UpdateImportLoadingStatusDto,
|
|
) {
|
|
return this.trainSchedulingService.updateImportLoadingStatus(id, dto);
|
|
}
|
|
|
|
@Patch("schedules/:id/loading-status")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary:
|
|
"Mark bookings loaded/unloaded on this schedule (any direction, pre-dispatch only)",
|
|
})
|
|
setBookingLoadingStatus(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: UpdateImportLoadingStatusDto,
|
|
) {
|
|
return this.trainSchedulingService.setBookingLoadingStatus(id, dto);
|
|
}
|
|
|
|
@Post("schedules/:id/pin-wagons")
|
|
@TrainSchedulingUpdate()
|
|
@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")
|
|
@BookingStaff(FREIGHT_PERMS.trainScheduling.dispatch)
|
|
@ApiOperation({ summary: "Finalize a draft train schedule" })
|
|
finalizeSchedule(@Param("id", ParseUUIDPipe) id: string) {
|
|
return this.trainSchedulingService.finalizeSchedule(id);
|
|
}
|
|
|
|
@Post("schedules/:id/dispatch")
|
|
@BookingStaff(FREIGHT_PERMS.trainScheduling.dispatch)
|
|
@ApiOperation({ summary: "Dispatch a scheduled train" })
|
|
dispatchSchedule(@Param("id", ParseUUIDPipe) id: string) {
|
|
return this.trainSchedulingService.dispatchSchedule(id);
|
|
}
|
|
|
|
@Get("intercity/bookings")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({
|
|
summary:
|
|
"Every intercity booking with its ride-along state, both yards' facility status, and where its train is",
|
|
})
|
|
listIntercityBookings() {
|
|
return this.intercityService.listBookings();
|
|
}
|
|
|
|
@Get("schedules/:id/intercity-candidates")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({
|
|
summary:
|
|
"Waiting intercity bookings this train could carry (corridor on route) + remaining wagon/weight/length capacity",
|
|
})
|
|
getIntercityCandidates(@Param("id", ParseUUIDPipe) id: string) {
|
|
return this.intercityService.listCandidates(id);
|
|
}
|
|
|
|
@Post("schedules/:id/intercity/accept")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary:
|
|
"Accept intercity bookings onto this train (opens their pay window; capacity re-checked per booking)",
|
|
})
|
|
acceptIntercityBookings(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: AcceptIntercityBookingsDto,
|
|
) {
|
|
return this.intercityService.acceptBookings(id, dto.bookingIds);
|
|
}
|
|
|
|
@Get("schedules/:id/yard-work")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({
|
|
summary:
|
|
"Per-yard operator worklist: which bookings board/alight at each stop, with journey state",
|
|
})
|
|
getYardWork(@Param("id", ParseUUIDPipe) id: string) {
|
|
return this.bookingJourneyService.listYardWork(id);
|
|
}
|
|
|
|
@Post("schedules/:id/bookings/:bookingId/load")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary:
|
|
"Confirm a booking's cargo loaded at its origin yard (any direction; train must be at that yard)",
|
|
})
|
|
loadScheduleBooking(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Param("bookingId", ParseUUIDPipe) bookingId: string,
|
|
) {
|
|
return this.bookingJourneyService.loadBooking(id, bookingId);
|
|
}
|
|
|
|
@Post("schedules/:id/bookings/:bookingId/unload")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary:
|
|
"Confirm a booking's cargo unloaded at its destination yard — per-booking arrival, may precede the train's final arrival",
|
|
})
|
|
unloadScheduleBooking(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Param("bookingId", ParseUUIDPipe) bookingId: string,
|
|
) {
|
|
return this.bookingJourneyService.unloadBooking(id, bookingId);
|
|
}
|
|
|
|
@Post("schedules/:id/intercity/:bookingId/load")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary: "Confirm intercity cargo loaded (train must be at the booking's origin yard)",
|
|
})
|
|
loadIntercityBooking(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Param("bookingId", ParseUUIDPipe) bookingId: string,
|
|
) {
|
|
return this.intercityService.loadBooking(id, bookingId);
|
|
}
|
|
|
|
@Post("schedules/:id/intercity/:bookingId/unload")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary:
|
|
"Confirm intercity cargo unloaded at the booking's destination yard (completes the booking)",
|
|
})
|
|
unloadIntercityBooking(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Param("bookingId", ParseUUIDPipe) bookingId: string,
|
|
) {
|
|
return this.intercityService.unloadBooking(id, bookingId);
|
|
}
|
|
|
|
@Get("schedules/:id/import-djibouti")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({ summary: "Batch 7 import Djibouti gatepass/loading status" })
|
|
getImportDjiboutiOperation(@Param("id", ParseUUIDPipe) id: string) {
|
|
return this.trainSchedulingService.getImportDjiboutiOperation(id);
|
|
}
|
|
|
|
@Post("schedules/:id/import-djibouti/documents")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({ summary: "Upload/check an import Djibouti-side document" })
|
|
uploadImportDjiboutiDocument(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: UploadImportDjiboutiDocumentDto,
|
|
) {
|
|
return this.trainSchedulingService.uploadImportDjiboutiDocument(id, dto);
|
|
}
|
|
|
|
@Post("schedules/:id/import-djibouti/gatepass-granted")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({ summary: "Mark import Djibouti gatepass permission granted" })
|
|
grantImportDjiboutiGatepass(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: ImportDjiboutiActionDto,
|
|
) {
|
|
return this.trainSchedulingService.grantImportDjiboutiGatepass(id, dto);
|
|
}
|
|
|
|
@Post("schedules/:id/import-djibouti/ready-for-loading")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({ summary: "Mark import train ready for loading at Djibouti" })
|
|
markImportReadyForLoading(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: ImportDjiboutiActionDto,
|
|
) {
|
|
return this.trainSchedulingService.markImportReadyForLoading(id, dto);
|
|
}
|
|
|
|
@Post("schedules/:id/import-djibouti/loaded-on-train")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({ summary: "Confirm import cargo loaded on train at Djibouti" })
|
|
confirmImportLoadedOnTrain(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: ImportDjiboutiActionDto,
|
|
) {
|
|
return this.trainSchedulingService.confirmImportLoadedOnTrain(id, dto);
|
|
}
|
|
|
|
@Post("schedules/:id/confirm-loading")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary:
|
|
"Confirm cargo loaded on the train (any direction; unblocks import-Djibouti dispatch)",
|
|
})
|
|
confirmScheduleLoading(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: ImportDjiboutiActionDto,
|
|
) {
|
|
return this.trainSchedulingService.confirmScheduleLoading(id, dto);
|
|
}
|
|
|
|
@Post("schedules/:id/import-djibouti/depart")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({ summary: "Depart loaded import train from Djibouti" })
|
|
departImportFromDjibouti(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: ImportDjiboutiActionDto,
|
|
) {
|
|
return this.trainSchedulingService.departImportFromDjibouti(id, dto);
|
|
}
|
|
|
|
@Post("schedules/:id/import-djibouti/load-list")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({ summary: "Generate import load list / marshalling document summary" })
|
|
generateImportLoadList(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: ImportDjiboutiActionDto,
|
|
) {
|
|
return this.trainSchedulingService.generateImportLoadList(id, dto);
|
|
}
|
|
|
|
@Get("schedules/:id/import-djibouti/load-list/document")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({ summary: "Download printable import load list / marshalling PDF" })
|
|
async importLoadListDocument(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Res() res: Response,
|
|
) {
|
|
const { filename, buffer } = await this.trainSchedulingService.importLoadListDocument(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("schedules/:id/export/load-list/document")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({ summary: "Download printable export marshalling / load list PDF" })
|
|
async exportLoadListDocument(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Res() res: Response,
|
|
) {
|
|
const { filename, buffer } = await this.trainSchedulingService.exportLoadListDocument(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("schedules/:id/intercity/marshalling/document")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({ summary: "Download current on-board intercity marshalling (Marshalling 2) PDF" })
|
|
async intercityMarshallingDocument(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Res() res: Response,
|
|
) {
|
|
const { filename, buffer } = await this.trainSchedulingService.intercityMarshallingDocument(id);
|
|
res.setHeader("Content-Type", "application/pdf");
|
|
res.setHeader("Content-Disposition", `inline; filename="${filename}"`);
|
|
res.setHeader("Content-Length", buffer.length);
|
|
return res.send(buffer);
|
|
}
|
|
|
|
// ---- batch / booking-window staff actions ----
|
|
|
|
@Post("schedules/:id/run-batch")
|
|
@TrainSchedulingUpdate()
|
|
@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")
|
|
@TrainSchedulingUpdate()
|
|
@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")
|
|
@TrainSchedulingUpdate()
|
|
@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);
|
|
}
|
|
|
|
@Patch("schedules/:id/window-rule")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary:
|
|
"Override the booking-window rule for one schedule (open/close hour, duration, doc-review, payment, lead days) — only before the window opens",
|
|
})
|
|
async updateScheduleWindowRule(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: UpdateScheduleWindowRuleDto,
|
|
) {
|
|
await this.trainSchedulingService.updateScheduleWindowRule(id, dto);
|
|
return this.trainSchedulingService.getContainerTrainScheduleById(id);
|
|
}
|
|
|
|
@Patch("schedules/:id/schedule-date")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary:
|
|
"Reschedule a train's departure date — only before the booking window opens, and only if the new date still leaves room for the booking lead window",
|
|
})
|
|
async updateScheduleDate(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: UpdateScheduleDateDto,
|
|
) {
|
|
await this.trainSchedulingService.updateScheduleDate(id, dto);
|
|
return this.trainSchedulingService.getContainerTrainScheduleById(id);
|
|
}
|
|
|
|
@Post("schedules/:id/maintenance")
|
|
@TrainSchedulingReschedule()
|
|
@ApiOperation({
|
|
summary:
|
|
"Maintenance reschedule: move the train to a new departure with every allocated booking aboard — links, wagons and window settings unchanged",
|
|
})
|
|
async maintenanceReschedule(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: MaintenanceRescheduleDto,
|
|
) {
|
|
await this.trainSchedulingService.maintenanceReschedule(id, dto);
|
|
return this.trainSchedulingService.getContainerTrainScheduleById(id);
|
|
}
|
|
|
|
@Get("doc-review-alert")
|
|
// Dedicated permission, not scheduling or bookings:view — the alarm is meant
|
|
// for the position types that actually decide operation requests.
|
|
@BookingDocReviewAlert()
|
|
@ApiOperation({
|
|
summary:
|
|
"Nearest document-review deadline that still has un-accepted booking requests behind it (null when there is none) — drives the backoffice header countdown",
|
|
})
|
|
async getDocReviewAlert() {
|
|
return this.bookingWindowService.getDocReviewAlert();
|
|
}
|
|
|
|
@Post("schedules/:id/doc-review-complete")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary:
|
|
"Staff finished document review early — run the batch/payment phase now (applies to the whole route-day group)",
|
|
})
|
|
async completeDocReview(@Param("id", ParseUUIDPipe) id: string) {
|
|
await this.bookingWindowService.completeDocReview(id);
|
|
return this.bookingBatchService.getBatchBoardDetail(id);
|
|
}
|
|
|
|
@Post("bookings/:bookingId/mark-paid")
|
|
@BookingStaff(FREIGHT_PERMS.trainScheduling.markPaid)
|
|
@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")
|
|
@BookingStaff(FREIGHT_PERMS.trainScheduling.expireBooking)
|
|
@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")
|
|
@TrainSchedulingUpdate()
|
|
@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("bookings/:bookingId/allocation-candidates")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({
|
|
summary:
|
|
"Trains a paid-unallocated booking fits, split same-day vs other days",
|
|
})
|
|
getAllocationCandidates(
|
|
@Param("bookingId", ParseUUIDPipe) bookingId: string,
|
|
) {
|
|
return this.bookingBatchService.allocationCandidates(bookingId);
|
|
}
|
|
|
|
@Post("bookings/:bookingId/allocate")
|
|
@TrainSchedulingUpdate()
|
|
@ApiOperation({
|
|
summary:
|
|
"Staff: place a paid booking onto a fitting train (notifies customer on date change)",
|
|
})
|
|
async allocatePaidBooking(
|
|
@Param("bookingId", ParseUUIDPipe) bookingId: string,
|
|
@Body("trainScheduleId", ParseUUIDPipe) trainScheduleId: string,
|
|
) {
|
|
await this.bookingBatchService.allocatePaid(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")
|
|
@TrainSchedulingUpdate()
|
|
@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")
|
|
@TrainSchedulingUpdate()
|
|
@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 (paginated)" })
|
|
async getContainerTrainSchedules(
|
|
@Query() query: ListTrainSchedulesQueryDto,
|
|
@CurrentUser() user: AuthUserPayload,
|
|
) {
|
|
const allowed =
|
|
await this.userTradeAccessService.resolveAllowedDirections(user);
|
|
return this.trainSchedulingService.getContainerTrainSchedules(
|
|
query,
|
|
allowed ?? undefined,
|
|
);
|
|
}
|
|
|
|
@Get("bulk/schedules")
|
|
@TrainSchedulingView()
|
|
@ApiOperation({ summary: "List bulk train schedules (paginated)" })
|
|
async getBulkTrainSchedules(
|
|
@Query() query: ListTrainSchedulesQueryDto,
|
|
@CurrentUser() user: AuthUserPayload,
|
|
) {
|
|
const allowed =
|
|
await this.userTradeAccessService.resolveAllowedDirections(user);
|
|
return this.trainSchedulingService.getContainerTrainSchedules(
|
|
query,
|
|
allowed ?? undefined,
|
|
);
|
|
}
|
|
|
|
@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")
|
|
@TrainSchedulingCancel()
|
|
@ApiOperation({ summary: "Cancel container train schedule" })
|
|
cancelTrainSchedule(@Param("id", ParseUUIDPipe) id: string) {
|
|
return this.trainSchedulingService.cancelTrainSchedule(id);
|
|
}
|
|
|
|
@Post('bulk/schedules/:id/cancel')
|
|
@TrainSchedulingCancel()
|
|
@ApiOperation({ summary: "Cancel bulk train schedule" })
|
|
cancelBulkTrainSchedule(@Param("id", ParseUUIDPipe) id: string) {
|
|
return this.trainSchedulingService.cancelTrainSchedule(id);
|
|
}
|
|
}
|