add yard distances management to rule engine

- Introduced new yard distances resource with CRUD operations.
- Created migration for yard distances table with necessary constraints.
- Implemented service and repository for yard distances handling.
- Added controller for API endpoints to manage yard distances.
- Updated rule engine configuration to include yard distances.
- Enhanced rule engine resource page to support yard distance selection.
- Updated contracts and train builder pages to handle new yard distance logic.
- Added error handling utility for better error message extraction.
This commit is contained in:
Marshal
2026-07-21 08:50:50 +00:00
parent 1647681840
commit 603537a20b
45 changed files with 1275 additions and 227 deletions

View File

@@ -0,0 +1,66 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Configured rail distance between two yards (Configuration → Yard Distances).
* Route creation resolves each segment's km from here (symmetric lookup:
* one A↔B row serves both directions) instead of accepting free-text km,
* and snapshots the value onto route_milestones.distance_km.
*
* Uniqueness is a partial index (deleted_at IS NULL) so a soft-deleted pair
* can be re-created.
*/
export class CreateYardDistances2060000000000 implements MigrationInterface {
name = 'CreateYardDistances2060000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.yard_distances (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
from_yard_id uuid NOT NULL REFERENCES freight.yards(id),
to_yard_id uuid NOT NULL REFERENCES freight.yards(id),
distance_km numeric(10,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_yard_distances_from_yard
ON freight.yard_distances (from_yard_id);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_yard_distances_to_yard
ON freight.yard_distances (to_yard_id);
`);
await queryRunner.query(`
CREATE UNIQUE INDEX IF NOT EXISTS uq_yard_distances_pair
ON freight.yard_distances (from_yard_id, to_yard_id)
WHERE deleted_at IS NULL;
`);
// Backfill from segments already stored on existing routes so editing them
// does not immediately fail the "pair not configured" check. One row per
// unordered pair; where routes disagree the longest segment wins.
await queryRunner.query(`
INSERT INTO freight.yard_distances (from_yard_id, to_yard_id, distance_km)
SELECT DISTINCT ON (LEAST(prev_yard_id, yard_id), GREATEST(prev_yard_id, yard_id))
prev_yard_id, yard_id, distance_km
FROM (
SELECT
yard_id,
distance_km,
LAG(yard_id) OVER (PARTITION BY route_id ORDER BY sequence_no) AS prev_yard_id
FROM freight.route_milestones
WHERE deleted_at IS NULL
) segments
WHERE prev_yard_id IS NOT NULL
AND distance_km IS NOT NULL
AND distance_km > 0
ORDER BY LEAST(prev_yard_id, yard_id), GREATEST(prev_yard_id, yard_id), distance_km DESC
ON CONFLICT DO NOTHING;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.yard_distances;`);
}
}