mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Train scheduling API,Routes and UI
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@ node_modules/
|
|||||||
coverage/
|
coverage/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
**/*.tsbuildinfo
|
**/*.tsbuildinfo
|
||||||
|
**/vite.config.ts.timestamp-*.mjs
|
||||||
|
|
||||||
# env
|
# env
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
|
"seed:wagons": "ts-node -r tsconfig-paths/register src/scripts/seed-edr-wagons.ts",
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ import {
|
|||||||
PositionType,
|
PositionType,
|
||||||
Position,
|
Position,
|
||||||
Project,
|
Project,
|
||||||
<<<<<<< HEAD
|
|
||||||
UnitConfiguration,
|
|
||||||
=======
|
|
||||||
>>>>>>> 95fb544ec20f01ec4a2d92f546954a5b4e464a4f
|
|
||||||
GlobalUnitConfiguration,
|
GlobalUnitConfiguration,
|
||||||
Unit,
|
Unit,
|
||||||
EmployeeSignature,
|
EmployeeSignature,
|
||||||
@@ -67,10 +63,6 @@ const iamEntities = [
|
|||||||
PositionType,
|
PositionType,
|
||||||
Position,
|
Position,
|
||||||
Project,
|
Project,
|
||||||
<<<<<<< HEAD
|
|
||||||
UnitConfiguration,
|
|
||||||
=======
|
|
||||||
>>>>>>> 95fb544ec20f01ec4a2d92f546954a5b4e464a4f
|
|
||||||
GlobalUnitConfiguration,
|
GlobalUnitConfiguration,
|
||||||
Unit,
|
Unit,
|
||||||
EmployeeSignature,
|
EmployeeSignature,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { DataSource } from 'typeorm';
|
|||||||
export const AppDataSource = new DataSource({
|
export const AppDataSource = new DataSource({
|
||||||
type: 'postgres',
|
type: 'postgres',
|
||||||
host: process.env.DB_HOST ?? 'localhost',
|
host: process.env.DB_HOST ?? 'localhost',
|
||||||
port: Number(process.env.DB_PORT ?? 5432),
|
port: Number(process.env.DB_PORT ?? 5433),
|
||||||
username: process.env.DB_USER ?? 'postgres',
|
username: process.env.DB_USER ?? 'postgres',
|
||||||
password: process.env.DB_PASSWORD ?? '',
|
password: process.env.DB_PASSWORD ?? '',
|
||||||
database: process.env.DB_NAME ?? 'edr_freight',
|
database: process.env.DB_NAME ?? 'edr_freight',
|
||||||
@@ -14,7 +14,7 @@ export const AppDataSource = new DataSource({
|
|||||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||||
migrations: [__dirname + '/migrations/*{.ts,.js}'],
|
migrations: [__dirname + '/migrations/*{.ts,.js}'],
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
logging: true,
|
logging: process.env.TYPEORM_LOGGING === 'true',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Optional: call ensurePostgresSchemas before initializing
|
// Optional: call ensurePostgresSchemas before initializing
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddPhysicalWagonToTrainSetWagons1750200000000 implements MigrationInterface {
|
||||||
|
name = 'AddPhysicalWagonToTrainSetWagons1750200000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE freight.train_set_wagons
|
||||||
|
ADD COLUMN IF NOT EXISTS physical_wagon_id UUID NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_schema = 'freight'
|
||||||
|
AND table_name = 'train_set_wagons'
|
||||||
|
AND constraint_name = 'fk_train_set_wagons_physical_wagon'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE freight.train_set_wagons
|
||||||
|
ADD CONSTRAINT fk_train_set_wagons_physical_wagon
|
||||||
|
FOREIGN KEY (physical_wagon_id)
|
||||||
|
REFERENCES freight.wagons(id)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_train_set_wagons_physical_wagon
|
||||||
|
ON freight.train_set_wagons(physical_wagon_id);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_train_set_wagons_physical_wagon;`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE freight.train_set_wagons
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_train_set_wagons_physical_wagon;
|
||||||
|
`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE freight.train_set_wagons
|
||||||
|
DROP COLUMN IF EXISTS physical_wagon_id;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddCurrentLocationToWagons1750300000000 implements MigrationInterface {
|
||||||
|
name = 'AddCurrentLocationToWagons1750300000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE freight.wagons
|
||||||
|
ADD COLUMN IF NOT EXISTS current_location_yard_id UUID NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_schema = 'freight'
|
||||||
|
AND table_name = 'wagons'
|
||||||
|
AND constraint_name = 'FK_wagons_current_location_yard_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE freight.wagons
|
||||||
|
ADD CONSTRAINT "FK_wagons_current_location_yard_id"
|
||||||
|
FOREIGN KEY (current_location_yard_id)
|
||||||
|
REFERENCES freight.yards(id)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS "IDX_wagons_current_location_yard_id"
|
||||||
|
ON freight.wagons(current_location_yard_id);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_wagons_current_location_yard_id";`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE freight.wagons
|
||||||
|
DROP CONSTRAINT IF EXISTS "FK_wagons_current_location_yard_id";
|
||||||
|
`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE freight.wagons
|
||||||
|
DROP COLUMN IF EXISTS current_location_yard_id;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
type FleetRow = {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
capacityTons: number;
|
||||||
|
tareWeight: number;
|
||||||
|
lengthMeters: number;
|
||||||
|
supportedLoadTypes: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const FLEET: FleetRow[] = [
|
||||||
|
{
|
||||||
|
code: 'PW2',
|
||||||
|
name: 'Box wagon',
|
||||||
|
count: 220,
|
||||||
|
start: 1,
|
||||||
|
end: 220,
|
||||||
|
capacityTons: 70,
|
||||||
|
tareWeight: 25.2,
|
||||||
|
lengthMeters: 17.066,
|
||||||
|
supportedLoadTypes: ['BULK', 'GENERAL_CARGO', 'BAGGED_CARGO', 'BOXED_CARGO'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'CW4',
|
||||||
|
name: 'Gondola wagon covered',
|
||||||
|
count: 110,
|
||||||
|
start: 221,
|
||||||
|
end: 330,
|
||||||
|
capacityTons: 70,
|
||||||
|
tareWeight: 24.8,
|
||||||
|
lengthMeters: 13.976,
|
||||||
|
supportedLoadTypes: ['CONTAINER'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'CW3',
|
||||||
|
name: 'Gondola wagon',
|
||||||
|
count: 20,
|
||||||
|
start: 331,
|
||||||
|
end: 350,
|
||||||
|
capacityTons: 70,
|
||||||
|
tareWeight: 23.4,
|
||||||
|
lengthMeters: 13.976,
|
||||||
|
supportedLoadTypes: ['BULK', 'COAL', 'ORE'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'KW2',
|
||||||
|
name: 'Hopper wagon covered',
|
||||||
|
count: 20,
|
||||||
|
start: 351,
|
||||||
|
end: 370,
|
||||||
|
capacityTons: 69,
|
||||||
|
tareWeight: 25.2,
|
||||||
|
lengthMeters: 16.466,
|
||||||
|
supportedLoadTypes: ['BULK', 'GRAIN'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'KW3',
|
||||||
|
name: 'Hopper wagon',
|
||||||
|
count: 20,
|
||||||
|
start: 371,
|
||||||
|
end: 390,
|
||||||
|
capacityTons: 70,
|
||||||
|
tareWeight: 24,
|
||||||
|
lengthMeters: 14.4,
|
||||||
|
supportedLoadTypes: ['BULK', 'COAL'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'NW5',
|
||||||
|
name: 'Flat wagon container',
|
||||||
|
count: 550,
|
||||||
|
start: 391,
|
||||||
|
end: 940,
|
||||||
|
capacityTons: 70,
|
||||||
|
tareWeight: 0,
|
||||||
|
lengthMeters: 14,
|
||||||
|
supportedLoadTypes: ['CONTAINER'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const wagonNumber = (sequence: number) => `ER${String(sequence).padStart(4, '0')}`;
|
||||||
|
|
||||||
|
export class SeedEdRWagonFleet1750400000000 implements MigrationInterface {
|
||||||
|
name = 'SeedEdRWagonFleet1750400000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
UPDATE freight.wagon_types
|
||||||
|
SET name = 'Flat wagon container',
|
||||||
|
capacity_tons = 70,
|
||||||
|
length_meters = 14.000,
|
||||||
|
supported_load_types = ARRAY['CONTAINER'],
|
||||||
|
max_wagons_per_train = 53,
|
||||||
|
is_active = true,
|
||||||
|
deleted_at = NULL,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE code = 'NW5';
|
||||||
|
`);
|
||||||
|
|
||||||
|
const [defaultLocation] = await queryRunner.query(`
|
||||||
|
SELECT id
|
||||||
|
FROM freight.yards
|
||||||
|
WHERE code IN ('DJIBOUTI', 'DJIB_PORT', 'NAGAD')
|
||||||
|
OR lower(label) LIKE '%djibouti%'
|
||||||
|
ORDER BY
|
||||||
|
CASE code
|
||||||
|
WHEN 'DJIBOUTI' THEN 1
|
||||||
|
WHEN 'DJIB_PORT' THEN 2
|
||||||
|
WHEN 'NAGAD' THEN 3
|
||||||
|
ELSE 4
|
||||||
|
END,
|
||||||
|
display_order ASC
|
||||||
|
LIMIT 1;
|
||||||
|
`);
|
||||||
|
const defaultLocationYardId = defaultLocation?.id ?? null;
|
||||||
|
|
||||||
|
for (const row of FLEET) {
|
||||||
|
await queryRunner.query(
|
||||||
|
`
|
||||||
|
INSERT INTO freight.wagon_types (
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
capacity_tons,
|
||||||
|
length_meters,
|
||||||
|
max_wagons_per_train,
|
||||||
|
supported_load_types,
|
||||||
|
is_active
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6::text[], true)
|
||||||
|
ON CONFLICT (code) DO UPDATE SET
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
capacity_tons = EXCLUDED.capacity_tons,
|
||||||
|
length_meters = EXCLUDED.length_meters,
|
||||||
|
max_wagons_per_train = EXCLUDED.max_wagons_per_train,
|
||||||
|
supported_load_types = EXCLUDED.supported_load_types,
|
||||||
|
is_active = true,
|
||||||
|
deleted_at = NULL,
|
||||||
|
updated_at = now();
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
row.code,
|
||||||
|
row.name,
|
||||||
|
row.capacityTons,
|
||||||
|
row.lengthMeters,
|
||||||
|
row.supportedLoadTypes.includes('CONTAINER') ? 53 : 37,
|
||||||
|
row.supportedLoadTypes,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [typeRecord] = await queryRunner.query(
|
||||||
|
`SELECT id FROM freight.wagon_types WHERE code = $1 LIMIT 1;`,
|
||||||
|
[row.code],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!typeRecord?.id) {
|
||||||
|
throw new Error(`wagon_type_seed_failed:${row.code}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.end - row.start + 1 !== row.count) {
|
||||||
|
throw new Error(`wagon_range_mismatch:${row.code}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let sequence = row.start; sequence <= row.end; sequence += 1) {
|
||||||
|
await queryRunner.query(
|
||||||
|
`
|
||||||
|
INSERT INTO freight.wagons (
|
||||||
|
wagon_number,
|
||||||
|
wagon_type_id,
|
||||||
|
tare_weight,
|
||||||
|
max_payload_weight,
|
||||||
|
current_location_yard_id,
|
||||||
|
status,
|
||||||
|
notes
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
ON CONFLICT (wagon_number) DO UPDATE SET
|
||||||
|
wagon_type_id = EXCLUDED.wagon_type_id,
|
||||||
|
tare_weight = EXCLUDED.tare_weight,
|
||||||
|
max_payload_weight = EXCLUDED.max_payload_weight,
|
||||||
|
current_location_yard_id = CASE
|
||||||
|
WHEN freight.wagons.train_id IS NULL THEN EXCLUDED.current_location_yard_id
|
||||||
|
ELSE freight.wagons.current_location_yard_id
|
||||||
|
END,
|
||||||
|
status = CASE
|
||||||
|
WHEN freight.wagons.train_id IS NULL THEN EXCLUDED.status
|
||||||
|
ELSE freight.wagons.status
|
||||||
|
END,
|
||||||
|
notes = EXCLUDED.notes,
|
||||||
|
updated_at = now();
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
wagonNumber(sequence),
|
||||||
|
typeRecord.id,
|
||||||
|
row.tareWeight,
|
||||||
|
row.capacityTons,
|
||||||
|
defaultLocationYardId,
|
||||||
|
defaultLocationYardId ? 'IMPORT_READY' : 'AVAILABLE',
|
||||||
|
`Seeded Ethio-Djibouti Railway ${row.code} fleet record.`,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
DELETE FROM freight.wagons
|
||||||
|
WHERE wagon_number BETWEEN 'ER0001' AND 'ER0940';
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,9 @@ export const BOOKING_STATUSES = [
|
|||||||
'PENDING_APPROVAL',
|
'PENDING_APPROVAL',
|
||||||
'APPROVED_PENDING_SIGNATURE',
|
'APPROVED_PENDING_SIGNATURE',
|
||||||
'APPROVED',
|
'APPROVED',
|
||||||
|
'READY_FOR_ASSIGNMENT',
|
||||||
|
'WAGON_ASSIGNED',
|
||||||
|
'INVOICED',
|
||||||
'CONTRACT_READY',
|
'CONTRACT_READY',
|
||||||
'SIGNED_CUSTOMER',
|
'SIGNED_CUSTOMER',
|
||||||
'FULLY_EXECUTED',
|
'FULLY_EXECUTED',
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import { TrainSet } from '../../train-sets/entities/train-set.entity';
|
|||||||
|
|
||||||
export const LOCOMOTIVE_STATUSES = [
|
export const LOCOMOTIVE_STATUSES = [
|
||||||
'AVAILABLE',
|
'AVAILABLE',
|
||||||
|
'UNAVAILABLE',
|
||||||
|
'IMPORT_READY',
|
||||||
|
'EXPORT_READY',
|
||||||
'ASSIGNED',
|
'ASSIGNED',
|
||||||
'MAINTENANCE',
|
'MAINTENANCE',
|
||||||
'OUT_OF_SERVICE',
|
'OUT_OF_SERVICE',
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import { TrainScheduleBooking } from './train-schedule-booking.entity';
|
|||||||
|
|
||||||
export const TRAIN_SCHEDULE_STATUSES = [
|
export const TRAIN_SCHEDULE_STATUSES = [
|
||||||
'DRAFT',
|
'DRAFT',
|
||||||
'SCHEDULED',
|
'READY',
|
||||||
'DISPATCHED',
|
'PUBLISHED',
|
||||||
|
'DEPARTED',
|
||||||
|
'IN_TRANSIT',
|
||||||
'ARRIVED',
|
'ARRIVED',
|
||||||
|
'COMPLETED',
|
||||||
'CANCELLED',
|
'CANCELLED',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsDateString, IsUUID } from 'class-validator';
|
import { ArrayMinSize, IsArray, IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
export class CreateContainerTrainScheduleDto {
|
export class CreateContainerTrainScheduleDto {
|
||||||
@ApiProperty({ format: 'uuid' })
|
@ApiProperty({ format: 'uuid' })
|
||||||
@@ -10,7 +10,31 @@ export class CreateContainerTrainScheduleDto {
|
|||||||
@IsDateString()
|
@IsDateString()
|
||||||
scheduleDate!: string;
|
scheduleDate!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2026-06-22T08:00:00.000Z', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
arrivalDate?: string;
|
||||||
|
|
||||||
@ApiProperty({ format: 'uuid' })
|
@ApiProperty({ format: 'uuid' })
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
locomotiveId!: string;
|
locomotiveId!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: ['CONTAINER', 'BULK'], default: 'CONTAINER' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['CONTAINER', 'BULK'])
|
||||||
|
assignmentType?: 'CONTAINER' | 'BULK';
|
||||||
|
|
||||||
|
@ApiProperty({ type: [String] })
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMinSize(1)
|
||||||
|
@IsUUID('4', { each: true })
|
||||||
|
bookingIds?: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: [String], required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMinSize(1)
|
||||||
|
@IsUUID('4', { each: true })
|
||||||
|
wagonIds?: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { IsDateString, IsOptional, IsUUID } from 'class-validator';
|
import { IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
export class GetEligibleContainerBookingsDto {
|
export class GetEligibleContainerBookingsDto {
|
||||||
@ApiPropertyOptional({ format: 'uuid' })
|
@ApiPropertyOptional({ format: 'uuid' })
|
||||||
@@ -16,4 +16,14 @@ export class GetEligibleContainerBookingsDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsDateString()
|
@IsDateString()
|
||||||
scheduleDate?: string;
|
scheduleDate?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: ['CONTAINER', 'BULK'] })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['CONTAINER', 'BULK'])
|
||||||
|
assignmentType?: 'CONTAINER' | 'BULK';
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: ['IMPORT', 'EXPORT', 'DOMESTIC'] })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['IMPORT', 'EXPORT', 'DOMESTIC'])
|
||||||
|
tradeDirection?: 'IMPORT' | 'EXPORT' | 'DOMESTIC';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { ArrayMinSize, IsArray, IsDateString, IsUUID } from 'class-validator';
|
import { ArrayMinSize, IsArray, IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
export class PreviewContainerTrainScheduleDto {
|
export class PreviewContainerTrainScheduleDto {
|
||||||
@ApiProperty({ type: [String] })
|
@ApiProperty({ type: [String] })
|
||||||
@@ -19,4 +19,9 @@ export class PreviewContainerTrainScheduleDto {
|
|||||||
@ApiProperty({ format: 'uuid' })
|
@ApiProperty({ format: 'uuid' })
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
destinationStationId!: string;
|
destinationStationId!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: ['CONTAINER', 'BULK'], default: 'CONTAINER' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['CONTAINER', 'BULK'])
|
||||||
|
assignmentType?: 'CONTAINER' | 'BULK';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,4 +55,10 @@ export class TrainSchedulingController {
|
|||||||
cancelTrainSchedule(@Param('id', ParseUUIDPipe) id: string) {
|
cancelTrainSchedule(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.trainSchedulingService.cancelTrainSchedule(id);
|
return this.trainSchedulingService.cancelTrainSchedule(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('container/schedules/:id/publish')
|
||||||
|
@ApiOperation({ summary: 'Publish container train schedule' })
|
||||||
|
publishTrainSchedule(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.trainSchedulingService.publishTrainSchedule(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { LocomotivesModule } from '../locomotives/locomotives.module';
|
|||||||
import { Locomotive } from '../locomotives/entities/locomotive.entity';
|
import { Locomotive } from '../locomotives/entities/locomotive.entity';
|
||||||
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
||||||
import { WagonTypesModule } from '../wagon-types/wagon-types.module';
|
import { WagonTypesModule } from '../wagon-types/wagon-types.module';
|
||||||
|
import { Wagon } from '../wagons/entities/wagon.entity';
|
||||||
import { TrainSet } from '../train-sets/entities/train-set.entity';
|
import { TrainSet } from '../train-sets/entities/train-set.entity';
|
||||||
import { TrainSetWagon } from '../train-sets/entities/train-set-wagon.entity';
|
import { TrainSetWagon } from '../train-sets/entities/train-set-wagon.entity';
|
||||||
import { TrainSetsModule } from '../train-sets/train-sets.module';
|
import { TrainSetsModule } from '../train-sets/train-sets.module';
|
||||||
@@ -26,6 +27,7 @@ import { TrainSchedulingService } from './train-scheduling.service';
|
|||||||
BookingContainer,
|
BookingContainer,
|
||||||
Locomotive,
|
Locomotive,
|
||||||
WagonType,
|
WagonType,
|
||||||
|
Wagon,
|
||||||
TrainSet,
|
TrainSet,
|
||||||
TrainSetWagon,
|
TrainSetWagon,
|
||||||
TrainSchedule,
|
TrainSchedule,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const makeBooking = (
|
|||||||
scheduledDate: new Date(scheduledDate),
|
scheduledDate: new Date(scheduledDate),
|
||||||
originYardId,
|
originYardId,
|
||||||
destinationYardId,
|
destinationYardId,
|
||||||
status: 'PAID',
|
status: 'APPROVED',
|
||||||
customer: { companyName: 'Demo Customer' },
|
customer: { companyName: 'Demo Customer' },
|
||||||
originYard: { label: 'Djibouti', code: 'DJIBOUTI' },
|
originYard: { label: 'Djibouti', code: 'DJIBOUTI' },
|
||||||
destinationYard: { label: 'Addis Ababa', code: 'ADDIS_ABABA' },
|
destinationYard: { label: 'Addis Ababa', code: 'ADDIS_ABABA' },
|
||||||
@@ -164,11 +164,11 @@ describe('TrainSchedulingService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects bookings that are not in schedulable status', async () => {
|
it('rejects bookings that are not in assignable status', async () => {
|
||||||
const bookings = [
|
const bookings = [
|
||||||
{
|
{
|
||||||
...makeBooking('b7', 'BKG-CONT-007', 120, 2, '40FT'),
|
...makeBooking('b7', 'BKG-CONT-007', 120, 2, '40FT'),
|
||||||
status: 'APPROVED',
|
status: 'PAID',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ describe('TrainSchedulingService', () => {
|
|||||||
|
|
||||||
expect(result.valid).toBe(false);
|
expect(result.valid).toBe(false);
|
||||||
expect(result.violations).toContain(
|
expect(result.violations).toContain(
|
||||||
'Only PAID bookings can be scheduled; received: APPROVED',
|
'Only APPROVED, READY_FOR_ASSIGNMENT bookings can be assigned; received: PAID',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { TrainSet } from "../train-sets/entities/train-set.entity";
|
|||||||
import { Route } from "../routes/entities/route.entity";
|
import { Route } from "../routes/entities/route.entity";
|
||||||
import { TrainScheduleBooking } from "../train-schedules/entities/train-schedule-booking.entity";
|
import { TrainScheduleBooking } from "../train-schedules/entities/train-schedule-booking.entity";
|
||||||
import { TrainSchedule } from "../train-schedules/entities/train-schedule.entity";
|
import { TrainSchedule } from "../train-schedules/entities/train-schedule.entity";
|
||||||
|
import { WagonBookingAllocation } from "../train-schedules/entities/wagon-booking-allocation.entity";
|
||||||
|
import { Wagon } from "../wagons/entities/wagon.entity";
|
||||||
import { WagonType } from "../wagon-types/entities/wagon-type.entity";
|
import { WagonType } from "../wagon-types/entities/wagon-type.entity";
|
||||||
import { WagonTypesRepository } from "../wagon-types/wagon-types.repository";
|
import { WagonTypesRepository } from "../wagon-types/wagon-types.repository";
|
||||||
import { CreateContainerTrainScheduleDto } from "./dto/create-container-train-schedule.dto";
|
import { CreateContainerTrainScheduleDto } from "./dto/create-container-train-schedule.dto";
|
||||||
@@ -27,7 +29,10 @@ import { PreviewContainerTrainScheduleDto } from "./dto/preview-container-train-
|
|||||||
const DEFAULT_WAGON_TYPE_CODE = "NW5";
|
const DEFAULT_WAGON_TYPE_CODE = "NW5";
|
||||||
const MAX_TRAIN_WEIGHT_TONS = 3500;
|
const MAX_TRAIN_WEIGHT_TONS = 3500;
|
||||||
const MAX_TRAIN_LENGTH_METERS = 760;
|
const MAX_TRAIN_LENGTH_METERS = 760;
|
||||||
const SCHEDULABLE_BOOKING_STATUSES = ["PAID"] as const;
|
const ASSIGNABLE_BOOKING_STATUSES = ["APPROVED", "READY_FOR_ASSIGNMENT"] as const;
|
||||||
|
const EXCLUDED_BOOKING_STATUSES = ["CANCELLED", "COMPLETED", "IN_TRANSIT", "ARRIVED"] as const;
|
||||||
|
const MAX_CONTAINER_WAGONS = 53;
|
||||||
|
const MAX_BULK_WAGONS = 37;
|
||||||
|
|
||||||
type EligibleBookingItem = {
|
type EligibleBookingItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -94,11 +99,13 @@ export class TrainSchedulingService {
|
|||||||
"scheduleBooking",
|
"scheduleBooking",
|
||||||
"scheduleBooking.booking_id = booking.id",
|
"scheduleBooking.booking_id = booking.id",
|
||||||
)
|
)
|
||||||
.where("booking.freightType = :freightType", { freightType: "CONTAINER" })
|
.where("booking.freightType = :freightType", {
|
||||||
|
freightType: query.assignmentType ?? "CONTAINER",
|
||||||
|
})
|
||||||
.andWhere("scheduleBooking.id IS NULL");
|
.andWhere("scheduleBooking.id IS NULL");
|
||||||
|
|
||||||
queryBuilder.andWhere("booking.status IN (:...schedulableStatuses)", {
|
queryBuilder.andWhere("booking.status IN (:...assignableStatuses)", {
|
||||||
schedulableStatuses: SCHEDULABLE_BOOKING_STATUSES,
|
assignableStatuses: ASSIGNABLE_BOOKING_STATUSES,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (query.originStationId) {
|
if (query.originStationId) {
|
||||||
@@ -116,6 +123,26 @@ export class TrainSchedulingService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.tradeDirection === "IMPORT") {
|
||||||
|
queryBuilder.andWhere(
|
||||||
|
`(
|
||||||
|
lower(originYard.country) IN ('djibouti', 'djoubti', 'dj')
|
||||||
|
OR lower(originYard.code) LIKE '%djib%'
|
||||||
|
OR lower(originYard.label) LIKE '%djib%'
|
||||||
|
)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.tradeDirection === "EXPORT") {
|
||||||
|
queryBuilder.andWhere(
|
||||||
|
`(
|
||||||
|
lower(destinationYard.country) IN ('djibouti', 'djoubti', 'dj')
|
||||||
|
OR lower(destinationYard.code) LIKE '%djib%'
|
||||||
|
OR lower(destinationYard.label) LIKE '%djib%'
|
||||||
|
)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (query.scheduleDate) {
|
if (query.scheduleDate) {
|
||||||
queryBuilder.andWhere(
|
queryBuilder.andWhere(
|
||||||
`DATE(booking.scheduled_date AT TIME ZONE 'UTC') = :scheduleDate`,
|
`DATE(booking.scheduled_date AT TIME ZONE 'UTC') = :scheduleDate`,
|
||||||
@@ -141,7 +168,7 @@ export class TrainSchedulingService {
|
|||||||
container.containerType?.code ??
|
container.containerType?.code ??
|
||||||
"Container",
|
"Container",
|
||||||
)
|
)
|
||||||
.join(", ") ?? "Container",
|
.join(", ") ?? (booking.freightType === "BULK" ? "Bulk cargo" : "Container"),
|
||||||
quantity:
|
quantity:
|
||||||
booking.bookingContainers?.reduce(
|
booking.bookingContainers?.reduce(
|
||||||
(sum, container) => sum + Number(container.quantity ?? 0),
|
(sum, container) => sum + Number(container.quantity ?? 0),
|
||||||
@@ -180,11 +207,27 @@ export class TrainSchedulingService {
|
|||||||
|
|
||||||
async createContainerTrainSchedule(dto: CreateContainerTrainScheduleDto) {
|
async createContainerTrainSchedule(dto: CreateContainerTrainScheduleDto) {
|
||||||
const route = await this.getActiveRoute(dto.routeId);
|
const route = await this.getActiveRoute(dto.routeId);
|
||||||
|
const validation = dto.bookingIds?.length
|
||||||
|
? await this.validateContainerBookingsForScheduling({
|
||||||
|
bookingIds: dto.bookingIds,
|
||||||
|
scheduleDate: dto.scheduleDate,
|
||||||
|
originStationId: route.originYardId,
|
||||||
|
destinationStationId: route.destinationYardId,
|
||||||
|
assignmentType: dto.assignmentType ?? "CONTAINER",
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (validation && !validation.valid) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
message: "Train schedule assignment is invalid",
|
||||||
|
violations: validation.violations,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const locomotive = await this.selectOrValidateLocomotive(
|
const locomotive = await this.selectOrValidateLocomotive(
|
||||||
dto.locomotiveId,
|
dto.locomotiveId,
|
||||||
0,
|
validation?.summary.totalWeightTons ?? 0,
|
||||||
0,
|
validation?.summary.totalLengthMeters ?? 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
const createdSchedule = await this.dataSource.transaction(
|
const createdSchedule = await this.dataSource.transaction(
|
||||||
@@ -205,10 +248,28 @@ export class TrainSchedulingService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const trainSet = await this.buildEmptyTrainSet(
|
const selectedPhysicalWagons = validation
|
||||||
|
? await this.lockSelectedWagonsForSchedule(
|
||||||
|
manager,
|
||||||
|
dto.wagonIds ?? [],
|
||||||
|
validation.wagonPlan.length,
|
||||||
|
route,
|
||||||
|
dto.assignmentType ?? "CONTAINER",
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const trainSetResult = validation
|
||||||
|
? await this.buildTrainSet(
|
||||||
manager,
|
manager,
|
||||||
lockedLocomotive,
|
lockedLocomotive,
|
||||||
);
|
validation.wagonType,
|
||||||
|
validation.summary.totalWeightTons,
|
||||||
|
validation.summary.totalLengthMeters,
|
||||||
|
validation.wagonPlan,
|
||||||
|
selectedPhysicalWagons,
|
||||||
|
)
|
||||||
|
: { trainSet: await this.buildEmptyTrainSet(manager, lockedLocomotive), wagons: [] };
|
||||||
|
const { trainSet, wagons } = trainSetResult;
|
||||||
|
|
||||||
const schedule = manager.getRepository(TrainSchedule).create({
|
const schedule = manager.getRepository(TrainSchedule).create({
|
||||||
trainSetId: trainSet.id,
|
trainSetId: trainSet.id,
|
||||||
@@ -216,13 +277,51 @@ export class TrainSchedulingService {
|
|||||||
originStationId: route.originYardId,
|
originStationId: route.originYardId,
|
||||||
destinationStationId: route.destinationYardId,
|
destinationStationId: route.destinationYardId,
|
||||||
scheduledDepartureDate: new Date(dto.scheduleDate),
|
scheduledDepartureDate: new Date(dto.scheduleDate),
|
||||||
status: "DRAFT",
|
scheduledArrivalDate: dto.arrivalDate ? new Date(dto.arrivalDate) : null,
|
||||||
|
status: validation ? "READY" : "DRAFT",
|
||||||
});
|
});
|
||||||
|
|
||||||
const savedSchedule = await manager
|
const savedSchedule = await manager
|
||||||
.getRepository(TrainSchedule)
|
.getRepository(TrainSchedule)
|
||||||
.save(schedule);
|
.save(schedule);
|
||||||
|
|
||||||
|
if (validation) {
|
||||||
|
await manager.getRepository(TrainScheduleBooking).save(
|
||||||
|
validation.bookings.map((booking) =>
|
||||||
|
manager.getRepository(TrainScheduleBooking).create({
|
||||||
|
trainScheduleId: savedSchedule.id,
|
||||||
|
bookingId: booking.id,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const wagonBySequence = new Map(wagons.map((wagon) => [wagon.sequenceNo, wagon]));
|
||||||
|
const allocations = validation.wagonPlan.flatMap((wagonPlan) => {
|
||||||
|
const savedWagon = wagonBySequence.get(wagonPlan.sequenceNo);
|
||||||
|
if (!savedWagon) return [];
|
||||||
|
|
||||||
|
return wagonPlan.allocations.map((allocation) =>
|
||||||
|
manager.getRepository(WagonBookingAllocation).create({
|
||||||
|
trainSetWagonId: savedWagon.id,
|
||||||
|
bookingId: allocation.bookingId,
|
||||||
|
allocatedWeightTons: allocation.allocatedWeightTons,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allocations.length > 0) {
|
||||||
|
await manager.getRepository(WagonBookingAllocation).save(allocations);
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.getRepository(Booking).update(
|
||||||
|
{ id: In(validation.bookings.map((booking) => booking.id)) },
|
||||||
|
{
|
||||||
|
status: "INVOICED",
|
||||||
|
paymentStatus: "PENDING",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await locomotiveRepository.update(lockedLocomotive.id, {
|
await locomotiveRepository.update(lockedLocomotive.id, {
|
||||||
status: "ASSIGNED",
|
status: "ASSIGNED",
|
||||||
});
|
});
|
||||||
@@ -276,24 +375,32 @@ export class TrainSchedulingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nonContainerBookings = bookings.filter(
|
const nonContainerBookings = bookings.filter(
|
||||||
(booking) => booking.freightType !== "CONTAINER",
|
(booking) => booking.freightType !== (dto.assignmentType ?? "CONTAINER"),
|
||||||
);
|
);
|
||||||
if (nonContainerBookings.length > 0) {
|
if (nonContainerBookings.length > 0) {
|
||||||
violations.push(
|
violations.push(
|
||||||
"Only CONTAINER bookings are supported for train scheduling",
|
`Only ${dto.assignmentType ?? "CONTAINER"} bookings are supported for this assignment`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const invalidStatusBookings = bookings.filter(
|
const invalidStatusBookings = bookings.filter(
|
||||||
(booking) => !SCHEDULABLE_BOOKING_STATUSES.includes(booking.status as "PAID"),
|
(booking) =>
|
||||||
|
!ASSIGNABLE_BOOKING_STATUSES.includes(booking.status as (typeof ASSIGNABLE_BOOKING_STATUSES)[number]),
|
||||||
);
|
);
|
||||||
if (invalidStatusBookings.length > 0) {
|
if (invalidStatusBookings.length > 0) {
|
||||||
const invalidStatuses = [...new Set(invalidStatusBookings.map((booking) => booking.status))];
|
const invalidStatuses = [...new Set(invalidStatusBookings.map((booking) => booking.status))];
|
||||||
violations.push(
|
violations.push(
|
||||||
`Only ${SCHEDULABLE_BOOKING_STATUSES.join(", ")} bookings can be scheduled; received: ${invalidStatuses.join(", ")}`,
|
`Only ${ASSIGNABLE_BOOKING_STATUSES.join(", ")} bookings can be assigned; received: ${invalidStatuses.join(", ")}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const excludedStatusBookings = bookings.filter((booking) =>
|
||||||
|
EXCLUDED_BOOKING_STATUSES.includes(booking.status as (typeof EXCLUDED_BOOKING_STATUSES)[number]),
|
||||||
|
);
|
||||||
|
if (excludedStatusBookings.length > 0) {
|
||||||
|
violations.push(`Cancelled, completed, in-transit, or arrived bookings cannot be assigned`);
|
||||||
|
}
|
||||||
|
|
||||||
const scheduleDateKey = this.toUtcDateKey(dto.scheduleDate);
|
const scheduleDateKey = this.toUtcDateKey(dto.scheduleDate);
|
||||||
const routeMismatch = bookings.some(
|
const routeMismatch = bookings.some(
|
||||||
(booking) =>
|
(booking) =>
|
||||||
@@ -366,11 +473,11 @@ export class TrainSchedulingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
wagonType.maxWagonsPerTrain != null &&
|
wagonPlan.length >
|
||||||
wagonPlan.length > Number(wagonType.maxWagonsPerTrain)
|
(dto.assignmentType === "BULK" ? MAX_BULK_WAGONS : MAX_CONTAINER_WAGONS)
|
||||||
) {
|
) {
|
||||||
violations.push(
|
violations.push(
|
||||||
`Wagon count ${wagonPlan.length} exceeds wagon marshalling limit ${wagonType.maxWagonsPerTrain}`,
|
`Wagon count ${wagonPlan.length} exceeds ${dto.assignmentType === "BULK" ? "bulk" : "container"} limit ${dto.assignmentType === "BULK" ? MAX_BULK_WAGONS : MAX_CONTAINER_WAGONS}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,6 +581,82 @@ export class TrainSchedulingService {
|
|||||||
return locomotive;
|
return locomotive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async lockSelectedWagonsForSchedule(
|
||||||
|
manager: EntityManager,
|
||||||
|
wagonIds: string[],
|
||||||
|
requiredCount: number,
|
||||||
|
route: Route,
|
||||||
|
assignmentType: "CONTAINER" | "BULK",
|
||||||
|
) {
|
||||||
|
const uniqueWagonIds = [...new Set(wagonIds)];
|
||||||
|
|
||||||
|
if (uniqueWagonIds.length < requiredCount) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Select at least ${requiredCount} available wagons for this schedule`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wagons = await manager
|
||||||
|
.getRepository(Wagon)
|
||||||
|
.createQueryBuilder("wagon")
|
||||||
|
.leftJoinAndSelect("wagon.wagonType", "wagonType")
|
||||||
|
.where("wagon.id IN (:...wagonIds)", { wagonIds: uniqueWagonIds })
|
||||||
|
.setLock("pessimistic_write")
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
if (wagons.length !== uniqueWagonIds.length) {
|
||||||
|
throw new BadRequestException("One or more selected wagons were not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedStatus = this.expectedWagonStatusForRoute(route);
|
||||||
|
const allowedStatuses = new Set([
|
||||||
|
expectedStatus,
|
||||||
|
"AVAILABLE",
|
||||||
|
...(expectedStatus === "EXPORT_READY" ? ["IMPORT_READY"] : []),
|
||||||
|
]);
|
||||||
|
const invalidWagon = wagons.find(
|
||||||
|
(wagon) =>
|
||||||
|
wagon.trainId ||
|
||||||
|
wagon.status === "ASSIGNED" ||
|
||||||
|
wagon.currentLocationYardId !== route.originYardId ||
|
||||||
|
!allowedStatuses.has(wagon.status) ||
|
||||||
|
!this.wagonTypeSupportsAssignment(wagon, assignmentType),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (invalidWagon) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Wagon ${invalidWagon.wagonNumber} is not at the route origin or is not ready for this ${this.routeDirection(route).toLowerCase()} route`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wagonById = new Map(wagons.map((wagon) => [wagon.id, wagon]));
|
||||||
|
return uniqueWagonIds.slice(0, requiredCount).map((wagonId) => wagonById.get(wagonId)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private wagonTypeSupportsAssignment(wagon: Wagon, assignmentType: "CONTAINER" | "BULK") {
|
||||||
|
const supportedLoadTypes = wagon.wagonType?.supportedLoadTypes ?? [];
|
||||||
|
const normalized = supportedLoadTypes.map((loadType) => loadType.trim().toUpperCase());
|
||||||
|
return normalized.includes(assignmentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private routeDirection(route: Route) {
|
||||||
|
const originCountry = route.originYard?.country?.trim().toLowerCase();
|
||||||
|
const destinationCountry = route.destinationYard?.country?.trim().toLowerCase();
|
||||||
|
const isOriginEthiopia = originCountry === "ethiopia" || originCountry === "et";
|
||||||
|
const isDestinationEthiopia = destinationCountry === "ethiopia" || destinationCountry === "et";
|
||||||
|
|
||||||
|
if (!isOriginEthiopia && isDestinationEthiopia) return "IMPORT";
|
||||||
|
if (isOriginEthiopia && !isDestinationEthiopia) return "EXPORT";
|
||||||
|
return "DOMESTIC";
|
||||||
|
}
|
||||||
|
|
||||||
|
private expectedWagonStatusForRoute(route: Route) {
|
||||||
|
const direction = this.routeDirection(route);
|
||||||
|
if (direction === "IMPORT") return "IMPORT_READY";
|
||||||
|
if (direction === "EXPORT" || direction === "DOMESTIC") return "EXPORT_READY";
|
||||||
|
return "AVAILABLE";
|
||||||
|
}
|
||||||
|
|
||||||
async buildTrainSet(
|
async buildTrainSet(
|
||||||
manager: EntityManager,
|
manager: EntityManager,
|
||||||
locomotive: Locomotive,
|
locomotive: Locomotive,
|
||||||
@@ -481,6 +664,7 @@ export class TrainSchedulingService {
|
|||||||
totalWeightTons: number,
|
totalWeightTons: number,
|
||||||
totalLengthMeters: number,
|
totalLengthMeters: number,
|
||||||
wagonPlan: WagonPlanRecord[],
|
wagonPlan: WagonPlanRecord[],
|
||||||
|
physicalWagons: Wagon[] = [],
|
||||||
) {
|
) {
|
||||||
const trainSet = manager.getRepository(TrainSet).create({
|
const trainSet = manager.getRepository(TrainSet).create({
|
||||||
locomotiveId: locomotive.id,
|
locomotiveId: locomotive.id,
|
||||||
@@ -491,20 +675,35 @@ export class TrainSchedulingService {
|
|||||||
});
|
});
|
||||||
const savedTrainSet = await manager.getRepository(TrainSet).save(trainSet);
|
const savedTrainSet = await manager.getRepository(TrainSet).save(trainSet);
|
||||||
|
|
||||||
const wagons = wagonPlan.map((wagon) =>
|
const wagons = wagonPlan.map((wagon, index) => {
|
||||||
manager.getRepository(TrainSetWagon).create({
|
const physicalWagon = physicalWagons[index];
|
||||||
|
const selectedWagonType = physicalWagon?.wagonType ?? wagonType;
|
||||||
|
|
||||||
|
return manager.getRepository(TrainSetWagon).create({
|
||||||
trainSetId: savedTrainSet.id,
|
trainSetId: savedTrainSet.id,
|
||||||
wagonTypeId: wagonType.id,
|
wagonTypeId: selectedWagonType.id,
|
||||||
|
physicalWagonId: physicalWagon?.id ?? null,
|
||||||
sequenceNo: wagon.sequenceNo,
|
sequenceNo: wagon.sequenceNo,
|
||||||
capacityTons: wagon.capacityTons,
|
capacityTons: Number(selectedWagonType.capacityTons),
|
||||||
lengthMeters: wagon.lengthMeters,
|
lengthMeters: Number(selectedWagonType.lengthMeters),
|
||||||
assignedWeightTons: wagon.assignedWeightTons,
|
assignedWeightTons: wagon.assignedWeightTons,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedWagons = await manager.getRepository(TrainSetWagon).save(wagons);
|
||||||
|
|
||||||
|
if (physicalWagons.length > 0) {
|
||||||
|
await Promise.all(
|
||||||
|
physicalWagons.map((wagon, index) =>
|
||||||
|
manager.getRepository(Wagon).update(wagon.id, {
|
||||||
|
status: "ASSIGNED",
|
||||||
|
sequenceNumber: index + 1,
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await manager.getRepository(TrainSetWagon).save(wagons);
|
return { trainSet: savedTrainSet, wagons: savedWagons };
|
||||||
|
|
||||||
return savedTrainSet;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildEmptyTrainSet(
|
async buildEmptyTrainSet(
|
||||||
@@ -627,7 +826,7 @@ export class TrainSchedulingService {
|
|||||||
route: true,
|
route: true,
|
||||||
trainSet: {
|
trainSet: {
|
||||||
locomotive: true,
|
locomotive: true,
|
||||||
wagons: { wagonType: true, allocations: { booking: true } },
|
wagons: { wagonType: true, physicalWagon: true, allocations: { booking: true } },
|
||||||
},
|
},
|
||||||
originStation: true,
|
originStation: true,
|
||||||
destinationStation: true,
|
destinationStation: true,
|
||||||
@@ -696,6 +895,13 @@ export class TrainSchedulingService {
|
|||||||
name: wagon.wagonType.name,
|
name: wagon.wagonType.name,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
physicalWagon: wagon.physicalWagon
|
||||||
|
? {
|
||||||
|
id: wagon.physicalWagon.id,
|
||||||
|
wagonNumber: wagon.physicalWagon.wagonNumber,
|
||||||
|
status: wagon.physicalWagon.status,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
allocations:
|
allocations:
|
||||||
wagon.allocations?.map((allocation) => ({
|
wagon.allocations?.map((allocation) => ({
|
||||||
id: allocation.id,
|
id: allocation.id,
|
||||||
@@ -729,7 +935,7 @@ export class TrainSchedulingService {
|
|||||||
.getRepository(TrainSchedule)
|
.getRepository(TrainSchedule)
|
||||||
.findOne({
|
.findOne({
|
||||||
where: { id },
|
where: { id },
|
||||||
relations: { trainSet: { locomotive: true } },
|
relations: { trainSet: { locomotive: true, wagons: true } },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!schedule) {
|
if (!schedule) {
|
||||||
@@ -754,8 +960,55 @@ export class TrainSchedulingService {
|
|||||||
status: "AVAILABLE",
|
status: "AVAILABLE",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const physicalWagonIds =
|
||||||
|
schedule.trainSet?.wagons
|
||||||
|
?.map((wagon) => wagon.physicalWagonId)
|
||||||
|
.filter((wagonId): wagonId is string => Boolean(wagonId)) ?? [];
|
||||||
|
|
||||||
|
if (physicalWagonIds.length > 0) {
|
||||||
|
const physicalWagons = await manager.getRepository(Wagon).find({
|
||||||
|
where: { id: In(physicalWagonIds) },
|
||||||
|
relations: { currentLocationYard: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
physicalWagons.map((wagon) =>
|
||||||
|
manager.getRepository(Wagon).update(wagon.id, {
|
||||||
|
status: this.expectedWagonStatusForYard(wagon.currentLocationYard),
|
||||||
|
sequenceNumber: null,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.getContainerTrainScheduleById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async publishTrainSchedule(id: string) {
|
||||||
|
const schedule = await this.dataSource.getRepository(TrainSchedule).findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: { trainSet: true, scheduleBookings: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!schedule) {
|
||||||
|
throw new NotFoundException(`Train schedule ${id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schedule.status === "CANCELLED") {
|
||||||
|
throw new BadRequestException("Cancelled schedules cannot be published");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!schedule.trainSet || schedule.trainSet.wagonCount <= 0) {
|
||||||
|
throw new BadRequestException("Allocate wagons before publishing the schedule");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((schedule.scheduleBookings?.length ?? 0) === 0) {
|
||||||
|
throw new BadRequestException("Assign bookings before publishing the schedule");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.dataSource.getRepository(TrainSchedule).update(id, { status: "PUBLISHED" });
|
||||||
return this.getContainerTrainScheduleById(id);
|
return this.getContainerTrainScheduleById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,6 +1028,7 @@ export class TrainSchedulingService {
|
|||||||
private async getActiveRoute(routeId: string) {
|
private async getActiveRoute(routeId: string) {
|
||||||
const route = await this.dataSource.getRepository(Route).findOne({
|
const route = await this.dataSource.getRepository(Route).findOne({
|
||||||
where: { id: routeId },
|
where: { id: routeId },
|
||||||
|
relations: { originYard: true, destinationYard: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!route) {
|
if (!route) {
|
||||||
@@ -788,6 +1042,13 @@ export class TrainSchedulingService {
|
|||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private expectedWagonStatusForYard(yard?: { country?: string } | null) {
|
||||||
|
const country = yard?.country?.trim().toLowerCase();
|
||||||
|
if (country === "ethiopia" || country === "et") return "EXPORT_READY";
|
||||||
|
if (country === "djibouti" || country === "djoubti" || country === "dj") return "IMPORT_READY";
|
||||||
|
return "AVAILABLE";
|
||||||
|
}
|
||||||
|
|
||||||
private toUtcDateKey(value: Date | string) {
|
private toUtcDateKey(value: Date | string) {
|
||||||
const date = value instanceof Date ? value : new Date(value);
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
return date.toISOString().slice(0, 10);
|
return date.toISOString().slice(0, 10);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { BaseEntity } from '@edr/api-common';
|
|||||||
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||||
|
|
||||||
import { WagonBookingAllocation } from '../../train-schedules/entities/wagon-booking-allocation.entity';
|
import { WagonBookingAllocation } from '../../train-schedules/entities/wagon-booking-allocation.entity';
|
||||||
|
import { Wagon } from '../../wagons/entities/wagon.entity';
|
||||||
import { WagonType } from '../../wagon-types/entities/wagon-type.entity';
|
import { WagonType } from '../../wagon-types/entities/wagon-type.entity';
|
||||||
import { TrainSet } from './train-set.entity';
|
import { TrainSet } from './train-set.entity';
|
||||||
|
|
||||||
@@ -18,6 +19,13 @@ export class TrainSetWagon extends BaseEntity {
|
|||||||
@Column({ name: 'wagon_type_id', type: 'uuid' })
|
@Column({ name: 'wagon_type_id', type: 'uuid' })
|
||||||
wagonTypeId!: string;
|
wagonTypeId!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'physical_wagon_id', type: 'uuid', nullable: true })
|
||||||
|
physicalWagonId!: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => Wagon, { nullable: true, onDelete: 'SET NULL' })
|
||||||
|
@JoinColumn({ name: 'physical_wagon_id' })
|
||||||
|
physicalWagon?: Wagon | null;
|
||||||
|
|
||||||
@ManyToOne(() => WagonType, (wagonType) => wagonType.trainSetWagons)
|
@ManyToOne(() => WagonType, (wagonType) => wagonType.trainSetWagons)
|
||||||
@JoinColumn({ name: 'wagon_type_id' })
|
@JoinColumn({ name: 'wagon_type_id' })
|
||||||
wagonType?: WagonType;
|
wagonType?: WagonType;
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import {
|
|||||||
Min,
|
Min,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
const toNumber = ({ value }: { value: unknown }) =>
|
const toNumber = ({ value }: { value: unknown }) =>
|
||||||
value === '' || value == null ? value : Number(value);
|
value === '' || value == null ? value : Number(value);
|
||||||
|
|
||||||
|
const toOptionalNumber = ({ value }: { value: unknown }) =>
|
||||||
|
value === '' || value == null ? undefined : Number(value);
|
||||||
|
|
||||||
const toBoolean = ({ value }: { value: unknown }) => {
|
const toBoolean = ({ value }: { value: unknown }) => {
|
||||||
if (typeof value === 'boolean') return value;
|
if (typeof value === 'boolean') return value;
|
||||||
if (value === 'true') return true;
|
if (value === 'true') return true;
|
||||||
@@ -23,7 +25,9 @@ const toBoolean = ({ value }: { value: unknown }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toStringArray = ({ value }: { value: unknown }) => {
|
const toStringArray = ({ value }: { value: unknown }) => {
|
||||||
if (Array.isArray(value)) return value;
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((entry) => String(entry).trim()).filter(Boolean);
|
||||||
|
}
|
||||||
if (typeof value !== 'string') return [];
|
if (typeof value !== 'string') return [];
|
||||||
return value
|
return value
|
||||||
.split(',')
|
.split(',')
|
||||||
@@ -37,71 +41,28 @@ export class CreateWagonTypeDto {
|
|||||||
@MaxLength(32)
|
@MaxLength(32)
|
||||||
code!: string;
|
code!: string;
|
||||||
|
|
||||||
@ApiProperty({ maxLength: 100, example: 'Flat wagon' })
|
|
||||||
=======
|
|
||||||
const parseLoadTypes = (value: unknown): string[] => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.map((item) => String(item).trim()).filter(Boolean);
|
|
||||||
}
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return value
|
|
||||||
.split(',')
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
|
|
||||||
export class CreateWagonTypeDto {
|
|
||||||
@ApiProperty({ description: 'Display name, e.g. "Flat Wagon"', maxLength: 100 })
|
@ApiProperty({ description: 'Display name, e.g. "Flat Wagon"', maxLength: 100 })
|
||||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
<<<<<<< HEAD
|
@ApiProperty({ description: 'Maximum payload capacity in metric tons', example: 60 })
|
||||||
@ApiProperty({ example: 60 })
|
|
||||||
@Transform(toNumber)
|
@Transform(toNumber)
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(0)
|
|
||||||
capacityTons!: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 14.2 })
|
|
||||||
@Transform(toNumber)
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
lengthMeters!: number;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 45 })
|
|
||||||
@IsOptional()
|
|
||||||
@Transform(toNumber)
|
|
||||||
@IsInt()
|
|
||||||
@Min(1)
|
|
||||||
maxWagonsPerTrain?: number;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ type: [String], example: ['container', 'break-bulk'] })
|
|
||||||
@IsOptional()
|
|
||||||
@Transform(toStringArray)
|
|
||||||
@IsArray()
|
|
||||||
@IsString({ each: true })
|
|
||||||
=======
|
|
||||||
@ApiProperty({ description: 'Maximum payload capacity in metric tons' })
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0.001)
|
@Min(0.001)
|
||||||
@Transform(({ value }) => Number(value))
|
|
||||||
capacityTons!: number;
|
capacityTons!: number;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Wagon length in meters' })
|
@ApiProperty({ description: 'Wagon length in meters', example: 14.2 })
|
||||||
|
@Transform(toNumber)
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(0.001)
|
@Min(0.001)
|
||||||
@Transform(({ value }) => Number(value))
|
|
||||||
lengthMeters!: number;
|
lengthMeters!: number;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Maximum wagons of this type per train' })
|
@ApiPropertyOptional({ description: 'Maximum wagons of this type per train', example: 45 })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@Transform(toOptionalNumber)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
@Transform(({ value }) => (value === '' || value === null || value === undefined ? undefined : Number(value)))
|
|
||||||
maxWagonsPerTrain?: number;
|
maxWagonsPerTrain?: number;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
@@ -110,18 +71,14 @@ export class CreateWagonTypeDto {
|
|||||||
default: [],
|
default: [],
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@Transform(toStringArray)
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
@Transform(({ value }) => parseLoadTypes(value))
|
|
||||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
|
||||||
supportedLoadTypes?: string[];
|
supportedLoadTypes?: string[];
|
||||||
|
|
||||||
@ApiPropertyOptional({ default: true })
|
@ApiPropertyOptional({ default: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
<<<<<<< HEAD
|
|
||||||
@Transform(toBoolean)
|
@Transform(toBoolean)
|
||||||
=======
|
|
||||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,18 +11,9 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
<<<<<<< HEAD
|
|
||||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
|
||||||
import { CreateWagonTypeDto } from './dto/create-wagon-type.dto';
|
|
||||||
import { UpdateWagonTypeDto } from './dto/update-wagon-type.dto';
|
|
||||||
import { WagonTypesService } from './wagon-types.service';
|
|
||||||
import { WagonType } from './entities/wagon-type.entity';
|
|
||||||
=======
|
|
||||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
|
||||||
|
|
||||||
import { RuleEngineManage, RuleEngineView } from '../../common/rule-engine-guards';
|
import { RuleEngineManage, RuleEngineView } from '../../common/rule-engine-guards';
|
||||||
|
|
||||||
import { CreateWagonTypeDto } from './dto/create-wagon-type.dto';
|
import { CreateWagonTypeDto } from './dto/create-wagon-type.dto';
|
||||||
import { UpdateWagonTypeDto } from './dto/update-wagon-type.dto';
|
import { UpdateWagonTypeDto } from './dto/update-wagon-type.dto';
|
||||||
import { WagonTypesService } from './wagon-types.service';
|
import { WagonTypesService } from './wagon-types.service';
|
||||||
@@ -33,41 +24,11 @@ import { WagonTypesService } from './wagon-types.service';
|
|||||||
export class WagonTypesController {
|
export class WagonTypesController {
|
||||||
constructor(private readonly wagonTypesService: WagonTypesService) {}
|
constructor(private readonly wagonTypesService: WagonTypesService) {}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
@Post()
|
|
||||||
@ApiOperation({ summary: 'Create a wagon type' })
|
|
||||||
async create(@Body() dto: CreateWagonTypeDto): Promise<WagonType> {
|
|
||||||
return this.wagonTypesService.create(dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@ApiOperation({ summary: 'Get wagon types' })
|
|
||||||
async findAll(@Query() query: Record<string, string | undefined>): Promise<WagonType[]> {
|
|
||||||
return this.wagonTypesService.findAll(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':id')
|
|
||||||
@ApiOperation({ summary: 'Get a wagon type by ID' })
|
|
||||||
async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<WagonType> {
|
|
||||||
return this.wagonTypesService.findById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch(':id')
|
|
||||||
@ApiOperation({ summary: 'Update a wagon type' })
|
|
||||||
async update(
|
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
|
||||||
@Body() dto: UpdateWagonTypeDto,
|
|
||||||
): Promise<WagonType> {
|
|
||||||
=======
|
|
||||||
@Get()
|
@Get()
|
||||||
@RuleEngineView('wagon-types')
|
@RuleEngineView('wagon-types')
|
||||||
@ApiOperation({ summary: 'List wagon types' })
|
@ApiOperation({ summary: 'List wagon types' })
|
||||||
findAll(@Query() query: Record<string, string>) {
|
findAll(@Query() query: Record<string, string | undefined>) {
|
||||||
return this.wagonTypesService.findAll({
|
return this.wagonTypesService.findAll(query);
|
||||||
isActive: query['isActive'] !== undefined ? query['isActive'] === 'true' : undefined,
|
|
||||||
page: query['page'] ? parseInt(query['page'], 10) : undefined,
|
|
||||||
pageSize: query['pageSize'] ? parseInt(query['pageSize'], 10) : undefined,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@@ -88,21 +49,14 @@ export class WagonTypesController {
|
|||||||
@RuleEngineManage('wagon-types')
|
@RuleEngineManage('wagon-types')
|
||||||
@ApiOperation({ summary: 'Update a wagon type' })
|
@ApiOperation({ summary: 'Update a wagon type' })
|
||||||
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWagonTypeDto) {
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWagonTypeDto) {
|
||||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
|
||||||
return this.wagonTypesService.update(id, dto);
|
return this.wagonTypesService.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
<<<<<<< HEAD
|
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
|
||||||
@ApiOperation({ summary: 'Deactivate a wagon type' })
|
|
||||||
async remove(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
|
|
||||||
=======
|
|
||||||
@RuleEngineManage('wagon-types')
|
@RuleEngineManage('wagon-types')
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
@ApiOperation({ summary: 'Soft-delete a wagon type' })
|
@ApiOperation({ summary: 'Soft-delete a wagon type' })
|
||||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
|
||||||
return this.wagonTypesService.remove(id);
|
return this.wagonTypesService.remove(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +1,23 @@
|
|||||||
<<<<<<< HEAD
|
|
||||||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { FindOptionsOrder } from 'typeorm';
|
import { FindOptionsOrder } from 'typeorm';
|
||||||
|
|
||||||
=======
|
|
||||||
import {
|
|
||||||
ConflictException,
|
|
||||||
Injectable,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { generateCode } from '../../common/utils/generate-code.util';
|
|
||||||
|
|
||||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
|
||||||
import { CreateWagonTypeDto } from './dto/create-wagon-type.dto';
|
import { CreateWagonTypeDto } from './dto/create-wagon-type.dto';
|
||||||
import { UpdateWagonTypeDto } from './dto/update-wagon-type.dto';
|
import { UpdateWagonTypeDto } from './dto/update-wagon-type.dto';
|
||||||
import { WagonType } from './entities/wagon-type.entity';
|
import { WagonType } from './entities/wagon-type.entity';
|
||||||
import { WagonTypesRepository } from './wagon-types.repository';
|
import { WagonTypesRepository } from './wagon-types.repository';
|
||||||
|
|
||||||
|
type WagonTypeListResponse = {
|
||||||
|
data: WagonType[];
|
||||||
|
meta: { total: number; page: number; pageSize: number; totalPages: number };
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WagonTypesService {
|
export class WagonTypesService {
|
||||||
constructor(private readonly wagonTypesRepository: WagonTypesRepository) {}
|
constructor(private readonly wagonTypesRepository: WagonTypesRepository) {}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
async findAll(query: Record<string, string | undefined> = {}): Promise<WagonTypeListResponse> {
|
||||||
async create(dto: CreateWagonTypeDto): Promise<WagonType> {
|
const page = Math.max(1, Number(query.page) || 1);
|
||||||
const code = dto.code.trim().toUpperCase();
|
const pageSize = Math.max(1, Number(query.pageSize) || 20);
|
||||||
const existing = await this.wagonTypesRepository.findAll({ where: { code } });
|
|
||||||
if (existing.length > 0) {
|
|
||||||
throw new ConflictException(`Wagon type code "${code}" already exists`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.wagonTypesRepository.create({
|
|
||||||
...dto,
|
|
||||||
code,
|
|
||||||
name: dto.name.trim(),
|
|
||||||
supportedLoadTypes: dto.supportedLoadTypes ?? [],
|
|
||||||
isActive: dto.isActive ?? true,
|
|
||||||
=======
|
|
||||||
async findAll(filter: {
|
|
||||||
isActive?: boolean;
|
|
||||||
page?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
} = {}): Promise<{
|
|
||||||
data: WagonType[];
|
|
||||||
meta: { total: number; page: number; pageSize: number; totalPages: number };
|
|
||||||
}> {
|
|
||||||
const page = filter.page ?? 1;
|
|
||||||
const pageSize = filter.pageSize ?? 20;
|
|
||||||
const where: Record<string, unknown> = {};
|
|
||||||
if (filter.isActive !== undefined) {
|
|
||||||
where.isActive = filter.isActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [data, total] = await this.wagonTypesRepository.findAndCount({
|
|
||||||
where,
|
|
||||||
order: { code: 'ASC' },
|
|
||||||
skip: (page - 1) * pageSize,
|
|
||||||
take: pageSize,
|
|
||||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
meta: {
|
|
||||||
total,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
totalPages: Math.max(1, Math.ceil(total / pageSize)),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(id: string): Promise<WagonType> {
|
|
||||||
const wagonType = await this.wagonTypesRepository.findById(id);
|
|
||||||
if (!wagonType) {
|
|
||||||
throw new NotFoundException(`Wagon type ${id} not found`);
|
|
||||||
}
|
|
||||||
return wagonType;
|
|
||||||
}
|
|
||||||
|
|
||||||
async findAll(query: Record<string, string | undefined> = {}): Promise<WagonType[]> {
|
|
||||||
const isActive =
|
const isActive =
|
||||||
query.isActive === 'all'
|
query.isActive === 'all'
|
||||||
? undefined
|
? undefined
|
||||||
@@ -92,19 +31,29 @@ export class WagonTypesService {
|
|||||||
: 'code';
|
: 'code';
|
||||||
const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
||||||
|
|
||||||
return this.wagonTypesRepository.findAll({
|
const [data, total] = await this.wagonTypesRepository.findAndCount({
|
||||||
where: isActive === undefined ? {} : { isActive },
|
where: isActive === undefined ? {} : { isActive },
|
||||||
order: { [sortBy]: sortOrder } as FindOptionsOrder<WagonType>,
|
order: { [sortBy]: sortOrder } as FindOptionsOrder<WagonType>,
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta: {
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages: Math.ceil(total / pageSize),
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(id: string): Promise<WagonType> {
|
async findById(id: string): Promise<WagonType> {
|
||||||
const wagonType = await this.wagonTypesRepository.findById(id);
|
const wagonType = await this.wagonTypesRepository.findById(id);
|
||||||
|
|
||||||
if (!wagonType) {
|
if (!wagonType) {
|
||||||
throw new NotFoundException(`Wagon type ${id} not found`);
|
throw new NotFoundException(`Wagon type ${id} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return wagonType;
|
return wagonType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,14 +65,30 @@ export class WagonTypesService {
|
|||||||
return wagonType;
|
return wagonType;
|
||||||
}
|
}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
async create(dto: CreateWagonTypeDto): Promise<WagonType> {
|
||||||
|
const code = dto.code.trim().toUpperCase();
|
||||||
|
const existing = await this.wagonTypesRepository.findByCode(code);
|
||||||
|
if (existing) {
|
||||||
|
throw new ConflictException(`Wagon type code "${code}" already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.wagonTypesRepository.create({
|
||||||
|
...dto,
|
||||||
|
code,
|
||||||
|
name: dto.name.trim(),
|
||||||
|
maxWagonsPerTrain: dto.maxWagonsPerTrain ?? null,
|
||||||
|
supportedLoadTypes: dto.supportedLoadTypes ?? [],
|
||||||
|
isActive: dto.isActive ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async update(id: string, dto: UpdateWagonTypeDto): Promise<WagonType> {
|
async update(id: string, dto: UpdateWagonTypeDto): Promise<WagonType> {
|
||||||
const wagonType = await this.findById(id);
|
const wagonType = await this.findById(id);
|
||||||
const nextCode = dto.code?.trim().toUpperCase();
|
const nextCode = dto.code?.trim().toUpperCase();
|
||||||
|
|
||||||
if (nextCode && nextCode !== wagonType.code) {
|
if (nextCode && nextCode !== wagonType.code) {
|
||||||
const existing = await this.wagonTypesRepository.findAll({ where: { code: nextCode } });
|
const existing = await this.wagonTypesRepository.findByCode(nextCode);
|
||||||
if (existing.length > 0) {
|
if (existing) {
|
||||||
throw new ConflictException(`Wagon type code "${nextCode}" already exists`);
|
throw new ConflictException(`Wagon type code "${nextCode}" already exists`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,43 +103,11 @@ export class WagonTypesService {
|
|||||||
throw new NotFoundException(`Wagon type ${id} not found`);
|
throw new NotFoundException(`Wagon type ${id} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
=======
|
|
||||||
async create(dto: CreateWagonTypeDto): Promise<WagonType> {
|
|
||||||
const code = generateCode(dto.name);
|
|
||||||
const existing = await this.wagonTypesRepository.findByCode(code);
|
|
||||||
if (existing) {
|
|
||||||
throw new ConflictException(
|
|
||||||
`Wagon type with name "${dto.name}" conflicts with existing code "${code}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.wagonTypesRepository.create({
|
|
||||||
code,
|
|
||||||
name: dto.name,
|
|
||||||
capacityTons: dto.capacityTons,
|
|
||||||
lengthMeters: dto.lengthMeters,
|
|
||||||
maxWagonsPerTrain: dto.maxWagonsPerTrain ?? null,
|
|
||||||
supportedLoadTypes: dto.supportedLoadTypes ?? [],
|
|
||||||
isActive: dto.isActive ?? true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(id: string, dto: UpdateWagonTypeDto): Promise<WagonType> {
|
|
||||||
await this.findById(id);
|
|
||||||
const updated = await this.wagonTypesRepository.update(id, dto);
|
|
||||||
if (!updated) {
|
|
||||||
throw new NotFoundException(`Wagon type ${id} not found`);
|
|
||||||
}
|
|
||||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(id: string): Promise<void> {
|
async remove(id: string): Promise<void> {
|
||||||
await this.findById(id);
|
await this.findById(id);
|
||||||
<<<<<<< HEAD
|
|
||||||
await this.wagonTypesRepository.update(id, { isActive: false });
|
|
||||||
=======
|
|
||||||
await this.wagonTypesRepository.softDelete(id);
|
await this.wagonTypesRepository.softDelete(id);
|
||||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export class CreateWagonDto {
|
|||||||
@Min(1)
|
@Min(1)
|
||||||
sequenceNumber?: number;
|
sequenceNumber?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
currentLocationYardId?: string;
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
tareWeight!: number;
|
tareWeight!: number;
|
||||||
@@ -25,7 +29,7 @@ export class CreateWagonDto {
|
|||||||
maxPayloadWeight!: number;
|
maxPayloadWeight!: number;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsIn(['AVAILABLE', 'ASSIGNED', 'MAINTENANCE', 'RETIRED'])
|
@IsIn(['AVAILABLE', 'IMPORT_READY', 'EXPORT_READY', 'ASSIGNED', 'MAINTENANCE', 'RETIRED'])
|
||||||
status?: string;
|
status?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { Entity, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
|
|||||||
import { BaseEntity } from '@edr/api-common';
|
import { BaseEntity } from '@edr/api-common';
|
||||||
import { Train } from '../../trains/entities/train.entity';
|
import { Train } from '../../trains/entities/train.entity';
|
||||||
import { Container } from '../../container-management/entities/container.entity';
|
import { Container } from '../../container-management/entities/container.entity';
|
||||||
|
import { Yard } from '../../rule-engine/entities/yard.entity';
|
||||||
|
import { WagonType } from '../../wagon-types/entities/wagon-type.entity';
|
||||||
|
|
||||||
@Entity({ name: 'wagons', schema: 'freight' })
|
@Entity({ name: 'wagons', schema: 'freight' })
|
||||||
export class Wagon extends BaseEntity {
|
export class Wagon extends BaseEntity {
|
||||||
@@ -12,12 +14,23 @@ export class Wagon extends BaseEntity {
|
|||||||
@Column({ name: 'wagon_type_id', type: 'uuid' })
|
@Column({ name: 'wagon_type_id', type: 'uuid' })
|
||||||
wagonTypeId!: string;
|
wagonTypeId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => WagonType, { onDelete: 'RESTRICT' })
|
||||||
|
@JoinColumn({ name: 'wagon_type_id' })
|
||||||
|
wagonType?: WagonType;
|
||||||
|
|
||||||
@Column({ name: 'train_id', type: 'uuid', nullable: true })
|
@Column({ name: 'train_id', type: 'uuid', nullable: true })
|
||||||
trainId!: string | null;
|
trainId!: string | null;
|
||||||
|
|
||||||
@Column({ name: 'sequence_number', type: 'int', nullable: true })
|
@Column({ name: 'sequence_number', type: 'int', nullable: true })
|
||||||
sequenceNumber!: number | null;
|
sequenceNumber!: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'current_location_yard_id', type: 'uuid', nullable: true })
|
||||||
|
currentLocationYardId!: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => Yard, { nullable: true, onDelete: 'SET NULL' })
|
||||||
|
@JoinColumn({ name: 'current_location_yard_id' })
|
||||||
|
currentLocationYard?: Yard | null;
|
||||||
|
|
||||||
@Column({ name: 'tare_weight', type: 'decimal', precision: 10, scale: 2 })
|
@Column({ name: 'tare_weight', type: 'decimal', precision: 10, scale: 2 })
|
||||||
tareWeight!: number;
|
tareWeight!: number;
|
||||||
|
|
||||||
@@ -25,7 +38,7 @@ export class Wagon extends BaseEntity {
|
|||||||
maxPayloadWeight!: number;
|
maxPayloadWeight!: number;
|
||||||
|
|
||||||
@Column({ type: 'varchar', default: 'AVAILABLE' })
|
@Column({ type: 'varchar', default: 'AVAILABLE' })
|
||||||
status!: string; // AVAILABLE, ASSIGNED, MAINTENANCE, RETIRED
|
status!: string; // AVAILABLE, IMPORT_READY, EXPORT_READY, ASSIGNED, MAINTENANCE, RETIRED
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
@Column({ type: 'text', nullable: true })
|
||||||
notes!: string | null;
|
notes!: string | null;
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Wagon } from './entities/wagon.entity';
|
import { Wagon } from './entities/wagon.entity';
|
||||||
import { Train } from '../trains/entities/train.entity';
|
import { Train } from '../trains/entities/train.entity';
|
||||||
|
import { Yard } from '../rule-engine/entities/yard.entity';
|
||||||
import { WagonsController, TrainWagonsReorderController } from './wagons.controller';
|
import { WagonsController, TrainWagonsReorderController } from './wagons.controller';
|
||||||
import { WagonsService } from './wagons.service';
|
import { WagonsService } from './wagons.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Wagon, Train])],
|
imports: [TypeOrmModule.forFeature([Wagon, Train, Yard])],
|
||||||
controllers: [WagonsController, TrainWagonsReorderController],
|
controllers: [WagonsController, TrainWagonsReorderController],
|
||||||
providers: [WagonsService],
|
providers: [WagonsService],
|
||||||
exports: [WagonsService],
|
exports: [WagonsService],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { AssignWagonToTrainDto } from './dto/assign-wagon-to-train.dto';
|
|||||||
import { ReorderWagonsDto } from './dto/reorder-wagons.dto';
|
import { ReorderWagonsDto } from './dto/reorder-wagons.dto';
|
||||||
import { Wagon } from './entities/wagon.entity';
|
import { Wagon } from './entities/wagon.entity';
|
||||||
import { Train } from '../trains/entities/train.entity';
|
import { Train } from '../trains/entities/train.entity';
|
||||||
|
import { Yard } from '../rule-engine/entities/yard.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WagonsService {
|
export class WagonsService {
|
||||||
@@ -15,6 +16,8 @@ export class WagonsService {
|
|||||||
private readonly wagonRepo: Repository<Wagon>,
|
private readonly wagonRepo: Repository<Wagon>,
|
||||||
@InjectRepository(Train)
|
@InjectRepository(Train)
|
||||||
private readonly trainRepo: Repository<Train>,
|
private readonly trainRepo: Repository<Train>,
|
||||||
|
@InjectRepository(Yard)
|
||||||
|
private readonly yardRepo: Repository<Yard>,
|
||||||
private readonly dataSource: DataSource,
|
private readonly dataSource: DataSource,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -23,6 +26,7 @@ export class WagonsService {
|
|||||||
// Convert undefined to null for nullable fields
|
// Convert undefined to null for nullable fields
|
||||||
if (dto.trainId === undefined) wagon.trainId = null;
|
if (dto.trainId === undefined) wagon.trainId = null;
|
||||||
if (dto.sequenceNumber === undefined) wagon.sequenceNumber = null;
|
if (dto.sequenceNumber === undefined) wagon.sequenceNumber = null;
|
||||||
|
wagon.status = await this.statusForLocation(dto.currentLocationYardId, dto.status);
|
||||||
return this.wagonRepo.save(wagon);
|
return this.wagonRepo.save(wagon);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,12 +35,14 @@ export class WagonsService {
|
|||||||
const search = query.search?.trim();
|
const search = query.search?.trim();
|
||||||
const status = query.status?.trim();
|
const status = query.status?.trim();
|
||||||
const trainId = query.trainId?.trim();
|
const trainId = query.trainId?.trim();
|
||||||
|
const currentLocationYardId = query.currentLocationYardId?.trim();
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
where.push({
|
where.push({
|
||||||
wagonNumber: ILike(`%${search}%`),
|
wagonNumber: ILike(`%${search}%`),
|
||||||
...(status ? { status } : {}),
|
...(status ? { status } : {}),
|
||||||
...(trainId ? { trainId } : {}),
|
...(trainId ? { trainId } : {}),
|
||||||
|
...(currentLocationYardId ? { currentLocationYardId } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +52,8 @@ export class WagonsService {
|
|||||||
const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
||||||
|
|
||||||
return this.wagonRepo.find({
|
return this.wagonRepo.find({
|
||||||
where: search ? where : { ...(status ? { status } : {}), ...(trainId ? { trainId } : {}) },
|
where: search ? where : { ...(status ? { status } : {}), ...(trainId ? { trainId } : {}), ...(currentLocationYardId ? { currentLocationYardId } : {}) },
|
||||||
|
relations: { currentLocationYard: true, wagonType: true },
|
||||||
order: { [sortBy]: sortOrder } as FindOptionsOrder<Wagon>,
|
order: { [sortBy]: sortOrder } as FindOptionsOrder<Wagon>,
|
||||||
skip: query.page && query.limit ? (Number(query.page) - 1) * Number(query.limit) : undefined,
|
skip: query.page && query.limit ? (Number(query.page) - 1) * Number(query.limit) : undefined,
|
||||||
take: query.limit ? Number(query.limit) : undefined,
|
take: query.limit ? Number(query.limit) : undefined,
|
||||||
@@ -54,7 +61,7 @@ export class WagonsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findById(id: string): Promise<Wagon> {
|
async findById(id: string): Promise<Wagon> {
|
||||||
const wagon = await this.wagonRepo.findOne({ where: { id } });
|
const wagon = await this.wagonRepo.findOne({ where: { id }, relations: { currentLocationYard: true, wagonType: true } });
|
||||||
if (!wagon) throw new NotFoundException(`Wagon ${id} not found`);
|
if (!wagon) throw new NotFoundException(`Wagon ${id} not found`);
|
||||||
return wagon;
|
return wagon;
|
||||||
}
|
}
|
||||||
@@ -62,6 +69,9 @@ export class WagonsService {
|
|||||||
async update(id: string, dto: UpdateWagonDto): Promise<Wagon> {
|
async update(id: string, dto: UpdateWagonDto): Promise<Wagon> {
|
||||||
const wagon = await this.findById(id);
|
const wagon = await this.findById(id);
|
||||||
Object.assign(wagon, dto);
|
Object.assign(wagon, dto);
|
||||||
|
if (dto.currentLocationYardId !== undefined) {
|
||||||
|
wagon.status = await this.statusForLocation(dto.currentLocationYardId, dto.status);
|
||||||
|
}
|
||||||
return this.wagonRepo.save(wagon);
|
return this.wagonRepo.save(wagon);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,10 +109,21 @@ export class WagonsService {
|
|||||||
const wagon = await this.findById(wagonId);
|
const wagon = await this.findById(wagonId);
|
||||||
wagon.trainId = null;
|
wagon.trainId = null;
|
||||||
wagon.sequenceNumber = null;
|
wagon.sequenceNumber = null;
|
||||||
wagon.status = 'AVAILABLE';
|
wagon.status = await this.statusForLocation(wagon.currentLocationYardId, 'AVAILABLE');
|
||||||
return this.wagonRepo.save(wagon);
|
return this.wagonRepo.save(wagon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async statusForLocation(yardId?: string | null, fallback = 'AVAILABLE') {
|
||||||
|
if (!yardId) return fallback;
|
||||||
|
|
||||||
|
const yard = await this.yardRepo.findOne({ where: { id: yardId } });
|
||||||
|
const country = yard?.country?.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (country === 'ethiopia' || country === 'et') return 'EXPORT_READY';
|
||||||
|
if (country === 'djibouti' || country === 'djoubti' || country === 'dj') return 'IMPORT_READY';
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
async reorderWagons(_trainId: string, dto: ReorderWagonsDto): Promise<void> {
|
async reorderWagons(_trainId: string, dto: ReorderWagonsDto): Promise<void> {
|
||||||
const queryRunner = this.dataSource.createQueryRunner();
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
|
|||||||
47
apps/edr-freight-api/src/scripts/seed-edr-wagons.ts
Normal file
47
apps/edr-freight-api/src/scripts/seed-edr-wagons.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { AppDataSource } from '../data-source';
|
||||||
|
import { SeedEdRWagonFleet1750400000000 } from '../migrations/1750400000000-SeedEdRWagonFleet';
|
||||||
|
|
||||||
|
async function seedEdRWagons() {
|
||||||
|
await AppDataSource.initialize();
|
||||||
|
|
||||||
|
const queryRunner = AppDataSource.createQueryRunner();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
await new SeedEdRWagonFleet1750400000000().up(queryRunner);
|
||||||
|
|
||||||
|
const [summary] = await queryRunner.query(`
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::int AS total,
|
||||||
|
COUNT(*) FILTER (WHERE wt.code = 'PW2')::int AS pw2,
|
||||||
|
COUNT(*) FILTER (WHERE wt.code = 'CW4')::int AS cw4,
|
||||||
|
COUNT(*) FILTER (WHERE wt.code = 'CW3')::int AS cw3,
|
||||||
|
COUNT(*) FILTER (WHERE wt.code = 'KW2')::int AS kw2,
|
||||||
|
COUNT(*) FILTER (WHERE wt.code = 'KW3')::int AS kw3,
|
||||||
|
COUNT(*) FILTER (WHERE wt.code = 'NW5')::int AS nw5,
|
||||||
|
COUNT(*) FILTER (WHERE wt.supported_load_types @> ARRAY['CONTAINER'])::int AS container_ready,
|
||||||
|
COUNT(*) FILTER (WHERE wt.supported_load_types @> ARRAY['BULK'])::int AS bulk_ready,
|
||||||
|
COUNT(*) FILTER (WHERE w.status = 'IMPORT_READY')::int AS import_ready
|
||||||
|
FROM freight.wagons w
|
||||||
|
JOIN freight.wagon_types wt ON wt.id = w.wagon_type_id
|
||||||
|
WHERE w.wagon_number BETWEEN 'ER0001' AND 'ER0940';
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
console.log('Seeded EDR wagon fleet:', summary);
|
||||||
|
} catch (error) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
await AppDataSource.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seedEdRWagons().catch((error) => {
|
||||||
|
console.error('Failed to seed EDR wagon fleet:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -36,21 +36,12 @@ import RuleEngineLegacyRedirect from "./pages/ruleEngine/RuleEngineLegacyRedirec
|
|||||||
import RuleEngineResourcePage from "./pages/ruleEngine/RuleEngineResourcePage";
|
import RuleEngineResourcePage from "./pages/ruleEngine/RuleEngineResourcePage";
|
||||||
import TrainsPage from "./pages/trains/TrainsPage";
|
import TrainsPage from "./pages/trains/TrainsPage";
|
||||||
import {
|
import {
|
||||||
<<<<<<< HEAD
|
|
||||||
CargoesCrudPage,
|
|
||||||
ContainersCrudPage,
|
|
||||||
TrainMasterDataPage,
|
|
||||||
WagonTypesCrudPage,
|
|
||||||
WagonsCrudPage,
|
|
||||||
} from "./pages/fleet/FleetCrudPages";
|
|
||||||
=======
|
|
||||||
CargoesCrudPage,
|
CargoesCrudPage,
|
||||||
ContainersCrudPage,
|
ContainersCrudPage,
|
||||||
LocomotivesCrudPage,
|
LocomotivesCrudPage,
|
||||||
TrainMasterDataPage,
|
TrainMasterDataPage,
|
||||||
WagonsCrudPage,
|
WagonsCrudPage,
|
||||||
} from "./pages/fleet/FleetCrudPages";
|
} from "./pages/fleet/FleetCrudPages";
|
||||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
|
||||||
import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
|
import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
|
||||||
import TrainDetailPage from "./pages/trains/TrainDetailPage";
|
import TrainDetailPage from "./pages/trains/TrainDetailPage";
|
||||||
import RoutesPage from "./pages/fleet/RoutesPage";
|
import RoutesPage from "./pages/fleet/RoutesPage";
|
||||||
@@ -255,12 +246,6 @@ const App = () => {
|
|||||||
element={<BookingContractPage />}
|
element={<BookingContractPage />}
|
||||||
/>
|
/>
|
||||||
<Route path="operations/train-scheduling" element={<TrainsPage />} />
|
<Route path="operations/train-scheduling" element={<TrainsPage />} />
|
||||||
<<<<<<< HEAD
|
|
||||||
<Route path="trains" element={<TrainMasterDataPage />} />
|
|
||||||
<Route path="trains/:id" element={<TrainDetailPage />} />
|
|
||||||
<Route path="wagon-types" element={<WagonTypesCrudPage />} />
|
|
||||||
<Route path="wagons" element={<WagonsCrudPage />} />
|
|
||||||
=======
|
|
||||||
<Route path="trains" element={<TrainMasterDataPage />} />
|
<Route path="trains" element={<TrainMasterDataPage />} />
|
||||||
<Route path="operations/train-scheduling" element={<TrainsPage />} />
|
<Route path="operations/train-scheduling" element={<TrainsPage />} />
|
||||||
<Route path="routes" element={<RoutesPage />} />
|
<Route path="routes" element={<RoutesPage />} />
|
||||||
@@ -268,7 +253,6 @@ const App = () => {
|
|||||||
<Route path="trains" element={<TrainMasterDataPage />} />
|
<Route path="trains" element={<TrainMasterDataPage />} />
|
||||||
<Route path="trains/:id" element={<TrainDetailPage />} />
|
<Route path="trains/:id" element={<TrainDetailPage />} />
|
||||||
<Route path="wagons" element={<WagonsCrudPage />} />
|
<Route path="wagons" element={<WagonsCrudPage />} />
|
||||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
|
||||||
<Route path="containers" element={<ContainersCrudPage />} />
|
<Route path="containers" element={<ContainersCrudPage />} />
|
||||||
<Route path="cargoes" element={<CargoesCrudPage />} />
|
<Route path="cargoes" element={<CargoesCrudPage />} />
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,8 @@ export const URL_CONSTANTS = {
|
|||||||
SCHEDULE_BY_ID: (id: string) => `/train-scheduling/container/schedules/${id}`,
|
SCHEDULE_BY_ID: (id: string) => `/train-scheduling/container/schedules/${id}`,
|
||||||
CANCEL_SCHEDULE: (id: string) =>
|
CANCEL_SCHEDULE: (id: string) =>
|
||||||
`/train-scheduling/container/schedules/${id}/cancel`,
|
`/train-scheduling/container/schedules/${id}/cancel`,
|
||||||
|
PUBLISH_SCHEDULE: (id: string) =>
|
||||||
|
`/train-scheduling/container/schedules/${id}/publish`,
|
||||||
},
|
},
|
||||||
|
|
||||||
RULE_ENGINE: {
|
RULE_ENGINE: {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import {
|
|||||||
useUpdateContainer,
|
useUpdateContainer,
|
||||||
} from '@/hooks/useContainers';
|
} from '@/hooks/useContainers';
|
||||||
import { useCreateTrain, useDeleteTrain, useTrains, useUpdateTrain } from '@/hooks/useTrains';
|
import { useCreateTrain, useDeleteTrain, useTrains, useUpdateTrain } from '@/hooks/useTrains';
|
||||||
|
import { useRouteYards } from '@/hooks/useRoutes';
|
||||||
import { useCreateWagon, useDeleteWagon, useUpdateWagon, useWagons } from '@/hooks/useWagons';
|
import { useCreateWagon, useDeleteWagon, useUpdateWagon, useWagons } from '@/hooks/useWagons';
|
||||||
import {
|
import {
|
||||||
useCreateLocomotive,
|
useCreateLocomotive,
|
||||||
@@ -892,10 +893,15 @@ export function WagonTypesCrudPage() {
|
|||||||
export function WagonsCrudPage() {
|
export function WagonsCrudPage() {
|
||||||
const query = useWagons();
|
const query = useWagons();
|
||||||
const { data: wagonTypes = [] } = useWagonTypes();
|
const { data: wagonTypes = [] } = useWagonTypes();
|
||||||
|
const { data: yards = [] } = useRouteYards();
|
||||||
const wagonTypeOptions = wagonTypes.map((type: any) => ({
|
const wagonTypeOptions = wagonTypes.map((type: any) => ({
|
||||||
value: type.id,
|
value: type.id,
|
||||||
label: `${type.code} - ${type.name}`,
|
label: `${type.code} - ${type.name}`,
|
||||||
}));
|
}));
|
||||||
|
const yardOptions = yards.map((yard: any) => ({
|
||||||
|
value: yard.id,
|
||||||
|
label: `${yard.label ?? yard.code} (${yard.country ?? '-'})`,
|
||||||
|
}));
|
||||||
return (
|
return (
|
||||||
<FleetCrudPage<Wagon>
|
<FleetCrudPage<Wagon>
|
||||||
title="Wagons"
|
title="Wagons"
|
||||||
@@ -906,10 +912,26 @@ export function WagonsCrudPage() {
|
|||||||
create={useCreateWagon()}
|
create={useCreateWagon()}
|
||||||
update={useUpdateWagon()}
|
update={useUpdateWagon()}
|
||||||
remove={useDeleteWagon()}
|
remove={useDeleteWagon()}
|
||||||
searchText={(wagon) => [wagon.wagonNumber, wagon.wagonTypeId, wagon.trainId, wagon.status].join(' ')}
|
searchText={(wagon) => [
|
||||||
|
wagon.wagonNumber,
|
||||||
|
wagon.wagonTypeId,
|
||||||
|
wagon.trainId,
|
||||||
|
wagon.status,
|
||||||
|
wagon.currentLocationYard?.label,
|
||||||
|
wagon.currentLocationYard?.code,
|
||||||
|
wagon.currentLocationYard?.country,
|
||||||
|
].join(' ')}
|
||||||
columns={[
|
columns={[
|
||||||
{ key: 'wagonNumber', label: 'Number' },
|
{ key: 'wagonNumber', label: 'Number' },
|
||||||
{ key: 'wagonTypeId', label: 'Type', render: (wagon) => optionLabel(wagonTypeOptions, wagon.wagonTypeId) },
|
{ key: 'wagonTypeId', label: 'Type', render: (wagon) => optionLabel(wagonTypeOptions, wagon.wagonTypeId) },
|
||||||
|
{
|
||||||
|
key: 'currentLocationYardId',
|
||||||
|
label: 'Location',
|
||||||
|
render: (wagon) =>
|
||||||
|
wagon.currentLocationYard
|
||||||
|
? `${wagon.currentLocationYard.label ?? wagon.currentLocationYard.code} (${wagon.currentLocationYard.country ?? '-'})`
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
{ key: 'maxPayloadWeight', label: 'Max payload' },
|
{ key: 'maxPayloadWeight', label: 'Max payload' },
|
||||||
{ key: 'status', label: 'Status', render: (wagon) => statusBadge(wagon.status) },
|
{ key: 'status', label: 'Status', render: (wagon) => statusBadge(wagon.status) },
|
||||||
]}
|
]}
|
||||||
@@ -927,12 +949,31 @@ export function WagonsCrudPage() {
|
|||||||
return { maxPayloadWeight: Number(selectedType.capacityTons) };
|
return { maxPayloadWeight: Number(selectedType.capacityTons) };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'currentLocationYardId',
|
||||||
|
label: 'Wagon location',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: yardOptions,
|
||||||
|
},
|
||||||
{ key: 'tareWeight', label: 'Tare weight', type: 'number', required: true },
|
{ key: 'tareWeight', label: 'Tare weight', type: 'number', required: true },
|
||||||
{ key: 'maxPayloadWeight', label: 'Max payload weight', type: 'number', required: true },
|
{ key: 'maxPayloadWeight', label: 'Max payload weight', type: 'number', required: true },
|
||||||
{ key: 'status', label: 'Status' },
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: 'Status',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ value: 'AVAILABLE', label: 'Available' },
|
||||||
|
{ value: 'IMPORT_READY', label: 'Import ready' },
|
||||||
|
{ value: 'EXPORT_READY', label: 'Export ready' },
|
||||||
|
{ value: 'ASSIGNED', label: 'Assigned' },
|
||||||
|
{ value: 'MAINTENANCE', label: 'Maintenance' },
|
||||||
|
{ value: 'RETIRED', label: 'Retired' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{ key: 'notes', label: 'Notes' },
|
{ key: 'notes', label: 'Notes' },
|
||||||
]}
|
]}
|
||||||
emptyValues={{ wagonNumber: '', wagonTypeId: '', tareWeight: 0, maxPayloadWeight: 0, status: 'AVAILABLE', notes: '' }}
|
emptyValues={{ wagonNumber: '', wagonTypeId: '', currentLocationYardId: '', tareWeight: 0, maxPayloadWeight: 0, status: 'AVAILABLE', notes: '' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,9 @@ import { URL_CONSTANTS } from '@/constants/URLS';
|
|||||||
export type LocomotiveType = 'DIESEL' | 'ELECTRIC';
|
export type LocomotiveType = 'DIESEL' | 'ELECTRIC';
|
||||||
export type LocomotiveStatus =
|
export type LocomotiveStatus =
|
||||||
| 'AVAILABLE'
|
| 'AVAILABLE'
|
||||||
|
| 'UNAVAILABLE'
|
||||||
|
| 'IMPORT_READY'
|
||||||
|
| 'EXPORT_READY'
|
||||||
| 'MAINTENANCE'
|
| 'MAINTENANCE'
|
||||||
| 'ASSIGNED'
|
| 'ASSIGNED'
|
||||||
| 'OUT_OF_SERVICE';
|
| 'OUT_OF_SERVICE';
|
||||||
|
|||||||
@@ -70,6 +70,14 @@ export const trainSchedulingService = {
|
|||||||
return unwrap(response.data);
|
return unwrap(response.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
publishSchedule: async (id: string): Promise<TrainScheduleDetail> => {
|
||||||
|
const response = await client.post<TrainScheduleDetail>(
|
||||||
|
URL_CONSTANTS.TRAIN_SCHEDULING.PUBLISH_SCHEDULE(id),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
return unwrap(response.data);
|
||||||
|
},
|
||||||
|
|
||||||
getAvailableLocomotives: async (): Promise<LocomotiveRecord[]> => {
|
getAvailableLocomotives: async (): Promise<LocomotiveRecord[]> => {
|
||||||
const response = await client.get<LocomotiveRecord[]>(URL_CONSTANTS.LOCOMOTIVES.BASE, {
|
const response = await client.get<LocomotiveRecord[]>(URL_CONSTANTS.LOCOMOTIVES.BASE, {
|
||||||
params: { status: 'AVAILABLE' },
|
params: { status: 'AVAILABLE' },
|
||||||
|
|||||||
@@ -6,6 +6,19 @@ export interface Wagon {
|
|||||||
wagonTypeId: string;
|
wagonTypeId: string;
|
||||||
trainId: string | null;
|
trainId: string | null;
|
||||||
sequenceNumber: number | null;
|
sequenceNumber: number | null;
|
||||||
|
currentLocationYardId: string | null;
|
||||||
|
currentLocationYard?: {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
label: string;
|
||||||
|
country?: string;
|
||||||
|
} | null;
|
||||||
|
wagonType?: {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
supportedLoadTypes?: string[];
|
||||||
|
} | null;
|
||||||
tareWeight: number;
|
tareWeight: number;
|
||||||
maxPayloadWeight: number;
|
maxPayloadWeight: number;
|
||||||
status: string;
|
status: string;
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ export const BOOKING_STATUSES = [
|
|||||||
"PENDING_APPROVAL",
|
"PENDING_APPROVAL",
|
||||||
"APPROVED_PENDING_SIGNATURE",
|
"APPROVED_PENDING_SIGNATURE",
|
||||||
"APPROVED",
|
"APPROVED",
|
||||||
|
"READY_FOR_ASSIGNMENT",
|
||||||
|
"WAGON_ASSIGNED",
|
||||||
|
"INVOICED",
|
||||||
"CONTRACT_READY",
|
"CONTRACT_READY",
|
||||||
"SIGNED_CUSTOMER",
|
"SIGNED_CUSTOMER",
|
||||||
"FULLY_EXECUTED",
|
"FULLY_EXECUTED",
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ export interface EligibleContainerBookingsResponse {
|
|||||||
items: EligibleContainerBooking[];
|
items: EligibleContainerBooking[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AssignmentType = "CONTAINER" | "BULK";
|
||||||
|
export type TradeDirection = "IMPORT" | "EXPORT" | "DOMESTIC";
|
||||||
|
|
||||||
export interface WagonPlanAllocation {
|
export interface WagonPlanAllocation {
|
||||||
bookingId: string;
|
bookingId: string;
|
||||||
bookingReference: string;
|
bookingReference: string;
|
||||||
@@ -57,7 +60,14 @@ export interface LocomotiveRecord {
|
|||||||
name?: string | null;
|
name?: string | null;
|
||||||
maxPullWeightTons: number;
|
maxPullWeightTons: number;
|
||||||
maxTrainLengthMeters: number;
|
maxTrainLengthMeters: number;
|
||||||
status: 'AVAILABLE' | 'ASSIGNED' | 'MAINTENANCE' | 'OUT_OF_SERVICE';
|
status:
|
||||||
|
| 'AVAILABLE'
|
||||||
|
| 'UNAVAILABLE'
|
||||||
|
| 'IMPORT_READY'
|
||||||
|
| 'EXPORT_READY'
|
||||||
|
| 'ASSIGNED'
|
||||||
|
| 'MAINTENANCE'
|
||||||
|
| 'OUT_OF_SERVICE';
|
||||||
locomotiveType?: 'DIESEL' | 'ELECTRIC';
|
locomotiveType?: 'DIESEL' | 'ELECTRIC';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +135,11 @@ export interface TrainScheduleDetail {
|
|||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
physicalWagon?: {
|
||||||
|
id: string;
|
||||||
|
wagonNumber: string;
|
||||||
|
status: string;
|
||||||
|
} | null;
|
||||||
allocations: Array<{
|
allocations: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
bookingId: string;
|
bookingId: string;
|
||||||
@@ -146,6 +161,8 @@ export interface TrainScheduleFilters {
|
|||||||
originStationId?: string;
|
originStationId?: string;
|
||||||
destinationStationId?: string;
|
destinationStationId?: string;
|
||||||
scheduleDate?: string;
|
scheduleDate?: string;
|
||||||
|
assignmentType?: AssignmentType;
|
||||||
|
tradeDirection?: TradeDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrainSchedulePreviewPayload {
|
export interface TrainSchedulePreviewPayload {
|
||||||
@@ -153,10 +170,15 @@ export interface TrainSchedulePreviewPayload {
|
|||||||
scheduleDate: string;
|
scheduleDate: string;
|
||||||
originStationId: string;
|
originStationId: string;
|
||||||
destinationStationId: string;
|
destinationStationId: string;
|
||||||
|
assignmentType?: AssignmentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateTrainSchedulePayload {
|
export interface CreateTrainSchedulePayload {
|
||||||
routeId: string;
|
routeId: string;
|
||||||
scheduleDate: string;
|
scheduleDate: string;
|
||||||
|
arrivalDate?: string;
|
||||||
locomotiveId: string;
|
locomotiveId: string;
|
||||||
|
assignmentType?: AssignmentType;
|
||||||
|
bookingIds: string[];
|
||||||
|
wagonIds?: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
// vite.config.ts
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { defineConfig } from "file:///C:/laragon/www/edr-platform/node_modules/.pnpm/vite@5.4.21_@types+node@20.19.41_lightningcss@1.32.0_terser@5.48.0/node_modules/vite/dist/node/index.js";
|
|
||||||
import react from "file:///C:/laragon/www/edr-platform/node_modules/.pnpm/@vitejs+plugin-react@4.7.0_vite@5.4.21_@types+node@20.19.41_lightningcss@1.32.0_terser@5.48.0_/node_modules/@vitejs/plugin-react/dist/index.js";
|
|
||||||
import tailwindcss from "file:///C:/laragon/www/edr-platform/node_modules/.pnpm/@tailwindcss+vite@4.3.0_vite@5.4.21_@types+node@20.19.41_lightningcss@1.32.0_terser@5.48.0_/node_modules/@tailwindcss/vite/dist/index.mjs";
|
|
||||||
var __vite_injected_original_import_meta_url = "file:///C:/laragon/www/edr-platform/apps/edr-freight-web/backoffice/vite.config.ts";
|
|
||||||
var __dirname = path.dirname(fileURLToPath(__vite_injected_original_import_meta_url));
|
|
||||||
var vite_config_default = defineConfig({
|
|
||||||
plugins: [react(), tailwindcss()],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
"@": path.resolve(__dirname, "./src")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
port: 5183,
|
|
||||||
host: "0.0.0.0"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
export {
|
|
||||||
vite_config_default as default
|
|
||||||
};
|
|
||||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxsYXJhZ29uXFxcXHd3d1xcXFxlZHItcGxhdGZvcm1cXFxcYXBwc1xcXFxlZHItZnJlaWdodC13ZWJcXFxcYmFja29mZmljZVwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiQzpcXFxcbGFyYWdvblxcXFx3d3dcXFxcZWRyLXBsYXRmb3JtXFxcXGFwcHNcXFxcZWRyLWZyZWlnaHQtd2ViXFxcXGJhY2tvZmZpY2VcXFxcdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL0M6L2xhcmFnb24vd3d3L2Vkci1wbGF0Zm9ybS9hcHBzL2Vkci1mcmVpZ2h0LXdlYi9iYWNrb2ZmaWNlL3ZpdGUuY29uZmlnLnRzXCI7aW1wb3J0IHBhdGggZnJvbSBcIm5vZGU6cGF0aFwiO1xyXG5pbXBvcnQgeyBmaWxlVVJMVG9QYXRoIH0gZnJvbSBcIm5vZGU6dXJsXCI7XHJcblxyXG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tIFwidml0ZVwiO1xyXG5pbXBvcnQgcmVhY3QgZnJvbSBcIkB2aXRlanMvcGx1Z2luLXJlYWN0XCI7XHJcbmltcG9ydCB0YWlsd2luZGNzcyBmcm9tIFwiQHRhaWx3aW5kY3NzL3ZpdGVcIjtcclxuXHJcbmNvbnN0IF9fZGlybmFtZSA9IHBhdGguZGlybmFtZShmaWxlVVJMVG9QYXRoKGltcG9ydC5tZXRhLnVybCkpO1xyXG5cclxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcclxuICBwbHVnaW5zOiBbcmVhY3QoKSwgdGFpbHdpbmRjc3MoKV0sXHJcbiAgcmVzb2x2ZToge1xyXG4gICAgYWxpYXM6IHtcclxuICAgICAgXCJAXCI6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsIFwiLi9zcmNcIiksXHJcbiAgICB9LFxyXG4gIH0sXHJcbiAgc2VydmVyOiB7XHJcbiAgICBwb3J0OiA1MTgzLFxyXG4gICAgaG9zdDogXCIwLjAuMC4wXCIsXHJcbiAgfSxcclxufSk7XHJcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBaVgsT0FBTyxVQUFVO0FBQ2xZLFNBQVMscUJBQXFCO0FBRTlCLFNBQVMsb0JBQW9CO0FBQzdCLE9BQU8sV0FBVztBQUNsQixPQUFPLGlCQUFpQjtBQUxtTixJQUFNLDJDQUEyQztBQU81UixJQUFNLFlBQVksS0FBSyxRQUFRLGNBQWMsd0NBQWUsQ0FBQztBQUU3RCxJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixTQUFTLENBQUMsTUFBTSxHQUFHLFlBQVksQ0FBQztBQUFBLEVBQ2hDLFNBQVM7QUFBQSxJQUNQLE9BQU87QUFBQSxNQUNMLEtBQUssS0FBSyxRQUFRLFdBQVcsT0FBTztBQUFBLElBQ3RDO0FBQUEsRUFDRjtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// vite.config.ts
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { defineConfig } from "file:///home/marshal/Desktop/EDR/edr-platform/node_modules/.pnpm/vite@5.4.21_@types+node@20.19.41_lightningcss@1.32.0_terser@5.48.0/node_modules/vite/dist/node/index.js";
|
|
||||||
import react from "file:///home/marshal/Desktop/EDR/edr-platform/node_modules/.pnpm/@vitejs+plugin-react@4.7.0_vite@5.4.21_@types+node@20.19.41_lightningcss@1.32.0_terser@5.48.0_/node_modules/@vitejs/plugin-react/dist/index.js";
|
|
||||||
import tailwindcss from "file:///home/marshal/Desktop/EDR/edr-platform/node_modules/.pnpm/@tailwindcss+vite@4.3.0_vite@5.4.21_@types+node@20.19.41_lightningcss@1.32.0_terser@5.48.0_/node_modules/@tailwindcss/vite/dist/index.mjs";
|
|
||||||
var __vite_injected_original_import_meta_url = "file:///home/marshal/Desktop/EDR/edr-platform/apps/edr-freight-web/backoffice/vite.config.ts";
|
|
||||||
var __dirname = path.dirname(fileURLToPath(__vite_injected_original_import_meta_url));
|
|
||||||
var vite_config_default = defineConfig({
|
|
||||||
plugins: [react(), tailwindcss()],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
"@": path.resolve(__dirname, "./src")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
port: 5183,
|
|
||||||
host: "0.0.0.0"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
export {
|
|
||||||
vite_config_default as default
|
|
||||||
};
|
|
||||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvaG9tZS9tYXJzaGFsL0Rlc2t0b3AvRURSL2Vkci1wbGF0Zm9ybS9hcHBzL2Vkci1mcmVpZ2h0LXdlYi9iYWNrb2ZmaWNlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvaG9tZS9tYXJzaGFsL0Rlc2t0b3AvRURSL2Vkci1wbGF0Zm9ybS9hcHBzL2Vkci1mcmVpZ2h0LXdlYi9iYWNrb2ZmaWNlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9ob21lL21hcnNoYWwvRGVza3RvcC9FRFIvZWRyLXBsYXRmb3JtL2FwcHMvZWRyLWZyZWlnaHQtd2ViL2JhY2tvZmZpY2Uvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgcGF0aCBmcm9tIFwibm9kZTpwYXRoXCI7XG5pbXBvcnQgeyBmaWxlVVJMVG9QYXRoIH0gZnJvbSBcIm5vZGU6dXJsXCI7XG5cbmltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gXCJ2aXRlXCI7XG5pbXBvcnQgcmVhY3QgZnJvbSBcIkB2aXRlanMvcGx1Z2luLXJlYWN0XCI7XG5pbXBvcnQgdGFpbHdpbmRjc3MgZnJvbSBcIkB0YWlsd2luZGNzcy92aXRlXCI7XG5cbmNvbnN0IF9fZGlybmFtZSA9IHBhdGguZGlybmFtZShmaWxlVVJMVG9QYXRoKGltcG9ydC5tZXRhLnVybCkpO1xuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbcmVhY3QoKSwgdGFpbHdpbmRjc3MoKV0sXG4gIHJlc29sdmU6IHtcbiAgICBhbGlhczoge1xuICAgICAgXCJAXCI6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsIFwiLi9zcmNcIiksXG4gICAgfSxcbiAgfSxcbiAgc2VydmVyOiB7XG4gICAgcG9ydDogNTE4MyxcbiAgICBob3N0OiBcIjAuMC4wLjBcIixcbiAgfSxcbn0pO1xuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFvWSxPQUFPLFVBQVU7QUFDclosU0FBUyxxQkFBcUI7QUFFOUIsU0FBUyxvQkFBb0I7QUFDN0IsT0FBTyxXQUFXO0FBQ2xCLE9BQU8saUJBQWlCO0FBTDROLElBQU0sMkNBQTJDO0FBT3JTLElBQU0sWUFBWSxLQUFLLFFBQVEsY0FBYyx3Q0FBZSxDQUFDO0FBRTdELElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQzFCLFNBQVMsQ0FBQyxNQUFNLEdBQUcsWUFBWSxDQUFDO0FBQUEsRUFDaEMsU0FBUztBQUFBLElBQ1AsT0FBTztBQUFBLE1BQ0wsS0FBSyxLQUFLLFFBQVEsV0FBVyxPQUFPO0FBQUEsSUFDdEM7QUFBQSxFQUNGO0FBQUEsRUFDQSxRQUFRO0FBQUEsSUFDTixNQUFNO0FBQUEsSUFDTixNQUFNO0FBQUEsRUFDUjtBQUNGLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg==
|
|
||||||
10
package.json
10
package.json
@@ -4,8 +4,14 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "turbo run dev",
|
"dev": "turbo run dev",
|
||||||
"dev:freight": "turbo run dev --filter=@edr/freight-api... --filter=@edr/freight-portal... --filter=@edr/freight-backoffice... --filter=@edr/ui-common...",
|
"dev:freight": "turbo run dev --filter=@edr/freight-api --filter=@edr/freight-portal --filter=@edr/freight-backoffice",
|
||||||
"dev:passenger": "turbo run dev --filter=@edr/passenger-api... --filter=@edr/passenger-portal... --filter=@edr/passenger-backoffice...",
|
"dev:freight:api": "turbo run dev --filter=@edr/freight-api",
|
||||||
|
"dev:freight:portal": "turbo run dev --filter=@edr/freight-portal",
|
||||||
|
"dev:freight:backoffice": "turbo run dev --filter=@edr/freight-backoffice",
|
||||||
|
"dev:passenger": "turbo run dev --filter=@edr/passenger-api --filter=@edr/passenger-portal --filter=@edr/passenger-backoffice",
|
||||||
|
"dev:passenger:api": "turbo run dev --filter=@edr/passenger-api",
|
||||||
|
"dev:passenger:portal": "turbo run dev --filter=@edr/passenger-portal",
|
||||||
|
"dev:passenger:backoffice": "turbo run dev --filter=@edr/passenger-backoffice",
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"build:freight": "turbo run build --filter=@edr/freight-api... --filter=@edr/freight-portal... --filter=@edr/freight-backoffice...",
|
"build:freight": "turbo run build --filter=@edr/freight-api... --filter=@edr/freight-portal... --filter=@edr/freight-backoffice...",
|
||||||
"build:passenger": "turbo run build --filter=@edr/passenger-api... --filter=@edr/passenger-portal... --filter=@edr/passenger-backoffice...",
|
"build:passenger": "turbo run build --filter=@edr/passenger-api... --filter=@edr/passenger-portal... --filter=@edr/passenger-backoffice...",
|
||||||
|
|||||||
Reference in New Issue
Block a user