booking operations and trains scheduling also allocations

This commit is contained in:
marshal
2026-06-10 00:48:32 +03:00
parent 675975bc08
commit 5774d7db9d
180 changed files with 13423 additions and 3877 deletions

View File

@@ -9,6 +9,8 @@ export interface BookingListFilter {
status?: string;
/** Comma-separated statuses for grouped tabs */
statuses?: string;
schedulingStatuses?: string;
assignedToSchedule?: "true" | "false";
/** Tab key for React Query cache (not sent to API) */
tab?: string;
// customerId?: string;
@@ -121,6 +123,8 @@ export const bookingsService = {
if (filter.pageSize != null) params.pageSize = filter.pageSize;
if (filter.sortBy) params.sortBy = filter.sortBy;
if (filter.sortOrder) params.sortOrder = filter.sortOrder;
if (filter.schedulingStatuses) params.schedulingStatuses = filter.schedulingStatuses;
if (filter.assignedToSchedule) params.assignedToSchedule = filter.assignedToSchedule;
if (filter.companyId) params.companyId = filter.companyId;
if (filter.freightType) params.freightType = filter.freightType;
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
@@ -210,6 +214,23 @@ export const bookingsService = {
cancel: (id: string, reason: string) =>
postBooking<BookingDetail>(B.CANCEL(id), { reason }),
create: async (payload: Record<string, unknown>): Promise<BookingDetail> => {
const response = await client.post<{ booking: BookingDetail } | BookingDetail>(
B.BASE,
payload,
);
const data = unwrap(response.data) as { booking?: BookingDetail };
return (data.booking ?? data) as BookingDetail;
},
getReferenceData: async () => {
const response = await client.get(B.REFERENCE_DATA);
return unwrap(response.data);
},
governmentExpedite: (id: string) =>
postBooking<BookingDetail>(B.GOVERNMENT_EXPEDITE(id)),
};
async function ensurePdfBlob(blob: Blob): Promise<Blob> {

View File

@@ -0,0 +1,51 @@
import { cargoService, type Cargo } from "@/services/cargoService";
import { containerService, type Container } from "@/services/containerService";
import { locomotivesService, type Locomotive } from "@/services/locomotives.service";
import { trainService, type Train } from "@/services/trains.service";
import { wagonService, type Wagon } from "@/services/wagon.service";
import type { FleetResourceSlug } from "@/pages/fleet/config/resources";
export type FleetRecord = Locomotive | Train | Wagon | Container | Cargo;
const listHandlers: Record<FleetResourceSlug, () => Promise<FleetRecord[]>> = {
locomotives: () => locomotivesService.getAll().then((r) => r.data),
trains: () => trainService.getAll().then((r) => r.data),
wagons: () => wagonService.getAll().then((r) => r.data),
containers: () => containerService.getAll().then((r) => r.data),
cargoes: () => cargoService.getAll().then((r) => r.data),
};
const createHandlers: Record<FleetResourceSlug, (data: Record<string, unknown>) => Promise<unknown>> = {
locomotives: (data) => locomotivesService.create(data),
trains: (data) => trainService.create(data),
wagons: (data) => wagonService.create(data),
containers: (data) => containerService.create(data),
cargoes: (data) => cargoService.create(data),
};
const updateHandlers: Record<
FleetResourceSlug,
(id: string, data: Record<string, unknown>) => Promise<unknown>
> = {
locomotives: (id, data) => locomotivesService.update(id, data),
trains: (id, data) => trainService.update(id, data),
wagons: (id, data) => wagonService.update(id, data),
containers: (id, data) => containerService.update(id, data),
cargoes: (id, data) => cargoService.update(id, data),
};
const removeHandlers: Record<FleetResourceSlug, (id: string) => Promise<unknown>> = {
locomotives: (id) => locomotivesService.decommission(id),
trains: (id) => trainService.delete(id),
wagons: (id) => wagonService.delete(id),
containers: (id) => containerService.delete(id),
cargoes: (id) => cargoService.delete(id),
};
export const fleetService = {
list: (slug: FleetResourceSlug) => listHandlers[slug](),
create: (slug: FleetResourceSlug, data: Record<string, unknown>) => createHandlers[slug](data),
update: (slug: FleetResourceSlug, id: string, data: Record<string, unknown>) =>
updateHandlers[slug](id, data),
remove: (slug: FleetResourceSlug, id: string) => removeHandlers[slug](id),
};

View File

@@ -2,14 +2,18 @@ import { api as client } from '../auth/http';
import { unwrap } from '@/utils/endpoint';
import { URL_CONSTANTS } from '@/constants/URLS';
import type {
AssignBookingsPayload,
CreateTrainSchedulePayload,
EligibleContainerBookingsResponse,
FreightType,
LocomotiveRecord,
PinWagonsPayload,
TrainScheduleDetail,
TrainScheduleFilters,
TrainScheduleListItem,
TrainSchedulePreviewPayload,
TrainSchedulePreviewResponse,
TrainSchedulingGlobalRules,
YardOption,
} from '@/types/trainScheduling';
@@ -17,22 +21,33 @@ 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";
const response = await client.get<EligibleContainerBookingsResponse>(
URL_CONSTANTS.TRAIN_SCHEDULING.ELIGIBLE_BOOKINGS,
{ params: filters },
useUnified
? URL_CONSTANTS.TRAIN_SCHEDULING.ELIGIBLE_BOOKINGS
: pathsFor(freightType).ELIGIBLE_BOOKINGS,
{ params: { ...filters, ...(freightType && freightType !== "MIXED" ? { freightType } : {}) } },
);
return unwrap(response.data);
},
preview: async (
payload: TrainSchedulePreviewPayload,
freightType?: FreightType,
): Promise<TrainSchedulePreviewResponse> => {
const useUnified = !freightType || freightType === "MIXED";
const response = await client.post<TrainSchedulePreviewResponse>(
URL_CONSTANTS.TRAIN_SCHEDULING.PREVIEW,
useUnified ? URL_CONSTANTS.TRAIN_SCHEDULING.PREVIEW : pathsFor(freightType).PREVIEW,
payload,
);
return unwrap(response.data);
@@ -40,31 +55,92 @@ export const trainSchedulingService = {
createSchedule: async (
payload: CreateTrainSchedulePayload,
freightType?: FreightType,
): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.SCHEDULES,
pathsFor(freightType).SCHEDULES,
payload,
);
return unwrap(response.data);
},
listSchedules: async (): Promise<TrainScheduleListItem[]> => {
listSchedules: async (
freightType: FreightType = "CONTAINER",
): Promise<TrainScheduleListItem[]> => {
const response = await client.get<TrainScheduleListItem[]>(
URL_CONSTANTS.TRAIN_SCHEDULING.SCHEDULES,
pathsFor(freightType === "MIXED" ? undefined : freightType).SCHEDULES,
);
return unwrap(response.data);
},
getScheduleById: async (id: string): Promise<TrainScheduleDetail> => {
getScheduleById: async (
id: string,
freightType?: FreightType,
): Promise<TrainScheduleDetail> => {
const response = await client.get<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.SCHEDULE_BY_ID(id),
pathsFor(freightType === "MIXED" ? undefined : freightType).SCHEDULE_BY_ID(id),
);
return unwrap(response.data);
},
cancelSchedule: async (id: string): Promise<TrainScheduleDetail> => {
assignBookings: async (
scheduleId: string,
payload: AssignBookingsPayload,
freightType?: FreightType,
): Promise<TrainScheduleDetail> => {
const useUnified = !freightType || freightType === "MIXED";
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.CANCEL_SCHEDULE(id),
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);
},
dispatchSchedule: async (scheduleId: string): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.DISPATCH(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);
@@ -77,6 +153,81 @@ export const trainSchedulingService = {
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);
},
updateGlobalRules: async (
payload: Partial<
Pick<
TrainSchedulingGlobalRules,
| "maxTrainLengthMeters"
| "maxTrainWeightTons"
| "maxWagonsPerTrain"
| "max20ftContainerWeightTons"
| "max20ftPairWeightDiffTons"
>
>,
): 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,

View File

@@ -1,3 +1,5 @@
import type { Freight } from "@edr/types";
import { api as apiClient } from "../auth/http";
export interface Wagon {
@@ -8,7 +10,8 @@ export interface Wagon {
sequenceNumber: number | null;
tareWeight: number;
maxPayloadWeight: number;
status: string;
status: Freight.WagonStatus;
readiness: Freight.WagonReadiness;
notes?: string;
}