automation of loading and unloading

This commit is contained in:
Hagernesh
2026-06-17 22:33:06 +00:00
1637 changed files with 375027 additions and 20437 deletions

View File

@@ -35,6 +35,8 @@ import {
type RejectStepPayload,
} from "./bookings.service";
import type { BookingDetail } from "@/types/booking";
import type { IOverviewDashboard, OverviewRange } from "@/types/overview";
import { overviewService } from "./overview.service";
export const api = {
fileUploadSettings: {
@@ -233,6 +235,20 @@ export const api = {
() => ruleEngineService.getApprovalChain(),
() => QUERY_KEYS.RULE_ENGINE.chain,
),
reorder: endpoint<
{ resource: RuleEngineResourceSlug; payload: { ids: string[]; requiresDirectorApproval?: boolean } },
void
>("rule-engine", "reorder", ({ resource, payload }) =>
ruleEngineService.reorder(resource, payload),
),
moveOrder: endpoint<
{ resource: RuleEngineResourceSlug; id: string; direction: "up" | "down" },
void
>("rule-engine", "moveOrder", ({ resource, id, direction }) =>
ruleEngineService.moveOrder(resource, id, direction),
),
},
bookings: {
@@ -329,4 +345,12 @@ export const api = {
({ id, reason }) => bookingsService.cancel(id, reason),
),
},
overview: {
get: endpoint<{ range?: OverviewRange }, IOverviewDashboard>(
"overview",
"get",
({ range }) => overviewService.getDashboard(range),
),
},
};

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;
@@ -78,6 +80,26 @@ export interface ContractView {
signedAt: string;
signatureImageUrl?: string | null;
}>;
/** Current viewer's reusable saved signature, if they have one. */
savedSignature?: {
signerDisplayName: string;
signatureImageUrl?: string | null;
} | null;
}
export interface ConsolidationWagonSlot {
containerTypeCode: string;
quantity: number;
containersPerWagon: number;
remainder: number;
slotsNeeded: number;
}
export interface ConsolidationDetails {
statusMessage: string;
wagonSlots: ConsolidationWagonSlot[];
partner: { id: string; reference: string } | null;
splitBilling: { bookingShare: number; partnerShare: number } | null;
}
export interface SignContractPayload {
@@ -121,6 +143,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;
@@ -189,6 +213,13 @@ export const bookingsService = {
return unwrap(response.data);
},
getConsolidationDetails: async (
id: string,
): Promise<ConsolidationDetails> => {
const response = await client.get<ConsolidationDetails>(B.CONSOLIDATION(id));
return unwrap(response.data) as ConsolidationDetails;
},
customerSign: (id: string, payload: SignContractPayload) =>
postBooking<BookingDetail>(B.CUSTOMER_SIGN(id), {
...payload,
@@ -210,6 +241,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,26 @@
import { api as client } from "../auth/http";
import { URL_CONSTANTS } from "@/constants/URLS";
const F = URL_CONSTANTS.FILES;
export const filesService = {
/** Stream a stored file by id (backend route: GET /files/:id). */
download: async (id: string): Promise<Blob> => {
const response = await client.get(F.BY_ID(id), { responseType: "blob" });
return response.data as Blob;
},
};
/** Download a file blob and trigger a browser save with the given name. */
export async function downloadBookingFile(
id: string,
filename: string,
): Promise<void> {
const blob = await filesService.download(id);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}

View File

@@ -0,0 +1,60 @@
import { cargoService, type Cargo } from "@/services/cargoService";
import { containerService, type Container } from "@/services/containerService";
import {
locomotivesService,
type Locomotive,
type LocomotiveListFilters,
} from "@/services/locomotives.service";
import { trainService, type Train } from "@/services/trains.service";
import { wagonService, type Wagon, type WagonListFilters } from "@/services/wagon.service";
import type { FleetResourceSlug } from "@/pages/fleet/config/resources";
export type FleetRecord = Locomotive | Train | Wagon | Container | Cargo;
export type FleetListFilters = WagonListFilters & LocomotiveListFilters;
const listHandlers: Record<
FleetResourceSlug,
(filters?: FleetListFilters) => Promise<FleetRecord[]>
> = {
locomotives: (filters) => locomotivesService.getAll(filters ?? {}).then((r) => r.data),
trains: () => trainService.getAll().then((r) => r.data),
wagons: (filters) => wagonService.getAll(filters ?? {}).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, filters?: FleetListFilters) => listHandlers[slug](filters),
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

@@ -12,12 +12,19 @@ export type LocomotiveStatus =
| 'ASSIGNED'
| 'OUT_OF_SERVICE';
export interface LocomotiveListFilters {
status?: LocomotiveStatus;
currentYardId?: string;
}
export interface Locomotive {
id: string;
code: string;
name?: string | null;
locomotiveType: LocomotiveType;
status: LocomotiveStatus;
currentYardId: string | null;
currentYard?: { id: string; label?: string; code?: string } | null;
maxPullWeightTons: number;
maxTrainLengthMeters: number;
powerKw?: number | null;
@@ -33,7 +40,15 @@ export type SaveLocomotivePayload = Omit<
>;
export const locomotivesService = {
getAll: () => apiClient.get<Locomotive[]>(URL_CONSTANTS.LOCOMOTIVES.BASE),
getAll: (filters: LocomotiveListFilters = {}) => {
const params = new URLSearchParams();
if (filters.status) params.set('status', filters.status);
if (filters.currentYardId) params.set('currentYardId', filters.currentYardId);
const qs = params.toString();
return apiClient.get<Locomotive[]>(
`${URL_CONSTANTS.LOCOMOTIVES.BASE}${qs ? `?${qs}` : ''}`,
);
},
getById: (id: string) => apiClient.get<Locomotive>(URL_CONSTANTS.LOCOMOTIVES.BY_ID(id)),
create: (data: Partial<SaveLocomotivePayload>) =>
apiClient.post(URL_CONSTANTS.LOCOMOTIVES.BASE, data),

View File

@@ -0,0 +1,56 @@
import { api as client } from "../auth/http";
import { unwrap } from "@/utils/endpoint";
import { URL_CONSTANTS } from "@/constants/URLS";
import type {
IOverviewBillingTab,
IOverviewBookingsTab,
IOverviewCustomersTab,
IOverviewDashboard,
IOverviewOperationsTab,
IOverviewStaffTab,
OverviewRange,
} from "@/types/overview";
const O = URL_CONSTANTS.OVERVIEW;
export const overviewService = {
getDashboard: async (range?: OverviewRange): Promise<IOverviewDashboard> => {
const response = await client.get<IOverviewDashboard>(O.BASE, {
params: range ? { range } : undefined,
});
return unwrap(response);
},
getBookingsTab: async (range?: OverviewRange): Promise<IOverviewBookingsTab> => {
const response = await client.get<IOverviewBookingsTab>(O.BOOKINGS, {
params: range ? { range } : undefined,
});
return unwrap(response);
},
getBillingTab: async (range?: OverviewRange): Promise<IOverviewBillingTab> => {
const response = await client.get<IOverviewBillingTab>(O.BILLING, {
params: range ? { range } : undefined,
});
return unwrap(response);
},
getOperationsTab: async (): Promise<IOverviewOperationsTab> => {
const response = await client.get<IOverviewOperationsTab>(O.OPERATIONS);
return unwrap(response);
},
getCustomersTab: async (range?: OverviewRange): Promise<IOverviewCustomersTab> => {
const response = await client.get<IOverviewCustomersTab>(O.CUSTOMERS, {
params: range ? { range } : undefined,
});
return unwrap(response);
},
getStaffTab: async (range?: OverviewRange): Promise<IOverviewStaffTab> => {
const response = await client.get<IOverviewStaffTab>(O.STAFF, {
params: range ? { range } : undefined,
});
return unwrap(response);
},
};

View File

@@ -0,0 +1,84 @@
import { api as client } from "../auth/http";
import { unwrap } from "@/utils/endpoint";
import { URL_CONSTANTS } from "@/constants/URLS";
const P = URL_CONSTANTS.PAYMENTS;
export type PaymentStatus =
| "action-required"
| "processing"
| "success"
| "failed"
| "canceled"
| "refunded";
export type PaymentMethod =
| "telebirr"
| "cbe-birr"
| "ebirr"
| "waafi"
| "card"
| "dmoney"
| "cac-bank";
export interface PaymentRow {
id: string;
bookingId: string;
amount: number;
currency: string;
method: PaymentMethod;
status: PaymentStatus;
merchantOrderId: string | null;
paidAt: string | null;
createdAt: string;
}
export interface PaymentListFilter {
search?: string;
status?: string;
method?: string;
page?: number;
pageSize?: number;
}
export interface PaginatedPayments {
items: PaymentRow[];
total: number;
page: number;
pageSize: number;
}
export interface PaymentSummary {
total: number;
success: number;
processing: number;
failed: number;
refunded: number;
paidAmount: number;
}
export const paymentsService = {
list: async (filter?: PaymentListFilter): Promise<PaginatedPayments> => {
const params: Record<string, string | number | undefined> = {};
if (filter) {
if (filter.search) params.search = filter.search;
if (filter.status) params.status = filter.status;
if (filter.method) params.method = filter.method;
if (filter.page != null) params.page = filter.page;
if (filter.pageSize != null) params.pageSize = filter.pageSize;
}
const response = await client.get<PaginatedPayments>(P.ALL, { params });
const data = unwrap(response.data) as PaginatedPayments;
return {
items: data.items ?? [],
total: data.total ?? 0,
page: data.page ?? 1,
pageSize: data.pageSize ?? 10,
};
},
getSummary: async (): Promise<PaymentSummary> => {
const response = await client.get<PaymentSummary>(P.SUMMARY);
return unwrap(response.data) as PaymentSummary;
},
};

View File

@@ -14,13 +14,21 @@ export interface RuleEngineListParams {
pageSize?: number;
isActive?: boolean;
status?: string;
sortBy?: string;
sortOrder?: "ASC" | "DESC";
requiresDirectorApproval?: boolean;
}
export interface RuleEngineReorderPayload {
ids: string[];
requiresDirectorApproval?: boolean;
}
const RESOURCE_BASE: Record<RuleEngineResourceSlug, string> = {
"cargo-types": URL_CONSTANTS.RULE_ENGINE.CARGO_TYPES,
"container-types": URL_CONSTANTS.RULE_ENGINE.CONTAINER_TYPES,
"wagon-types": URL_CONSTANTS.RULE_ENGINE.WAGON_TYPES,
"priority-rules": URL_CONSTANTS.RULE_ENGINE.PRIORITY_RULES,
"priority-configs": URL_CONSTANTS.RULE_ENGINE.PRIORITY_CONFIGS,
"service-types": URL_CONSTANTS.RULE_ENGINE.SERVICE_TYPES,
"surcharge-types": URL_CONSTANTS.RULE_ENGINE.SURCHARGE_TYPES,
"weight-limit-rules": URL_CONSTANTS.RULE_ENGINE.WEIGHT_LIMIT_RULES,
@@ -38,8 +46,8 @@ const byIdPath = (resource: RuleEngineResourceSlug, id: string): string => {
return URL_CONSTANTS.RULE_ENGINE.CONTAINER_TYPE_BY_ID(id);
case "wagon-types":
return URL_CONSTANTS.RULE_ENGINE.WAGON_TYPE_BY_ID(id);
case "priority-rules":
return URL_CONSTANTS.RULE_ENGINE.PRIORITY_RULE_BY_ID(id);
case "priority-configs":
return URL_CONSTANTS.RULE_ENGINE.PRIORITY_CONFIG_BY_ID(id);
case "service-types":
return URL_CONSTANTS.RULE_ENGINE.SERVICE_TYPE_BY_ID(id);
case "surcharge-types":
@@ -59,25 +67,39 @@ const byIdPath = (resource: RuleEngineResourceSlug, id: string): string => {
}
};
const defaultMeta = (dataLength: number, page = 1, pageSize = 20): RuleEngineListMeta => ({
const defaultMeta = (dataLength: number, page = 1, pageSize = 10): RuleEngineListMeta => ({
total: dataLength,
page,
pageSize,
totalPages: Math.max(1, Math.ceil(dataLength / pageSize)),
});
const isPaginatedListResult = <T extends RuleEngineRecord>(
value: unknown,
): value is RuleEngineListResult<T> =>
Boolean(value) &&
typeof value === "object" &&
"data" in value &&
Array.isArray((value as RuleEngineListResult<T>).data);
const normalizeList = <T extends RuleEngineRecord>(
payload: unknown,
page = 1,
pageSize = 20,
pageSize = 10,
): RuleEngineListResult<T> => {
if (isPaginatedListResult<T>(payload)) {
return {
data: payload.data,
meta: payload.meta ?? defaultMeta(payload.data.length, page, pageSize),
};
}
const body = unwrap(payload as { data: unknown }) as unknown;
if (body && typeof body === "object" && "data" in body && Array.isArray((body as RuleEngineListResult<T>).data)) {
const typed = body as RuleEngineListResult<T>;
if (isPaginatedListResult<T>(body)) {
return {
data: typed.data,
meta: typed.meta ?? defaultMeta(typed.data.length, page, pageSize),
data: body.data,
meta: body.meta ?? defaultMeta(body.data.length, page, pageSize),
};
}
@@ -98,7 +120,7 @@ export const ruleEngineService = {
params?: RuleEngineListParams,
): Promise<RuleEngineListResult<T>> => {
const page = params?.page ?? 1;
const pageSize = params?.pageSize ?? 20;
const pageSize = params?.pageSize ?? 10;
const response = await client.get(RESOURCE_BASE[resource], {
params: {
page,
@@ -106,6 +128,9 @@ export const ruleEngineService = {
search: params?.search,
isActive: params?.isActive,
status: params?.status,
sortBy: params?.sortBy,
sortOrder: params?.sortOrder,
requiresDirectorApproval: params?.requiresDirectorApproval,
},
});
return normalizeList<T>(response.data, page, pageSize);
@@ -140,6 +165,21 @@ export const ruleEngineService = {
await client.delete(byIdPath(resource, id));
},
reorder: async (
resource: RuleEngineResourceSlug,
payload: RuleEngineReorderPayload,
): Promise<void> => {
await client.post(`${RESOURCE_BASE[resource]}/reorder`, payload);
},
moveOrder: async (
resource: RuleEngineResourceSlug,
id: string,
direction: "up" | "down",
): Promise<void> => {
await client.post(`${byIdPath(resource, id)}/move-order`, { direction });
},
submitRate: async <T extends RuleEngineRecord>(id: string): Promise<T> => {
const response = await client.post(URL_CONSTANTS.RULE_ENGINE.RATE_SUBMIT(id));
return normalizeEntity<T>(response.data);

View File

@@ -0,0 +1,32 @@
import { api as client } from "../auth/http";
import { unwrap } from "@/utils/endpoint";
const SIGNATURE_URL = "/me/signature";
export interface SavedSignature {
signerDisplayName: string;
signatureImageUrl?: string | null;
}
export interface SaveSignaturePayload {
signerDisplayName: string;
signatureImageBase64: string;
}
export const signaturesService = {
/** Returns the current user's reusable signature, or null if none saved. */
getMySignature: async (): Promise<SavedSignature | null> => {
const response = await client.get<SavedSignature | null>(SIGNATURE_URL);
return (unwrap(response.data) as SavedSignature | null) ?? null;
},
saveMySignature: async (
payload: SaveSignaturePayload,
): Promise<SavedSignature | null> => {
const response = await client.put<SavedSignature | null>(
SIGNATURE_URL,
payload,
);
return (unwrap(response.data) as SavedSignature | null) ?? null;
},
};

View File

@@ -2,14 +2,27 @@ 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,
CompositionUnassignedBooking,
UnassignedBookingsResponse,
CreateTrainSchedulePayload,
EligibleContainerBookingsResponse,
FreightType,
LocomotiveRecord,
PinWagonsPayload,
RecordCheckpointPayload,
TrainScheduleDetail,
TrainScheduleFilters,
TrainScheduleListItem,
TrainSchedulePreviewPayload,
TrainSchedulePreviewResponse,
TrainSchedulingGlobalRules,
TrainTrackResponse,
WagonAllocationAttemptResult,
YardOption,
} from '@/types/trainScheduling';
@@ -17,22 +30,35 @@ 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>(
URL_CONSTANTS.TRAIN_SCHEDULING.ELIGIBLE_BOOKINGS,
{ params: filters },
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>(
URL_CONSTANTS.TRAIN_SCHEDULING.PREVIEW,
useUnified ? URL_CONSTANTS.TRAIN_SCHEDULING.PREVIEW : pathsFor(freightType).PREVIEW,
payload,
);
return unwrap(response.data);
@@ -40,51 +66,292 @@ 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> => {
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);
},
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);
},
runBatch: async (scheduleId: string): Promise<BatchBoardScheduleDetail> => {
const response = await client.post<BatchBoardScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.RUN_BATCH(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);
},
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>(
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> => {
assignUnassignedBooking: async (
scheduleId: string,
bookingId: string,
): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.CANCEL_SCHEDULE(id),
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);
},
publishSchedule: async (id: string): Promise<TrainScheduleDetail> => {
dispatchSchedule: async (scheduleId: string): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.PUBLISH_SCHEDULE(id),
URL_CONSTANTS.TRAIN_SCHEDULING.DISPATCH(scheduleId),
{},
);
return unwrap(response.data);
},
getAvailableLocomotives: async (): Promise<LocomotiveRecord[]> => {
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);
},
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,
@@ -97,4 +364,41 @@ export const trainSchedulingService = {
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);
},
};

View File

@@ -19,7 +19,7 @@ const asList = <T>(payload: ListResponse<T>): T[] =>
export const wagonTypesService = {
async getWagonTypes() {
const response = await api.get<ListResponse<WagonType>>('/wagon-types', {
params: { isActive: 'all' },
params: { isActive: 'all', pageSize: 500 },
});
return asList(response.data);
},

View File

@@ -1,3 +1,5 @@
import type { Freight } from "@edr/types";
import { api as apiClient } from "../auth/http";
export interface Wagon {
@@ -21,12 +23,31 @@ export interface Wagon {
} | null;
tareWeight: number;
maxPayloadWeight: number;
status: string;
status: Freight.WagonStatus;
currentYardId: string | null;
currentYard?: { id: string; label?: string; code?: string } | null;
notes?: string;
}
export interface WagonListFilters {
search?: string;
status?: Freight.WagonStatus;
currentYardId?: string;
wagonTypeId?: string;
trainId?: string;
}
export const wagonService = {
getAll: () => apiClient.get<Wagon[]>('/wagons'),
getAll: (filters: WagonListFilters = {}) => {
const params = new URLSearchParams();
if (filters.search?.trim()) params.set('search', filters.search.trim());
if (filters.status) params.set('status', filters.status);
if (filters.currentYardId) params.set('currentYardId', filters.currentYardId);
if (filters.wagonTypeId) params.set('wagonTypeId', filters.wagonTypeId);
if (filters.trainId) params.set('trainId', filters.trainId);
const qs = params.toString();
return apiClient.get<Wagon[]>(`/wagons${qs ? `?${qs}` : ''}`);
},
getById: (id: string) => apiClient.get<Wagon>(`/wagons/${id}`),
getByTrain: (trainId: string) => apiClient.get<Wagon[]>(`/wagons?trainId=${trainId}`),
assignToTrain: (wagonId: string, trainId: string, sequenceNumber?: number) =>