import type { PaginatedResponse } from '@edr/types'; import { api as apiClient } from '../auth/http'; import { URL_CONSTANTS } from '@/constants/URLS'; import { ruleEngineService } from './ruleEngine/ruleEngine.service'; export type RouteStatus = 'AVAILABLE' | 'MAINTENANCE' | 'DAMAGED' | 'STOP_WORKING'; /** Frozen from yard countries: ET→DJ EXPORT, DJ→ET IMPORT, same country DOMESTIC (intercity, disabled). */ export type RouteDirection = 'IMPORT' | 'EXPORT' | 'DOMESTIC'; export interface YardRef { id: string; code: string; label: string; country?: string; } export interface RouteMilestone { id: string; routeId: string; yardId: string; sequenceNo: number; distanceKm?: number | null; yard?: YardRef | null; } export interface RouteRecord { id: string; status: RouteStatus; direction?: RouteDirection; originYardId: string; destinationYardId: string; originYard?: YardRef | null; destinationYard?: YardRef | null; milestones?: RouteMilestone[]; } /** Segment km are resolved server-side from configured yard distances. */ export interface SaveRoutePayload { milestones: Array<{ yardId: string }>; status?: RouteStatus; } /** * Human-readable route label: yard names, not yard codes. Staff read * "Addis Ababa → Dire Dawa", not "ADDIS_ABABA → DIRE_DAWA". Falls back to the * code only when a yard has no label. * * When milestones are present they ARE the full ordered corridor (origin first, * destination last), so the label shows every stop: * "Addis Ababa → Adama → Dire Dawa". */ export function formatRouteLabel(route: RouteRecord): string { const stops = [...(route.milestones ?? [])] .sort((a, b) => a.sequenceNo - b.sequenceNo) .map((m) => m.yard?.label ?? m.yard?.code) .filter((name): name is string => Boolean(name)); if (stops.length >= 2) return stops.join(' → '); const origin = route.originYard?.label ?? route.originYard?.code ?? 'Origin'; const dest = route.destinationYard?.label ?? route.destinationYard?.code ?? 'Destination'; return `${origin} → ${dest}`; } export function totalRouteDistanceKm(route: RouteRecord): number { return (route.milestones ?? []).reduce( (sum, m) => sum + Number(m.distanceKm ?? 0), 0, ); } export const ROUTE_STATUS_OPTIONS: Array<{ value: RouteStatus; label: string }> = [ { value: 'AVAILABLE', label: 'Available' }, { value: 'MAINTENANCE', label: 'Maintenance' }, { value: 'DAMAGED', label: 'Damaged' }, { value: 'STOP_WORKING', label: 'Stop working' }, ]; export interface RouteListFilters { status?: RouteStatus; search?: string; page?: number; pageSize?: number; } export const routesService = { getAll: (params?: { status?: RouteStatus; search?: string }) => apiClient.get(URL_CONSTANTS.ROUTES.BASE, { params }), /** Same filters as `getAll`, server-paginated ({items, meta}). */ getPaged: (params: RouteListFilters = {}) => apiClient.get>( `${URL_CONSTANTS.ROUTES.BASE}/paged`, { params }, ), getById: (id: string) => apiClient.get(URL_CONSTANTS.ROUTES.BY_ID(id)), create: (data: SaveRoutePayload) => apiClient.post(URL_CONSTANTS.ROUTES.BASE, data), update: (id: string, data: Partial) => apiClient.patch(URL_CONSTANTS.ROUTES.BY_ID(id), data), deactivate: (id: string) => apiClient.delete(URL_CONSTANTS.ROUTES.BY_ID(id)), /** Irreversible purge — the API refuses it while any train schedule uses the route. */ purge: (id: string) => apiClient.delete(`${URL_CONSTANTS.ROUTES.BY_ID(id)}/permanent`), /** All active yards (page-walked — the yards list API caps pageSize at 100). */ getYards: async (): Promise => { const rows = await ruleEngineService.listAll("yards", { isActive: true }); return rows as unknown as YardRef[]; }, };