Batch 1 to 3 implementation

This commit is contained in:
hagiye
2026-06-16 11:36:24 +03:00
parent c2b5b3b926
commit f5d6d8e7da
26 changed files with 50 additions and 1250 deletions

View File

View File

@@ -0,0 +1 @@
{"dependencies":{"pnpm":"11.1.1"}}

BIN
.pnpm-store/v11/index.db Normal file

Binary file not shown.

View File

@@ -13,13 +13,8 @@
"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",
<<<<<<< HEAD
"seed:wagons": "ts-node -r tsconfig-paths/register src/scripts/seed-edr-wagons.ts", "seed:wagons": "ts-node -r tsconfig-paths/register src/scripts/seed-edr-wagons.ts",
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
=======
"type-check": "tsc --noEmit",
"seed:demo-scheduling": "ts-node -r tsconfig-paths/register src/scripts/seed-demo-scheduling.ts"
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
}, },
"dependencies": { "dependencies": {
"@edr/api-common": "workspace:*", "@edr/api-common": "workspace:*",

View File

@@ -50,11 +50,8 @@ import { WagonsModule } from './modules/wagons/wagons.module';
import { ContainersModule } from './modules/container-management/containers.module'; import { ContainersModule } from './modules/container-management/containers.module';
import { CargoesModule } from './modules/cargoes/cargoes.module'; import { CargoesModule } from './modules/cargoes/cargoes.module';
import { RoutesModule } from './modules/routes/routes.module'; import { RoutesModule } from './modules/routes/routes.module';
<<<<<<< HEAD
import { WarehousesModule } from './modules/warehouses/warehouses.module'; import { WarehousesModule } from './modules/warehouses/warehouses.module';
=======
import { OverviewModule } from './modules/overview/overview.module'; import { OverviewModule } from './modules/overview/overview.module';
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
@Module({ @Module({
imports: [ imports: [
@@ -109,11 +106,8 @@ import { OverviewModule } from './modules/overview/overview.module';
ContainersModule, ContainersModule,
CargoesModule, CargoesModule,
RoutesModule, RoutesModule,
<<<<<<< HEAD
WarehousesModule, WarehousesModule,
=======
OverviewModule, OverviewModule,
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
], ],
providers: [EdrOrgSeeder, DemoUsersSeeder,FreightStaffUsersSeeder, DemoBookingsSeeder, PricingDataSeeder, FileUploadSettingsSeeder], providers: [EdrOrgSeeder, DemoUsersSeeder,FreightStaffUsersSeeder, DemoBookingsSeeder, PricingDataSeeder, FileUploadSettingsSeeder],
}) })

View File

@@ -12,7 +12,6 @@ import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import * as Handlebars from "handlebars"; import * as Handlebars from "handlebars";
import { ConfigService } from "@nestjs/config"; import { ConfigService } from "@nestjs/config";
import { SchedulingStatus } from "@edr/types";
import { Booking } from "../bookings/entities/booking.entity"; import { Booking } from "../bookings/entities/booking.entity";
import { import {
@@ -62,107 +61,6 @@ export class PaymentService {
const result = await this.telebirrProvider.initiate(input); const result = await this.telebirrProvider.initiate(input);
<<<<<<< HEAD
const queryRunner = this.datasource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
console.log(paymentResp.expiresAt);
try {
const resp = await cb(queryRunner);
const payment = await this.paymentRepo.createTr(queryRunner, {
amount,
currency,
method,
refId: resp.id,
type: resp.type,
merchantOrderId: orderId,
rawInitiation: paymentResp.rawInitiation,
clientAction: paymentResp.clientAction,
expiresAt: paymentResp.expiresAt,
reason,
});
await queryRunner.commitTransaction();
return {
refId: payment.refId,
clientAction: paymentResp.clientAction,
status: payment.status,
paidAt: payment.paidAt?.toISOString(),
failureCode: payment.failerCode ?? undefined,
failureMessage: payment.failureMessage ?? undefined,
};
} catch (err) {
await queryRunner.rollbackTransaction();
throw new Error("payment failed");
} finally {
await queryRunner.release();
}
}
async getActivePaymentByRefIdAndMethod(
refId: string,
method: PaymentEntity["method"],
): Promise<PaymentEntity | null> {
return this.paymentRepo.getActivePaymentByRefIdAndMethod(refId, method);
}
async genReceiptHtml(orderId: string) {
const payment = await this.paymentRepo.findOneBy({
merchantOrderId: orderId,
status: "success",
});
if (!payment) {
throw new BadRequestException();
}
const filePath = path.join(__dirname, "templates", "receipt.hbs");
if (!fs.existsSync(filePath)) {
throw new InternalServerErrorException();
}
const source = fs.readFileSync(filePath, "utf8");
const template = Handlebars.compile(source);
const html = template({
vendorName: "Ethio Djibouti Railway Ticket Booking",
vendorAddress: "Addis Ababa",
receiptDate: payment.paidAt,
paymentMethod: payment?.method,
subtotal: payment?.amount.toString(),
total: payment?.amount.toString(),
currency: payment?.currency,
reason: payment?.reason,
});
return html;
}
async checkStatusAndUpdate(orderId: string) {
const resp = await this.paymentRepo.findOneBy({ merchantOrderId: orderId });
if (!resp) {
throw new NotFoundException("order id not found");
}
try {
const result = await this.telebirrPaymentStategy.queryStatus(
resp.merchantOrderId,
);
const bizContent = result.rawResponse.biz_content as {
order_status: string;
};
const ordersStatus = bizContent.order_status;
if (ordersStatus == "PAY_SUCCESS") {
await this.datasource.transaction(async (mg) => {
const now = new Date();
const holdExpires = new Date(now.getTime() + 3 * 60 * 60 * 1000);
await mg.update(Booking, { id: resp.refId }, {
status: "PAID",
schedulingStatus: SchedulingStatus.Holding,
holdStartedAt: now,
holdExpiresAt: holdExpires,
});
await mg.update(PaymentEntity, { id: resp.id }, { status: "success" });
=======
const payment = await this.paymentRepo.create({ const payment = await this.paymentRepo.create({
amount: amount, amount: amount,
currency: DEFAULT_CURRENCY, currency: DEFAULT_CURRENCY,
@@ -174,7 +72,6 @@ export class PaymentService {
clientAction: result.clientAction as Record<string, unknown>, clientAction: result.clientAction as Record<string, unknown>,
expiresAt: result.expiresAt, expiresAt: result.expiresAt,
reason: `Payment for booking`, reason: `Payment for booking`,
>>>>>>> eda21e22d872344b74c0c72308f87ce7435b299f
}); });
return { return {

View File

@@ -8,22 +8,11 @@ import { TrainSet } from '../../train-sets/entities/train-set.entity';
import { TrainScheduleBooking } from './train-schedule-booking.entity'; import { TrainScheduleBooking } from './train-schedule-booking.entity';
export const TRAIN_SCHEDULE_STATUSES = [ export const TRAIN_SCHEDULE_STATUSES = [
<<<<<<< HEAD
'DRAFT',
'READY',
'PUBLISHED',
'DEPARTED',
'IN_TRANSIT',
'ARRIVED',
'COMPLETED',
'CANCELLED',
=======
TrainScheduleStatusEnum.Draft, TrainScheduleStatusEnum.Draft,
TrainScheduleStatusEnum.Scheduled, TrainScheduleStatusEnum.Scheduled,
TrainScheduleStatusEnum.Dispatched, TrainScheduleStatusEnum.Dispatched,
TrainScheduleStatusEnum.Arrived, TrainScheduleStatusEnum.Arrived,
TrainScheduleStatusEnum.Cancelled, TrainScheduleStatusEnum.Cancelled,
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
] as const; ] as const;
export type TrainScheduleStatus = (typeof TRAIN_SCHEDULE_STATUSES)[number]; export type TrainScheduleStatus = (typeof TRAIN_SCHEDULE_STATUSES)[number];

View File

@@ -1,11 +1,6 @@
<<<<<<< HEAD
import { ApiProperty } from '@nestjs/swagger';
import { ArrayMinSize, IsArray, IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
=======
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsDateString, IsInt, IsNumber, IsOptional, IsUUID, Min } from 'class-validator'; import { IsDateString, IsInt, IsNumber, IsOptional, IsUUID, Min } from 'class-validator';
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
export class CreateContainerTrainScheduleDto { export class CreateContainerTrainScheduleDto {
@ApiProperty({ format: 'uuid' }) @ApiProperty({ format: 'uuid' })
@@ -25,26 +20,6 @@ export class CreateContainerTrainScheduleDto {
@IsUUID() @IsUUID()
locomotiveId!: string; locomotiveId!: string;
<<<<<<< HEAD
@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[];
=======
@ApiPropertyOptional({ description: 'Maximum total booking weight allowed on this train' }) @ApiPropertyOptional({ description: 'Maximum total booking weight allowed on this train' })
@IsOptional() @IsOptional()
@Type(() => Number) @Type(() => Number)
@@ -65,5 +40,4 @@ export class CreateContainerTrainScheduleDto {
@IsInt() @IsInt()
@Min(1) @Min(1)
maxWagonsPerTrain?: number; maxWagonsPerTrain?: number;
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
} }

View File

@@ -1,9 +1,5 @@
import { ApiPropertyOptional } from '@nestjs/swagger'; import { ApiPropertyOptional } from '@nestjs/swagger';
<<<<<<< HEAD
import { IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
=======
import { IsOptional, IsUUID } from 'class-validator'; import { IsOptional, IsUUID } from 'class-validator';
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
export class GetEligibleContainerBookingsDto { export class GetEligibleContainerBookingsDto {
@ApiPropertyOptional({ format: 'uuid' }) @ApiPropertyOptional({ format: 'uuid' })
@@ -18,20 +14,5 @@ export class GetEligibleContainerBookingsDto {
@ApiPropertyOptional({ example: 'HOLDING' }) @ApiPropertyOptional({ example: 'HOLDING' })
@IsOptional() @IsOptional()
<<<<<<< HEAD
@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';
=======
schedulingStatus?: string; schedulingStatus?: string;
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
} }

View File

@@ -1,33 +1,3 @@
<<<<<<< HEAD
import { ApiProperty } from '@nestjs/swagger';
import { ArrayMinSize, IsArray, IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
export class PreviewContainerTrainScheduleDto {
@ApiProperty({ type: [String] })
@IsArray()
@ArrayMinSize(1)
@IsUUID('4', { each: true })
bookingIds!: string[];
@ApiProperty({ example: '2026-06-20T08:00:00.000Z' })
@IsDateString()
scheduleDate!: string;
@ApiProperty({ format: 'uuid' })
@IsUUID()
originStationId!: string;
@ApiProperty({ format: 'uuid' })
@IsUUID()
destinationStationId!: string;
@ApiProperty({ enum: ['CONTAINER', 'BULK'], default: 'CONTAINER' })
@IsOptional()
@IsIn(['CONTAINER', 'BULK'])
assignmentType?: 'CONTAINER' | 'BULK';
}
=======
import { PreviewTrainScheduleDto } from './preview-train-schedule.dto'; import { PreviewTrainScheduleDto } from './preview-train-schedule.dto';
export class PreviewContainerTrainScheduleDto extends PreviewTrainScheduleDto {} export class PreviewContainerTrainScheduleDto extends PreviewTrainScheduleDto {}
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db

View File

@@ -196,17 +196,10 @@ export class TrainSchedulingController {
return this.trainSchedulingService.cancelTrainSchedule(id); return this.trainSchedulingService.cancelTrainSchedule(id);
} }
<<<<<<< HEAD
@Post('container/schedules/:id/publish')
@ApiOperation({ summary: 'Publish container train schedule' })
publishTrainSchedule(@Param('id', ParseUUIDPipe) id: string) {
return this.trainSchedulingService.publishTrainSchedule(id);
=======
@Post('bulk/schedules/:id/cancel') @Post('bulk/schedules/:id/cancel')
@TrainSchedulingManage() @TrainSchedulingManage()
@ApiOperation({ summary: 'Cancel bulk train schedule' }) @ApiOperation({ summary: 'Cancel bulk train schedule' })
cancelBulkTrainSchedule(@Param('id', ParseUUIDPipe) id: string) { cancelBulkTrainSchedule(@Param('id', ParseUUIDPipe) id: string) {
return this.trainSchedulingService.cancelTrainSchedule(id); return this.trainSchedulingService.cancelTrainSchedule(id);
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
} }
} }

View File

@@ -14,18 +14,7 @@ import { TrainSchedulesModule } from '../train-schedules/train-schedules.module'
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 { Wagon } from '../wagons/entities/wagon.entity';
<<<<<<< HEAD
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';
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 { TrainSchedulesModule } from '../train-schedules/train-schedules.module';
import { Yard } from '../rule-engine/entities/yard.entity';
=======
import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity'; import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity';
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
import { TrainSchedulingController } from './train-scheduling.controller'; import { TrainSchedulingController } from './train-scheduling.controller';
import { TrainSchedulingService } from './train-scheduling.service'; import { TrainSchedulingService } from './train-scheduling.service';

View File

@@ -58,15 +58,10 @@ const makeBooking = (
scheduledDate: new Date(scheduledDate), scheduledDate: new Date(scheduledDate),
originYardId, originYardId,
destinationYardId, destinationYardId,
<<<<<<< HEAD
status: 'APPROVED',
customer: { companyName: 'Demo Customer' },
=======
status: 'PAID', status: 'PAID',
schedulingStatus: 'HOLDING', schedulingStatus: 'HOLDING',
holdExpiresAt: new Date(Date.now() + 60 * 60 * 1000), holdExpiresAt: new Date(Date.now() + 60 * 60 * 1000),
company: { companyName: 'Demo Customer' }, company: { companyName: 'Demo Customer' },
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
originYard: { label: 'Djibouti', code: 'DJIBOUTI' }, originYard: { label: 'Djibouti', code: 'DJIBOUTI' },
destinationYard: { label: 'Addis Ababa', code: 'ADDIS_ABABA' }, destinationYard: { label: 'Addis Ababa', code: 'ADDIS_ABABA' },
bookingContainers: [ bookingContainers: [
@@ -352,14 +347,7 @@ describe('TrainSchedulingService', () => {
it('rejects bookings that are not in assignable status', async () => { it('rejects bookings that are not in assignable status', async () => {
const bookings = [ const bookings = [
<<<<<<< HEAD
{
...makeBooking('b7', 'BKG-CONT-007', 120, 2, '40FT'),
status: 'PAID',
},
=======
{ ...makeBooking('b7', 'BKG-CONT-007', 120, 2, '40FT', 2), status: 'APPROVED' }, { ...makeBooking('b7', 'BKG-CONT-007', 120, 2, '40FT', 2), status: 'APPROVED' },
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
]; ];
wagonTypesRepository.findAll.mockResolvedValue([nw5]); wagonTypesRepository.findAll.mockResolvedValue([nw5]);

View File

@@ -46,73 +46,6 @@ import { PreviewTrainScheduleDto } from './dto/preview-train-schedule.dto';
import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity'; import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity';
import { UpdateTrainSchedulingGlobalRulesDto } from './dto/update-train-scheduling-global-rules.dto'; import { UpdateTrainSchedulingGlobalRulesDto } from './dto/update-train-scheduling-global-rules.dto';
import { import {
<<<<<<< HEAD
Locomotive,
type LocomotiveStatus,
} from "../locomotives/entities/locomotive.entity";
import { LocomotivesRepository } from "../locomotives/locomotives.repository";
import { TrainSetWagon } from "../train-sets/entities/train-set-wagon.entity";
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";
import { GetEligibleContainerBookingsDto } from "./dto/get-eligible-container-bookings.dto";
import { PreviewContainerTrainScheduleDto } from "./dto/preview-container-train-schedule.dto";
const DEFAULT_WAGON_TYPE_CODE = "NW5";
const MAX_TRAIN_WEIGHT_TONS = 3500;
const MAX_TRAIN_LENGTH_METERS = 760;
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;
reference: string;
customer: string;
containerType: string;
quantity: number;
weightTons: number;
origin: string;
destination: string;
preferredDepartureDate: string;
status: string;
};
type WagonAllocationRecord = {
bookingId: string;
bookingReference: string;
allocatedWeightTons: number;
};
type WagonPlanRecord = {
sequenceNo: number;
capacityTons: number;
lengthMeters: number;
assignedWeightTons: number;
allocations: WagonAllocationRecord[];
};
type ValidationResult = {
valid: boolean;
violations: string[];
bookings: Booking[];
wagonType: WagonType;
summary: {
totalBookings: number;
totalWeightTons: number;
wagonType: string;
wagonsNeeded: number;
totalLengthMeters: number;
};
wagonPlan: WagonPlanRecord[];
=======
buildCappedWagonPlan, buildCappedWagonPlan,
computeFleetAvailability, computeFleetAvailability,
selectBookingsWithinFleetCap, selectBookingsWithinFleetCap,
@@ -150,7 +83,6 @@ const DEFAULT_TRAIN_LIMITS: Required<TrainLimitConfig> = {
maxWagonsPerTrain: 53, maxWagonsPerTrain: 53,
max20ftContainerWeightTons: 30, max20ftContainerWeightTons: 30,
max20ftPairWeightDiffTons: 10, max20ftPairWeightDiffTons: 10,
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
}; };
@Injectable() @Injectable()
@@ -180,36 +112,12 @@ export class TrainSchedulingService {
} }
async getEligibleContainerBookings(query: GetEligibleContainerBookingsDto) { async getEligibleContainerBookings(query: GetEligibleContainerBookingsDto) {
<<<<<<< HEAD
const bookingRepository = this.dataSource.getRepository(Booking);
const queryBuilder = bookingRepository
.createQueryBuilder("booking")
.leftJoinAndSelect("booking.company", "company")
.leftJoinAndSelect("booking.originYard", "originYard")
.leftJoinAndSelect("booking.destinationYard", "destinationYard")
.leftJoinAndSelect("booking.bookingContainers", "bookingContainer")
.leftJoinAndSelect("bookingContainer.containerType", "containerType")
.leftJoin(
TrainScheduleBooking,
"scheduleBooking",
"scheduleBooking.booking_id = booking.id",
)
.where("booking.freightType = :freightType", {
freightType: query.assignmentType ?? "CONTAINER",
})
.andWhere("scheduleBooking.id IS NULL");
queryBuilder.andWhere("booking.status IN (:...assignableStatuses)", {
assignableStatuses: ASSIGNABLE_BOOKING_STATUSES,
});
=======
return this.getEligibleBookings({ ...query, freightType: 'CONTAINER' }); return this.getEligibleBookings({ ...query, freightType: 'CONTAINER' });
} }
async getEligibleBulkBookings(query: GetEligibleBulkBookingsDto) { async getEligibleBulkBookings(query: GetEligibleBulkBookingsDto) {
return this.getEligibleBookings({ ...query, freightType: 'BULK' }); return this.getEligibleBookings({ ...query, freightType: 'BULK' });
} }
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
async getTrainSchedulingGlobalRules() { async getTrainSchedulingGlobalRules() {
return this.loadGlobalRulesRow(); return this.loadGlobalRulesRow();
@@ -226,84 +134,12 @@ export class TrainSchedulingService {
if (dto.max20ftContainerWeightTons != null) { if (dto.max20ftContainerWeightTons != null) {
row.max20ftContainerWeightTons = dto.max20ftContainerWeightTons; row.max20ftContainerWeightTons = dto.max20ftContainerWeightTons;
} }
<<<<<<< HEAD
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`,
{ scheduleDate: this.toUtcDateKey(query.scheduleDate) },
);
=======
if (dto.max20ftPairWeightDiffTons != null) { if (dto.max20ftPairWeightDiffTons != null) {
row.max20ftPairWeightDiffTons = dto.max20ftPairWeightDiffTons; row.max20ftPairWeightDiffTons = dto.max20ftPairWeightDiffTons;
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
} }
return this.dataSource.getRepository(TrainSchedulingGlobalRules).save(row); return this.dataSource.getRepository(TrainSchedulingGlobalRules).save(row);
} }
<<<<<<< HEAD
const bookings = await queryBuilder
.orderBy("booking.scheduled_date", "ASC")
.addOrderBy("booking.created_at", "ASC")
.getMany();
const items: EligibleBookingItem[] = bookings.map((booking) => ({
id: booking.id,
reference: booking.reference,
customer:
booking.company?.name ?? booking.company?.email ?? "Unknown customer",
containerType:
booking.bookingContainers
?.map(
(container) =>
container.containerType?.label ??
container.containerType?.code ??
"Container",
)
.join(", ") ?? (booking.freightType === "BULK" ? "Bulk cargo" : "Container"),
quantity:
booking.bookingContainers?.reduce(
(sum, container) => sum + Number(container.quantity ?? 0),
0,
) ?? 0,
weightTons: this.roundTons(booking.cargoTotalWeightVgm),
origin:
booking.originYard?.label ??
booking.originYard?.code ??
"Unknown origin",
destination:
booking.destinationYard?.label ??
booking.destinationYard?.code ??
"Unknown destination",
preferredDepartureDate: booking.scheduledDate.toISOString(),
status: booking.status,
}));
return {
count: items.length,
items,
};
=======
async previewTrainSchedule(dto: PreviewTrainScheduleDto) { async previewTrainSchedule(dto: PreviewTrainScheduleDto) {
const limits = await this.resolveTrainLimitConfig(dto); const limits = await this.resolveTrainLimitConfig(dto);
return this.buildPreviewResponse( return this.buildPreviewResponse(
@@ -317,7 +153,6 @@ export class TrainSchedulingService {
dto.targetScheduleId, dto.targetScheduleId,
), ),
); );
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
} }
async previewContainerTrainSchedule(dto: PreviewContainerTrainScheduleDto) { async previewContainerTrainSchedule(dto: PreviewContainerTrainScheduleDto) {
@@ -370,29 +205,6 @@ 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);
<<<<<<< HEAD
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,
validation?.summary.totalWeightTons ?? 0,
validation?.summary.totalLengthMeters ?? 0,
=======
const locomotive = await this.selectOrValidateLocomotive(dto.locomotiveId, 0, 0); const locomotive = await this.selectOrValidateLocomotive(dto.locomotiveId, 0, 0);
const createdScheduleId = await this.dataSource.transaction(async (manager) => { const createdScheduleId = await this.dataSource.transaction(async (manager) => {
@@ -467,7 +279,6 @@ export class TrainSchedulingService {
true, true,
limits, limits,
scheduleId, scheduleId,
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
); );
if (!validation.valid) { if (!validation.valid) {
@@ -671,42 +482,8 @@ export class TrainSchedulingService {
); );
} }
<<<<<<< HEAD
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,
routeId: route.id,
originStationId: route.originYardId,
destinationStationId: route.destinationYardId,
scheduledDepartureDate: new Date(dto.scheduleDate),
scheduledArrivalDate: dto.arrivalDate ? new Date(dto.arrivalDate) : null,
status: validation ? "READY" : "DRAFT",
=======
const physicalWagon = await manager.getRepository(Wagon).findOne({ const physicalWagon = await manager.getRepository(Wagon).findOne({
where: { id: assignment.physicalWagonId }, where: { id: assignment.physicalWagonId },
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
}); });
if (!physicalWagon) { if (!physicalWagon) {
throw new NotFoundException(`Wagon ${assignment.physicalWagonId} not found`); throw new NotFoundException(`Wagon ${assignment.physicalWagonId} not found`);
@@ -725,55 +502,9 @@ export class TrainSchedulingService {
); );
} }
<<<<<<< HEAD
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",
=======
await manager.getRepository(TrainSetWagon).update(assignment.trainSetWagonId, { await manager.getRepository(TrainSetWagon).update(assignment.trainSetWagonId, {
physicalWagonId: assignment.physicalWagonId, physicalWagonId: assignment.physicalWagonId,
status: 'RESERVED', status: 'RESERVED',
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
}); });
await manager.getRepository(Wagon).update(assignment.physicalWagonId, { await manager.getRepository(Wagon).update(assignment.physicalWagonId, {
trainSetWagonId: assignment.trainSetWagonId, trainSetWagonId: assignment.trainSetWagonId,
@@ -950,10 +681,6 @@ export class TrainSchedulingService {
violations.push('One or more selected bookings are already assigned to a train schedule'); violations.push('One or more selected bookings are already assigned to a train schedule');
} }
<<<<<<< HEAD
const nonContainerBookings = bookings.filter(
(booking) => booking.freightType !== (dto.assignmentType ?? "CONTAINER"),
=======
const bookingTypes = new Set(bookings.map((b) => b.freightType)); const bookingTypes = new Set(bookings.map((b) => b.freightType));
const isMixed = bookingTypes.size > 1; const isMixed = bookingTypes.size > 1;
const resolvedMode: 'CONTAINER' | 'BULK' | 'MIXED' = const resolvedMode: 'CONTAINER' | 'BULK' | 'MIXED' =
@@ -968,117 +695,15 @@ export class TrainSchedulingService {
const invalidStatus = bookings.filter( const invalidStatus = bookings.filter(
(b) => !SCHEDULABLE_BOOKING_STATUSES.includes(b.status as 'PAID'), (b) => !SCHEDULABLE_BOOKING_STATUSES.includes(b.status as 'PAID'),
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
); );
if (invalidStatus.length) { if (invalidStatus.length) {
const statuses = [...new Set(invalidStatus.map((b) => b.status))]; const statuses = [...new Set(invalidStatus.map((b) => b.status))];
violations.push( violations.push(
<<<<<<< HEAD
`Only ${dto.assignmentType ?? "CONTAINER"} bookings are supported for this assignment`,
);
}
const invalidStatusBookings = bookings.filter(
(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 ${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) =>
booking.originYardId !== dto.originStationId ||
booking.destinationYardId !== dto.destinationStationId,
);
if (routeMismatch) {
violations.push(
"Selected bookings must share the same origin and destination as the schedule",
);
}
const dateMismatch = bookings.some(
(booking) => this.toUtcDateKey(booking.scheduledDate) !== scheduleDateKey,
);
if (dateMismatch) {
violations.push("Selected bookings must share the same schedule date");
}
const uniqueOriginCount = new Set(
bookings.map((booking) => booking.originYardId),
).size;
if (uniqueOriginCount > 1) {
violations.push("Selected bookings must share the same origin station");
}
const uniqueDestinationCount = new Set(
bookings.map((booking) => booking.destinationYardId),
).size;
if (uniqueDestinationCount > 1) {
violations.push(
"Selected bookings must share the same destination station",
);
}
const uniqueDateCount = new Set(
bookings.map((booking) => this.toUtcDateKey(booking.scheduledDate)),
).size;
if (uniqueDateCount > 1) {
violations.push(
"Selected bookings must share the same preferred departure date",
);
}
const totalWeightTons = this.roundTons(
bookings.reduce(
(sum, booking) => sum + Number(booking.cargoTotalWeightVgm ?? 0),
0,
),
);
const wagonPlan = this.allocateBookingsToWagons(
bookings,
this.calculateNW5WagonPlan(totalWeightTons, wagonType),
);
const totalLengthMeters = this.roundTons(
wagonPlan.reduce((sum, wagon) => sum + wagon.lengthMeters, 0),
);
if (totalWeightTons > MAX_TRAIN_WEIGHT_TONS) {
violations.push(
`Total booking weight ${totalWeightTons}T exceeds max train weight ${MAX_TRAIN_WEIGHT_TONS}T`,
);
}
if (totalLengthMeters > MAX_TRAIN_LENGTH_METERS) {
violations.push(
`Total wagon length ${totalLengthMeters}m exceeds max train length ${MAX_TRAIN_LENGTH_METERS}m`,
=======
`Only ${SCHEDULABLE_BOOKING_STATUSES.join(', ')} bookings can be scheduled; received: ${statuses.join(', ')}`, `Only ${SCHEDULABLE_BOOKING_STATUSES.join(', ')} bookings can be scheduled; received: ${statuses.join(', ')}`,
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
); );
} }
if ( if (
<<<<<<< HEAD
wagonPlan.length >
(dto.assignmentType === "BULK" ? MAX_BULK_WAGONS : MAX_CONTAINER_WAGONS)
) {
violations.push(
`Wagon count ${wagonPlan.length} exceeds ${dto.assignmentType === "BULK" ? "bulk" : "container"} limit ${dto.assignmentType === "BULK" ? MAX_BULK_WAGONS : MAX_CONTAINER_WAGONS}`,
);
=======
bookings.some( bookings.some(
(b) => (b) =>
b.originYardId !== dto.originStationId || b.originYardId !== dto.originStationId ||
@@ -1086,7 +711,6 @@ export class TrainSchedulingService {
) )
) { ) {
violations.push('Selected bookings must share the same origin and destination as the schedule'); violations.push('Selected bookings must share the same origin and destination as the schedule');
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
} }
if (!forceAssign) { if (!forceAssign) {
@@ -1644,139 +1268,7 @@ export class TrainSchedulingService {
return locomotive; return locomotive;
} }
<<<<<<< HEAD
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,
wagonType: WagonType,
totalWeightTons: number,
totalLengthMeters: number,
wagonPlan: WagonPlanRecord[],
physicalWagons: Wagon[] = [],
) {
const trainSet = manager.getRepository(TrainSet).create({
locomotiveId: locomotive.id,
totalWeightTons,
totalLengthMeters,
wagonCount: wagonPlan.length,
status: "ASSIGNED",
});
const savedTrainSet = await manager.getRepository(TrainSet).save(trainSet);
const wagons = wagonPlan.map((wagon, index) => {
const physicalWagon = physicalWagons[index];
const selectedWagonType = physicalWagon?.wagonType ?? wagonType;
return manager.getRepository(TrainSetWagon).create({
trainSetId: savedTrainSet.id,
wagonTypeId: selectedWagonType.id,
physicalWagonId: physicalWagon?.id ?? null,
sequenceNo: wagon.sequenceNo,
capacityTons: Number(selectedWagonType.capacityTons),
lengthMeters: Number(selectedWagonType.lengthMeters),
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,
}),
),
);
}
return { trainSet: savedTrainSet, wagons: savedWagons };
}
async buildEmptyTrainSet(
manager: EntityManager,
locomotive: Locomotive,
) {
=======
private async buildEmptyTrainSet(manager: EntityManager, locomotive: Locomotive) { private async buildEmptyTrainSet(manager: EntityManager, locomotive: Locomotive) {
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
const trainSet = manager.getRepository(TrainSet).create({ const trainSet = manager.getRepository(TrainSet).create({
locomotiveId: locomotive.id, locomotiveId: locomotive.id,
totalWeightTons: 0, totalWeightTons: 0,
@@ -1787,313 +1279,6 @@ export class TrainSchedulingService {
return manager.getRepository(TrainSet).save(trainSet); return manager.getRepository(TrainSet).save(trainSet);
} }
<<<<<<< HEAD
allocateBookingsToWagons(
bookings: Booking[],
baseWagonPlan: WagonPlanRecord[],
): WagonPlanRecord[] {
const remaining = bookings.map((booking) => ({
bookingId: booking.id,
bookingReference: booking.reference,
remainingWeightTons: this.roundTons(
Number(booking.cargoTotalWeightVgm ?? 0),
),
}));
let bookingIndex = 0;
return baseWagonPlan.map((wagon) => {
let wagonRemaining = this.roundTons(wagon.capacityTons);
const allocations: WagonAllocationRecord[] = [];
let assignedWeightTons = 0;
while (wagonRemaining > 0 && bookingIndex < remaining.length) {
const booking = remaining[bookingIndex];
const allocatedWeightTons = this.roundTons(
Math.min(wagonRemaining, booking.remainingWeightTons),
);
if (allocatedWeightTons <= 0) {
bookingIndex += 1;
continue;
}
allocations.push({
bookingId: booking.bookingId,
bookingReference: booking.bookingReference,
allocatedWeightTons,
});
booking.remainingWeightTons = this.roundTons(
booking.remainingWeightTons - allocatedWeightTons,
);
wagonRemaining = this.roundTons(wagonRemaining - allocatedWeightTons);
assignedWeightTons = this.roundTons(
assignedWeightTons + allocatedWeightTons,
);
if (booking.remainingWeightTons <= 0) {
bookingIndex += 1;
}
}
return {
...wagon,
assignedWeightTons,
allocations,
};
});
}
async getContainerTrainSchedules() {
const schedules = await this.dataSource.getRepository(TrainSchedule).find({
relations: {
trainSet: { locomotive: true },
route: true,
originStation: true,
destinationStation: true,
scheduleBookings: true,
},
order: { scheduledDepartureDate: "DESC", createdAt: "DESC" },
});
return schedules.map((schedule) => ({
id: schedule.id,
scheduleDate: schedule.scheduledDepartureDate,
routeName: schedule.route?.name ?? null,
origin:
schedule.originStation?.label ?? schedule.originStation?.code ?? null,
destination:
schedule.destinationStation?.label ??
schedule.destinationStation?.code ??
null,
locomotive: schedule.trainSet?.locomotive
? {
id: schedule.trainSet.locomotive.id,
code: schedule.trainSet.locomotive.code,
name: schedule.trainSet.locomotive.name ?? null,
}
: null,
wagonCount: schedule.trainSet?.wagonCount ?? 0,
totalWeightTons: this.roundTons(
Number(schedule.trainSet?.totalWeightTons ?? 0),
),
totalLengthMeters: this.roundTons(
Number(schedule.trainSet?.totalLengthMeters ?? 0),
),
bookingsCount: schedule.scheduleBookings?.length ?? 0,
status: schedule.status,
}));
}
async getContainerTrainScheduleById(id: string) {
const schedule = await this.dataSource
.getRepository(TrainSchedule)
.findOne({
where: { id },
relations: {
route: true,
trainSet: {
locomotive: true,
wagons: { wagonType: true, physicalWagon: true, allocations: { booking: true } },
},
originStation: true,
destinationStation: true,
scheduleBookings: {
booking: { company: true, originYard: true, destinationYard: true },
},
},
});
if (!schedule) {
throw new NotFoundException(`Train schedule ${id} not found`);
}
return {
id: schedule.id,
status: schedule.status,
route: schedule.route
? {
id: schedule.route.id,
name: schedule.route.name,
}
: null,
scheduledDepartureDate: schedule.scheduledDepartureDate,
scheduledArrivalDate: schedule.scheduledArrivalDate,
originStation: schedule.originStation,
destinationStation: schedule.destinationStation,
trainSet: schedule.trainSet
? {
id: schedule.trainSet.id,
status: schedule.trainSet.status,
wagonCount: schedule.trainSet.wagonCount,
totalWeightTons: this.roundTons(
Number(schedule.trainSet.totalWeightTons),
),
totalLengthMeters: this.roundTons(
Number(schedule.trainSet.totalLengthMeters),
),
locomotive: schedule.trainSet.locomotive
? {
id: schedule.trainSet.locomotive.id,
code: schedule.trainSet.locomotive.code,
name: schedule.trainSet.locomotive.name,
status: schedule.trainSet.locomotive.status,
maxPullWeightTons: this.roundTons(
Number(schedule.trainSet.locomotive.maxPullWeightTons),
),
maxTrainLengthMeters: this.roundTons(
Number(schedule.trainSet.locomotive.maxTrainLengthMeters),
),
}
: null,
wagons: [...(schedule.trainSet.wagons ?? [])]
.sort((left, right) => left.sequenceNo - right.sequenceNo)
.map((wagon) => ({
id: wagon.id,
sequenceNo: wagon.sequenceNo,
capacityTons: this.roundTons(Number(wagon.capacityTons)),
lengthMeters: this.roundTons(Number(wagon.lengthMeters)),
assignedWeightTons: this.roundTons(
Number(wagon.assignedWeightTons),
),
wagonType: wagon.wagonType
? {
id: wagon.wagonType.id,
code: wagon.wagonType.code,
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,
bookingId: allocation.bookingId,
bookingReference: allocation.booking?.reference ?? null,
allocatedWeightTons: this.roundTons(
Number(allocation.allocatedWeightTons),
),
})) ?? [],
})),
}
: null,
bookings:
schedule.scheduleBookings?.map((scheduleBooking) => ({
id: scheduleBooking.booking?.id ?? scheduleBooking.bookingId,
reference: scheduleBooking.booking?.reference ?? null,
customer:
scheduleBooking.booking?.company?.name ??
scheduleBooking.booking?.company?.email ??
null,
weightTons: this.roundTons(
Number(scheduleBooking.booking?.cargoTotalWeightVgm ?? 0),
),
status: scheduleBooking.booking?.status ?? null,
})) ?? [],
};
}
async cancelTrainSchedule(id: string) {
const schedule = await this.dataSource
.getRepository(TrainSchedule)
.findOne({
where: { id },
relations: { trainSet: { locomotive: true, wagons: true } },
});
if (!schedule) {
throw new NotFoundException(`Train schedule ${id} not found`);
}
await this.dataSource.transaction(async (manager) => {
await manager.getRepository(TrainSchedule).update(schedule.id, {
status: "CANCELLED",
});
if (schedule.trainSetId) {
await manager.getRepository(TrainSet).update(schedule.trainSetId, {
status: "CANCELLED",
});
}
if (schedule.trainSet?.locomotiveId) {
await manager
.getRepository(Locomotive)
.update(schedule.trainSet.locomotiveId, {
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) },
relations: {
company: true,
originYard: true,
destinationYard: true,
bookingContainers: { containerType: true },
},
order: { createdAt: "ASC" },
});
}
=======
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
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 },
@@ -2104,18 +1289,6 @@ export class TrainSchedulingService {
return route; return route;
} }
<<<<<<< HEAD
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);
=======
private mapEligibleBooking(booking: Booking) { private mapEligibleBooking(booking: Booking) {
return { return {
id: booking.id, id: booking.id,
@@ -2137,7 +1310,6 @@ export class TrainSchedulingService {
preferredDepartureDate: booking.scheduledDate.toISOString(), preferredDepartureDate: booking.scheduledDate.toISOString(),
status: booking.status, status: booking.status,
}; };
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
} }
private resolveScheduleFreightType( private resolveScheduleFreightType(

View File

@@ -52,13 +52,6 @@ export class TrainSetWagon extends BaseEntity {
@Column({ name: 'assigned_weight_tons', type: 'numeric', precision: 10, scale: 3, default: 0 }) @Column({ name: 'assigned_weight_tons', type: 'numeric', precision: 10, scale: 3, default: 0 })
assignedWeightTons!: number; assignedWeightTons!: number;
@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;
@Column({ name: 'status', type: 'varchar', length: 20, default: 'PLANNED' }) @Column({ name: 'status', type: 'varchar', length: 20, default: 'PLANNED' })
status!: string; status!: string;

View File

@@ -28,10 +28,7 @@ const toStringArray = ({ value }: { value: unknown }) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map((entry) => String(entry).trim()).filter(Boolean); return value.map((entry) => String(entry).trim()).filter(Boolean);
} }
<<<<<<< HEAD
=======
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
if (typeof value !== 'string') return []; if (typeof value !== 'string') return [];
return value return value
@@ -46,40 +43,24 @@ export class CreateWagonTypeDto {
@MaxLength(32) @MaxLength(32)
code!: string; code!: string;
<<<<<<< HEAD
@ApiProperty({ description: 'Display name, e.g. "Flat Wagon"', maxLength: 100 })
=======
@ApiProperty({ maxLength: 100, example: 'Flat wagon container' }) @ApiProperty({ maxLength: 100, example: 'Flat wagon container' })
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
@IsString() @IsString()
@MaxLength(100) @MaxLength(100)
name!: string; name!: string;
<<<<<<< HEAD
@ApiProperty({ description: 'Maximum payload capacity in metric tons', example: 60 })
=======
@ApiProperty({ description: 'Maximum payload capacity in metric tons', example: 70 }) @ApiProperty({ description: 'Maximum payload capacity in metric tons', example: 70 })
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
@Transform(toNumber) @Transform(toNumber)
@IsNumber() @IsNumber()
@Min(0.001) @Min(0.001)
capacityTons!: number; capacityTons!: number;
<<<<<<< HEAD
@ApiProperty({ description: 'Wagon length in meters', example: 14.2 })
=======
@ApiProperty({ description: 'Wagon length in meters', example: 14 }) @ApiProperty({ description: 'Wagon length in meters', example: 14 })
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
@Transform(toNumber) @Transform(toNumber)
@IsNumber() @IsNumber()
@Min(0.001) @Min(0.001)
lengthMeters!: number; lengthMeters!: number;
<<<<<<< HEAD
@ApiPropertyOptional({ description: 'Maximum wagons of this type per train', example: 45 })
=======
@ApiPropertyOptional({ description: 'Maximum wagons of this type per train', example: 53 }) @ApiPropertyOptional({ description: 'Maximum wagons of this type per train', example: 53 })
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
@IsOptional() @IsOptional()
@Transform(toOptionalNumber) @Transform(toOptionalNumber)
@IsInt() @IsInt()

View File

@@ -28,9 +28,6 @@ export class WagonTypesController {
@RuleEngineView('wagon-types') @RuleEngineView('wagon-types')
@ApiOperation({ summary: 'List wagon types' }) @ApiOperation({ summary: 'List wagon types' })
findAll(@Query() query: Record<string, string | undefined>) { findAll(@Query() query: Record<string, string | undefined>) {
<<<<<<< HEAD
return this.wagonTypesService.findAll(query);
=======
return this.wagonTypesService.findAll({ return this.wagonTypesService.findAll({
isActive: isActive:
query.isActive === 'all' query.isActive === 'all'
@@ -43,7 +40,6 @@ export class WagonTypesController {
sortBy: query.sortBy, sortBy: query.sortBy,
sortOrder: query.sortOrder, sortOrder: query.sortOrder,
}); });
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
} }
@Get(':id') @Get(':id')

View File

@@ -6,44 +6,18 @@ 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';
<<<<<<< HEAD
type WagonTypeListResponse = {
data: WagonType[];
meta: { total: number; page: number; pageSize: number; totalPages: number };
=======
type WagonTypeListFilter = { type WagonTypeListFilter = {
isActive?: boolean; isActive?: boolean;
page?: number; page?: number;
pageSize?: number; pageSize?: number;
sortBy?: string; sortBy?: string;
sortOrder?: string; sortOrder?: string;
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
}; };
@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> {
const page = Math.max(1, Number(query.page) || 1);
const pageSize = Math.max(1, Number(query.pageSize) || 20);
const isActive =
query.isActive === 'all'
? undefined
: query.isActive === undefined
? true
: query.isActive === 'true';
const sortBy = ['code', 'name', 'capacityTons', 'lengthMeters', 'isActive'].includes(
query.sortBy ?? '',
)
? (query.sortBy as keyof WagonType)
: 'code';
const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
const [data, total] = await this.wagonTypesRepository.findAndCount({
where: isActive === undefined ? {} : { isActive },
=======
async findAll(filter: WagonTypeListFilter = {}): Promise<{ async findAll(filter: WagonTypeListFilter = {}): Promise<{
data: WagonType[]; data: WagonType[];
meta: { total: number; page: number; pageSize: number; totalPages: number }; meta: { total: number; page: number; pageSize: number; totalPages: number };
@@ -59,7 +33,6 @@ export class WagonTypesService {
const [data, total] = await this.wagonTypesRepository.findAndCount({ const [data, total] = await this.wagonTypesRepository.findAndCount({
where: filter.isActive === undefined ? {} : { isActive: filter.isActive }, where: filter.isActive === undefined ? {} : { isActive: filter.isActive },
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
order: { [sortBy]: sortOrder } as FindOptionsOrder<WagonType>, order: { [sortBy]: sortOrder } as FindOptionsOrder<WagonType>,
skip: (page - 1) * pageSize, skip: (page - 1) * pageSize,
take: pageSize, take: pageSize,
@@ -71,21 +44,14 @@ export class WagonTypesService {
total, total,
page, page,
pageSize, pageSize,
<<<<<<< HEAD
totalPages: Math.ceil(total / pageSize),
=======
totalPages: Math.max(1, Math.ceil(total / pageSize)), totalPages: Math.max(1, Math.ceil(total / pageSize)),
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
}, },
}; };
} }
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);
<<<<<<< HEAD
=======
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
if (!wagonType) { if (!wagonType) {
throw new NotFoundException(`Wagon type ${id} not found`); throw new NotFoundException(`Wagon type ${id} not found`);
} }
@@ -103,25 +69,16 @@ export class WagonTypesService {
async create(dto: CreateWagonTypeDto): Promise<WagonType> { async create(dto: CreateWagonTypeDto): Promise<WagonType> {
const code = dto.code.trim().toUpperCase(); const code = dto.code.trim().toUpperCase();
const existing = await this.wagonTypesRepository.findByCode(code); const existing = await this.wagonTypesRepository.findByCode(code);
<<<<<<< HEAD
=======
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
if (existing) { if (existing) {
throw new ConflictException(`Wagon type code "${code}" already exists`); throw new ConflictException(`Wagon type code "${code}" already exists`);
} }
return this.wagonTypesRepository.create({ return this.wagonTypesRepository.create({
<<<<<<< HEAD
...dto,
code,
name: dto.name.trim(),
=======
code, code,
name: dto.name.trim(), name: dto.name.trim(),
capacityTons: dto.capacityTons, capacityTons: dto.capacityTons,
lengthMeters: dto.lengthMeters, lengthMeters: dto.lengthMeters,
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
maxWagonsPerTrain: dto.maxWagonsPerTrain ?? null, maxWagonsPerTrain: dto.maxWagonsPerTrain ?? null,
supportedLoadTypes: dto.supportedLoadTypes ?? [], supportedLoadTypes: dto.supportedLoadTypes ?? [],
isActive: dto.isActive ?? true, isActive: dto.isActive ?? true,
@@ -143,12 +100,9 @@ export class WagonTypesService {
...dto, ...dto,
...(nextCode ? { code: nextCode } : {}), ...(nextCode ? { code: nextCode } : {}),
...(dto.name ? { name: dto.name.trim() } : {}), ...(dto.name ? { name: dto.name.trim() } : {}),
<<<<<<< HEAD
=======
maxWagonsPerTrain: maxWagonsPerTrain:
dto.maxWagonsPerTrain === undefined ? undefined : dto.maxWagonsPerTrain ?? null, dto.maxWagonsPerTrain === undefined ? undefined : dto.maxWagonsPerTrain ?? null,
supportedLoadTypes: dto.supportedLoadTypes ?? undefined, supportedLoadTypes: dto.supportedLoadTypes ?? undefined,
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
}); });
if (!updated) { if (!updated) {

View File

@@ -30,17 +30,12 @@ export class CreateWagonDto {
maxPayloadWeight!: number; maxPayloadWeight!: number;
@IsOptional() @IsOptional()
<<<<<<< HEAD
@IsIn(['AVAILABLE', 'IMPORT_READY', 'EXPORT_READY', 'ASSIGNED', 'MAINTENANCE', 'RETIRED'])
status?: string;
=======
@IsEnum(WagonStatus) @IsEnum(WagonStatus)
status?: WagonStatus; status?: WagonStatus;
@IsOptional() @IsOptional()
@IsEnum(WagonReadiness) @IsEnum(WagonReadiness)
readiness?: WagonReadiness; readiness?: WagonReadiness;
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
@IsOptional() @IsOptional()
@IsString() @IsString()

View File

@@ -12,6 +12,8 @@ import { WagonType } from '../../wagon-types/entities/wagon-type.entity';
export const WAGON_STATUSES = [ export const WAGON_STATUSES = [
WagonStatus.Available, WagonStatus.Available,
WagonStatus.Assigned, WagonStatus.Assigned,
WagonStatus.ImportReady,
WagonStatus.ExportReady,
WagonStatus.Maintenance, WagonStatus.Maintenance,
WagonStatus.Retired, WagonStatus.Retired,
] as const; ] as const;
@@ -56,16 +58,11 @@ export class Wagon extends BaseEntity {
@Column({ name: 'max_payload_weight', type: 'decimal', precision: 10, scale: 2 }) @Column({ name: 'max_payload_weight', type: 'decimal', precision: 10, scale: 2 })
maxPayloadWeight!: number; maxPayloadWeight!: number;
<<<<<<< HEAD
@Column({ type: 'varchar', default: 'AVAILABLE' })
status!: string; // AVAILABLE, IMPORT_READY, EXPORT_READY, ASSIGNED, MAINTENANCE, RETIRED
=======
@Column({ type: 'varchar', length: 20, default: WagonStatus.Available }) @Column({ type: 'varchar', length: 20, default: WagonStatus.Available })
status!: WagonStatusType; status!: WagonStatusType;
@Column({ type: 'varchar', length: 20, default: WagonReadiness.ImportReady }) @Column({ type: 'varchar', length: 20, default: WagonReadiness.ImportReady })
readiness!: WagonReadinessType; readiness!: WagonReadinessType;
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
@Column({ type: 'text', nullable: true }) @Column({ type: 'text', nullable: true })
notes!: string | null; notes!: string | null;
@@ -84,7 +81,7 @@ export class Wagon extends BaseEntity {
@JoinColumn({ name: 'current_train_schedule_id' }) @JoinColumn({ name: 'current_train_schedule_id' })
currentTrainSchedule?: TrainSchedule | null; currentTrainSchedule?: TrainSchedule | null;
/** Fleet master consist grouping separate from operational train_schedules. */ /** Fleet master consist grouping — separate from operational train_schedules. */
@ManyToOne(() => Train, (train) => train.wagons, { onDelete: 'SET NULL' }) @ManyToOne(() => Train, (train) => train.wagons, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'train_id' }) @JoinColumn({ name: 'train_id' })
train!: Train | null; train!: Train | null;

View File

@@ -41,26 +41,16 @@ export class WagonsService {
const status = query.status?.trim(); const status = query.status?.trim();
const readiness = query.readiness?.trim(); const readiness = query.readiness?.trim();
const trainId = query.trainId?.trim(); const trainId = query.trainId?.trim();
<<<<<<< HEAD
const currentLocationYardId = query.currentLocationYardId?.trim();
=======
const filters = { const filters = {
...(status ? { status: status as Wagon['status'] } : {}), ...(status ? { status: status as Wagon['status'] } : {}),
...(readiness ? { readiness: readiness as Wagon['readiness'] } : {}), ...(readiness ? { readiness: readiness as Wagon['readiness'] } : {}),
...(trainId ? { trainId } : {}), ...(trainId ? { trainId } : {}),
}; };
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
if (search) { if (search) {
where.push({ where.push({
wagonNumber: ILike(`%${search}%`), wagonNumber: ILike(`%${search}%`),
<<<<<<< HEAD
...(status ? { status } : {}),
...(trainId ? { trainId } : {}),
...(currentLocationYardId ? { currentLocationYardId } : {}),
=======
...filters, ...filters,
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
}); });
} }
@@ -70,12 +60,7 @@ 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({
<<<<<<< HEAD
where: search ? where : { ...(status ? { status } : {}), ...(trainId ? { trainId } : {}), ...(currentLocationYardId ? { currentLocationYardId } : {}) },
relations: { currentLocationYard: true, wagonType: true },
=======
where: search ? where : filters, where: search ? where : filters,
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
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,
@@ -131,22 +116,23 @@ 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;
<<<<<<< HEAD
wagon.status = await this.statusForLocation(wagon.currentLocationYardId, 'AVAILABLE');
=======
wagon.status = WagonStatus.Available; wagon.status = WagonStatus.Available;
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
return this.wagonRepo.save(wagon); return this.wagonRepo.save(wagon);
} }
private async statusForLocation(yardId?: string | null, fallback = 'AVAILABLE') { private async statusForLocation(
yardId?: string | null,
fallback: Wagon['status'] = WagonStatus.Available,
): Promise<Wagon['status']> {
if (!yardId) return fallback; if (!yardId) return fallback;
const yard = await this.yardRepo.findOne({ where: { id: yardId } }); const yard = await this.yardRepo.findOne({ where: { id: yardId } });
const country = yard?.country?.trim().toLowerCase(); const country = yard?.country?.trim().toLowerCase();
if (country === 'ethiopia' || country === 'et') return 'EXPORT_READY'; if (country === 'ethiopia' || country === 'et') return WagonStatus.ExportReady;
if (country === 'djibouti' || country === 'djoubti' || country === 'dj') return 'IMPORT_READY'; if (country === 'djibouti' || country === 'djoubti' || country === 'dj') {
return WagonStatus.ImportReady;
}
return fallback; return fallback;
} }

View File

@@ -33,24 +33,14 @@ import UserManagementPage from "./pages/dashboard/user-management/UserManagement
import UsersPage from "./pages/dashboard/user-management/UsersPage"; import UsersPage from "./pages/dashboard/user-management/UsersPage";
import DropdownSettingsPage from "./pages/dropdown_settings/DropdownSettingsPage"; import DropdownSettingsPage from "./pages/dropdown_settings/DropdownSettingsPage";
import FileUploadSettingsPage from "./pages/documents/FileUploadSettingsPage"; import FileUploadSettingsPage from "./pages/documents/FileUploadSettingsPage";
import RuleEngineLegacyRedirect from "./pages/ruleEngine/RuleEngineLegacyRedirect"; import RuleEngineLegacyRedirect from "./pages/ruleEngine/RuleEngineLegacyRedirect";
import RuleEngineResourcePage from "./pages/ruleEngine/RuleEngineResourcePage"; import RuleEngineResourcePage from "./pages/ruleEngine/RuleEngineResourcePage";
import TrainsPage from "./pages/trains/TrainsPage"; import TrainsPage from "./pages/trains/TrainsPage";
<<<<<<< HEAD import TrainScheduleV2ListPage from "./pages/trainScheduling/TrainScheduleV2ListPage";
import { import TrainScheduleV2DetailPage from "./pages/trainScheduling/TrainScheduleV2DetailPage";
CargoesCrudPage, import TrainSchedulingGlobalRulesPage from "./pages/trainScheduling/TrainSchedulingGlobalRulesPage";
ContainersCrudPage, import FleetResourcePage from "./pages/fleet/FleetResourcePage";
LocomotivesCrudPage, import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
TrainMasterDataPage,
WagonsCrudPage,
} from "./pages/fleet/FleetCrudPages";
=======
import TrainScheduleV2ListPage from "./pages/trainScheduling/TrainScheduleV2ListPage";
import TrainScheduleV2DetailPage from "./pages/trainScheduling/TrainScheduleV2DetailPage";
import TrainSchedulingGlobalRulesPage from "./pages/trainScheduling/TrainSchedulingGlobalRulesPage";
import FleetResourcePage from "./pages/fleet/FleetResourcePage";
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
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";
import WarehouseListPage from "./pages/warehouses/WarehouseListPage"; import WarehouseListPage from "./pages/warehouses/WarehouseListPage";
@@ -289,35 +279,24 @@ const App = () => {
path="booking-requests/:id/contract" path="booking-requests/:id/contract"
element={<BookingContractPage />} element={<BookingContractPage />}
/> />
<Route path="operations/train-scheduling" element={<TrainsPage />} /> <Route path="operations/train-scheduling" element={<TrainsPage />} />
<<<<<<< HEAD <Route
<Route path="trains" element={<TrainMasterDataPage />} /> path="operations/train-scheduling-v2"
<Route path="operations/train-scheduling" element={<TrainsPage />} /> element={<TrainScheduleV2ListPage />}
======= />
<Route <Route
path="operations/train-scheduling-v2" path="operations/train-scheduling-v2/:scheduleId"
element={<TrainScheduleV2ListPage />} element={<TrainScheduleV2DetailPage />}
/> />
<Route <Route path="routes" element={<RoutesPage />} />
path="operations/train-scheduling-v2/:scheduleId" <Route path="locomotives" element={<FleetResourcePage />} />
element={<TrainScheduleV2DetailPage />} <Route path="trains" element={<FleetResourcePage />} />
/> <Route path="trains/:id" element={<TrainDetailPage />} />
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db <Route path="wagons" element={<FleetResourcePage />} />
<Route path="routes" element={<RoutesPage />} /> <Route path="containers" element={<FleetResourcePage />} />
<Route path="locomotives" element={<FleetResourcePage />} /> <Route path="cargoes" element={<FleetResourcePage />} />
<Route path="trains" element={<FleetResourcePage />} />
<Route path="trains/:id" element={<TrainDetailPage />} /> <Route path="warehouses" element={<WarehouseListPage />} />
<<<<<<< HEAD
<Route path="wagons" element={<WagonsCrudPage />} />
<Route path="containers" element={<ContainersCrudPage />} />
<Route path="cargoes" element={<CargoesCrudPage />} />
=======
<Route path="wagons" element={<FleetResourcePage />} />
<Route path="containers" element={<FleetResourcePage />} />
<Route path="cargoes" element={<FleetResourcePage />} />
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
<Route path="warehouses" element={<WarehouseListPage />} />
<Route path="warehouses/:id" element={<WarehouseDetailPage />} /> <Route path="warehouses/:id" element={<WarehouseDetailPage />} />
<Route path="warehouse-inventory" element={<WarehouseInventoryPage />} /> <Route path="warehouse-inventory" element={<WarehouseInventoryPage />} />
<Route path="inventory-inquiry" element={<InventoryInquiryPage />} /> <Route path="inventory-inquiry" element={<InventoryInquiryPage />} />

View File

@@ -19,11 +19,7 @@ const asList = <T>(payload: ListResponse<T>): T[] =>
export const wagonTypesService = { export const wagonTypesService = {
async getWagonTypes() { async getWagonTypes() {
const response = await api.get<ListResponse<WagonType>>('/wagon-types', { const response = await api.get<ListResponse<WagonType>>('/wagon-types', {
<<<<<<< HEAD
params: { isActive: 'all' },
=======
params: { isActive: 'all', pageSize: 500 }, params: { isActive: 'all', pageSize: 500 },
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
}); });
return asList(response.data); return asList(response.data);
}, },

View File

@@ -136,20 +136,15 @@ export interface LocomotiveRecord {
name?: string | null; name?: string | null;
maxPullWeightTons: number; maxPullWeightTons: number;
maxTrainLengthMeters: number; maxTrainLengthMeters: number;
<<<<<<< HEAD
status: status:
| 'AVAILABLE' | "AVAILABLE"
| 'UNAVAILABLE' | "UNAVAILABLE"
| 'IMPORT_READY' | "IMPORT_READY"
| 'EXPORT_READY' | "EXPORT_READY"
| 'ASSIGNED' | "ASSIGNED"
| 'MAINTENANCE' | "MAINTENANCE"
| 'OUT_OF_SERVICE'; | "OUT_OF_SERVICE";
locomotiveType?: 'DIESEL' | 'ELECTRIC';
=======
status: "AVAILABLE" | "ASSIGNED" | "MAINTENANCE" | "OUT_OF_SERVICE";
locomotiveType?: "DIESEL" | "ELECTRIC"; locomotiveType?: "DIESEL" | "ELECTRIC";
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
} }
export interface TrainScheduleListItem { export interface TrainScheduleListItem {
@@ -250,21 +245,12 @@ export interface TrainScheduleDetail {
code: string; code: string;
name: string; name: string;
} | null; } | null;
<<<<<<< HEAD
physicalWagon?: { physicalWagon?: {
id: string; id: string;
wagonNumber: string; wagonNumber: string;
status: string; status: string;
} | null; } | null;
allocations: Array<{
id: string;
bookingId: string;
bookingReference: string | null;
allocatedWeightTons: number;
}>;
=======
allocations: TrainScheduleWagonAllocation[]; allocations: TrainScheduleWagonAllocation[];
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
}>; }>;
} | null; } | null;
bookings: Array<{ bookings: Array<{
@@ -281,13 +267,10 @@ export interface TrainScheduleDetail {
export interface TrainScheduleFilters { export interface TrainScheduleFilters {
originStationId?: string; originStationId?: string;
destinationStationId?: string; destinationStationId?: string;
<<<<<<< HEAD
scheduleDate?: string; scheduleDate?: string;
assignmentType?: AssignmentType; assignmentType?: AssignmentType;
tradeDirection?: TradeDirection; tradeDirection?: TradeDirection;
=======
schedulingStatus?: SchedulingStatus; schedulingStatus?: SchedulingStatus;
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
} }
export interface TrainSchedulePreviewPayload { export interface TrainSchedulePreviewPayload {
@@ -295,9 +278,7 @@ export interface TrainSchedulePreviewPayload {
scheduleDate: string; scheduleDate: string;
originStationId: string; originStationId: string;
destinationStationId: string; destinationStationId: string;
<<<<<<< HEAD
assignmentType?: AssignmentType; assignmentType?: AssignmentType;
=======
targetScheduleId?: string; targetScheduleId?: string;
maxTrainWeightTons?: number; maxTrainWeightTons?: number;
maxTrainLengthMeters?: number; maxTrainLengthMeters?: number;
@@ -320,7 +301,6 @@ export interface ReschedulePlan {
readmitted: RescheduleBookingSummary[]; readmitted: RescheduleBookingSummary[];
finalBookingIds: string[]; finalBookingIds: string[];
warnings: string[]; warnings: string[];
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
} }
export interface CreateTrainSchedulePayload { export interface CreateTrainSchedulePayload {
@@ -328,11 +308,9 @@ export interface CreateTrainSchedulePayload {
scheduleDate: string; scheduleDate: string;
arrivalDate?: string; arrivalDate?: string;
locomotiveId: string; locomotiveId: string;
<<<<<<< HEAD
assignmentType?: AssignmentType; assignmentType?: AssignmentType;
bookingIds: string[]; bookingIds: string[];
wagonIds?: string[]; wagonIds?: string[];
=======
maxTrainWeightTons?: number; maxTrainWeightTons?: number;
maxTrainLengthMeters?: number; maxTrainLengthMeters?: number;
maxWagonsPerTrain?: number; maxWagonsPerTrain?: number;
@@ -354,5 +332,4 @@ export interface PinWagonAssignment {
export interface PinWagonsPayload { export interface PinWagonsPayload {
assignments: PinWagonAssignment[]; assignments: PinWagonAssignment[];
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
} }

View File

@@ -135,6 +135,8 @@ export enum TrainSetWagonStatus {
export enum WagonStatus { export enum WagonStatus {
Available = "AVAILABLE", Available = "AVAILABLE",
Assigned = "ASSIGNED", Assigned = "ASSIGNED",
ImportReady = "IMPORT_READY",
ExportReady = "EXPORT_READY",
Maintenance = "MAINTENANCE", Maintenance = "MAINTENANCE",
Retired = "RETIRED", Retired = "RETIRED",
} }
@@ -335,7 +337,7 @@ export interface IInvoice extends BaseEntity {
dueAt: string; dueAt: string;
} }
// ── Reference Data (booking form catalog) ────────────────────────────────────── // ── Reference Data (booking form catalog) ──────────────────────────────────────
export interface BookingReferenceYard { export interface BookingReferenceYard {
id: string; id: string;
@@ -391,7 +393,7 @@ export interface BookingReferenceData {
cargo_type: BookingReferenceCargoTypeGroup[]; cargo_type: BookingReferenceCargoTypeGroup[];
} }
// ── DTOs ─────────────────────────────────────────────────────────────────────── // ── DTOs ───────────────────────────────────────────────────────────────────────
export interface CreateBookingContainerDto { export interface CreateBookingContainerDto {
containerTypeId: string; containerTypeId: string;

View File

@@ -1,5 +1,6 @@
{ {
"$schema": "https://turbo.build/schema.json", "$schema": "https://turbo.build/schema.json",
"concurrency": "11",
"tasks": { "tasks": {
"build": { "build": {
"dependsOn": ["^build"], "dependsOn": ["^build"],