mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 12:41:04 +00:00
Batch 1 to 3 implementation
This commit is contained in:
0
.pnpm-store/v11/.pnpm-needs-build-marker
Normal file
0
.pnpm-store/v11/.pnpm-needs-build-marker
Normal file
@@ -0,0 +1 @@
|
||||
{"dependencies":{"pnpm":"11.1.1"}}
|
||||
BIN
.pnpm-store/v11/index.db
Normal file
BIN
.pnpm-store/v11/index.db
Normal file
Binary file not shown.
@@ -13,13 +13,8 @@
|
||||
"lint": "eslint src",
|
||||
"test": "jest",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
<<<<<<< HEAD
|
||||
"seed:wagons": "ts-node -r tsconfig-paths/register src/scripts/seed-edr-wagons.ts",
|
||||
"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": {
|
||||
"@edr/api-common": "workspace:*",
|
||||
|
||||
@@ -50,11 +50,8 @@ import { WagonsModule } from './modules/wagons/wagons.module';
|
||||
import { ContainersModule } from './modules/container-management/containers.module';
|
||||
import { CargoesModule } from './modules/cargoes/cargoes.module';
|
||||
import { RoutesModule } from './modules/routes/routes.module';
|
||||
<<<<<<< HEAD
|
||||
import { WarehousesModule } from './modules/warehouses/warehouses.module';
|
||||
=======
|
||||
import { OverviewModule } from './modules/overview/overview.module';
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -109,11 +106,8 @@ import { OverviewModule } from './modules/overview/overview.module';
|
||||
ContainersModule,
|
||||
CargoesModule,
|
||||
RoutesModule,
|
||||
<<<<<<< HEAD
|
||||
WarehousesModule,
|
||||
=======
|
||||
OverviewModule,
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
],
|
||||
providers: [EdrOrgSeeder, DemoUsersSeeder,FreightStaffUsersSeeder, DemoBookingsSeeder, PricingDataSeeder, FileUploadSettingsSeeder],
|
||||
})
|
||||
|
||||
@@ -12,7 +12,6 @@ import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as Handlebars from "handlebars";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { SchedulingStatus } from "@edr/types";
|
||||
import { Booking } from "../bookings/entities/booking.entity";
|
||||
|
||||
import {
|
||||
@@ -62,107 +61,6 @@ export class PaymentService {
|
||||
|
||||
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({
|
||||
amount: amount,
|
||||
currency: DEFAULT_CURRENCY,
|
||||
@@ -174,7 +72,6 @@ export class PaymentService {
|
||||
clientAction: result.clientAction as Record<string, unknown>,
|
||||
expiresAt: result.expiresAt,
|
||||
reason: `Payment for booking`,
|
||||
>>>>>>> eda21e22d872344b74c0c72308f87ce7435b299f
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,22 +8,11 @@ import { TrainSet } from '../../train-sets/entities/train-set.entity';
|
||||
import { TrainScheduleBooking } from './train-schedule-booking.entity';
|
||||
|
||||
export const TRAIN_SCHEDULE_STATUSES = [
|
||||
<<<<<<< HEAD
|
||||
'DRAFT',
|
||||
'READY',
|
||||
'PUBLISHED',
|
||||
'DEPARTED',
|
||||
'IN_TRANSIT',
|
||||
'ARRIVED',
|
||||
'COMPLETED',
|
||||
'CANCELLED',
|
||||
=======
|
||||
TrainScheduleStatusEnum.Draft,
|
||||
TrainScheduleStatusEnum.Scheduled,
|
||||
TrainScheduleStatusEnum.Dispatched,
|
||||
TrainScheduleStatusEnum.Arrived,
|
||||
TrainScheduleStatusEnum.Cancelled,
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
] as const;
|
||||
|
||||
export type TrainScheduleStatus = (typeof TRAIN_SCHEDULE_STATUSES)[number];
|
||||
|
||||
@@ -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 { Type } from 'class-transformer';
|
||||
import { IsDateString, IsInt, IsNumber, IsOptional, IsUUID, Min } from 'class-validator';
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
|
||||
export class CreateContainerTrainScheduleDto {
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
@@ -25,26 +20,6 @@ export class CreateContainerTrainScheduleDto {
|
||||
@IsUUID()
|
||||
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' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@@ -65,5 +40,4 @@ export class CreateContainerTrainScheduleDto {
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
maxWagonsPerTrain?: number;
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
<<<<<<< HEAD
|
||||
import { IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
|
||||
=======
|
||||
import { IsOptional, IsUUID } from 'class-validator';
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
|
||||
export class GetEligibleContainerBookingsDto {
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@@ -18,20 +14,5 @@ export class GetEligibleContainerBookingsDto {
|
||||
|
||||
@ApiPropertyOptional({ example: 'HOLDING' })
|
||||
@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;
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
export class PreviewContainerTrainScheduleDto extends PreviewTrainScheduleDto {}
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
|
||||
@@ -196,17 +196,10 @@ export class TrainSchedulingController {
|
||||
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')
|
||||
@TrainSchedulingManage()
|
||||
@ApiOperation({ summary: 'Cancel bulk train schedule' })
|
||||
cancelBulkTrainSchedule(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.trainSchedulingService.cancelTrainSchedule(id);
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,18 +14,7 @@ import { TrainSchedulesModule } from '../train-schedules/train-schedules.module'
|
||||
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
||||
import { WagonTypesModule } from '../wagon-types/wagon-types.module';
|
||||
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';
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
import { TrainSchedulingController } from './train-scheduling.controller';
|
||||
import { TrainSchedulingService } from './train-scheduling.service';
|
||||
|
||||
|
||||
@@ -58,15 +58,10 @@ const makeBooking = (
|
||||
scheduledDate: new Date(scheduledDate),
|
||||
originYardId,
|
||||
destinationYardId,
|
||||
<<<<<<< HEAD
|
||||
status: 'APPROVED',
|
||||
customer: { companyName: 'Demo Customer' },
|
||||
=======
|
||||
status: 'PAID',
|
||||
schedulingStatus: 'HOLDING',
|
||||
holdExpiresAt: new Date(Date.now() + 60 * 60 * 1000),
|
||||
company: { companyName: 'Demo Customer' },
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
originYard: { label: 'Djibouti', code: 'DJIBOUTI' },
|
||||
destinationYard: { label: 'Addis Ababa', code: 'ADDIS_ABABA' },
|
||||
bookingContainers: [
|
||||
@@ -352,14 +347,7 @@ describe('TrainSchedulingService', () => {
|
||||
|
||||
it('rejects bookings that are not in assignable status', async () => {
|
||||
const bookings = [
|
||||
<<<<<<< HEAD
|
||||
{
|
||||
...makeBooking('b7', 'BKG-CONT-007', 120, 2, '40FT'),
|
||||
status: 'PAID',
|
||||
},
|
||||
=======
|
||||
{ ...makeBooking('b7', 'BKG-CONT-007', 120, 2, '40FT', 2), status: 'APPROVED' },
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
];
|
||||
|
||||
wagonTypesRepository.findAll.mockResolvedValue([nw5]);
|
||||
|
||||
@@ -46,73 +46,6 @@ import { PreviewTrainScheduleDto } from './dto/preview-train-schedule.dto';
|
||||
import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity';
|
||||
import { UpdateTrainSchedulingGlobalRulesDto } from './dto/update-train-scheduling-global-rules.dto';
|
||||
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,
|
||||
computeFleetAvailability,
|
||||
selectBookingsWithinFleetCap,
|
||||
@@ -150,7 +83,6 @@ const DEFAULT_TRAIN_LIMITS: Required<TrainLimitConfig> = {
|
||||
maxWagonsPerTrain: 53,
|
||||
max20ftContainerWeightTons: 30,
|
||||
max20ftPairWeightDiffTons: 10,
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -180,36 +112,12 @@ export class TrainSchedulingService {
|
||||
}
|
||||
|
||||
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' });
|
||||
}
|
||||
|
||||
async getEligibleBulkBookings(query: GetEligibleBulkBookingsDto) {
|
||||
return this.getEligibleBookings({ ...query, freightType: 'BULK' });
|
||||
}
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
|
||||
async getTrainSchedulingGlobalRules() {
|
||||
return this.loadGlobalRulesRow();
|
||||
@@ -226,84 +134,12 @@ export class TrainSchedulingService {
|
||||
if (dto.max20ftContainerWeightTons != null) {
|
||||
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) {
|
||||
row.max20ftPairWeightDiffTons = dto.max20ftPairWeightDiffTons;
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
}
|
||||
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) {
|
||||
const limits = await this.resolveTrainLimitConfig(dto);
|
||||
return this.buildPreviewResponse(
|
||||
@@ -317,7 +153,6 @@ export class TrainSchedulingService {
|
||||
dto.targetScheduleId,
|
||||
),
|
||||
);
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
}
|
||||
|
||||
async previewContainerTrainSchedule(dto: PreviewContainerTrainScheduleDto) {
|
||||
@@ -370,29 +205,6 @@ export class TrainSchedulingService {
|
||||
|
||||
async createContainerTrainSchedule(dto: CreateContainerTrainScheduleDto) {
|
||||
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 createdScheduleId = await this.dataSource.transaction(async (manager) => {
|
||||
@@ -467,7 +279,6 @@ export class TrainSchedulingService {
|
||||
true,
|
||||
limits,
|
||||
scheduleId,
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
);
|
||||
|
||||
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({
|
||||
where: { id: assignment.physicalWagonId },
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
});
|
||||
if (!physicalWagon) {
|
||||
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, {
|
||||
physicalWagonId: assignment.physicalWagonId,
|
||||
status: 'RESERVED',
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
});
|
||||
await manager.getRepository(Wagon).update(assignment.physicalWagonId, {
|
||||
trainSetWagonId: assignment.trainSetWagonId,
|
||||
@@ -950,10 +681,6 @@ export class TrainSchedulingService {
|
||||
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 isMixed = bookingTypes.size > 1;
|
||||
const resolvedMode: 'CONTAINER' | 'BULK' | 'MIXED' =
|
||||
@@ -968,117 +695,15 @@ export class TrainSchedulingService {
|
||||
|
||||
const invalidStatus = bookings.filter(
|
||||
(b) => !SCHEDULABLE_BOOKING_STATUSES.includes(b.status as 'PAID'),
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
);
|
||||
if (invalidStatus.length) {
|
||||
const statuses = [...new Set(invalidStatus.map((b) => b.status))];
|
||||
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(', ')}`,
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
(b) =>
|
||||
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');
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
}
|
||||
|
||||
if (!forceAssign) {
|
||||
@@ -1644,139 +1268,7 @@ export class TrainSchedulingService {
|
||||
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) {
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
const trainSet = manager.getRepository(TrainSet).create({
|
||||
locomotiveId: locomotive.id,
|
||||
totalWeightTons: 0,
|
||||
@@ -1787,313 +1279,6 @@ export class TrainSchedulingService {
|
||||
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) {
|
||||
const route = await this.dataSource.getRepository(Route).findOne({
|
||||
where: { id: routeId },
|
||||
@@ -2104,18 +1289,6 @@ export class TrainSchedulingService {
|
||||
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) {
|
||||
return {
|
||||
id: booking.id,
|
||||
@@ -2137,7 +1310,6 @@ export class TrainSchedulingService {
|
||||
preferredDepartureDate: booking.scheduledDate.toISOString(),
|
||||
status: booking.status,
|
||||
};
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
}
|
||||
|
||||
private resolveScheduleFreightType(
|
||||
|
||||
@@ -52,13 +52,6 @@ export class TrainSetWagon extends BaseEntity {
|
||||
@Column({ name: 'assigned_weight_tons', type: 'numeric', precision: 10, scale: 3, default: 0 })
|
||||
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' })
|
||||
status!: string;
|
||||
|
||||
|
||||
@@ -28,10 +28,7 @@ const toStringArray = ({ value }: { value: unknown }) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => String(entry).trim()).filter(Boolean);
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
if (typeof value !== 'string') return [];
|
||||
|
||||
return value
|
||||
@@ -46,40 +43,24 @@ export class CreateWagonTypeDto {
|
||||
@MaxLength(32)
|
||||
code!: string;
|
||||
|
||||
<<<<<<< HEAD
|
||||
@ApiProperty({ description: 'Display name, e.g. "Flat Wagon"', maxLength: 100 })
|
||||
=======
|
||||
@ApiProperty({ maxLength: 100, example: 'Flat wagon container' })
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
name!: string;
|
||||
|
||||
<<<<<<< HEAD
|
||||
@ApiProperty({ description: 'Maximum payload capacity in metric tons', example: 60 })
|
||||
=======
|
||||
@ApiProperty({ description: 'Maximum payload capacity in metric tons', example: 70 })
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
@Transform(toNumber)
|
||||
@IsNumber()
|
||||
@Min(0.001)
|
||||
capacityTons!: number;
|
||||
|
||||
<<<<<<< HEAD
|
||||
@ApiProperty({ description: 'Wagon length in meters', example: 14.2 })
|
||||
=======
|
||||
@ApiProperty({ description: 'Wagon length in meters', example: 14 })
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
@Transform(toNumber)
|
||||
@IsNumber()
|
||||
@Min(0.001)
|
||||
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 })
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
@IsOptional()
|
||||
@Transform(toOptionalNumber)
|
||||
@IsInt()
|
||||
|
||||
@@ -28,9 +28,6 @@ export class WagonTypesController {
|
||||
@RuleEngineView('wagon-types')
|
||||
@ApiOperation({ summary: 'List wagon types' })
|
||||
findAll(@Query() query: Record<string, string | undefined>) {
|
||||
<<<<<<< HEAD
|
||||
return this.wagonTypesService.findAll(query);
|
||||
=======
|
||||
return this.wagonTypesService.findAll({
|
||||
isActive:
|
||||
query.isActive === 'all'
|
||||
@@ -43,7 +40,6 @@ export class WagonTypesController {
|
||||
sortBy: query.sortBy,
|
||||
sortOrder: query.sortOrder,
|
||||
});
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
||||
@@ -6,44 +6,18 @@ import { UpdateWagonTypeDto } from './dto/update-wagon-type.dto';
|
||||
import { WagonType } from './entities/wagon-type.entity';
|
||||
import { WagonTypesRepository } from './wagon-types.repository';
|
||||
|
||||
<<<<<<< HEAD
|
||||
type WagonTypeListResponse = {
|
||||
data: WagonType[];
|
||||
meta: { total: number; page: number; pageSize: number; totalPages: number };
|
||||
=======
|
||||
type WagonTypeListFilter = {
|
||||
isActive?: boolean;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: string;
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class WagonTypesService {
|
||||
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<{
|
||||
data: WagonType[];
|
||||
meta: { total: number; page: number; pageSize: number; totalPages: number };
|
||||
@@ -59,7 +33,6 @@ export class WagonTypesService {
|
||||
|
||||
const [data, total] = await this.wagonTypesRepository.findAndCount({
|
||||
where: filter.isActive === undefined ? {} : { isActive: filter.isActive },
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
order: { [sortBy]: sortOrder } as FindOptionsOrder<WagonType>,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
@@ -71,21 +44,14 @@ export class WagonTypesService {
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
<<<<<<< HEAD
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
=======
|
||||
totalPages: Math.max(1, Math.ceil(total / pageSize)),
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<WagonType> {
|
||||
const wagonType = await this.wagonTypesRepository.findById(id);
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
if (!wagonType) {
|
||||
throw new NotFoundException(`Wagon type ${id} not found`);
|
||||
}
|
||||
@@ -103,25 +69,16 @@ export class WagonTypesService {
|
||||
async create(dto: CreateWagonTypeDto): Promise<WagonType> {
|
||||
const code = dto.code.trim().toUpperCase();
|
||||
const existing = await this.wagonTypesRepository.findByCode(code);
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
if (existing) {
|
||||
throw new ConflictException(`Wagon type code "${code}" already exists`);
|
||||
}
|
||||
|
||||
return this.wagonTypesRepository.create({
|
||||
<<<<<<< HEAD
|
||||
...dto,
|
||||
code,
|
||||
name: dto.name.trim(),
|
||||
=======
|
||||
code,
|
||||
name: dto.name.trim(),
|
||||
capacityTons: dto.capacityTons,
|
||||
lengthMeters: dto.lengthMeters,
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
maxWagonsPerTrain: dto.maxWagonsPerTrain ?? null,
|
||||
supportedLoadTypes: dto.supportedLoadTypes ?? [],
|
||||
isActive: dto.isActive ?? true,
|
||||
@@ -143,12 +100,9 @@ export class WagonTypesService {
|
||||
...dto,
|
||||
...(nextCode ? { code: nextCode } : {}),
|
||||
...(dto.name ? { name: dto.name.trim() } : {}),
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
maxWagonsPerTrain:
|
||||
dto.maxWagonsPerTrain === undefined ? undefined : dto.maxWagonsPerTrain ?? null,
|
||||
supportedLoadTypes: dto.supportedLoadTypes ?? undefined,
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
|
||||
@@ -30,17 +30,12 @@ export class CreateWagonDto {
|
||||
maxPayloadWeight!: number;
|
||||
|
||||
@IsOptional()
|
||||
<<<<<<< HEAD
|
||||
@IsIn(['AVAILABLE', 'IMPORT_READY', 'EXPORT_READY', 'ASSIGNED', 'MAINTENANCE', 'RETIRED'])
|
||||
status?: string;
|
||||
=======
|
||||
@IsEnum(WagonStatus)
|
||||
status?: WagonStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(WagonReadiness)
|
||||
readiness?: WagonReadiness;
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -12,6 +12,8 @@ import { WagonType } from '../../wagon-types/entities/wagon-type.entity';
|
||||
export const WAGON_STATUSES = [
|
||||
WagonStatus.Available,
|
||||
WagonStatus.Assigned,
|
||||
WagonStatus.ImportReady,
|
||||
WagonStatus.ExportReady,
|
||||
WagonStatus.Maintenance,
|
||||
WagonStatus.Retired,
|
||||
] as const;
|
||||
@@ -56,16 +58,11 @@ export class Wagon extends BaseEntity {
|
||||
@Column({ name: 'max_payload_weight', type: 'decimal', precision: 10, scale: 2 })
|
||||
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 })
|
||||
status!: WagonStatusType;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, default: WagonReadiness.ImportReady })
|
||||
readiness!: WagonReadinessType;
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes!: string | null;
|
||||
@@ -84,7 +81,7 @@ export class Wagon extends BaseEntity {
|
||||
@JoinColumn({ name: 'current_train_schedule_id' })
|
||||
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' })
|
||||
@JoinColumn({ name: 'train_id' })
|
||||
train!: Train | null;
|
||||
|
||||
@@ -41,26 +41,16 @@ export class WagonsService {
|
||||
const status = query.status?.trim();
|
||||
const readiness = query.readiness?.trim();
|
||||
const trainId = query.trainId?.trim();
|
||||
<<<<<<< HEAD
|
||||
const currentLocationYardId = query.currentLocationYardId?.trim();
|
||||
=======
|
||||
const filters = {
|
||||
...(status ? { status: status as Wagon['status'] } : {}),
|
||||
...(readiness ? { readiness: readiness as Wagon['readiness'] } : {}),
|
||||
...(trainId ? { trainId } : {}),
|
||||
};
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
|
||||
if (search) {
|
||||
where.push({
|
||||
wagonNumber: ILike(`%${search}%`),
|
||||
<<<<<<< HEAD
|
||||
...(status ? { status } : {}),
|
||||
...(trainId ? { trainId } : {}),
|
||||
...(currentLocationYardId ? { currentLocationYardId } : {}),
|
||||
=======
|
||||
...filters,
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,12 +60,7 @@ export class WagonsService {
|
||||
const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
||||
|
||||
return this.wagonRepo.find({
|
||||
<<<<<<< HEAD
|
||||
where: search ? where : { ...(status ? { status } : {}), ...(trainId ? { trainId } : {}), ...(currentLocationYardId ? { currentLocationYardId } : {}) },
|
||||
relations: { currentLocationYard: true, wagonType: true },
|
||||
=======
|
||||
where: search ? where : filters,
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
order: { [sortBy]: sortOrder } as FindOptionsOrder<Wagon>,
|
||||
skip: query.page && query.limit ? (Number(query.page) - 1) * Number(query.limit) : undefined,
|
||||
take: query.limit ? Number(query.limit) : undefined,
|
||||
@@ -131,22 +116,23 @@ export class WagonsService {
|
||||
const wagon = await this.findById(wagonId);
|
||||
wagon.trainId = null;
|
||||
wagon.sequenceNumber = null;
|
||||
<<<<<<< HEAD
|
||||
wagon.status = await this.statusForLocation(wagon.currentLocationYardId, 'AVAILABLE');
|
||||
=======
|
||||
wagon.status = WagonStatus.Available;
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
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;
|
||||
|
||||
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';
|
||||
if (country === 'ethiopia' || country === 'et') return WagonStatus.ExportReady;
|
||||
if (country === 'djibouti' || country === 'djoubti' || country === 'dj') {
|
||||
return WagonStatus.ImportReady;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,20 +36,10 @@ import FileUploadSettingsPage from "./pages/documents/FileUploadSettingsPage";
|
||||
import RuleEngineLegacyRedirect from "./pages/ruleEngine/RuleEngineLegacyRedirect";
|
||||
import RuleEngineResourcePage from "./pages/ruleEngine/RuleEngineResourcePage";
|
||||
import TrainsPage from "./pages/trains/TrainsPage";
|
||||
<<<<<<< HEAD
|
||||
import {
|
||||
CargoesCrudPage,
|
||||
ContainersCrudPage,
|
||||
LocomotivesCrudPage,
|
||||
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 RoutesPage from "./pages/fleet/RoutesPage";
|
||||
@@ -290,10 +280,6 @@ const App = () => {
|
||||
element={<BookingContractPage />}
|
||||
/>
|
||||
<Route path="operations/train-scheduling" element={<TrainsPage />} />
|
||||
<<<<<<< HEAD
|
||||
<Route path="trains" element={<TrainMasterDataPage />} />
|
||||
<Route path="operations/train-scheduling" element={<TrainsPage />} />
|
||||
=======
|
||||
<Route
|
||||
path="operations/train-scheduling-v2"
|
||||
element={<TrainScheduleV2ListPage />}
|
||||
@@ -302,20 +288,13 @@ const App = () => {
|
||||
path="operations/train-scheduling-v2/:scheduleId"
|
||||
element={<TrainScheduleV2DetailPage />}
|
||||
/>
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
<Route path="routes" element={<RoutesPage />} />
|
||||
<Route path="locomotives" element={<FleetResourcePage />} />
|
||||
<Route path="trains" element={<FleetResourcePage />} />
|
||||
<Route path="trains/:id" element={<TrainDetailPage />} />
|
||||
<<<<<<< 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 />} />
|
||||
|
||||
@@ -19,11 +19,7 @@ const asList = <T>(payload: ListResponse<T>): T[] =>
|
||||
export const wagonTypesService = {
|
||||
async getWagonTypes() {
|
||||
const response = await api.get<ListResponse<WagonType>>('/wagon-types', {
|
||||
<<<<<<< HEAD
|
||||
params: { isActive: 'all' },
|
||||
=======
|
||||
params: { isActive: 'all', pageSize: 500 },
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
});
|
||||
return asList(response.data);
|
||||
},
|
||||
|
||||
@@ -136,20 +136,15 @@ export interface LocomotiveRecord {
|
||||
name?: string | null;
|
||||
maxPullWeightTons: number;
|
||||
maxTrainLengthMeters: number;
|
||||
<<<<<<< HEAD
|
||||
status:
|
||||
| 'AVAILABLE'
|
||||
| 'UNAVAILABLE'
|
||||
| 'IMPORT_READY'
|
||||
| 'EXPORT_READY'
|
||||
| 'ASSIGNED'
|
||||
| 'MAINTENANCE'
|
||||
| 'OUT_OF_SERVICE';
|
||||
locomotiveType?: 'DIESEL' | 'ELECTRIC';
|
||||
=======
|
||||
status: "AVAILABLE" | "ASSIGNED" | "MAINTENANCE" | "OUT_OF_SERVICE";
|
||||
| "AVAILABLE"
|
||||
| "UNAVAILABLE"
|
||||
| "IMPORT_READY"
|
||||
| "EXPORT_READY"
|
||||
| "ASSIGNED"
|
||||
| "MAINTENANCE"
|
||||
| "OUT_OF_SERVICE";
|
||||
locomotiveType?: "DIESEL" | "ELECTRIC";
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
}
|
||||
|
||||
export interface TrainScheduleListItem {
|
||||
@@ -250,21 +245,12 @@ export interface TrainScheduleDetail {
|
||||
code: string;
|
||||
name: string;
|
||||
} | null;
|
||||
<<<<<<< HEAD
|
||||
physicalWagon?: {
|
||||
id: string;
|
||||
wagonNumber: string;
|
||||
status: string;
|
||||
} | null;
|
||||
allocations: Array<{
|
||||
id: string;
|
||||
bookingId: string;
|
||||
bookingReference: string | null;
|
||||
allocatedWeightTons: number;
|
||||
}>;
|
||||
=======
|
||||
allocations: TrainScheduleWagonAllocation[];
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
}>;
|
||||
} | null;
|
||||
bookings: Array<{
|
||||
@@ -281,13 +267,10 @@ export interface TrainScheduleDetail {
|
||||
export interface TrainScheduleFilters {
|
||||
originStationId?: string;
|
||||
destinationStationId?: string;
|
||||
<<<<<<< HEAD
|
||||
scheduleDate?: string;
|
||||
assignmentType?: AssignmentType;
|
||||
tradeDirection?: TradeDirection;
|
||||
=======
|
||||
schedulingStatus?: SchedulingStatus;
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
}
|
||||
|
||||
export interface TrainSchedulePreviewPayload {
|
||||
@@ -295,9 +278,7 @@ export interface TrainSchedulePreviewPayload {
|
||||
scheduleDate: string;
|
||||
originStationId: string;
|
||||
destinationStationId: string;
|
||||
<<<<<<< HEAD
|
||||
assignmentType?: AssignmentType;
|
||||
=======
|
||||
targetScheduleId?: string;
|
||||
maxTrainWeightTons?: number;
|
||||
maxTrainLengthMeters?: number;
|
||||
@@ -320,7 +301,6 @@ export interface ReschedulePlan {
|
||||
readmitted: RescheduleBookingSummary[];
|
||||
finalBookingIds: string[];
|
||||
warnings: string[];
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
}
|
||||
|
||||
export interface CreateTrainSchedulePayload {
|
||||
@@ -328,11 +308,9 @@ export interface CreateTrainSchedulePayload {
|
||||
scheduleDate: string;
|
||||
arrivalDate?: string;
|
||||
locomotiveId: string;
|
||||
<<<<<<< HEAD
|
||||
assignmentType?: AssignmentType;
|
||||
bookingIds: string[];
|
||||
wagonIds?: string[];
|
||||
=======
|
||||
maxTrainWeightTons?: number;
|
||||
maxTrainLengthMeters?: number;
|
||||
maxWagonsPerTrain?: number;
|
||||
@@ -354,5 +332,4 @@ export interface PinWagonAssignment {
|
||||
|
||||
export interface PinWagonsPayload {
|
||||
assignments: PinWagonAssignment[];
|
||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
||||
}
|
||||
|
||||
@@ -135,6 +135,8 @@ export enum TrainSetWagonStatus {
|
||||
export enum WagonStatus {
|
||||
Available = "AVAILABLE",
|
||||
Assigned = "ASSIGNED",
|
||||
ImportReady = "IMPORT_READY",
|
||||
ExportReady = "EXPORT_READY",
|
||||
Maintenance = "MAINTENANCE",
|
||||
Retired = "RETIRED",
|
||||
}
|
||||
@@ -335,7 +337,7 @@ export interface IInvoice extends BaseEntity {
|
||||
dueAt: string;
|
||||
}
|
||||
|
||||
// ── Reference Data (booking form catalog) ──────────────────────────────────────
|
||||
// ── Reference Data (booking form catalog) ──────────────────────────────────────
|
||||
|
||||
export interface BookingReferenceYard {
|
||||
id: string;
|
||||
@@ -391,7 +393,7 @@ export interface BookingReferenceData {
|
||||
cargo_type: BookingReferenceCargoTypeGroup[];
|
||||
}
|
||||
|
||||
// ── DTOs ───────────────────────────────────────────────────────────────────────
|
||||
// ── DTOs ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface CreateBookingContainerDto {
|
||||
containerTypeId: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"concurrency": "11",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
|
||||
Reference in New Issue
Block a user