mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-02 23:13:40 +00:00
76 lines
2.8 KiB
TypeScript
76 lines
2.8 KiB
TypeScript
import { BaseEntity } from '@edr/api-common';
|
|
import type { ScheduleTradeDirection } from '@edr/types';
|
|
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
|
|
|
import { Yard } from '../../rule-engine/entities/yard.entity';
|
|
import { RouteMilestone } from './route-milestone.entity';
|
|
|
|
export type RouteStatus = 'AVAILABLE' | 'MAINTENANCE' | 'DAMAGED' | 'STOP_WORKING';
|
|
|
|
@Entity({ schema: 'freight', name: 'routes' })
|
|
@Index(['status'])
|
|
export class Route extends BaseEntity {
|
|
@Column({ name: 'origin_yard_id', type: 'uuid' })
|
|
originYardId!: string;
|
|
|
|
@ManyToOne(() => Yard)
|
|
@JoinColumn({ name: 'origin_yard_id' })
|
|
originYard?: Yard;
|
|
|
|
@Column({ name: 'destination_yard_id', type: 'uuid' })
|
|
destinationYardId!: string;
|
|
|
|
@ManyToOne(() => Yard)
|
|
@JoinColumn({ name: 'destination_yard_id' })
|
|
destinationYard?: Yard;
|
|
|
|
@Column({ name: 'status', type: 'varchar', length: 32, default: 'AVAILABLE' })
|
|
status!: RouteStatus;
|
|
|
|
/**
|
|
* Trade direction frozen from the yard countries at create/update
|
|
* (ET→DJ = EXPORT, DJ→ET = IMPORT, same country = DOMESTIC/"Intercity").
|
|
* Consumers (scheduling, booking windows) read this instead of re-deriving.
|
|
*/
|
|
@Column({ name: 'direction', type: 'varchar', length: 10 })
|
|
direction!: ScheduleTradeDirection;
|
|
|
|
@OneToMany(() => RouteMilestone, (milestone) => milestone.route, { cascade: false })
|
|
milestones?: RouteMilestone[];
|
|
}
|
|
|
|
/**
|
|
* Human-readable route label: yard names, not yard codes — "Addis Ababa → Dire Dawa",
|
|
* not "ADDIS_ABABA → DIRE_DAWA". A yard's display name is its `label`; `code` is the
|
|
* machine identifier and is only a fallback for a yard missing one.
|
|
*
|
|
* When the route's milestones are loaded (with their yards), the label is the FULL
|
|
* ordered corridor — "Addis Ababa → Adama → Dire Dawa" — since milestones already
|
|
* include the origin (first) and destination (last). Without milestones it falls
|
|
* back to origin → destination.
|
|
*/
|
|
export function formatRouteLabel(route: {
|
|
originYard?: { code?: string; label?: string } | null;
|
|
destinationYard?: { code?: string; label?: string } | null;
|
|
milestones?: Array<{
|
|
sequenceNo: number;
|
|
yard?: { code?: string; label?: string } | null;
|
|
}> | null;
|
|
}): 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(
|
|
milestones: Array<{ distanceKm?: number | string | null }>,
|
|
): number {
|
|
return milestones.reduce((sum, m) => sum + Number(m.distanceKm ?? 0), 0);
|
|
}
|