mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 19:30:57 +00:00
schedule logic
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { WagonReadiness } from '@edr/types';
|
||||
import { Column, Entity, Index, OneToMany } from 'typeorm';
|
||||
|
||||
import { TrainSet } from '../../train-sets/entities/train-set.entity';
|
||||
@@ -12,12 +13,20 @@ export const LOCOMOTIVE_STATUSES = [
|
||||
|
||||
export const LOCOMOTIVE_TYPES = ['DIESEL', 'ELECTRIC'] as const;
|
||||
|
||||
/** Locomotives reuse the wagon readiness values (IMPORT_READY / EXPORT_READY). */
|
||||
export const LOCOMOTIVE_READINESS_VALUES = [
|
||||
WagonReadiness.ImportReady,
|
||||
WagonReadiness.ExportReady,
|
||||
] as const;
|
||||
|
||||
export type LocomotiveStatus = (typeof LOCOMOTIVE_STATUSES)[number];
|
||||
export type LocomotiveType = (typeof LOCOMOTIVE_TYPES)[number];
|
||||
export type LocomotiveReadiness = (typeof LOCOMOTIVE_READINESS_VALUES)[number];
|
||||
|
||||
@Entity({ schema: 'freight', name: 'locomotives' })
|
||||
@Index(['code'])
|
||||
@Index(['status'])
|
||||
@Index(['readiness'])
|
||||
export class Locomotive extends BaseEntity {
|
||||
@Column({ name: 'code', type: 'varchar', length: 32, unique: true })
|
||||
code!: string;
|
||||
@@ -37,6 +46,9 @@ export class Locomotive extends BaseEntity {
|
||||
@Column({ name: 'status', type: 'varchar', length: 20, default: 'AVAILABLE' })
|
||||
status!: LocomotiveStatus;
|
||||
|
||||
@Column({ name: 'readiness', type: 'varchar', length: 20, default: WagonReadiness.ImportReady })
|
||||
readiness!: LocomotiveReadiness;
|
||||
|
||||
@Column({ name: 'power_kw', type: 'numeric', precision: 10, scale: 3, nullable: true })
|
||||
powerKw?: number | null;
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { TrainCheckpointKind } from '@edr/types';
|
||||
import {
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsISO8601,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
|
||||
export class RecordCheckpointDto {
|
||||
@ApiProperty({ description: 'Station position along the route (0 = origin).' })
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
sequenceNo!: number;
|
||||
|
||||
@ApiProperty({ enum: TrainCheckpointKind, required: false })
|
||||
@IsOptional()
|
||||
@IsEnum(TrainCheckpointKind)
|
||||
kind?: TrainCheckpointKind;
|
||||
|
||||
@ApiProperty({ required: false, description: 'ISO timestamp; defaults to now.' })
|
||||
@IsOptional()
|
||||
@IsISO8601()
|
||||
occurredAt?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(500)
|
||||
note?: string;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { TrainCheckpointKind } from '@edr/types';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
||||
|
||||
import { Yard } from '../../rule-engine/entities/yard.entity';
|
||||
import { TrainSchedule } from '../../train-schedules/entities/train-schedule.entity';
|
||||
|
||||
/**
|
||||
* One staff-logged tracking checkpoint for a dispatched train as it passes a
|
||||
* station along its route (origin → milestones → destination).
|
||||
*/
|
||||
@Entity({ schema: 'freight', name: 'train_checkpoint_events' })
|
||||
@Index(['trainScheduleId'])
|
||||
@Index(['trainScheduleId', 'sequenceNo'])
|
||||
export class TrainCheckpointEvent extends BaseEntity {
|
||||
@Column({ name: 'train_schedule_id', type: 'uuid' })
|
||||
trainScheduleId!: string;
|
||||
|
||||
@ManyToOne(() => TrainSchedule, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'train_schedule_id' })
|
||||
trainSchedule?: TrainSchedule;
|
||||
|
||||
@Column({ name: 'yard_id', type: 'uuid' })
|
||||
yardId!: string;
|
||||
|
||||
@ManyToOne(() => Yard)
|
||||
@JoinColumn({ name: 'yard_id' })
|
||||
yard?: Yard;
|
||||
|
||||
/** Position along the corridor: 0 = origin, N+1 = destination. */
|
||||
@Column({ name: 'sequence_no', type: 'int' })
|
||||
sequenceNo!: number;
|
||||
|
||||
@Column({ name: 'kind', type: 'varchar', length: 20 })
|
||||
kind!: TrainCheckpointKind;
|
||||
|
||||
@Column({ name: 'occurred_at', type: 'timestamptz' })
|
||||
occurredAt!: Date;
|
||||
|
||||
@Column({ name: 'note', type: 'text', nullable: true })
|
||||
note?: string | null;
|
||||
|
||||
@Column({ name: 'recorded_by_user_id', type: 'uuid', nullable: true })
|
||||
recordedByUserId?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { BaseRepository } from '@edr/api-common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { TrainCheckpointEvent } from './entities/train-checkpoint-event.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TrainCheckpointEventsRepository extends BaseRepository<TrainCheckpointEvent> {
|
||||
constructor(
|
||||
@InjectRepository(TrainCheckpointEvent)
|
||||
repository: Repository<TrainCheckpointEvent>,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
findBySchedule(trainScheduleId: string): Promise<TrainCheckpointEvent[]> {
|
||||
return this.findAll({
|
||||
where: { trainScheduleId },
|
||||
relations: { yard: true },
|
||||
order: { sequenceNo: 'ASC', occurredAt: 'ASC' },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { PinWagonsDto } from './dto/pin-wagons.dto';
|
||||
import { PreviewBulkTrainScheduleDto } from './dto/preview-bulk-train-schedule.dto';
|
||||
import { PreviewContainerTrainScheduleDto } from './dto/preview-container-train-schedule.dto';
|
||||
import { PreviewTrainScheduleDto } from './dto/preview-train-schedule.dto';
|
||||
import { RecordCheckpointDto } from './dto/record-checkpoint.dto';
|
||||
import { UpdateTrainSchedulingGlobalRulesDto } from './dto/update-train-scheduling-global-rules.dto';
|
||||
import { TrainSchedulingService } from './train-scheduling.service';
|
||||
|
||||
@@ -161,6 +162,30 @@ export class TrainSchedulingController {
|
||||
return this.trainSchedulingService.dispatchSchedule(id);
|
||||
}
|
||||
|
||||
@Get('schedules/:id/checkpoints')
|
||||
@TrainSchedulingView()
|
||||
@ApiOperation({ summary: 'Get the tracking corridor + logged checkpoints for a train' })
|
||||
getScheduleCheckpoints(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.trainSchedulingService.getScheduleCheckpoints(id);
|
||||
}
|
||||
|
||||
@Post('schedules/:id/checkpoints')
|
||||
@TrainSchedulingManage()
|
||||
@ApiOperation({ summary: 'Log the train passing a station (final station triggers arrival)' })
|
||||
recordCheckpoint(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() dto: RecordCheckpointDto,
|
||||
) {
|
||||
return this.trainSchedulingService.recordCheckpoint(id, dto);
|
||||
}
|
||||
|
||||
@Post('schedules/:id/arrive')
|
||||
@TrainSchedulingManage()
|
||||
@ApiOperation({ summary: 'Mark a dispatched train arrived (flip readiness, free assets)' })
|
||||
arriveSchedule(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.trainSchedulingService.arriveSchedule(id);
|
||||
}
|
||||
|
||||
@Get('container/schedules')
|
||||
@TrainSchedulingView()
|
||||
@ApiOperation({ summary: 'List container train schedules' })
|
||||
|
||||
@@ -14,7 +14,9 @@ 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';
|
||||
import { TrainCheckpointEvent } from './entities/train-checkpoint-event.entity';
|
||||
import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity';
|
||||
import { TrainCheckpointEventsRepository } from './train-checkpoint-events.repository';
|
||||
import { TrainSchedulingController } from './train-scheduling.controller';
|
||||
import { TrainSchedulingService } from './train-scheduling.service';
|
||||
|
||||
@@ -29,6 +31,7 @@ import { TrainSchedulingService } from './train-scheduling.service';
|
||||
Wagon,
|
||||
Container,
|
||||
TrainSchedulingGlobalRules,
|
||||
TrainCheckpointEvent,
|
||||
]),
|
||||
BookingsModule,
|
||||
LocomotivesModule,
|
||||
@@ -38,7 +41,7 @@ import { TrainSchedulingService } from './train-scheduling.service';
|
||||
RuleEngineModule,
|
||||
],
|
||||
controllers: [TrainSchedulingController],
|
||||
providers: [TrainSchedulingService],
|
||||
providers: [TrainSchedulingService, TrainCheckpointEventsRepository],
|
||||
exports: [TrainSchedulingService],
|
||||
})
|
||||
export class TrainSchedulingModule {}
|
||||
|
||||
@@ -125,6 +125,13 @@ describe('TrainSchedulingService', () => {
|
||||
findAll: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
const trainCheckpointEventsRepository = {
|
||||
findBySchedule: jest.fn().mockResolvedValue([]),
|
||||
findAll: jest.fn().mockResolvedValue([]),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
service = new TrainSchedulingService(
|
||||
dataSource as never,
|
||||
bookingsRepository as never,
|
||||
@@ -135,6 +142,7 @@ describe('TrainSchedulingService', () => {
|
||||
wagonBookingAllocationsRepository as never,
|
||||
wagonAllocationContainerItemsRepository as never,
|
||||
wagonAllocationBulkLoadsRepository as never,
|
||||
trainCheckpointEventsRepository as never,
|
||||
);
|
||||
|
||||
const defaultFleetWagons = [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
AllocationLoadType,
|
||||
SchedulingStatus,
|
||||
TrainCheckpointKind,
|
||||
TrainScheduleStatus as TrainScheduleStatusEnum,
|
||||
WagonStatus,
|
||||
} from '@edr/types';
|
||||
@@ -74,7 +75,11 @@ import {
|
||||
pickBulkWagonType,
|
||||
} from './wagon-type-resolver.util';
|
||||
import { deriveScheduleDirection } from './derive-schedule-direction.util';
|
||||
import { wagonReadinessMatchesSchedule } from './wagon-readiness.util';
|
||||
import { flipReadiness, wagonReadinessMatchesSchedule } from './wagon-readiness.util';
|
||||
import { TrainCheckpointEvent } from './entities/train-checkpoint-event.entity';
|
||||
import { TrainCheckpointEventsRepository } from './train-checkpoint-events.repository';
|
||||
import { RecordCheckpointDto } from './dto/record-checkpoint.dto';
|
||||
import { RouteMilestone } from '../routes/entities/route-milestone.entity';
|
||||
|
||||
const SCHEDULABLE_BOOKING_STATUSES = ['PAID'] as const;
|
||||
const DEFAULT_TRAIN_LIMITS: Required<TrainLimitConfig> = {
|
||||
@@ -98,6 +103,7 @@ export class TrainSchedulingService {
|
||||
private readonly wagonBookingAllocationsRepository: WagonBookingAllocationsRepository,
|
||||
private readonly wagonAllocationContainerItemsRepository: WagonAllocationContainerItemsRepository,
|
||||
private readonly wagonAllocationBulkLoadsRepository: WagonAllocationBulkLoadsRepository,
|
||||
private readonly trainCheckpointEventsRepository: TrainCheckpointEventsRepository,
|
||||
private readonly configService?: ConfigService,
|
||||
) {}
|
||||
|
||||
@@ -219,11 +225,17 @@ export class TrainSchedulingService {
|
||||
throw new ConflictException(`Locomotive ${lockedLocomotive.code} is not available`);
|
||||
}
|
||||
|
||||
const trainSet = await this.buildEmptyTrainSet(manager, lockedLocomotive);
|
||||
const direction = deriveScheduleDirection(
|
||||
route.originYard ?? { country: null },
|
||||
route.destinationYard ?? { country: null },
|
||||
);
|
||||
if (!wagonReadinessMatchesSchedule(lockedLocomotive.readiness, direction)) {
|
||||
throw new ConflictException(
|
||||
`Locomotive ${lockedLocomotive.code} is ${lockedLocomotive.readiness} and cannot run a ${direction} schedule`,
|
||||
);
|
||||
}
|
||||
|
||||
const trainSet = await this.buildEmptyTrainSet(manager, lockedLocomotive);
|
||||
const schedule = manager.getRepository(TrainSchedule).create({
|
||||
trainSetId: trainSet.id,
|
||||
routeId: route.id,
|
||||
@@ -581,6 +593,237 @@ export class TrainSchedulingService {
|
||||
return this.getTrainScheduleById(scheduleId);
|
||||
}
|
||||
|
||||
/** Build the ordered station list for a schedule's corridor (origin → milestones → destination). */
|
||||
private async buildScheduleStations(schedule: TrainSchedule) {
|
||||
type Station = { sequenceNo: number; yardId: string; label: string; code: string };
|
||||
const stations: Station[] = [];
|
||||
|
||||
const route = schedule.routeId
|
||||
? await this.dataSource.getRepository(Route).findOne({
|
||||
where: { id: schedule.routeId },
|
||||
relations: { originYard: true, destinationYard: true, milestones: { yard: true } },
|
||||
})
|
||||
: null;
|
||||
|
||||
if (route) {
|
||||
const origin = route.originYard;
|
||||
const destination = route.destinationYard;
|
||||
const milestones = [...(route.milestones ?? [])].sort(
|
||||
(a: RouteMilestone, b: RouteMilestone) => a.sequenceNo - b.sequenceNo,
|
||||
);
|
||||
stations.push({
|
||||
sequenceNo: 0,
|
||||
yardId: route.originYardId,
|
||||
label: origin?.label ?? origin?.code ?? 'Origin',
|
||||
code: origin?.code ?? '',
|
||||
});
|
||||
milestones.forEach((m, i) =>
|
||||
stations.push({
|
||||
sequenceNo: i + 1,
|
||||
yardId: m.yardId,
|
||||
label: m.yard?.label ?? m.yard?.code ?? `Stop ${i + 1}`,
|
||||
code: m.yard?.code ?? '',
|
||||
}),
|
||||
);
|
||||
stations.push({
|
||||
sequenceNo: milestones.length + 1,
|
||||
yardId: route.destinationYardId,
|
||||
label: destination?.label ?? destination?.code ?? 'Destination',
|
||||
code: destination?.code ?? '',
|
||||
});
|
||||
return stations;
|
||||
}
|
||||
|
||||
// Fallback: no route milestones — just origin → destination from the schedule stations.
|
||||
stations.push({
|
||||
sequenceNo: 0,
|
||||
yardId: schedule.originStationId,
|
||||
label: schedule.originStation?.label ?? schedule.originStation?.code ?? 'Origin',
|
||||
code: schedule.originStation?.code ?? '',
|
||||
});
|
||||
stations.push({
|
||||
sequenceNo: 1,
|
||||
yardId: schedule.destinationStationId,
|
||||
label:
|
||||
schedule.destinationStation?.label ?? schedule.destinationStation?.code ?? 'Destination',
|
||||
code: schedule.destinationStation?.code ?? '',
|
||||
});
|
||||
return stations;
|
||||
}
|
||||
|
||||
/** Track payload for a schedule: ordered stations, logged checkpoints, current position. */
|
||||
async getScheduleCheckpoints(scheduleId: string) {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`Train schedule ${scheduleId} not found`);
|
||||
}
|
||||
|
||||
const stations = await this.buildScheduleStations(schedule);
|
||||
const events = await this.trainCheckpointEventsRepository.findBySchedule(scheduleId);
|
||||
const currentSequenceNo = events.length
|
||||
? Math.max(...events.map((e) => e.sequenceNo))
|
||||
: -1;
|
||||
|
||||
return {
|
||||
scheduleId,
|
||||
status: schedule.status,
|
||||
direction: schedule.direction ?? null,
|
||||
trainNumber: schedule.trainNumber ?? null,
|
||||
actualDepartureAt: schedule.actualDepartureAt
|
||||
? schedule.actualDepartureAt.toISOString()
|
||||
: null,
|
||||
actualArrivalAt: schedule.actualArrivalAt
|
||||
? schedule.actualArrivalAt.toISOString()
|
||||
: null,
|
||||
origin: stations[0]?.label ?? null,
|
||||
destination: stations[stations.length - 1]?.label ?? null,
|
||||
stations,
|
||||
currentSequenceNo,
|
||||
checkpoints: events.map((e) => ({
|
||||
id: e.id,
|
||||
sequenceNo: e.sequenceNo,
|
||||
yardId: e.yardId,
|
||||
label: e.yard?.label ?? e.yard?.code ?? null,
|
||||
kind: e.kind,
|
||||
occurredAt: e.occurredAt.toISOString(),
|
||||
note: e.note ?? null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/** Log the train passing a station. Logging the destination station triggers arrival. */
|
||||
async recordCheckpoint(scheduleId: string, dto: RecordCheckpointDto) {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`Train schedule ${scheduleId} not found`);
|
||||
}
|
||||
if (schedule.status !== TrainScheduleStatusEnum.Dispatched) {
|
||||
throw new BadRequestException('Only DISPATCHED trains can be tracked');
|
||||
}
|
||||
|
||||
const stations = await this.buildScheduleStations(schedule);
|
||||
const finalSeq = stations[stations.length - 1].sequenceNo;
|
||||
const station = stations.find((s) => s.sequenceNo === dto.sequenceNo);
|
||||
if (!station) {
|
||||
throw new BadRequestException(`Station ${dto.sequenceNo} is not on this route`);
|
||||
}
|
||||
|
||||
const kind =
|
||||
dto.kind ??
|
||||
(dto.sequenceNo === 0
|
||||
? TrainCheckpointKind.Departed
|
||||
: dto.sequenceNo === finalSeq
|
||||
? TrainCheckpointKind.Arrived
|
||||
: TrainCheckpointKind.Passed);
|
||||
const occurredAt = dto.occurredAt ? new Date(dto.occurredAt) : new Date();
|
||||
|
||||
// Upsert by (scheduleId, sequenceNo) so re-logging a station updates rather than duplicates.
|
||||
const [existing] = await this.trainCheckpointEventsRepository.findAll({
|
||||
where: { trainScheduleId: scheduleId, sequenceNo: dto.sequenceNo },
|
||||
});
|
||||
if (existing) {
|
||||
await this.trainCheckpointEventsRepository.update(existing.id, {
|
||||
kind,
|
||||
occurredAt,
|
||||
note: dto.note ?? null,
|
||||
yardId: station.yardId,
|
||||
});
|
||||
} else {
|
||||
await this.trainCheckpointEventsRepository.create({
|
||||
trainScheduleId: scheduleId,
|
||||
yardId: station.yardId,
|
||||
sequenceNo: dto.sequenceNo,
|
||||
kind,
|
||||
occurredAt,
|
||||
note: dto.note ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
if (dto.sequenceNo === finalSeq) {
|
||||
await this.arriveSchedule(scheduleId);
|
||||
}
|
||||
|
||||
return this.getScheduleCheckpoints(scheduleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a dispatched train arrived: close out the schedule, flip readiness on the
|
||||
* locomotive + wagons (they have repositioned), and free the assets for re-use.
|
||||
*/
|
||||
async arriveSchedule(scheduleId: string) {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`Train schedule ${scheduleId} not found`);
|
||||
}
|
||||
if (schedule.status !== TrainScheduleStatusEnum.Dispatched) {
|
||||
throw new BadRequestException('Only DISPATCHED trains can arrive');
|
||||
}
|
||||
|
||||
const isDomestic = schedule.direction === 'DOMESTIC';
|
||||
const now = new Date();
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
await this.trainSchedulesRepository.updateStatus(
|
||||
scheduleId,
|
||||
TrainScheduleStatusEnum.Arrived,
|
||||
{ actualArrivalAt: now },
|
||||
manager,
|
||||
);
|
||||
|
||||
if (schedule.trainSetId) {
|
||||
await manager.getRepository(TrainSet).update(schedule.trainSetId, {
|
||||
status: 'COMPLETED',
|
||||
});
|
||||
}
|
||||
|
||||
if (schedule.trainSet?.locomotiveId) {
|
||||
const loco = await manager
|
||||
.getRepository(Locomotive)
|
||||
.findOne({ where: { id: schedule.trainSet.locomotiveId } });
|
||||
if (loco) {
|
||||
await manager.getRepository(Locomotive).update(loco.id, {
|
||||
status: 'AVAILABLE',
|
||||
readiness: isDomestic ? loco.readiness : flipReadiness(loco.readiness),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const slot of schedule.trainSet?.wagons ?? []) {
|
||||
if (!slot.physicalWagonId) continue;
|
||||
const wagon = await manager
|
||||
.getRepository(Wagon)
|
||||
.findOne({ where: { id: slot.physicalWagonId } });
|
||||
if (!wagon) continue;
|
||||
await manager.getRepository(Wagon).update(wagon.id, {
|
||||
currentTrainScheduleId: null,
|
||||
trainSetWagonId: null,
|
||||
status: WagonStatus.Available,
|
||||
readiness: isDomestic ? wagon.readiness : flipReadiness(wagon.readiness),
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure a destination checkpoint exists so the timeline shows ARRIVED.
|
||||
const stations = await this.buildScheduleStations(schedule);
|
||||
const finalStation = stations[stations.length - 1];
|
||||
const [existingFinal] = await this.trainCheckpointEventsRepository.findAll({
|
||||
where: { trainScheduleId: scheduleId, sequenceNo: finalStation.sequenceNo },
|
||||
});
|
||||
if (!existingFinal) {
|
||||
await manager.getRepository(TrainCheckpointEvent).save(
|
||||
manager.getRepository(TrainCheckpointEvent).create({
|
||||
trainScheduleId: scheduleId,
|
||||
yardId: finalStation.yardId,
|
||||
sequenceNo: finalStation.sequenceNo,
|
||||
kind: TrainCheckpointKind.Arrived,
|
||||
occurredAt: now,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return this.getTrainScheduleById(scheduleId);
|
||||
}
|
||||
|
||||
async getContainerTrainSchedules() {
|
||||
const schedules = await this.trainSchedulesRepository.findAll({
|
||||
relations: {
|
||||
@@ -1339,6 +1582,7 @@ export class TrainSchedulingService {
|
||||
id: schedule.trainSet.locomotive.id,
|
||||
code: schedule.trainSet.locomotive.code,
|
||||
name: schedule.trainSet.locomotive.name ?? null,
|
||||
readiness: schedule.trainSet.locomotive.readiness ?? null,
|
||||
}
|
||||
: null,
|
||||
wagonCount: schedule.trainSet?.wagonCount ?? 0,
|
||||
@@ -1407,6 +1651,7 @@ export class TrainSchedulingService {
|
||||
code: schedule.trainSet.locomotive.code,
|
||||
name: schedule.trainSet.locomotive.name,
|
||||
status: schedule.trainSet.locomotive.status,
|
||||
readiness: schedule.trainSet.locomotive.readiness ?? null,
|
||||
maxPullWeightTons: roundTons(
|
||||
Number(schedule.trainSet.locomotive.maxPullWeightTons),
|
||||
),
|
||||
|
||||
@@ -16,3 +16,16 @@ export function wagonReadinessMatchesSchedule(
|
||||
if (!required) return true;
|
||||
return wagonReadiness === required;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle a readiness value (IMPORT_READY ↔ EXPORT_READY). Used when a train
|
||||
* reaches its destination: the asset has repositioned, so it is now ready for
|
||||
* the opposite direction. Direction-agnostic so it handles round trips.
|
||||
*/
|
||||
export function flipReadiness(
|
||||
readiness: WagonReadiness | string,
|
||||
): WagonReadiness {
|
||||
return readiness === WagonReadiness.ImportReady
|
||||
? WagonReadiness.ExportReady
|
||||
: WagonReadiness.ImportReady;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user