mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
342 lines
11 KiB
TypeScript
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`,
|
|
),
|
|
};
|