Add train deactivation feature and import train number management

- Introduced DEACTIVATED status for trains, allowing staff to park trains indefinitely.
- Implemented methods to deactivate and reactivate trains in the TrainBuilderService.
- Added UI components for train deactivation and reactivation in TrainBuilderDetailPage.
- Created a dropdown setting for admin-managed import train numbers, with corresponding migrations.
- Updated yard code length to accommodate soft-delete suffix.
- Enhanced train status handling to include DEACTIVATED state.
This commit is contained in:
Marshal
2026-07-20 07:04:23 +00:00
parent ab355d0e16
commit 771aa2a605
20 changed files with 467 additions and 36 deletions

View File

@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* New built-train lifecycle status DEACTIVATED: staff park a train indefinitely
* (only allowed while it has no DRAFT/SCHEDULED/DISPATCHED schedule). Like
* UNDER_MAINTENANCE / OUT_OF_SERVICE it is staff-owned — the scheduler never
* overwrites it and refuses to schedule a deactivated train.
*
* Postgres cannot drop an enum value, so down() is a no-op.
*/
export class AddTrainDeactivatedStatus2380000000000 implements MigrationInterface {
name = 'AddTrainDeactivatedStatus2380000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TYPE "freight"."train_status" ADD VALUE IF NOT EXISTS 'DEACTIVATED'`,
);
}
public async down(): Promise<void> {
// Enum values cannot be removed in Postgres; leaving the label is harmless.
}
}

View File

@@ -0,0 +1,62 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Admin-managed catalog of IMPORT run numbers (even, Djibouti → Ethiopia)
* selectable in the Train Builder. The paired EXPORT number is derived
* (import 1), so only the import side is configured. Seeded with the runs
* historically hardcoded in the backoffice's trainRuns constants; admins add
* new runs from the Dropdown Settings editor.
*/
export class SeedImportTrainNumbers2390000000000 implements MigrationInterface {
name = 'SeedImportTrainNumbers2390000000000';
private readonly code = 'import_train_numbers';
private readonly options: string[] = [
'8002',
'8102',
'8202',
'8302',
'8402',
'8502',
'8602',
'8702',
'8802',
'8902',
'9002',
];
public async up(queryRunner: QueryRunner): Promise<void> {
const existing = await queryRunner.query(
`SELECT id FROM freight.dropdown_settings WHERE code = $1 LIMIT 1;`,
[this.code],
);
if (existing.length > 0) return;
const inserted = await queryRunner.query(
`INSERT INTO freight.dropdown_settings (code, label, description, multiple, meta)
VALUES ($1, $2, $3, false, $4::jsonb)
RETURNING id;`,
[
this.code,
'Import train numbers',
'Even IMPORT run numbers (Djibouti → Ethiopia) selectable when building a train. The paired export number is derived automatically (import 1).',
JSON.stringify({ searchable: true, clearable: true }),
],
);
const settingId = inserted[0].id;
for (let i = 0; i < this.options.length; i++) {
const value = this.options[i];
await queryRunner.query(
`INSERT INTO freight.dropdown_options (setting_id, value, label, display_order)
VALUES ($1, $2, $3, $4);`,
[settingId, value, value, i],
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DELETE FROM freight.dropdown_settings WHERE code = $1;`, [
this.code,
]);
}
}

View File

@@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Yard soft-delete now appends `@<epoch-ms>` to the unique code (SEBETA →
* SEBETA@1755612345678) so the name can be reused by a new yard while
* UQ_yards_code still spans soft-deleted rows. varchar(20) can't hold long
* codes plus the 14-char suffix, so widen to 40.
*/
export class WidenYardCodeForSoftDeleteSuffix2390000000000 implements MigrationInterface {
name = 'WidenYardCodeForSoftDeleteSuffix2390000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "freight"."yards" ALTER COLUMN "code" TYPE varchar(40)`,
);
}
public async down(): Promise<void> {
// Narrowing would fail on suffixed codes; keep 40.
}
}