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/
|
||||
*.tsbuildinfo
|
||||
**/*.tsbuildinfo
|
||||
**/vite.config.ts.timestamp-*.mjs
|
||||
|
||||
# env
|
||||
.env
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"lint": "eslint src",
|
||||
"test": "jest",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -17,10 +17,6 @@ import {
|
||||
PositionType,
|
||||
Position,
|
||||
Project,
|
||||
<<<<<<< HEAD
|
||||
UnitConfiguration,
|
||||
=======
|
||||
>>>>>>> 95fb544ec20f01ec4a2d92f546954a5b4e464a4f
|
||||
GlobalUnitConfiguration,
|
||||
Unit,
|
||||
EmployeeSignature,
|
||||
@@ -67,10 +63,6 @@ const iamEntities = [
|
||||
PositionType,
|
||||
Position,
|
||||
Project,
|
||||
<<<<<<< HEAD
|
||||
UnitConfiguration,
|
||||
=======
|
||||
>>>>>>> 95fb544ec20f01ec4a2d92f546954a5b4e464a4f
|
||||
GlobalUnitConfiguration,
|
||||
Unit,
|
||||
EmployeeSignature,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DataSource } from 'typeorm';
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'postgres',
|
||||
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',
|
||||
password: process.env.DB_PASSWORD ?? '',
|
||||
database: process.env.DB_NAME ?? 'edr_freight',
|
||||
@@ -14,7 +14,7 @@ export const AppDataSource = new DataSource({
|
||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||
migrations: [__dirname + '/migrations/*{.ts,.js}'],
|
||||
synchronize: false,
|
||||
logging: true,
|
||||
logging: process.env.TYPEORM_LOGGING === 'true',
|
||||
});
|
||||
|
||||
// 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',
|
||||
'APPROVED_PENDING_SIGNATURE',
|
||||
'APPROVED',
|
||||
'READY_FOR_ASSIGNMENT',
|
||||
'WAGON_ASSIGNED',
|
||||
'INVOICED',
|
||||
'CONTRACT_READY',
|
||||
'SIGNED_CUSTOMER',
|
||||
'FULLY_EXECUTED',
|
||||
|
||||
@@ -5,6 +5,9 @@ import { TrainSet } from '../../train-sets/entities/train-set.entity';
|
||||
|
||||
export const LOCOMOTIVE_STATUSES = [
|
||||
'AVAILABLE',
|
||||
'UNAVAILABLE',
|
||||
'IMPORT_READY',
|
||||
'EXPORT_READY',
|
||||
'ASSIGNED',
|
||||
'MAINTENANCE',
|
||||
'OUT_OF_SERVICE',
|
||||
|
||||
@@ -8,9 +8,12 @@ import { TrainScheduleBooking } from './train-schedule-booking.entity';
|
||||
|
||||
export const TRAIN_SCHEDULE_STATUSES = [
|
||||
'DRAFT',
|
||||
'SCHEDULED',
|
||||
'DISPATCHED',
|
||||
'READY',
|
||||
'PUBLISHED',
|
||||
'DEPARTED',
|
||||
'IN_TRANSIT',
|
||||
'ARRIVED',
|
||||
'COMPLETED',
|
||||
'CANCELLED',
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsDateString, IsUUID } from 'class-validator';
|
||||
import { ArrayMinSize, IsArray, IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
|
||||
|
||||
export class CreateContainerTrainScheduleDto {
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
@@ -10,7 +10,31 @@ export class CreateContainerTrainScheduleDto {
|
||||
@IsDateString()
|
||||
scheduleDate!: string;
|
||||
|
||||
@ApiProperty({ example: '2026-06-22T08:00:00.000Z', required: false })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
arrivalDate?: string;
|
||||
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
@IsUUID()
|
||||
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 { IsDateString, IsOptional, IsUUID } from 'class-validator';
|
||||
import { IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
|
||||
|
||||
export class GetEligibleContainerBookingsDto {
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@@ -16,4 +16,14 @@ export class GetEligibleContainerBookingsDto {
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
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 { ArrayMinSize, IsArray, IsDateString, IsUUID } from 'class-validator';
|
||||
import { ArrayMinSize, IsArray, IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
|
||||
|
||||
export class PreviewContainerTrainScheduleDto {
|
||||
@ApiProperty({ type: [String] })
|
||||
@@ -19,4 +19,9 @@ export class PreviewContainerTrainScheduleDto {
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
@IsUUID()
|
||||
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) {
|
||||
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 { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
||||
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 { TrainSetWagon } from '../train-sets/entities/train-set-wagon.entity';
|
||||
import { TrainSetsModule } from '../train-sets/train-sets.module';
|
||||
@@ -26,6 +27,7 @@ import { TrainSchedulingService } from './train-scheduling.service';
|
||||
BookingContainer,
|
||||
Locomotive,
|
||||
WagonType,
|
||||
Wagon,
|
||||
TrainSet,
|
||||
TrainSetWagon,
|
||||
TrainSchedule,
|
||||
|
||||
@@ -38,7 +38,7 @@ const makeBooking = (
|
||||
scheduledDate: new Date(scheduledDate),
|
||||
originYardId,
|
||||
destinationYardId,
|
||||
status: 'PAID',
|
||||
status: 'APPROVED',
|
||||
customer: { companyName: 'Demo Customer' },
|
||||
originYard: { label: 'Djibouti', code: 'DJIBOUTI' },
|
||||
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 = [
|
||||
{
|
||||
...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.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 { TrainScheduleBooking } from "../train-schedules/entities/train-schedule-booking.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 { WagonTypesRepository } from "../wagon-types/wagon-types.repository";
|
||||
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 MAX_TRAIN_WEIGHT_TONS = 3500;
|
||||
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 = {
|
||||
id: string;
|
||||
@@ -94,11 +99,13 @@ export class TrainSchedulingService {
|
||||
"scheduleBooking",
|
||||
"scheduleBooking.booking_id = booking.id",
|
||||
)
|
||||
.where("booking.freightType = :freightType", { freightType: "CONTAINER" })
|
||||
.where("booking.freightType = :freightType", {
|
||||
freightType: query.assignmentType ?? "CONTAINER",
|
||||
})
|
||||
.andWhere("scheduleBooking.id IS NULL");
|
||||
|
||||
queryBuilder.andWhere("booking.status IN (:...schedulableStatuses)", {
|
||||
schedulableStatuses: SCHEDULABLE_BOOKING_STATUSES,
|
||||
queryBuilder.andWhere("booking.status IN (:...assignableStatuses)", {
|
||||
assignableStatuses: ASSIGNABLE_BOOKING_STATUSES,
|
||||
});
|
||||
|
||||
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) {
|
||||
queryBuilder.andWhere(
|
||||
`DATE(booking.scheduled_date AT TIME ZONE 'UTC') = :scheduleDate`,
|
||||
@@ -141,7 +168,7 @@ export class TrainSchedulingService {
|
||||
container.containerType?.code ??
|
||||
"Container",
|
||||
)
|
||||
.join(", ") ?? "Container",
|
||||
.join(", ") ?? (booking.freightType === "BULK" ? "Bulk cargo" : "Container"),
|
||||
quantity:
|
||||
booking.bookingContainers?.reduce(
|
||||
(sum, container) => sum + Number(container.quantity ?? 0),
|
||||
@@ -180,11 +207,27 @@ export class TrainSchedulingService {
|
||||
|
||||
async createContainerTrainSchedule(dto: CreateContainerTrainScheduleDto) {
|
||||
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(
|
||||
dto.locomotiveId,
|
||||
0,
|
||||
0,
|
||||
validation?.summary.totalWeightTons ?? 0,
|
||||
validation?.summary.totalLengthMeters ?? 0,
|
||||
);
|
||||
|
||||
const createdSchedule = await this.dataSource.transaction(
|
||||
@@ -205,10 +248,28 @@ export class TrainSchedulingService {
|
||||
);
|
||||
}
|
||||
|
||||
const trainSet = await this.buildEmptyTrainSet(
|
||||
manager,
|
||||
lockedLocomotive,
|
||||
);
|
||||
const selectedPhysicalWagons = validation
|
||||
? await this.lockSelectedWagonsForSchedule(
|
||||
manager,
|
||||
dto.wagonIds ?? [],
|
||||
validation.wagonPlan.length,
|
||||
route,
|
||||
dto.assignmentType ?? "CONTAINER",
|
||||
)
|
||||
: [];
|
||||
|
||||
const trainSetResult = validation
|
||||
? await this.buildTrainSet(
|
||||
manager,
|
||||
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({
|
||||
trainSetId: trainSet.id,
|
||||
@@ -216,13 +277,51 @@ export class TrainSchedulingService {
|
||||
originStationId: route.originYardId,
|
||||
destinationStationId: route.destinationYardId,
|
||||
scheduledDepartureDate: new Date(dto.scheduleDate),
|
||||
status: "DRAFT",
|
||||
scheduledArrivalDate: dto.arrivalDate ? new Date(dto.arrivalDate) : null,
|
||||
status: validation ? "READY" : "DRAFT",
|
||||
});
|
||||
|
||||
const savedSchedule = await manager
|
||||
.getRepository(TrainSchedule)
|
||||
.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, {
|
||||
status: "ASSIGNED",
|
||||
});
|
||||
@@ -276,24 +375,32 @@ export class TrainSchedulingService {
|
||||
}
|
||||
|
||||
const nonContainerBookings = bookings.filter(
|
||||
(booking) => booking.freightType !== "CONTAINER",
|
||||
(booking) => booking.freightType !== (dto.assignmentType ?? "CONTAINER"),
|
||||
);
|
||||
if (nonContainerBookings.length > 0) {
|
||||
violations.push(
|
||||
"Only CONTAINER bookings are supported for train scheduling",
|
||||
`Only ${dto.assignmentType ?? "CONTAINER"} bookings are supported for this assignment`,
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
const invalidStatuses = [...new Set(invalidStatusBookings.map((booking) => booking.status))];
|
||||
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 routeMismatch = bookings.some(
|
||||
(booking) =>
|
||||
@@ -366,11 +473,11 @@ export class TrainSchedulingService {
|
||||
}
|
||||
|
||||
if (
|
||||
wagonType.maxWagonsPerTrain != null &&
|
||||
wagonPlan.length > Number(wagonType.maxWagonsPerTrain)
|
||||
wagonPlan.length >
|
||||
(dto.assignmentType === "BULK" ? MAX_BULK_WAGONS : MAX_CONTAINER_WAGONS)
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
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(
|
||||
manager: EntityManager,
|
||||
locomotive: Locomotive,
|
||||
@@ -481,6 +664,7 @@ export class TrainSchedulingService {
|
||||
totalWeightTons: number,
|
||||
totalLengthMeters: number,
|
||||
wagonPlan: WagonPlanRecord[],
|
||||
physicalWagons: Wagon[] = [],
|
||||
) {
|
||||
const trainSet = manager.getRepository(TrainSet).create({
|
||||
locomotiveId: locomotive.id,
|
||||
@@ -491,20 +675,35 @@ export class TrainSchedulingService {
|
||||
});
|
||||
const savedTrainSet = await manager.getRepository(TrainSet).save(trainSet);
|
||||
|
||||
const wagons = wagonPlan.map((wagon) =>
|
||||
manager.getRepository(TrainSetWagon).create({
|
||||
const wagons = wagonPlan.map((wagon, index) => {
|
||||
const physicalWagon = physicalWagons[index];
|
||||
const selectedWagonType = physicalWagon?.wagonType ?? wagonType;
|
||||
|
||||
return manager.getRepository(TrainSetWagon).create({
|
||||
trainSetId: savedTrainSet.id,
|
||||
wagonTypeId: wagonType.id,
|
||||
wagonTypeId: selectedWagonType.id,
|
||||
physicalWagonId: physicalWagon?.id ?? null,
|
||||
sequenceNo: wagon.sequenceNo,
|
||||
capacityTons: wagon.capacityTons,
|
||||
lengthMeters: wagon.lengthMeters,
|
||||
capacityTons: Number(selectedWagonType.capacityTons),
|
||||
lengthMeters: Number(selectedWagonType.lengthMeters),
|
||||
assignedWeightTons: wagon.assignedWeightTons,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await manager.getRepository(TrainSetWagon).save(wagons);
|
||||
const savedWagons = await manager.getRepository(TrainSetWagon).save(wagons);
|
||||
|
||||
return savedTrainSet;
|
||||
if (physicalWagons.length > 0) {
|
||||
await Promise.all(
|
||||
physicalWagons.map((wagon, index) =>
|
||||
manager.getRepository(Wagon).update(wagon.id, {
|
||||
status: "ASSIGNED",
|
||||
sequenceNumber: index + 1,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return { trainSet: savedTrainSet, wagons: savedWagons };
|
||||
}
|
||||
|
||||
async buildEmptyTrainSet(
|
||||
@@ -627,7 +826,7 @@ export class TrainSchedulingService {
|
||||
route: true,
|
||||
trainSet: {
|
||||
locomotive: true,
|
||||
wagons: { wagonType: true, allocations: { booking: true } },
|
||||
wagons: { wagonType: true, physicalWagon: true, allocations: { booking: true } },
|
||||
},
|
||||
originStation: true,
|
||||
destinationStation: true,
|
||||
@@ -696,6 +895,13 @@ export class TrainSchedulingService {
|
||||
name: wagon.wagonType.name,
|
||||
}
|
||||
: null,
|
||||
physicalWagon: wagon.physicalWagon
|
||||
? {
|
||||
id: wagon.physicalWagon.id,
|
||||
wagonNumber: wagon.physicalWagon.wagonNumber,
|
||||
status: wagon.physicalWagon.status,
|
||||
}
|
||||
: null,
|
||||
allocations:
|
||||
wagon.allocations?.map((allocation) => ({
|
||||
id: allocation.id,
|
||||
@@ -729,7 +935,7 @@ export class TrainSchedulingService {
|
||||
.getRepository(TrainSchedule)
|
||||
.findOne({
|
||||
where: { id },
|
||||
relations: { trainSet: { locomotive: true } },
|
||||
relations: { trainSet: { locomotive: true, wagons: true } },
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
@@ -754,11 +960,58 @@ export class TrainSchedulingService {
|
||||
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);
|
||||
}
|
||||
|
||||
private async loadBookingsForScheduling(bookingIds: string[]) {
|
||||
return this.dataSource.getRepository(Booking).find({
|
||||
where: { id: In(bookingIds) },
|
||||
@@ -775,6 +1028,7 @@ export class TrainSchedulingService {
|
||||
private async getActiveRoute(routeId: string) {
|
||||
const route = await this.dataSource.getRepository(Route).findOne({
|
||||
where: { id: routeId },
|
||||
relations: { originYard: true, destinationYard: true },
|
||||
});
|
||||
|
||||
if (!route) {
|
||||
@@ -788,6 +1042,13 @@ export class TrainSchedulingService {
|
||||
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) {
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
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 { 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 { TrainSet } from './train-set.entity';
|
||||
|
||||
@@ -18,6 +19,13 @@ export class TrainSetWagon extends BaseEntity {
|
||||
@Column({ name: 'wagon_type_id', type: 'uuid' })
|
||||
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)
|
||||
@JoinColumn({ name: 'wagon_type_id' })
|
||||
wagonType?: WagonType;
|
||||
|
||||
@@ -11,10 +11,12 @@ import {
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
|
||||
<<<<<<< HEAD
|
||||
const toNumber = ({ value }: { value: unknown }) =>
|
||||
value === '' || value == null ? value : Number(value);
|
||||
|
||||
const toOptionalNumber = ({ value }: { value: unknown }) =>
|
||||
value === '' || value == null ? undefined : Number(value);
|
||||
|
||||
const toBoolean = ({ value }: { value: unknown }) => {
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (value === 'true') return true;
|
||||
@@ -23,7 +25,9 @@ const toBoolean = ({ 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 [];
|
||||
return value
|
||||
.split(',')
|
||||
@@ -37,71 +41,28 @@ export class CreateWagonTypeDto {
|
||||
@MaxLength(32)
|
||||
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 })
|
||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
name!: string;
|
||||
|
||||
<<<<<<< HEAD
|
||||
@ApiProperty({ example: 60 })
|
||||
@ApiProperty({ description: 'Maximum payload capacity in metric tons', example: 60 })
|
||||
@Transform(toNumber)
|
||||
@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)
|
||||
@Transform(({ value }) => Number(value))
|
||||
capacityTons!: number;
|
||||
|
||||
@ApiProperty({ description: 'Wagon length in meters' })
|
||||
@ApiProperty({ description: 'Wagon length in meters', example: 14.2 })
|
||||
@Transform(toNumber)
|
||||
@IsNumber()
|
||||
@Min(0.001)
|
||||
@Transform(({ value }) => Number(value))
|
||||
lengthMeters!: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Maximum wagons of this type per train' })
|
||||
@ApiPropertyOptional({ description: 'Maximum wagons of this type per train', example: 45 })
|
||||
@IsOptional()
|
||||
@Transform(toOptionalNumber)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Transform(({ value }) => (value === '' || value === null || value === undefined ? undefined : Number(value)))
|
||||
maxWagonsPerTrain?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
@@ -110,18 +71,14 @@ export class CreateWagonTypeDto {
|
||||
default: [],
|
||||
})
|
||||
@IsOptional()
|
||||
@Transform(toStringArray)
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@Transform(({ value }) => parseLoadTypes(value))
|
||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
||||
supportedLoadTypes?: string[];
|
||||
|
||||
@ApiPropertyOptional({ default: true })
|
||||
@IsOptional()
|
||||
<<<<<<< HEAD
|
||||
@Transform(toBoolean)
|
||||
=======
|
||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
@@ -11,18 +11,9 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
} 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';
|
||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
||||
|
||||
import { RuleEngineManage, RuleEngineView } from '../../common/rule-engine-guards';
|
||||
|
||||
import { CreateWagonTypeDto } from './dto/create-wagon-type.dto';
|
||||
import { UpdateWagonTypeDto } from './dto/update-wagon-type.dto';
|
||||
import { WagonTypesService } from './wagon-types.service';
|
||||
@@ -33,41 +24,11 @@ import { WagonTypesService } from './wagon-types.service';
|
||||
export class WagonTypesController {
|
||||
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()
|
||||
@RuleEngineView('wagon-types')
|
||||
@ApiOperation({ summary: 'List wagon types' })
|
||||
findAll(@Query() query: Record<string, string>) {
|
||||
return this.wagonTypesService.findAll({
|
||||
isActive: query['isActive'] !== undefined ? query['isActive'] === 'true' : undefined,
|
||||
page: query['page'] ? parseInt(query['page'], 10) : undefined,
|
||||
pageSize: query['pageSize'] ? parseInt(query['pageSize'], 10) : undefined,
|
||||
});
|
||||
findAll(@Query() query: Record<string, string | undefined>) {
|
||||
return this.wagonTypesService.findAll(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@@ -88,21 +49,14 @@ export class WagonTypesController {
|
||||
@RuleEngineManage('wagon-types')
|
||||
@ApiOperation({ summary: 'Update a wagon type' })
|
||||
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWagonTypeDto) {
|
||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
||||
return this.wagonTypesService.update(id, dto);
|
||||
}
|
||||
|
||||
@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')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@ApiOperation({ summary: 'Soft-delete a wagon type' })
|
||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
||||
return this.wagonTypesService.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +1,23 @@
|
||||
<<<<<<< HEAD
|
||||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
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 { UpdateWagonTypeDto } from './dto/update-wagon-type.dto';
|
||||
import { WagonType } from './entities/wagon-type.entity';
|
||||
import { WagonTypesRepository } from './wagon-types.repository';
|
||||
|
||||
type WagonTypeListResponse = {
|
||||
data: WagonType[];
|
||||
meta: { total: number; page: number; pageSize: number; totalPages: number };
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class WagonTypesService {
|
||||
constructor(private readonly wagonTypesRepository: WagonTypesRepository) {}
|
||||
|
||||
<<<<<<< HEAD
|
||||
async create(dto: CreateWagonTypeDto): Promise<WagonType> {
|
||||
const code = dto.code.trim().toUpperCase();
|
||||
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[]> {
|
||||
async findAll(query: Record<string, string | undefined> = {}): Promise<WagonTypeListResponse> {
|
||||
const page = Math.max(1, Number(query.page) || 1);
|
||||
const pageSize = Math.max(1, Number(query.pageSize) || 20);
|
||||
const isActive =
|
||||
query.isActive === 'all'
|
||||
? undefined
|
||||
@@ -92,19 +31,29 @@ export class WagonTypesService {
|
||||
: 'code';
|
||||
const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
||||
|
||||
return this.wagonTypesRepository.findAll({
|
||||
const [data, total] = await this.wagonTypesRepository.findAndCount({
|
||||
where: isActive === undefined ? {} : { isActive },
|
||||
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> {
|
||||
const wagonType = await this.wagonTypesRepository.findById(id);
|
||||
|
||||
if (!wagonType) {
|
||||
throw new NotFoundException(`Wagon type ${id} not found`);
|
||||
}
|
||||
|
||||
return wagonType;
|
||||
}
|
||||
|
||||
@@ -116,14 +65,30 @@ export class WagonTypesService {
|
||||
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> {
|
||||
const wagonType = await this.findById(id);
|
||||
const nextCode = dto.code?.trim().toUpperCase();
|
||||
|
||||
if (nextCode && nextCode !== wagonType.code) {
|
||||
const existing = await this.wagonTypesRepository.findAll({ where: { code: nextCode } });
|
||||
if (existing.length > 0) {
|
||||
const existing = await this.wagonTypesRepository.findByCode(nextCode);
|
||||
if (existing) {
|
||||
throw new ConflictException(`Wagon type code "${nextCode}" already exists`);
|
||||
}
|
||||
}
|
||||
@@ -138,43 +103,11 @@ export class WagonTypesService {
|
||||
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;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findById(id);
|
||||
<<<<<<< HEAD
|
||||
await this.wagonTypesRepository.update(id, { isActive: false });
|
||||
=======
|
||||
await this.wagonTypesRepository.softDelete(id);
|
||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ export class CreateWagonDto {
|
||||
@Min(1)
|
||||
sequenceNumber?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
currentLocationYardId?: string;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
tareWeight!: number;
|
||||
@@ -25,10 +29,10 @@ export class CreateWagonDto {
|
||||
maxPayloadWeight!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['AVAILABLE', 'ASSIGNED', 'MAINTENANCE', 'RETIRED'])
|
||||
@IsIn(['AVAILABLE', 'IMPORT_READY', 'EXPORT_READY', 'ASSIGNED', 'MAINTENANCE', 'RETIRED'])
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Entity, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Train } from '../../trains/entities/train.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' })
|
||||
export class Wagon extends BaseEntity {
|
||||
@@ -12,12 +14,23 @@ export class Wagon extends BaseEntity {
|
||||
@Column({ name: 'wagon_type_id', type: 'uuid' })
|
||||
wagonTypeId!: string;
|
||||
|
||||
@ManyToOne(() => WagonType, { onDelete: 'RESTRICT' })
|
||||
@JoinColumn({ name: 'wagon_type_id' })
|
||||
wagonType?: WagonType;
|
||||
|
||||
@Column({ name: 'train_id', type: 'uuid', nullable: true })
|
||||
trainId!: string | null;
|
||||
|
||||
@Column({ name: 'sequence_number', type: 'int', nullable: true })
|
||||
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 })
|
||||
tareWeight!: number;
|
||||
|
||||
@@ -25,7 +38,7 @@ export class Wagon extends BaseEntity {
|
||||
maxPayloadWeight!: number;
|
||||
|
||||
@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 })
|
||||
notes!: string | null;
|
||||
@@ -38,4 +51,4 @@ export class Wagon extends BaseEntity {
|
||||
// Relationship to Container
|
||||
@OneToMany(() => Container, (container) => container.wagon)
|
||||
containers!: Container[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Wagon } from './entities/wagon.entity';
|
||||
import { Train } from '../trains/entities/train.entity';
|
||||
import { Yard } from '../rule-engine/entities/yard.entity';
|
||||
import { WagonsController, TrainWagonsReorderController } from './wagons.controller';
|
||||
import { WagonsService } from './wagons.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Wagon, Train])],
|
||||
imports: [TypeOrmModule.forFeature([Wagon, Train, Yard])],
|
||||
controllers: [WagonsController, TrainWagonsReorderController],
|
||||
providers: [WagonsService],
|
||||
exports: [WagonsService],
|
||||
})
|
||||
export class WagonsModule {}
|
||||
export class WagonsModule {}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AssignWagonToTrainDto } from './dto/assign-wagon-to-train.dto';
|
||||
import { ReorderWagonsDto } from './dto/reorder-wagons.dto';
|
||||
import { Wagon } from './entities/wagon.entity';
|
||||
import { Train } from '../trains/entities/train.entity';
|
||||
import { Yard } from '../rule-engine/entities/yard.entity';
|
||||
|
||||
@Injectable()
|
||||
export class WagonsService {
|
||||
@@ -15,6 +16,8 @@ export class WagonsService {
|
||||
private readonly wagonRepo: Repository<Wagon>,
|
||||
@InjectRepository(Train)
|
||||
private readonly trainRepo: Repository<Train>,
|
||||
@InjectRepository(Yard)
|
||||
private readonly yardRepo: Repository<Yard>,
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
@@ -23,6 +26,7 @@ export class WagonsService {
|
||||
// Convert undefined to null for nullable fields
|
||||
if (dto.trainId === undefined) wagon.trainId = null;
|
||||
if (dto.sequenceNumber === undefined) wagon.sequenceNumber = null;
|
||||
wagon.status = await this.statusForLocation(dto.currentLocationYardId, dto.status);
|
||||
return this.wagonRepo.save(wagon);
|
||||
}
|
||||
|
||||
@@ -31,12 +35,14 @@ export class WagonsService {
|
||||
const search = query.search?.trim();
|
||||
const status = query.status?.trim();
|
||||
const trainId = query.trainId?.trim();
|
||||
const currentLocationYardId = query.currentLocationYardId?.trim();
|
||||
|
||||
if (search) {
|
||||
where.push({
|
||||
wagonNumber: ILike(`%${search}%`),
|
||||
...(status ? { status } : {}),
|
||||
...(trainId ? { trainId } : {}),
|
||||
...(currentLocationYardId ? { currentLocationYardId } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,7 +52,8 @@ export class WagonsService {
|
||||
const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
||||
|
||||
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>,
|
||||
skip: query.page && query.limit ? (Number(query.page) - 1) * Number(query.limit) : undefined,
|
||||
take: query.limit ? Number(query.limit) : undefined,
|
||||
@@ -54,7 +61,7 @@ export class WagonsService {
|
||||
}
|
||||
|
||||
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`);
|
||||
return wagon;
|
||||
}
|
||||
@@ -62,6 +69,9 @@ export class WagonsService {
|
||||
async update(id: string, dto: UpdateWagonDto): Promise<Wagon> {
|
||||
const wagon = await this.findById(id);
|
||||
Object.assign(wagon, dto);
|
||||
if (dto.currentLocationYardId !== undefined) {
|
||||
wagon.status = await this.statusForLocation(dto.currentLocationYardId, dto.status);
|
||||
}
|
||||
return this.wagonRepo.save(wagon);
|
||||
}
|
||||
|
||||
@@ -99,10 +109,21 @@ export class WagonsService {
|
||||
const wagon = await this.findById(wagonId);
|
||||
wagon.trainId = null;
|
||||
wagon.sequenceNumber = null;
|
||||
wagon.status = 'AVAILABLE';
|
||||
wagon.status = await this.statusForLocation(wagon.currentLocationYardId, 'AVAILABLE');
|
||||
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> {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
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 TrainsPage from "./pages/trains/TrainsPage";
|
||||
import {
|
||||
<<<<<<< HEAD
|
||||
CargoesCrudPage,
|
||||
ContainersCrudPage,
|
||||
TrainMasterDataPage,
|
||||
WagonTypesCrudPage,
|
||||
WagonsCrudPage,
|
||||
} from "./pages/fleet/FleetCrudPages";
|
||||
=======
|
||||
CargoesCrudPage,
|
||||
ContainersCrudPage,
|
||||
LocomotivesCrudPage,
|
||||
TrainMasterDataPage,
|
||||
WagonsCrudPage,
|
||||
} from "./pages/fleet/FleetCrudPages";
|
||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
||||
import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
|
||||
import TrainDetailPage from "./pages/trains/TrainDetailPage";
|
||||
import RoutesPage from "./pages/fleet/RoutesPage";
|
||||
@@ -101,15 +92,15 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
href: "/dashboard/trains",
|
||||
icon: <Train />,
|
||||
},
|
||||
{
|
||||
label: "Wagon types",
|
||||
href: "/dashboard/wagon-types",
|
||||
icon: <Boxes />,
|
||||
},
|
||||
{
|
||||
label: "Wagons",
|
||||
href: "/dashboard/wagons",
|
||||
icon: <Truck />,
|
||||
{
|
||||
label: "Wagon types",
|
||||
href: "/dashboard/wagon-types",
|
||||
icon: <Boxes />,
|
||||
},
|
||||
{
|
||||
label: "Wagons",
|
||||
href: "/dashboard/wagons",
|
||||
icon: <Truck />,
|
||||
},
|
||||
{
|
||||
label: "Containers",
|
||||
@@ -255,12 +246,6 @@ const App = () => {
|
||||
element={<BookingContractPage />}
|
||||
/>
|
||||
<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="operations/train-scheduling" element={<TrainsPage />} />
|
||||
<Route path="routes" element={<RoutesPage />} />
|
||||
@@ -268,7 +253,6 @@ const App = () => {
|
||||
<Route path="trains" element={<TrainMasterDataPage />} />
|
||||
<Route path="trains/:id" element={<TrainDetailPage />} />
|
||||
<Route path="wagons" element={<WagonsCrudPage />} />
|
||||
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
|
||||
<Route path="containers" element={<ContainersCrudPage />} />
|
||||
<Route path="cargoes" element={<CargoesCrudPage />} />
|
||||
|
||||
|
||||
@@ -127,6 +127,8 @@ export const URL_CONSTANTS = {
|
||||
SCHEDULE_BY_ID: (id: string) => `/train-scheduling/container/schedules/${id}`,
|
||||
CANCEL_SCHEDULE: (id: string) =>
|
||||
`/train-scheduling/container/schedules/${id}/cancel`,
|
||||
PUBLISH_SCHEDULE: (id: string) =>
|
||||
`/train-scheduling/container/schedules/${id}/publish`,
|
||||
},
|
||||
|
||||
RULE_ENGINE: {
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
useUpdateContainer,
|
||||
} from '@/hooks/useContainers';
|
||||
import { useCreateTrain, useDeleteTrain, useTrains, useUpdateTrain } from '@/hooks/useTrains';
|
||||
import { useRouteYards } from '@/hooks/useRoutes';
|
||||
import { useCreateWagon, useDeleteWagon, useUpdateWagon, useWagons } from '@/hooks/useWagons';
|
||||
import {
|
||||
useCreateLocomotive,
|
||||
@@ -892,10 +893,15 @@ export function WagonTypesCrudPage() {
|
||||
export function WagonsCrudPage() {
|
||||
const query = useWagons();
|
||||
const { data: wagonTypes = [] } = useWagonTypes();
|
||||
const { data: yards = [] } = useRouteYards();
|
||||
const wagonTypeOptions = wagonTypes.map((type: any) => ({
|
||||
value: type.id,
|
||||
label: `${type.code} - ${type.name}`,
|
||||
}));
|
||||
const yardOptions = yards.map((yard: any) => ({
|
||||
value: yard.id,
|
||||
label: `${yard.label ?? yard.code} (${yard.country ?? '-'})`,
|
||||
}));
|
||||
return (
|
||||
<FleetCrudPage<Wagon>
|
||||
title="Wagons"
|
||||
@@ -906,10 +912,26 @@ export function WagonsCrudPage() {
|
||||
create={useCreateWagon()}
|
||||
update={useUpdateWagon()}
|
||||
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={[
|
||||
{ key: 'wagonNumber', label: 'Number' },
|
||||
{ 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: 'status', label: 'Status', render: (wagon) => statusBadge(wagon.status) },
|
||||
]}
|
||||
@@ -927,12 +949,31 @@ export function WagonsCrudPage() {
|
||||
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: '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' },
|
||||
]}
|
||||
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 LocomotiveStatus =
|
||||
| 'AVAILABLE'
|
||||
| 'UNAVAILABLE'
|
||||
| 'IMPORT_READY'
|
||||
| 'EXPORT_READY'
|
||||
| 'MAINTENANCE'
|
||||
| 'ASSIGNED'
|
||||
| 'OUT_OF_SERVICE';
|
||||
|
||||
@@ -70,6 +70,14 @@ export const trainSchedulingService = {
|
||||
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[]> => {
|
||||
const response = await client.get<LocomotiveRecord[]>(URL_CONSTANTS.LOCOMOTIVES.BASE, {
|
||||
params: { status: 'AVAILABLE' },
|
||||
|
||||
@@ -6,6 +6,19 @@ export interface Wagon {
|
||||
wagonTypeId: string;
|
||||
trainId: string | 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;
|
||||
maxPayloadWeight: number;
|
||||
status: string;
|
||||
|
||||
@@ -6,6 +6,9 @@ export const BOOKING_STATUSES = [
|
||||
"PENDING_APPROVAL",
|
||||
"APPROVED_PENDING_SIGNATURE",
|
||||
"APPROVED",
|
||||
"READY_FOR_ASSIGNMENT",
|
||||
"WAGON_ASSIGNED",
|
||||
"INVOICED",
|
||||
"CONTRACT_READY",
|
||||
"SIGNED_CUSTOMER",
|
||||
"FULLY_EXECUTED",
|
||||
|
||||
@@ -23,6 +23,9 @@ export interface EligibleContainerBookingsResponse {
|
||||
items: EligibleContainerBooking[];
|
||||
}
|
||||
|
||||
export type AssignmentType = "CONTAINER" | "BULK";
|
||||
export type TradeDirection = "IMPORT" | "EXPORT" | "DOMESTIC";
|
||||
|
||||
export interface WagonPlanAllocation {
|
||||
bookingId: string;
|
||||
bookingReference: string;
|
||||
@@ -57,7 +60,14 @@ export interface LocomotiveRecord {
|
||||
name?: string | null;
|
||||
maxPullWeightTons: 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';
|
||||
}
|
||||
|
||||
@@ -125,6 +135,11 @@ export interface TrainScheduleDetail {
|
||||
code: string;
|
||||
name: string;
|
||||
} | null;
|
||||
physicalWagon?: {
|
||||
id: string;
|
||||
wagonNumber: string;
|
||||
status: string;
|
||||
} | null;
|
||||
allocations: Array<{
|
||||
id: string;
|
||||
bookingId: string;
|
||||
@@ -146,6 +161,8 @@ export interface TrainScheduleFilters {
|
||||
originStationId?: string;
|
||||
destinationStationId?: string;
|
||||
scheduleDate?: string;
|
||||
assignmentType?: AssignmentType;
|
||||
tradeDirection?: TradeDirection;
|
||||
}
|
||||
|
||||
export interface TrainSchedulePreviewPayload {
|
||||
@@ -153,10 +170,15 @@ export interface TrainSchedulePreviewPayload {
|
||||
scheduleDate: string;
|
||||
originStationId: string;
|
||||
destinationStationId: string;
|
||||
assignmentType?: AssignmentType;
|
||||
}
|
||||
|
||||
export interface CreateTrainSchedulePayload {
|
||||
routeId: string;
|
||||
scheduleDate: string;
|
||||
arrivalDate?: 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",
|
||||
"scripts": {
|
||||
"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:passenger": "turbo run dev --filter=@edr/passenger-api... --filter=@edr/passenger-portal... --filter=@edr/passenger-backoffice...",
|
||||
"dev:freight": "turbo run dev --filter=@edr/freight-api --filter=@edr/freight-portal --filter=@edr/freight-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: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...",
|
||||
|
||||
Reference in New Issue
Block a user