Files
edr-platform/apps/edr-freight-web/backoffice/src/services/trainScheduling.service.ts
Marshal e2189040fa feat: add pagination to schedule history and consolidation approvals
- Implemented pagination in ScheduleHistoryPanel to manage large history entries.
- Updated API to support pagination parameters for schedule history.
- Enhanced ConsolidationApprovalsPage with tabbed navigation and pagination for approval rows.
- Introduced new types for paginated responses in bookings and train scheduling services.
- Added a database migration to create an index on wagon_booking_allocations for performance improvements.
2026-08-23 04:49:58 +00:00

922 lines
28 KiB
TypeScript

import type { Freight } from "@edr/types";
import { api as client } from "../auth/http";
import { unwrap } from "@/utils/endpoint";
import { URL_CONSTANTS } from "@/constants/URLS";
import type {
AllocationCandidates,
BatchBoardFilters,
BatchBoardListResponse,
BatchBoardScheduleDetail,
BookableSchedule,
BookingWindow,
AssignBookingsPayload,
BookingLoadResult,
BookingUnloadResult,
CompositionRemovalEntry,
DocReviewAlert,
UnassignedBookingsResponse,
CreateTrainSchedulePayload,
EligibleContainerBookingsResponse,
FreightType,
ImportDjiboutiActionPayload,
ImportDjiboutiLoadList,
ImportDjiboutiOperation,
ImportLoadingBookingsResponse,
IntercityAcceptResult,
IntercityCandidatesResult,
LoadingStatus,
LocomotiveRecord,
PinWagonsPayload,
RecordCheckpointPayload,
UpdateCheckpointPayload,
DispatchSchedulePayload,
StaffBookingWindow,
ScheduleMergePreview,
TrainScheduleDetail,
UpdateScheduleWindowRulePayload,
TrainScheduleFilters,
TrainScheduleListFilters,
TrainScheduleListResponse,
TrainSchedulePreviewPayload,
TrainSchedulePreviewResponse,
TrainSchedulingGlobalRules,
TrainTrackResponse,
UploadImportDjiboutiDocumentPayload,
WagonAllocationAttemptResult,
YardOption,
YardWorkResult,
} from "@/types/trainScheduling";
interface BookingReferenceDataResponse {
yard?: Array<YardOption & { label?: string }>;
}
/** Lightweight polling snapshot — refetch the full detail only when this changes. */
export interface SchedulePhaseSnapshot {
status: string;
bookingWindowStatus: string | null;
windowPhase: string | null;
windowOpensAt: string | null;
windowClosesAt: string | null;
docReviewEndsAt: string | null;
paymentPhaseEndsAt: string | null;
updatedAt: string;
}
const pathsFor = (freightType?: FreightType) =>
freightType === "BULK"
? URL_CONSTANTS.TRAIN_SCHEDULING.BULK
: URL_CONSTANTS.TRAIN_SCHEDULING.CONTAINER;
export const trainSchedulingService = {
getSchedulePhase: async (id: string): Promise<SchedulePhaseSnapshot> => {
const response = await client.get<SchedulePhaseSnapshot>(
`/train-scheduling/schedules/${id}/phase`,
);
return unwrap(response.data);
},
getEligibleBookings: async (
filters?: TrainScheduleFilters,
freightType?: FreightType,
): Promise<EligibleContainerBookingsResponse> => {
const useUnified = !freightType || freightType === "MIXED";
// The container/bulk endpoints already encode freight type in the path, and their
// query DTOs reject an extra `freightType` param — so only pass the station filters.
const response = await client.get<EligibleContainerBookingsResponse>(
useUnified
? URL_CONSTANTS.TRAIN_SCHEDULING.ELIGIBLE_BOOKINGS
: pathsFor(freightType).ELIGIBLE_BOOKINGS,
{ params: { ...filters } },
);
return unwrap(response.data);
},
preview: async (
payload: TrainSchedulePreviewPayload,
freightType?: FreightType,
): Promise<TrainSchedulePreviewResponse> => {
const useUnified = !freightType || freightType === "MIXED";
const response = await client.post<TrainSchedulePreviewResponse>(
useUnified
? URL_CONSTANTS.TRAIN_SCHEDULING.PREVIEW
: pathsFor(freightType).PREVIEW,
payload,
);
return unwrap(response.data);
},
createSchedule: async (
payload: CreateTrainSchedulePayload,
freightType?: FreightType,
): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
pathsFor(freightType).SCHEDULES,
payload,
);
return unwrap(response.data);
},
listSchedules: async (
freightType: FreightType = "CONTAINER",
filters: TrainScheduleListFilters = {},
): Promise<TrainScheduleListResponse> => {
const params: Record<string, string | number> = {};
if (filters.page) params.page = filters.page;
if (filters.pageSize) params.pageSize = filters.pageSize;
if (filters.search?.trim()) params.search = filters.search.trim();
if (filters.status) params.status = filters.status;
if (filters.freightType) params.freightType = filters.freightType;
if (filters.originStationId) params.originStationId = filters.originStationId;
if (filters.destinationStationId)
params.destinationStationId = filters.destinationStationId;
if (filters.sortBy) params.sortBy = filters.sortBy;
if (filters.sortOrder) params.sortOrder = filters.sortOrder;
const response = await client.get<TrainScheduleListResponse>(
pathsFor(freightType === "MIXED" ? undefined : freightType).SCHEDULES,
{ params },
);
return unwrap(response.data);
},
getBatchBoard: async (
filters: BatchBoardFilters = {},
): Promise<BatchBoardListResponse> => {
const params: Record<string, string | number> = {};
if (filters.page) params.page = filters.page;
if (filters.pageSize) params.pageSize = filters.pageSize;
if (filters.statuses?.length) params.statuses = filters.statuses.join(",");
if (filters.bookingWindowStatus)
params.bookingWindowStatus = filters.bookingWindowStatus;
if (filters.search?.trim()) params.search = filters.search.trim();
if (filters.departureFrom) params.departureFrom = filters.departureFrom;
if (filters.departureTo) params.departureTo = filters.departureTo;
if (filters.createdFrom) params.createdFrom = filters.createdFrom;
if (filters.createdTo) params.createdTo = filters.createdTo;
if (filters.sortBy) params.sortBy = filters.sortBy;
if (filters.sortOrder) params.sortOrder = filters.sortOrder;
const response = await client.get<BatchBoardListResponse>(
URL_CONSTANTS.TRAIN_SCHEDULING.BATCH_BOARD,
{ params },
);
return unwrap(response.data);
},
getBatchBoardDetail: async (
scheduleId: string,
): Promise<BatchBoardScheduleDetail> => {
const response = await client.get<BatchBoardScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.BATCH_BOARD_DETAIL(scheduleId),
);
return unwrap(response.data);
},
/**
* Booking windows for every route/schedule of a contract. A window with
* `isOpenNow === true` means GL may create a booking right now for that route.
*/
getContractBookingWindows: async (
contractId: string,
): Promise<BookingWindow[]> => {
const response = await client.get<BookingWindow[]>(
URL_CONSTANTS.TRAIN_SCHEDULING.CONTRACT_BOOKING_WINDOWS(contractId),
);
return unwrap(response.data);
},
getBookableSchedules: async (
originYardId?: string,
destinationYardId?: string,
): Promise<BookableSchedule[]> => {
const response = await client.get<BookableSchedule[]>(
URL_CONSTANTS.TRAIN_SCHEDULING.BOOKABLE_SCHEDULES,
{ params: { originYardId, destinationYardId } },
);
return unwrap(response.data);
},
/**
* Day-level pool: the days that have an OPEN departure on the route. Staff pick
* a day; the batch engine assigns the train. No capacity is returned.
*/
getAvailableDays: async (
originYardId?: string,
destinationYardId?: string,
): Promise<string[]> => {
const response = await client.get<{ days: string[] }>(
URL_CONSTANTS.TRAIN_SCHEDULING.AVAILABLE_DAYS,
{ params: { originYardId, destinationYardId } },
);
return unwrap(response.data).days;
},
// Cargo-aware day pool (matching wagons + open train capacity). `containers`
// is serialized as a JSON string param (the server parses it).
// Export train picker for a booking's shipment day; cargo params cover bare
// instances whose cargo only exists on the form so far.
getExportTrains: async (
bookingId: string,
date: string,
cargo?: { containerSizes?: string[]; cargoTypeCode?: string; wagons?: number },
): Promise<Freight.ExportTrainOption[]> => {
const response = await client.get<Freight.ExportTrainOption[]>(
`/bookings/${bookingId}/export-trains`,
{
params: {
date,
...(cargo?.containerSizes?.length
? { containerSizes: cargo.containerSizes.join(",") }
: {}),
...(cargo?.cargoTypeCode ? { cargoTypeCode: cargo.cargoTypeCode } : {}),
...(cargo?.wagons ? { wagons: cargo.wagons } : {}),
},
},
);
return unwrap(response.data);
},
getAvailableDaysForCargo: async (
query: Freight.AvailableDaysForCargoQuery,
): Promise<string[]> => {
const { containers, ...rest } = query;
const response = await client.get<{ days: string[] }>(
URL_CONSTANTS.TRAIN_SCHEDULING.AVAILABLE_DAYS_FOR_CARGO,
{
params: {
...rest,
...(containers ? { containers: JSON.stringify(containers) } : {}),
},
},
);
return unwrap(response.data).days;
},
runBatch: async (scheduleId: string): Promise<BatchBoardScheduleDetail> => {
const response = await client.post<BatchBoardScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.RUN_BATCH(scheduleId),
{},
);
return unwrap(response.data);
},
/**
* Staff finished reviewing documents early — runs the batch immediately
* for the schedule's whole route-day group.
*/
completeDocReview: async (
scheduleId: string,
): Promise<BatchBoardScheduleDetail> => {
const response = await client.post<BatchBoardScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.DOC_REVIEW_COMPLETE(scheduleId),
{},
);
return unwrap(response.data);
},
runAllocation: async (
scheduleId: string,
): Promise<WagonAllocationAttemptResult> => {
const response = await client.post<WagonAllocationAttemptResult>(
URL_CONSTANTS.TRAIN_SCHEDULING.RUN_ALLOCATION(scheduleId),
{},
);
return unwrap(response.data);
},
setBookingWindow: async (
scheduleId: string,
status: "OPEN" | "CLOSED",
): Promise<TrainScheduleDetail> => {
const response = await client.patch<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.BOOKING_WINDOW(scheduleId),
{ status },
);
return unwrap(response.data);
},
updateScheduleWindowRule: async (
scheduleId: string,
payload: UpdateScheduleWindowRulePayload,
): Promise<TrainScheduleDetail> => {
const response = await client.patch<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.WINDOW_RULE(scheduleId),
payload,
);
return unwrap(response.data);
},
updateScheduleDate: async (
scheduleId: string,
scheduleDate: string,
): Promise<TrainScheduleDetail> => {
const response = await client.patch<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.SCHEDULE_DATE(scheduleId),
{ scheduleDate },
);
return unwrap(response.data);
},
/** What a merge would do — drives the confirmation modal. Read-only. */
previewScheduleMerge: async (
scheduleId: string,
targetTrainId: string,
): Promise<ScheduleMergePreview> => {
const response = await client.get<ScheduleMergePreview>(
URL_CONSTANTS.TRAIN_SCHEDULING.MERGE_PREVIEW(scheduleId, targetTrainId),
);
return unwrap(response.data);
},
/** Merge another train into this schedule. This schedule always survives. */
mergeScheduleTrain: async (
scheduleId: string,
payload: { targetTrainId: string; reason?: string },
): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.MERGE_TRAIN(scheduleId),
payload,
);
return unwrap(response.data);
},
markBookingPaid: async (bookingId: string): Promise<void> => {
await client.post(
URL_CONSTANTS.TRAIN_SCHEDULING.MARK_BOOKING_PAID(bookingId),
{},
);
},
expireBooking: async (bookingId: string): Promise<void> => {
await client.post(
URL_CONSTANTS.TRAIN_SCHEDULING.EXPIRE_BOOKING(bookingId),
{},
);
},
moveBookingSchedule: async (
bookingId: string,
trainScheduleId: string,
): Promise<void> => {
await client.post(
URL_CONSTANTS.TRAIN_SCHEDULING.MOVE_BOOKING_SCHEDULE(bookingId),
{
trainScheduleId,
},
);
},
getAllocationCandidates: async (
bookingId: string,
): Promise<AllocationCandidates> => {
const response = await client.get<AllocationCandidates>(
URL_CONSTANTS.TRAIN_SCHEDULING.ALLOCATION_CANDIDATES(bookingId),
);
return unwrap(response.data);
},
allocatePaidBooking: async (
bookingId: string,
trainScheduleId: string,
): Promise<void> => {
await client.post(
URL_CONSTANTS.TRAIN_SCHEDULING.ALLOCATE_BOOKING(bookingId),
{ trainScheduleId },
);
},
getScheduleById: async (
id: string,
freightType?: FreightType,
): Promise<TrainScheduleDetail> => {
const response = await client.get<TrainScheduleDetail>(
pathsFor(
freightType === "MIXED" ? undefined : freightType,
).SCHEDULE_BY_ID(id),
);
return unwrap(response.data);
},
assignUnassignedBooking: async (
scheduleId: string,
bookingId: string,
): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.ASSIGN_UNASSIGNED_BOOKING(scheduleId),
{ bookingId },
);
return unwrap(response.data);
},
assignBookings: async (
scheduleId: string,
payload: AssignBookingsPayload,
freightType?: FreightType,
): Promise<TrainScheduleDetail> => {
const useUnified = !freightType || freightType === "MIXED";
const response = await client.post<TrainScheduleDetail>(
useUnified
? URL_CONSTANTS.TRAIN_SCHEDULING.ASSIGN_BOOKINGS(scheduleId)
: pathsFor(freightType).ASSIGN_BOOKINGS(scheduleId),
payload,
);
return unwrap(response.data);
},
unassignBooking: async (
scheduleId: string,
bookingId: string,
): Promise<TrainScheduleDetail> => {
const response = await client.delete<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.UNASSIGN_BOOKING(scheduleId, bookingId),
);
return unwrap(response.data);
},
switchGovernmentBooking: async (
scheduleId: string,
governmentBookingId: string,
removeBookingIds: string[],
): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.SWITCH_GOVERNMENT_BOOKING(scheduleId),
{ governmentBookingId, removeBookingIds },
);
return unwrap(response.data);
},
pinWagons: async (
scheduleId: string,
payload: PinWagonsPayload,
): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.PIN_WAGONS(scheduleId),
payload,
);
return unwrap(response.data);
},
finalizeSchedule: async (
scheduleId: string,
): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.FINALIZE(scheduleId),
{},
);
return unwrap(response.data);
},
getYardWork: async (scheduleId: string): Promise<YardWorkResult> => {
const response = await client.get<YardWorkResult>(
URL_CONSTANTS.TRAIN_SCHEDULING.YARD_WORK(scheduleId),
);
return unwrap(response.data);
},
loadScheduleBooking: async (
scheduleId: string,
bookingId: string,
): Promise<BookingLoadResult> => {
const response = await client.post<BookingLoadResult>(
URL_CONSTANTS.TRAIN_SCHEDULING.BOOKING_LOAD(scheduleId, bookingId),
{},
);
return unwrap(response.data);
},
unloadScheduleBooking: async (
scheduleId: string,
bookingId: string,
): Promise<BookingUnloadResult> => {
const response = await client.post<BookingUnloadResult>(
URL_CONSTANTS.TRAIN_SCHEDULING.BOOKING_UNLOAD(scheduleId, bookingId),
{},
);
return unwrap(response.data);
},
listIntercityBookings: async (): Promise<
import("@/types/trainScheduling").IntercityRideAlongRow[]
> => {
const response = await client.get<
import("@/types/trainScheduling").IntercityRideAlongRow[]
>(URL_CONSTANTS.TRAIN_SCHEDULING.INTERCITY_BOOKINGS);
return response.data ?? [];
},
getIntercityCandidates: async (
scheduleId: string,
): Promise<IntercityCandidatesResult> => {
const response = await client.get<IntercityCandidatesResult>(
URL_CONSTANTS.TRAIN_SCHEDULING.INTERCITY_CANDIDATES(scheduleId),
);
return unwrap(response.data);
},
acceptIntercityBookings: async (
scheduleId: string,
bookingIds: string[],
): Promise<IntercityAcceptResult> => {
const response = await client.post<IntercityAcceptResult>(
URL_CONSTANTS.TRAIN_SCHEDULING.INTERCITY_ACCEPT(scheduleId),
{ bookingIds },
);
return unwrap(response.data);
},
loadIntercityBooking: async (
scheduleId: string,
bookingId: string,
): Promise<void> => {
await client.post(
URL_CONSTANTS.TRAIN_SCHEDULING.INTERCITY_LOAD(scheduleId, bookingId),
{},
);
},
unloadIntercityBooking: async (
scheduleId: string,
bookingId: string,
): Promise<void> => {
await client.post(
URL_CONSTANTS.TRAIN_SCHEDULING.INTERCITY_UNLOAD(scheduleId, bookingId),
{},
);
},
dispatchSchedule: async (
scheduleId: string,
payload: DispatchSchedulePayload = {},
): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.DISPATCH(scheduleId),
payload,
);
return unwrap(response.data);
},
getImportLoadingBookings: async (
scheduleId: string,
): Promise<ImportLoadingBookingsResponse> => {
const response = await client.get<ImportLoadingBookingsResponse>(
URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_LOADING_BOOKINGS(scheduleId),
);
return unwrap(response.data);
},
updateImportLoadingStatus: async (
scheduleId: string,
payload: { bookingIds: string[]; loadingStatus: LoadingStatus },
): Promise<ImportLoadingBookingsResponse> => {
const response = await client.patch<ImportLoadingBookingsResponse>(
URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_LOADING_STATUS(scheduleId),
payload,
);
return unwrap(response.data);
},
setLoadingStatus: async (
scheduleId: string,
payload: { bookingIds: string[]; loadingStatus: LoadingStatus },
): Promise<TrainScheduleDetail> => {
const response = await client.patch<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.LOADING_STATUS(scheduleId),
payload,
);
return unwrap(response.data);
},
confirmLoading: async (
scheduleId: string,
): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.CONFIRM_LOADING(scheduleId),
{},
);
return unwrap(response.data);
},
getImportDjiboutiOperation: async (
scheduleId: string,
): Promise<ImportDjiboutiOperation> => {
const response = await client.get<ImportDjiboutiOperation>(
URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI(scheduleId),
);
return unwrap(response.data);
},
uploadImportDjiboutiDocument: async (
scheduleId: string,
payload: UploadImportDjiboutiDocumentPayload,
): Promise<ImportDjiboutiOperation> => {
const response = await client.post<ImportDjiboutiOperation>(
URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI_DOCUMENTS(scheduleId),
payload,
);
return unwrap(response.data);
},
grantImportDjiboutiGatepass: async (
scheduleId: string,
payload: ImportDjiboutiActionPayload = {},
): Promise<ImportDjiboutiOperation> => {
const response = await client.post<ImportDjiboutiOperation>(
URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI_GATEPASS_GRANTED(scheduleId),
payload,
);
return unwrap(response.data);
},
markImportDjiboutiReadyForLoading: async (
scheduleId: string,
payload: ImportDjiboutiActionPayload = {},
): Promise<ImportDjiboutiOperation> => {
const response = await client.post<ImportDjiboutiOperation>(
URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI_READY_FOR_LOADING(scheduleId),
payload,
);
return unwrap(response.data);
},
confirmImportDjiboutiLoadedOnTrain: async (
scheduleId: string,
payload: ImportDjiboutiActionPayload = {},
): Promise<ImportDjiboutiOperation> => {
const response = await client.post<ImportDjiboutiOperation>(
URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI_LOADED_ON_TRAIN(scheduleId),
payload,
);
return unwrap(response.data);
},
departImportFromDjibouti: async (
scheduleId: string,
payload: ImportDjiboutiActionPayload = {},
): Promise<ImportDjiboutiOperation> => {
const response = await client.post<ImportDjiboutiOperation>(
URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI_DEPART(scheduleId),
payload,
);
return unwrap(response.data);
},
generateImportDjiboutiLoadList: async (
scheduleId: string,
payload: ImportDjiboutiActionPayload = {},
): Promise<ImportDjiboutiLoadList> => {
const response = await client.post<ImportDjiboutiLoadList>(
URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI_LOAD_LIST(scheduleId),
payload,
);
return unwrap(response.data);
},
downloadImportDjiboutiLoadListDocument: async (
scheduleId: string,
): Promise<Blob> => {
const response = await client.get(
URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI_LOAD_LIST_DOCUMENT(scheduleId),
{ responseType: "blob" },
);
return response.data;
},
downloadExportLoadListDocument: async (
scheduleId: string,
): Promise<Blob> => {
const response = await client.get(
URL_CONSTANTS.TRAIN_SCHEDULING.EXPORT_LOAD_LIST_DOCUMENT(scheduleId),
{ responseType: "blob" },
);
return response.data;
},
downloadIntercityMarshallingDocument: async (
scheduleId: string,
): Promise<Blob> => {
const response = await client.get(
URL_CONSTANTS.TRAIN_SCHEDULING.INTERCITY_MARSHALLING_DOCUMENT(scheduleId),
{ responseType: "blob" },
);
return response.data;
},
getTrack: async (scheduleId: string): Promise<TrainTrackResponse> => {
const response = await client.get<TrainTrackResponse>(
URL_CONSTANTS.TRAIN_SCHEDULING.CHECKPOINTS(scheduleId),
);
return unwrap(response.data);
},
recordCheckpoint: async (
scheduleId: string,
payload: RecordCheckpointPayload,
): Promise<TrainTrackResponse> => {
const response = await client.post<TrainTrackResponse>(
URL_CONSTANTS.TRAIN_SCHEDULING.CHECKPOINTS(scheduleId),
payload,
);
return unwrap(response.data);
},
updateCheckpoint: async (
scheduleId: string,
sequenceNo: number,
payload: UpdateCheckpointPayload,
): Promise<TrainTrackResponse> => {
const response = await client.patch<TrainTrackResponse>(
URL_CONSTANTS.TRAIN_SCHEDULING.CHECKPOINT(scheduleId, sequenceNo),
payload,
);
return unwrap(response.data);
},
arriveSchedule: async (scheduleId: string): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.ARRIVE(scheduleId),
{},
);
return unwrap(response.data);
},
cancelSchedule: async (
id: string,
freightType: FreightType = "CONTAINER",
): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
pathsFor(
freightType === "MIXED" ? undefined : freightType,
).CANCEL_SCHEDULE(id),
{},
);
return unwrap(response.data);
},
getAvailableLocomotives: async (
routeId?: string,
): Promise<LocomotiveRecord[]> => {
if (routeId) {
const response = await client.get<LocomotiveRecord[]>(
URL_CONSTANTS.TRAIN_SCHEDULING.AVAILABLE_LOCOMOTIVES,
{ params: { routeId } },
);
return unwrap(response.data);
}
const response = await client.get<LocomotiveRecord[]>(
URL_CONSTANTS.LOCOMOTIVES.BASE,
{
params: { status: "AVAILABLE" },
},
);
return unwrap(response.data);
},
previewReschedule: async (
scheduleId: string,
payload: {
incomingBookingIds: string[];
trigger: string;
reason?: string;
newDepartureDate?: string;
},
) => {
const response = await client.post(
URL_CONSTANTS.TRAIN_SCHEDULING.RESCHEDULE_PREVIEW(scheduleId),
payload,
);
return unwrap(response.data);
},
executeReschedule: async (
scheduleId: string,
payload: {
incomingBookingIds: string[];
trigger: string;
reason?: string;
newDepartureDate?: string;
finalBookingIds: string[];
displacedBookingIds: string[];
},
) => {
const response = await client.post(
URL_CONSTANTS.TRAIN_SCHEDULING.RESCHEDULE_EXECUTE(scheduleId),
payload,
);
return unwrap(response.data);
},
maintenanceReschedule: async (
scheduleId: string,
payload: {
incomingBookingIds: string[];
newDepartureDate: string;
reason?: string;
},
) => {
const response = await client.post(
URL_CONSTANTS.TRAIN_SCHEDULING.MAINTENANCE(scheduleId),
{ ...payload, trigger: "TRAIN_MAINTENANCE" },
);
return unwrap(response.data);
},
getGlobalRules: async (): Promise<TrainSchedulingGlobalRules> => {
const response = await client.get<TrainSchedulingGlobalRules>(
URL_CONSTANTS.TRAIN_SCHEDULING.GLOBAL_RULES,
);
return unwrap(response.data);
},
getAllBookingWindows: async (): Promise<StaffBookingWindow[]> => {
const response = await client.get<StaffBookingWindow[]>(
URL_CONSTANTS.TRAIN_SCHEDULING.BOOKING_WINDOWS,
);
return unwrap(response.data);
},
getDocReviewAlert: async (): Promise<DocReviewAlert | null> => {
const response = await client.get<DocReviewAlert | null>(
URL_CONSTANTS.TRAIN_SCHEDULING.DOC_REVIEW_ALERT,
);
// "No alert" comes back as null — which Nest sends as an empty body, so
// coerce anything falsy to null (react-query rejects undefined).
return unwrap(response.data) || null;
},
updateGlobalRules: async (
payload: Partial<Omit<TrainSchedulingGlobalRules, "id">>,
): Promise<TrainSchedulingGlobalRules> => {
const response = await client.patch<TrainSchedulingGlobalRules>(
URL_CONSTANTS.TRAIN_SCHEDULING.GLOBAL_RULES,
payload,
);
return unwrap(response.data);
},
getStations: async (): Promise<YardOption[]> => {
const response = await client.get<BookingReferenceDataResponse>(
URL_CONSTANTS.BOOKINGS.REFERENCE_DATA,
);
const data = unwrap(response.data);
return (data.yard ?? []).map((yard) => ({
id: yard.id,
name: yard.name ?? yard.label ?? yard.code,
code: yard.code,
country: yard.country,
}));
},
removeWagonSlot: async (
scheduleId: string,
wagonId: string,
): Promise<TrainScheduleDetail> => {
const response = await client.delete<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.REMOVE_WAGON_SLOT(scheduleId, wagonId),
);
return unwrap(response.data);
},
updateContainerItem: async (
scheduleId: string,
itemId: string,
payload: { containerNumber: string | null },
): Promise<{ id: string; containerNumber: string | null }> => {
const response = await client.patch<{
id: string;
containerNumber: string | null;
}>(
URL_CONSTANTS.TRAIN_SCHEDULING.UPDATE_CONTAINER_ITEM(scheduleId, itemId),
payload,
);
return unwrap(response.data);
},
moveWagonLoad: async (
scheduleId: string,
wagonId: string,
payload: { targetWagonId: string },
): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.MOVE_WAGON_LOAD(scheduleId, wagonId),
payload,
);
return unwrap(response.data);
},
getUnassignedBookings: async (
scheduleId: string,
): Promise<UnassignedBookingsResponse> => {
const response = await client.get<UnassignedBookingsResponse>(
URL_CONSTANTS.TRAIN_SCHEDULING.UNASSIGNED_BOOKINGS(scheduleId),
);
return unwrap(response.data);
},
getCompositionRemovals: async (
scheduleId: string,
): Promise<CompositionRemovalEntry[]> => {
const response = await client.get<CompositionRemovalEntry[]>(
URL_CONSTANTS.TRAIN_SCHEDULING.COMPOSITION_REMOVALS(scheduleId),
);
return unwrap(response.data);
},
};