mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 21:08:12 +00:00
Merge pull request #672 from Tria-plc/freight_feature/usermanagement
train
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Train Builder: a `Train` becomes a first-class buildable consist — a coded
|
||||
* train (e.g. 81001) assembled in one yard from 2+ locomotives and ordered
|
||||
* wagons, then reused by scheduling ("schedule the train" instead of picking
|
||||
* locomotives per departure).
|
||||
*
|
||||
* - `freight.train_locomotives` — link table train ⇄ locomotive with an order
|
||||
* index (mirrors `train_set_locomotives`).
|
||||
* - `trains.current_yard_id` — yard the train sits in; wagons/locomotives may
|
||||
* only be attached from this yard.
|
||||
* - `train_sets.train_id` — which built train an operational set was formed
|
||||
* from, so schedules can surface the train code and the lifecycle can sync
|
||||
* the train's status/yard on dispatch/arrival/cancel.
|
||||
*
|
||||
* NOTE: the shared dev DB has no applied migration history, so this is also
|
||||
* hand-applied there. IF NOT EXISTS keeps that idempotent.
|
||||
*/
|
||||
export class TrainBuilder2150000000000 implements MigrationInterface {
|
||||
name = 'TrainBuilder2150000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS freight.train_locomotives (
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
train_id uuid NOT NULL,
|
||||
locomotive_id uuid NOT NULL,
|
||||
sequence_no int NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz,
|
||||
CONSTRAINT "PK_train_locomotives" PRIMARY KEY (id),
|
||||
CONSTRAINT "FK_train_locomotives_train" FOREIGN KEY (train_id)
|
||||
REFERENCES freight.trains (id) ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_train_locomotives_locomotive" FOREIGN KEY (locomotive_id)
|
||||
REFERENCES freight.locomotives (id)
|
||||
);
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_train_locomotives_train_loco"
|
||||
ON freight.train_locomotives (train_id, locomotive_id);
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.trains
|
||||
ADD COLUMN IF NOT EXISTS current_yard_id uuid;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'FK_trains_current_yard'
|
||||
) THEN
|
||||
ALTER TABLE freight.trains
|
||||
ADD CONSTRAINT "FK_trains_current_yard" FOREIGN KEY (current_yard_id)
|
||||
REFERENCES freight.yards (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS "IDX_trains_current_yard_id"
|
||||
ON freight.trains (current_yard_id);
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.train_sets
|
||||
ADD COLUMN IF NOT EXISTS train_id uuid;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'FK_train_sets_train'
|
||||
) THEN
|
||||
ALTER TABLE freight.train_sets
|
||||
ADD CONSTRAINT "FK_train_sets_train" FOREIGN KEY (train_id)
|
||||
REFERENCES freight.trains (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS "IDX_train_sets_train_id"
|
||||
ON freight.train_sets (train_id);
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_train_sets_train_id";`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.train_sets
|
||||
DROP CONSTRAINT IF EXISTS "FK_train_sets_train",
|
||||
DROP COLUMN IF EXISTS train_id;
|
||||
`);
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_trains_current_yard_id";`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.trains
|
||||
DROP CONSTRAINT IF EXISTS "FK_trains_current_yard",
|
||||
DROP COLUMN IF EXISTS current_yard_id;
|
||||
`);
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_train_locomotives_train_loco";`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.train_locomotives;`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* A container type / cargo type can now be carried by SEVERAL wagon types
|
||||
* (e.g. a 20ft container rides NX70 or NW5). Replaces the single
|
||||
* `wagon_type_id` FK on both tables with proper link tables; train scheduling
|
||||
* resolves the wagon type from the list, picking whichever type the schedule's
|
||||
* built train (or the yard) actually has.
|
||||
*
|
||||
* Backfills one link row from each existing `wagon_type_id`, then drops the
|
||||
* old column — the single-FK field is removed from the API and UI entirely.
|
||||
*
|
||||
* NOTE: the shared dev DB has no applied migration history, so this is also
|
||||
* hand-applied there. IF NOT EXISTS keeps that idempotent.
|
||||
*/
|
||||
export class MultiWagonTypePerCargoAndContainer2160000000000 implements MigrationInterface {
|
||||
name = 'MultiWagonTypePerCargoAndContainer2160000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS freight.container_type_wagon_types (
|
||||
container_type_id uuid NOT NULL,
|
||||
wagon_type_id uuid NOT NULL,
|
||||
CONSTRAINT "PK_container_type_wagon_types" PRIMARY KEY (container_type_id, wagon_type_id),
|
||||
CONSTRAINT "FK_ctwt_container_type" FOREIGN KEY (container_type_id)
|
||||
REFERENCES freight.container_types (id) ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_ctwt_wagon_type" FOREIGN KEY (wagon_type_id)
|
||||
REFERENCES freight.wagon_types (id) ON DELETE RESTRICT
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS freight.cargo_type_wagon_types (
|
||||
cargo_type_id uuid NOT NULL,
|
||||
wagon_type_id uuid NOT NULL,
|
||||
CONSTRAINT "PK_cargo_type_wagon_types" PRIMARY KEY (cargo_type_id, wagon_type_id),
|
||||
CONSTRAINT "FK_cgwt_cargo_type" FOREIGN KEY (cargo_type_id)
|
||||
REFERENCES freight.cargo_types (id) ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_cgwt_wagon_type" FOREIGN KEY (wagon_type_id)
|
||||
REFERENCES freight.wagon_types (id) ON DELETE RESTRICT
|
||||
);
|
||||
`);
|
||||
|
||||
// Backfill from the old single FK (column may already be gone on re-run).
|
||||
await queryRunner.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'freight' AND table_name = 'container_types'
|
||||
AND column_name = 'wagon_type_id'
|
||||
) THEN
|
||||
INSERT INTO freight.container_type_wagon_types (container_type_id, wagon_type_id)
|
||||
SELECT ct.id, ct.wagon_type_id
|
||||
FROM freight.container_types ct
|
||||
WHERE ct.wagon_type_id IS NOT NULL
|
||||
ON CONFLICT DO NOTHING;
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'freight' AND table_name = 'cargo_types'
|
||||
AND column_name = 'wagon_type_id'
|
||||
) THEN
|
||||
INSERT INTO freight.cargo_type_wagon_types (cargo_type_id, wagon_type_id)
|
||||
SELECT cg.id, cg.wagon_type_id
|
||||
FROM freight.cargo_types cg
|
||||
WHERE cg.wagon_type_id IS NOT NULL
|
||||
ON CONFLICT DO NOTHING;
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
// Old single-FK column is fully retired (API + UI now use the lists).
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.container_types DROP COLUMN IF EXISTS wagon_type_id;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.cargo_types DROP COLUMN IF EXISTS wagon_type_id;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.container_types ADD COLUMN IF NOT EXISTS wagon_type_id uuid
|
||||
REFERENCES freight.wagon_types (id) ON DELETE RESTRICT;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.cargo_types ADD COLUMN IF NOT EXISTS wagon_type_id uuid
|
||||
REFERENCES freight.wagon_types (id) ON DELETE RESTRICT;
|
||||
`);
|
||||
// Restore the first linked wagon type per row, then drop the link tables.
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.container_types ct
|
||||
SET wagon_type_id = link.wagon_type_id
|
||||
FROM (
|
||||
SELECT DISTINCT ON (container_type_id) container_type_id, wagon_type_id
|
||||
FROM freight.container_type_wagon_types
|
||||
ORDER BY container_type_id, wagon_type_id
|
||||
) link
|
||||
WHERE link.container_type_id = ct.id;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.cargo_types cg
|
||||
SET wagon_type_id = link.wagon_type_id
|
||||
FROM (
|
||||
SELECT DISTINCT ON (cargo_type_id) cargo_type_id, wagon_type_id
|
||||
FROM freight.cargo_type_wagon_types
|
||||
ORDER BY cargo_type_id, wagon_type_id
|
||||
) link
|
||||
WHERE link.cargo_type_id = cg.id;
|
||||
`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.container_type_wagon_types;`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.cargo_type_wagon_types;`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Two-person wagon-transfer request queue. A requester records a count-only
|
||||
* request (N wagons of a type, from yard → to yard); OCC staff later pick the
|
||||
* physical wagons and execute the move. Replaces the single-step instant
|
||||
* bulk-transfer as the customer-facing yard-to-yard relocation path.
|
||||
*/
|
||||
export class CreateWagonTransferRequests2170000000000
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'CreateWagonTransferRequests2170000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS freight.wagon_transfer_requests (
|
||||
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
from_yard_id uuid NOT NULL,
|
||||
to_yard_id uuid NOT NULL,
|
||||
wagon_type_id uuid NOT NULL,
|
||||
quantity integer NOT NULL,
|
||||
status varchar(20) NOT NULL DEFAULT 'PENDING',
|
||||
requested_by_user_id uuid NULL,
|
||||
fulfilled_by_user_id uuid NULL,
|
||||
fulfilled_at timestamptz NULL,
|
||||
note text NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz NULL,
|
||||
CONSTRAINT pk_wagon_transfer_requests PRIMARY KEY (id),
|
||||
CONSTRAINT fk_wtr_from_yard FOREIGN KEY (from_yard_id) REFERENCES freight.yards (id),
|
||||
CONSTRAINT fk_wtr_to_yard FOREIGN KEY (to_yard_id) REFERENCES freight.yards (id),
|
||||
CONSTRAINT fk_wtr_wagon_type FOREIGN KEY (wagon_type_id) REFERENCES freight.wagon_types (id),
|
||||
CONSTRAINT chk_wtr_quantity CHECK (quantity > 0)
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_wtr_status_from_yard
|
||||
ON freight.wagon_transfer_requests (status, from_yard_id)
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS freight.idx_wtr_status_from_yard`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TABLE IF EXISTS freight.wagon_transfer_requests`,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user