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; } const pathsFor = (freightType?: FreightType) => freightType === "BULK" ? URL_CONSTANTS.TRAIN_SCHEDULING.BULK : URL_CONSTANTS.TRAIN_SCHEDULING.CONTAINER; export const trainSchedulingService = { getEligibleBookings: async ( filters?: TrainScheduleFilters, freightType?: FreightType, ): Promise => { 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( useUnified ? URL_CONSTANTS.TRAIN_SCHEDULING.ELIGIBLE_BOOKINGS : pathsFor(freightType).ELIGIBLE_BOOKINGS, { params: { ...filters } }, ); return unwrap(response.data); }, preview: async ( payload: TrainSchedulePreviewPayload, freightType?: FreightType, ): Promise => { const useUnified = !freightType || freightType === "MIXED"; const response = await client.post( useUnified ? URL_CONSTANTS.TRAIN_SCHEDULING.PREVIEW : pathsFor(freightType).PREVIEW, payload, ); return unwrap(response.data); }, createSchedule: async ( payload: CreateTrainSchedulePayload, freightType?: FreightType, ): Promise => { const response = await client.post( pathsFor(freightType).SCHEDULES, payload, ); return unwrap(response.data); }, listSchedules: async ( freightType: FreightType = "CONTAINER", filters: TrainScheduleListFilters = {}, ): Promise => { const params: Record = {}; 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( pathsFor(freightType === "MIXED" ? undefined : freightType).SCHEDULES, { params }, ); return unwrap(response.data); }, getBatchBoard: async ( filters: BatchBoardFilters = {}, ): Promise => { const params: Record = {}; 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( URL_CONSTANTS.TRAIN_SCHEDULING.BATCH_BOARD, { params }, ); return unwrap(response.data); }, getBatchBoardDetail: async ( scheduleId: string, ): Promise => { const response = await client.get( 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 => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.CONTRACT_BOOKING_WINDOWS(contractId), ); return unwrap(response.data); }, getBookableSchedules: async ( originYardId?: string, destinationYardId?: string, ): Promise => { const response = await client.get( 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 => { 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 => { const response = await client.get( `/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 => { 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 => { const response = await client.post( 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 => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.DOC_REVIEW_COMPLETE(scheduleId), {}, ); return unwrap(response.data); }, runAllocation: async ( scheduleId: string, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.RUN_ALLOCATION(scheduleId), {}, ); return unwrap(response.data); }, setBookingWindow: async ( scheduleId: string, status: "OPEN" | "CLOSED", ): Promise => { const response = await client.patch( URL_CONSTANTS.TRAIN_SCHEDULING.BOOKING_WINDOW(scheduleId), { status }, ); return unwrap(response.data); }, updateScheduleWindowRule: async ( scheduleId: string, payload: UpdateScheduleWindowRulePayload, ): Promise => { const response = await client.patch( URL_CONSTANTS.TRAIN_SCHEDULING.WINDOW_RULE(scheduleId), payload, ); return unwrap(response.data); }, updateScheduleDate: async ( scheduleId: string, scheduleDate: string, ): Promise => { const response = await client.patch( 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 => { const response = await client.get( 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 => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.MERGE_TRAIN(scheduleId), payload, ); return unwrap(response.data); }, markBookingPaid: async (bookingId: string): Promise => { await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.MARK_BOOKING_PAID(bookingId), {}, ); }, expireBooking: async (bookingId: string): Promise => { await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.EXPIRE_BOOKING(bookingId), {}, ); }, moveBookingSchedule: async ( bookingId: string, trainScheduleId: string, ): Promise => { await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.MOVE_BOOKING_SCHEDULE(bookingId), { trainScheduleId, }, ); }, getAllocationCandidates: async ( bookingId: string, ): Promise => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.ALLOCATION_CANDIDATES(bookingId), ); return unwrap(response.data); }, allocatePaidBooking: async ( bookingId: string, trainScheduleId: string, ): Promise => { await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.ALLOCATE_BOOKING(bookingId), { trainScheduleId }, ); }, getScheduleById: async ( id: string, freightType?: FreightType, ): Promise => { const response = await client.get( pathsFor( freightType === "MIXED" ? undefined : freightType, ).SCHEDULE_BY_ID(id), ); return unwrap(response.data); }, assignUnassignedBooking: async ( scheduleId: string, bookingId: string, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.ASSIGN_UNASSIGNED_BOOKING(scheduleId), { bookingId }, ); return unwrap(response.data); }, assignBookings: async ( scheduleId: string, payload: AssignBookingsPayload, freightType?: FreightType, ): Promise => { const useUnified = !freightType || freightType === "MIXED"; const response = await client.post( 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 => { const response = await client.delete( URL_CONSTANTS.TRAIN_SCHEDULING.UNASSIGN_BOOKING(scheduleId, bookingId), ); return unwrap(response.data); }, switchGovernmentBooking: async ( scheduleId: string, governmentBookingId: string, removeBookingIds: string[], ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.SWITCH_GOVERNMENT_BOOKING(scheduleId), { governmentBookingId, removeBookingIds }, ); return unwrap(response.data); }, pinWagons: async ( scheduleId: string, payload: PinWagonsPayload, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.PIN_WAGONS(scheduleId), payload, ); return unwrap(response.data); }, finalizeSchedule: async ( scheduleId: string, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.FINALIZE(scheduleId), {}, ); return unwrap(response.data); }, getYardWork: async (scheduleId: string): Promise => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.YARD_WORK(scheduleId), ); return unwrap(response.data); }, loadScheduleBooking: async ( scheduleId: string, bookingId: string, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.BOOKING_LOAD(scheduleId, bookingId), {}, ); return unwrap(response.data); }, unloadScheduleBooking: async ( scheduleId: string, bookingId: string, ): Promise => { const response = await client.post( 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 => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.INTERCITY_CANDIDATES(scheduleId), ); return unwrap(response.data); }, acceptIntercityBookings: async ( scheduleId: string, bookingIds: string[], ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.INTERCITY_ACCEPT(scheduleId), { bookingIds }, ); return unwrap(response.data); }, loadIntercityBooking: async ( scheduleId: string, bookingId: string, ): Promise => { await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.INTERCITY_LOAD(scheduleId, bookingId), {}, ); }, unloadIntercityBooking: async ( scheduleId: string, bookingId: string, ): Promise => { await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.INTERCITY_UNLOAD(scheduleId, bookingId), {}, ); }, dispatchSchedule: async ( scheduleId: string, payload: DispatchSchedulePayload = {}, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.DISPATCH(scheduleId), payload, ); return unwrap(response.data); }, getImportLoadingBookings: async ( scheduleId: string, ): Promise => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_LOADING_BOOKINGS(scheduleId), ); return unwrap(response.data); }, updateImportLoadingStatus: async ( scheduleId: string, payload: { bookingIds: string[]; loadingStatus: LoadingStatus }, ): Promise => { const response = await client.patch( URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_LOADING_STATUS(scheduleId), payload, ); return unwrap(response.data); }, setLoadingStatus: async ( scheduleId: string, payload: { bookingIds: string[]; loadingStatus: LoadingStatus }, ): Promise => { const response = await client.patch( URL_CONSTANTS.TRAIN_SCHEDULING.LOADING_STATUS(scheduleId), payload, ); return unwrap(response.data); }, confirmLoading: async ( scheduleId: string, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.CONFIRM_LOADING(scheduleId), {}, ); return unwrap(response.data); }, getImportDjiboutiOperation: async ( scheduleId: string, ): Promise => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI(scheduleId), ); return unwrap(response.data); }, uploadImportDjiboutiDocument: async ( scheduleId: string, payload: UploadImportDjiboutiDocumentPayload, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI_DOCUMENTS(scheduleId), payload, ); return unwrap(response.data); }, grantImportDjiboutiGatepass: async ( scheduleId: string, payload: ImportDjiboutiActionPayload = {}, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI_GATEPASS_GRANTED(scheduleId), payload, ); return unwrap(response.data); }, markImportDjiboutiReadyForLoading: async ( scheduleId: string, payload: ImportDjiboutiActionPayload = {}, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI_READY_FOR_LOADING(scheduleId), payload, ); return unwrap(response.data); }, confirmImportDjiboutiLoadedOnTrain: async ( scheduleId: string, payload: ImportDjiboutiActionPayload = {}, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI_LOADED_ON_TRAIN(scheduleId), payload, ); return unwrap(response.data); }, departImportFromDjibouti: async ( scheduleId: string, payload: ImportDjiboutiActionPayload = {}, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI_DEPART(scheduleId), payload, ); return unwrap(response.data); }, generateImportDjiboutiLoadList: async ( scheduleId: string, payload: ImportDjiboutiActionPayload = {}, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.IMPORT_DJIBOUTI_LOAD_LIST(scheduleId), payload, ); return unwrap(response.data); }, downloadImportDjiboutiLoadListDocument: async ( scheduleId: string, ): Promise => { 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 => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.EXPORT_LOAD_LIST_DOCUMENT(scheduleId), { responseType: "blob" }, ); return response.data; }, downloadIntercityMarshallingDocument: async ( scheduleId: string, ): Promise => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.INTERCITY_MARSHALLING_DOCUMENT(scheduleId), { responseType: "blob" }, ); return response.data; }, getTrack: async (scheduleId: string): Promise => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.CHECKPOINTS(scheduleId), ); return unwrap(response.data); }, recordCheckpoint: async ( scheduleId: string, payload: RecordCheckpointPayload, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.CHECKPOINTS(scheduleId), payload, ); return unwrap(response.data); }, updateCheckpoint: async ( scheduleId: string, sequenceNo: number, payload: UpdateCheckpointPayload, ): Promise => { const response = await client.patch( URL_CONSTANTS.TRAIN_SCHEDULING.CHECKPOINT(scheduleId, sequenceNo), payload, ); return unwrap(response.data); }, arriveSchedule: async (scheduleId: string): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.ARRIVE(scheduleId), {}, ); return unwrap(response.data); }, cancelSchedule: async ( id: string, freightType: FreightType = "CONTAINER", ): Promise => { const response = await client.post( pathsFor( freightType === "MIXED" ? undefined : freightType, ).CANCEL_SCHEDULE(id), {}, ); return unwrap(response.data); }, getAvailableLocomotives: async ( routeId?: string, ): Promise => { if (routeId) { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.AVAILABLE_LOCOMOTIVES, { params: { routeId } }, ); return unwrap(response.data); } const response = await client.get( 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 => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.GLOBAL_RULES, ); return unwrap(response.data); }, getAllBookingWindows: async (): Promise => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.BOOKING_WINDOWS, ); return unwrap(response.data); }, getDocReviewAlert: async (): Promise => { const response = await client.get( 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>, ): Promise => { const response = await client.patch( URL_CONSTANTS.TRAIN_SCHEDULING.GLOBAL_RULES, payload, ); return unwrap(response.data); }, getStations: async (): Promise => { const response = await client.get( 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 => { const response = await client.delete( 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 => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.MOVE_WAGON_LOAD(scheduleId, wagonId), payload, ); return unwrap(response.data); }, getUnassignedBookings: async ( scheduleId: string, ): Promise => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.UNASSIGNED_BOOKINGS(scheduleId), ); return unwrap(response.data); }, getCompositionRemovals: async ( scheduleId: string, ): Promise => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.COMPOSITION_REMOVALS(scheduleId), ); return unwrap(response.data); }, };