Train scheduling API,Routes and UI

This commit is contained in:
hagiye
2026-06-08 16:31:28 +03:00
parent 3d1f972e52
commit 7facbeda22
38 changed files with 1993 additions and 696 deletions

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@ node_modules/
coverage/ coverage/
*.tsbuildinfo *.tsbuildinfo
**/*.tsbuildinfo **/*.tsbuildinfo
**/vite.config.ts.timestamp-*.mjs
# env # env
.env .env

View File

@@ -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": {

View File

@@ -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,

View File

@@ -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

View File

@@ -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;
`);
}
}

View File

@@ -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;
`);
}
}

View File

@@ -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';
`);
}
}

View File

@@ -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',

View File

@@ -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',

View File

@@ -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;

View File

@@ -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[];
} }

View File

@@ -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';
} }

View File

@@ -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';
} }

View File

@@ -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);
}
} }

View File

@@ -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,

View File

@@ -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',
); );
}); });

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -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);
} }
} }

View File

@@ -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
} }
} }

View File

@@ -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()

View File

@@ -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;

View File

@@ -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],

View File

@@ -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();

View 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);
});

View File

@@ -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 />} />

View File

@@ -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: {

View File

@@ -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

View File

@@ -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';

View File

@@ -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' },

View File

@@ -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;

View File

@@ -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",

View File

@@ -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[];
} }

View File

@@ -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=

View File

@@ -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==

View File

@@ -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...",