Files
edr-platform/apps/edr-freight-web/backoffice/src/services/trainBuilder.service.ts
2026-07-31 10:50:16 +00:00

342 lines
11 KiB
TypeScript

import { api as apiClient } from "../auth/http";
// ---------------------------------------------------------------------------
// Types — mirror the freight API's train-builder responses
// ---------------------------------------------------------------------------
export type BuiltTrainStatus =
| "AVAILABLE"
| "SCHEDULED"
| "IN_SERVICE"
| "UNDER_MAINTENANCE"
| "OUT_OF_SERVICE"
| "DEACTIVATED";
export interface YardRefLite {
id: string;
code: string;
label: string;
}
export type TradeDirection = "IMPORT" | "EXPORT" | "DOMESTIC";
/** The one active (DRAFT/SCHEDULED/DISPATCHED) schedule surfaced per built train. */
export interface ActiveScheduleRef {
id: string;
status: string;
reference: string | null;
direction: TradeDirection | null;
trainNumber: string | null;
}
export interface BuiltTrainSummary {
id: string;
code: string;
trainName: string | null;
status: BuiltTrainStatus;
/** Fixed IMPORT (even) run number typed at build time. */
importTrainNumber: string | null;
/** Fixed EXPORT (odd) run number typed at build time. */
exportTrainNumber: string | null;
activeSchedule: ActiveScheduleRef | null;
createdAt: string;
currentYard: YardRefLite | null;
locomotives: Array<{ id: string; code: string; name: string | null }>;
wagonCount: number;
totalTareTons: number;
totalLengthMeters: number;
maxPullWeightTons: number;
}
export interface TrainCompositionLocomotive {
id: string;
code: string;
name: string | null;
locomotiveType: "DIESEL" | "ELECTRIC";
status: string;
sequenceNo: number;
role: "LEAD" | "ASSIST";
currentYardId: string | null;
currentYard: YardRefLite | null;
maxPullWeightTons: number;
maxTrainLengthMeters: number;
}
export interface TrainCompositionWagon {
id: string;
wagonNumber: string;
sequenceNumber: number | null;
status: string;
wagonType: {
id: string;
code: string;
name: string;
capacityTons: number;
tareWeightTons: number;
lengthMeters: number;
} | null;
}
export interface TrainCompositionTotals {
wagonCount: number;
totalTareTons: number;
/** Informational only — building never checks against full capacity. */
totalCapacityTons: number;
totalLengthMeters: number;
maxPullWeightTons: number;
maxTrainLengthMeters: number;
/** Cargo the locomotives can still haul once pulling the empty consist. */
payloadCapacityTons: number;
/** Share of the haul limit consumed by the empty wagons alone. */
tareUtilizationPct: number | null;
lengthUtilizationPct: number | null;
}
export interface TrainComposition {
id: string;
code: string;
trainName: string | null;
status: BuiltTrainStatus;
importTrainNumber: string | null;
exportTrainNumber: string | null;
notes: string | null;
createdAt: string;
currentYard: YardRefLite | null;
locomotives: TrainCompositionLocomotive[];
wagons: TrainCompositionWagon[];
totals: TrainCompositionTotals;
activeSchedules: ActiveScheduleRef[];
editable: boolean;
}
export interface BuiltTrainListFilters {
page?: number;
pageSize?: number;
search?: string;
status?: BuiltTrainStatus;
currentYardId?: string;
sortBy?: "code" | "trainName" | "status" | "createdAt";
sortOrder?: "ASC" | "DESC";
}
export interface BuiltTrainListResponse {
items: BuiltTrainSummary[];
meta: {
total: number;
page: number;
pageSize: number;
totalPages: number;
};
}
export interface BuildTrainPayload {
/** EXPORT run number — odd, unique across trains (e.g. 8001). */
exportTrainNumber: string;
/** IMPORT run number — even, unique across trains (e.g. 8002). */
importTrainNumber: string;
currentYardId: string;
locomotiveIds: string[];
wagonIds?: string[];
trainName?: string;
notes?: string;
}
/** Run numbers already claimed by existing (non-deleted) trains. */
export interface UsedTrainNumbers {
importTrainNumbers: string[];
exportTrainNumbers: string[];
}
/** Edit a built train's display identity; omitted fields keep their value. */
export interface UpdateTrainDetailsPayload {
/** Empty string clears the name. */
trainName?: string;
importTrainNumber?: string;
exportTrainNumber?: string;
}
/** Built train annotated for the schedule-creation picker. */
export interface AvailableTrain {
id: string;
code: string;
trainName: string | null;
status: BuiltTrainStatus;
importTrainNumber: string | null;
exportTrainNumber: string | null;
currentYardId: string | null;
currentYard: YardRefLite | null;
locomotives: Array<{ id: string; code: string; name: string | null }>;
wagonCount: number;
totalTareTons: number;
totalLengthMeters: number;
maxPullWeightTons: number;
atOriginYard: boolean;
futureScheduleCount: number;
}
// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------
const BASE = "/train-builder";
const toQuery = (filters: BuiltTrainListFilters = {}) => {
const params = new URLSearchParams();
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== "") {
params.set(key, String(value));
}
});
const qs = params.toString();
return qs ? `?${qs}` : "";
};
// ---------------------------------------------------------------------------
// Schedule consist adjustment (train-bound schedules)
// ---------------------------------------------------------------------------
export interface ConsistWagonRef {
id: string;
wagonNumber: string;
sequenceNumber: number | null;
wagonType: {
id: string;
code: string;
tareWeightTons: number;
capacityTons: number;
lengthMeters: number;
} | null;
}
export interface ScheduleConsist {
schedule: { id: string; reference: string | null; status: string };
train: {
id: string;
code: string;
trainName: string | null;
currentYardId: string | null;
};
limits: {
maxPullWeightTons: number;
overageToleranceTons: number;
pullCapTons: number;
maxTrainLengthMeters: number;
overageToleranceMeters: number;
lengthCapMeters: number;
};
totals: {
wagonCount: number;
cargoTons: number;
consistTareTons: number;
grossTons: number;
consistLengthMeters: number;
};
wagons: Array<
ConsistWagonRef & {
loaded: boolean;
removable: boolean;
/** Loaded wagons can't leave, but their SLOT can change wagon. */
switchable: boolean;
blockReason: string | null;
}
>;
addableWagons: ConsistWagonRef[];
adjustments: Array<{
id: string;
action: "ADD" | "REMOVE" | "SWITCH";
wagonId: string;
wagonNumber: string;
adjustedByUserId: string | null;
yardId: string | null;
occurredAt: string;
}>;
editable: boolean;
/** Where the train stands — mid-route this is the checkpointed stop. */
currentStop: { yardId: string; label: string; isMidRoute: boolean } | null;
/**
* Wagon-slot picture of the schedule: the consist IS the booking capacity
* (weight/length only bind while building the consist), so the dialog can
* project FULL / reopen / over-allocation live. Null on legacy schedules.
*/
scheduleCapacity: {
maxWagons: number;
allocatedWagons: number;
remainingSlots: number;
overAllocatedBy: number;
bookingWindowStatus: string | null;
} | null;
}
export interface AdjustConsistPayload {
addWagonIds?: string[];
removeWagonIds?: string[];
/** Replacement takes the outgoing wagon's position and slot, cargo included. */
switches?: Array<{ fromWagonId: string; toWagonId: string }>;
}
/** One row of the schedule's unified change history (History tab). */
export interface ScheduleHistoryEntry {
id: string;
kind: "WAGON" | "BOOKING";
action: "ADD" | "REMOVE" | "SWITCH" | "BOOKING_REMOVED";
subject: string | null;
yardLabel: string | null;
actor: string | null;
note: string | null;
occurredAt: string;
}
/** Adjust response: fresh consist + schedule-impact warnings to surface. */
export type AdjustConsistResult = ScheduleConsist & { warnings: string[] };
export const trainBuilderService = {
list: (filters: BuiltTrainListFilters = {}) =>
apiClient.get<BuiltTrainListResponse>(`${BASE}${toQuery(filters)}`),
getComposition: (id: string) => apiClient.get<TrainComposition>(`${BASE}/${id}`),
/** Import/export run numbers already claimed by existing trains. */
usedTrainNumbers: () => apiClient.get<UsedTrainNumbers>(`${BASE}/used-train-numbers`),
build: (payload: BuildTrainPayload) => apiClient.post<TrainComposition>(BASE, payload),
setLocomotives: (id: string, locomotiveIds: string[]) =>
apiClient.put<TrainComposition>(`${BASE}/${id}/locomotives`, { locomotiveIds }),
/** Edit the train's name and fixed import/export run numbers. */
updateDetails: (id: string, payload: UpdateTrainDetailsPayload) =>
apiClient.patch<TrainComposition>(`${BASE}/${id}/details`, payload),
/** Relocate the train — coupled locomotives and wagons move with it. */
setYard: (id: string, currentYardId: string) =>
apiClient.patch<TrainComposition>(`${BASE}/${id}/yard`, { currentYardId }),
assignWagons: (id: string, wagonIds: string[]) =>
apiClient.post<TrainComposition>(`${BASE}/${id}/wagons`, { wagonIds }),
removeWagon: (id: string, wagonId: string) =>
apiClient.delete<TrainComposition>(`${BASE}/${id}/wagons/${wagonId}`),
/** Detach a wagon and move it to MAINTENANCE status. */
sendWagonToMaintenance: (id: string, wagonId: string) =>
apiClient.post<TrainComposition>(`${BASE}/${id}/wagons/${wagonId}/maintenance`),
reorderWagons: (id: string, wagonIds: string[]) =>
apiClient.post<TrainComposition>(`${BASE}/${id}/reorder-wagons`, { wagonIds }),
/** Park the train indefinitely — only allowed with no active schedule. */
deactivate: (id: string) =>
apiClient.post<TrainComposition>(`${BASE}/${id}/deactivate`),
/** Bring a DEACTIVATED train back to AVAILABLE. */
activate: (id: string) => apiClient.post<TrainComposition>(`${BASE}/${id}/activate`),
disband: (id: string) => apiClient.delete<void>(`${BASE}/${id}`),
/** Built trains schedulable on a route (train-scheduling picker). */
availableTrains: (routeId: string) =>
apiClient.get<AvailableTrain[]>(`/train-scheduling/available-trains`, {
params: { routeId },
}),
/** Consist snapshot for a train-bound schedule (adjust-consist UI). */
scheduleConsist: (scheduleId: string) =>
apiClient.get<ScheduleConsist>(`/train-scheduling/schedules/${scheduleId}/consist`),
/** Permanently trim/add/switch wagons on the schedule's built train. */
adjustConsist: (scheduleId: string, payload: AdjustConsistPayload) =>
apiClient.post<AdjustConsistResult>(
`/train-scheduling/schedules/${scheduleId}/adjust-consist`,
payload,
),
/** Unified wagon/booking change history for the schedule's History tab. */
scheduleHistory: (scheduleId: string) =>
apiClient.get<ScheduleHistoryEntry[]>(
`/train-scheduling/schedules/${scheduleId}/history`,
),
};