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 { BatchBoardSchedule, BatchBoardScheduleDetail, BookableSchedule, AssignBookingsPayload, CompositionRemovalEntry, UnassignedBookingsResponse, CreateTrainSchedulePayload, EligibleContainerBookingsResponse, FreightType, ImportDjiboutiActionPayload, ImportDjiboutiLoadList, ImportDjiboutiOperation, ImportLoadingBookingsResponse, LoadingStatus, LocomotiveRecord, PinWagonsPayload, RecordCheckpointPayload, TrainScheduleDetail, TrainScheduleFilters, TrainScheduleListItem, TrainSchedulePreviewPayload, TrainSchedulePreviewResponse, TrainSchedulingGlobalRules, TrainTrackResponse, UploadImportDjiboutiDocumentPayload, WagonAllocationAttemptResult, YardOption, } 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", ): Promise => { const response = await client.get( pathsFor(freightType === "MIXED" ? undefined : freightType).SCHEDULES, ); return unwrap(response.data); }, getBatchBoard: async (): Promise => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.BATCH_BOARD, ); 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); }, 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). 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); }, 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, }, ); }, 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); }, 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); }, dispatchSchedule: async ( scheduleId: string, ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.DISPATCH(scheduleId), {}, ); 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); }, 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; }, 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); }, 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); }, 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); }, 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); }, };