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; currentYardId: string | null; currentYard: YardRefLite | null; wagonType: { id: string; code: string; name: string; capacityTons: number; tareWeightTons: number; lengthMeters: number; } | null; } /** Where a built train's wagons physically stand, largest group first. */ export interface TrainWagonYardGroup { yardId: string | null; code: string | null; label: string | null; wagonCount: number; } 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[]; wagonYards: TrainWagonYardGroup[]; 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" | "BOOKING_LOADED" | "BOOKING_UNLOADED"; 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[] }; /** One consist wagon in the schedule-yards tab: where this departure plans it vs where it stands. */ export interface ScheduleWagonYardRow { id: string; wagonNumber: string; sequenceNumber: number | null; wagonType: { id: string; code: string; name: string }; physicalYardId: string | null; physicalYardLabel: string | null; plannedYardId: string | null; plannedYardLabel: string | null; /** Drop stop this departure cuts the wagon at; null = rides to the destination. */ cutYardId: string | null; cutYardLabel: string | null; aligned: boolean; locked: boolean; lockReason: string | null; } export interface ScheduleWagonYardStop { yardId: string; label: string; /** Origin or intermediate stop — wagons can board here. The destination cannot. */ pickup: boolean; planned: number; physical: number; /** Wagons this departure cuts (detaches and leaves) at this stop. */ cut: number; } export interface ScheduleWagonYards { scheduleId: string; train: { id: string; code: string }; editable: boolean; stops: ScheduleWagonYardStop[]; wagons: ScheduleWagonYardRow[]; misaligned: number; } export interface UpdateScheduleWagonYardsPayload { /** Omit a field to leave it unchanged; cutYardId null clears the cut (rides to destination). */ moves: Array<{ wagonId: string; yardId?: string; cutYardId?: string | null }>; } export type UpdateScheduleWagonYardsResult = ScheduleWagonYards & { warnings: string[] }; export const trainBuilderService = { list: (filters: BuiltTrainListFilters = {}) => apiClient.get(`${BASE}${toQuery(filters)}`), getComposition: (id: string) => apiClient.get(`${BASE}/${id}`), /** Import/export run numbers already claimed by existing trains. */ usedTrainNumbers: () => apiClient.get(`${BASE}/used-train-numbers`), build: (payload: BuildTrainPayload) => apiClient.post(BASE, payload), setLocomotives: (id: string, locomotiveIds: string[]) => apiClient.put(`${BASE}/${id}/locomotives`, { locomotiveIds }), /** Edit the train's name and fixed import/export run numbers. */ updateDetails: (id: string, payload: UpdateTrainDetailsPayload) => apiClient.patch(`${BASE}/${id}/details`, payload), /** Relocate the train — coupled locomotives and wagons move with it. */ setYard: (id: string, currentYardId: string) => apiClient.patch(`${BASE}/${id}/yard`, { currentYardId }), /** Move one coupled wagon to another yard; the train stays put. */ setWagonYard: (id: string, wagonId: string, currentYardId: string) => apiClient.patch(`${BASE}/${id}/wagons/${wagonId}/yard`, { currentYardId }), /** Move several coupled wagons to another yard in one transaction (all-or-nothing). */ setWagonsYard: (id: string, wagonIds: string[], currentYardId: string) => apiClient.patch(`${BASE}/${id}/wagons/yard`, { wagonIds, currentYardId, }), assignWagons: (id: string, wagonIds: string[]) => apiClient.post(`${BASE}/${id}/wagons`, { wagonIds }), removeWagon: (id: string, wagonId: string) => apiClient.delete(`${BASE}/${id}/wagons/${wagonId}`), /** Detach a wagon and move it to MAINTENANCE status. */ /** `note` is the maintenance reason — recorded with the train it came off. */ sendWagonToMaintenance: (id: string, wagonId: string, note?: string) => apiClient.post(`${BASE}/${id}/wagons/${wagonId}/maintenance`, { note, }), reorderWagons: (id: string, wagonIds: string[]) => apiClient.post(`${BASE}/${id}/reorder-wagons`, { wagonIds }), /** Park the train indefinitely — only allowed with no active schedule. */ deactivate: (id: string) => apiClient.post(`${BASE}/${id}/deactivate`), /** Bring a DEACTIVATED train back to AVAILABLE. */ activate: (id: string) => apiClient.post(`${BASE}/${id}/activate`), disband: (id: string) => apiClient.delete(`${BASE}/${id}`), /** Built trains schedulable on a route (train-scheduling picker). */ availableTrains: (routeId: string) => apiClient.get(`/train-scheduling/available-trains`, { params: { routeId }, }), /** Consist snapshot for a train-bound schedule (adjust-consist UI). */ scheduleConsist: (scheduleId: string) => apiClient.get(`/train-scheduling/schedules/${scheduleId}/consist`), /** Permanently trim/add/switch wagons on the schedule's built train. */ adjustConsist: (scheduleId: string, payload: AdjustConsistPayload) => apiClient.post( `/train-scheduling/schedules/${scheduleId}/adjust-consist`, payload, ), /** Schedule-only wagon yard plan (where THIS departure boards each wagon). */ scheduleWagonYards: (scheduleId: string) => apiClient.get(`/train-scheduling/schedules/${scheduleId}/wagon-yards`), /** Re-plan boarding yards for this schedule; physical wagon yards untouched. */ updateScheduleWagonYards: (scheduleId: string, payload: UpdateScheduleWagonYardsPayload) => apiClient.patch( `/train-scheduling/schedules/${scheduleId}/wagon-yards`, payload, ), /** Unified wagon/booking change history for the schedule's History tab. */ scheduleHistory: (scheduleId: string) => apiClient.get( `/train-scheduling/schedules/${scheduleId}/history`, ), };