Files
edr-platform/apps/edr-freight-web/backoffice/src/services/trainScheduling.service.ts

706 lines
21 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 {
BatchBoardSchedule,
BatchBoardScheduleDetail,
BookableSchedule,
BookingWindow,
AssignBookingsPayload,
CompositionRemovalEntry,
UnassignedBookingsResponse,
CreateTrainSchedulePayload,
EligibleContainerBookingsResponse,
FreightType,
ImportDjiboutiActionPayload,
ImportDjiboutiLoadList,
ImportDjiboutiOperation,
ImportLoadingBookingsResponse,
IntercityAcceptResult,
IntercityCandidatesResult,
LoadingStatus,
LocomotiveRecord,
PinWagonsPayload,
RecordCheckpointPayload,
StaffBookingWindow,
TrainScheduleDetail,
UpdateScheduleWindowRulePayload,
TrainScheduleFilters,
TrainScheduleListItem,
TrainSchedulePreviewPayload,
TrainSchedulePreviewResponse,
TrainSchedulingGlobalRules,
TrainTrackResponse,
UploadImportDjiboutiDocumentPayload,
WagonAllocationAttemptResult,
YardOption,
} from "@/types/trainScheduling";
interface BookingReferenceDataResponse {
yard?: Array<YardOption & { label?: string }>;
}
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<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",
): Promise<TrainScheduleListItem[]> => {
const response = await client.get<TrainScheduleListItem[]>(
pathsFor(freightType === "MIXED" ? undefined : freightType).SCHEDULES,
);
return unwrap(response.data);
},
getBatchBoard: async (): Promise<BatchBoardSchedule[]> => {
const response = await client.get<BatchBoardSchedule[]>(
URL_CONSTANTS.TRAIN_SCHEDULING.BATCH_BOARD,
);
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).
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);
},
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,
},
);
},
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);
},
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);
},
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,
): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.DISPATCH(scheduleId),
{},
);
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;
},
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);
},
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);
},
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);
},
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);
},
};