mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 04:50:54 +00:00
remove reopen delay minutes from global rules and update related types
- Removed the field from and related components. - Updated to reflect the removal of the reopen delay input field. - Modified to include new train number fields: and . - Added interface to manage active schedules with trade direction. - Introduced interface to track wagon shortages in bookings. - Updated logic to ensure consistent UI state representation. - Created migrations to drop the column and add and columns to the table. - Added tests for the new booking window display logic and wagon planning functionality.
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Matches,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
@@ -14,6 +15,22 @@ export class BuildTrainDto {
|
||||
@MaxLength(32)
|
||||
code!: string;
|
||||
|
||||
@ApiProperty({ example: '8001', description: 'EXPORT run number (odd, unique across trains)' })
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
@Matches(/^\d*[13579]$/, {
|
||||
message: 'Export train number must be numeric and odd (e.g. 8001)',
|
||||
})
|
||||
exportTrainNumber!: string;
|
||||
|
||||
@ApiProperty({ example: '8002', description: 'IMPORT run number (even, unique across trains)' })
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
@Matches(/^\d*[02468]$/, {
|
||||
message: 'Import train number must be numeric and even (e.g. 8002)',
|
||||
})
|
||||
importTrainNumber!: string;
|
||||
|
||||
@ApiProperty({ format: 'uuid', description: 'Yard the train is built in' })
|
||||
@IsUUID()
|
||||
currentYardId!: string;
|
||||
|
||||
@@ -32,6 +32,14 @@ export class Train extends BaseEntity {
|
||||
@Column({ name: 'notes', type: 'text', nullable: true })
|
||||
notes?: string | null;
|
||||
|
||||
/** Fixed IMPORT (even) run number typed at build time; unique via partial index. */
|
||||
@Column({ name: 'import_train_number', type: 'varchar', length: 20, nullable: true })
|
||||
importTrainNumber!: string | null;
|
||||
|
||||
/** Fixed EXPORT (odd) run number typed at build time; unique via partial index. */
|
||||
@Column({ name: 'export_train_number', type: 'varchar', length: 20, nullable: true })
|
||||
exportTrainNumber!: string | null;
|
||||
|
||||
// --- new required fields ---
|
||||
@Column({ name: 'train_number', type: 'varchar', length: 20, unique: true, nullable: true })
|
||||
trainNumber?: string;
|
||||
|
||||
@@ -27,6 +27,15 @@ import {
|
||||
|
||||
const round = (value: unknown) => Math.round((Number(value) || 0) * 100) / 100;
|
||||
|
||||
/** The one active (DRAFT/SCHEDULED/DISPATCHED) schedule surfaced per built train. */
|
||||
export interface ActiveScheduleRef {
|
||||
id: string;
|
||||
status: string;
|
||||
reference: string | null;
|
||||
direction: string | null;
|
||||
trainNumber: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Train Builder — assembles persistent fleet trains (code + 2+ locomotives +
|
||||
* ordered wagons, all in one yard) that scheduling can later reference as a
|
||||
@@ -56,6 +65,25 @@ export class TrainBuilderService {
|
||||
throw new ConflictException(`Train code ${code} is already in use`);
|
||||
}
|
||||
|
||||
// Friendly 409 before the partial unique indexes (the race-proof backstop):
|
||||
// the typed pair may not collide with any train's pair or legacy number.
|
||||
const importTrainNumber = dto.importTrainNumber.trim();
|
||||
const exportTrainNumber = dto.exportTrainNumber.trim();
|
||||
const numberClash: { code: string }[] = await manager.query(
|
||||
`SELECT code FROM freight.trains
|
||||
WHERE deleted_at IS NULL
|
||||
AND (import_train_number IN ($1, $2)
|
||||
OR export_train_number IN ($1, $2)
|
||||
OR train_number IN ($1, $2))
|
||||
LIMIT 1`,
|
||||
[importTrainNumber, exportTrainNumber],
|
||||
);
|
||||
if (numberClash.length) {
|
||||
throw new ConflictException(
|
||||
`Train number ${importTrainNumber}/${exportTrainNumber} is already used by train ${numberClash[0].code}`,
|
||||
);
|
||||
}
|
||||
|
||||
const yard = await manager.getRepository(Yard).findOne({ where: { id: dto.currentYardId } });
|
||||
if (!yard) throw new NotFoundException(`Yard ${dto.currentYardId} not found`);
|
||||
|
||||
@@ -76,6 +104,8 @@ export class TrainBuilderService {
|
||||
status: Freight.TrainStatus.Available,
|
||||
trainName: dto.trainName?.trim() || undefined,
|
||||
notes: dto.notes?.trim() || undefined,
|
||||
importTrainNumber,
|
||||
exportTrainNumber,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -117,12 +147,42 @@ export class TrainBuilderService {
|
||||
take,
|
||||
});
|
||||
|
||||
const activeByTrain = await this.loadActiveScheduleByTrain(trains.map((t) => t.id));
|
||||
|
||||
return {
|
||||
items: trains.map((train) => this.mapSummary(train)),
|
||||
items: trains.map((train) => this.mapSummary(train, activeByTrain.get(train.id) ?? null)),
|
||||
meta: buildPaginationMeta(total, page, pageSize),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* One ACTIVE schedule per train for the page (prefer the DISPATCHED run,
|
||||
* else the earliest upcoming departure) — feeds the list's direction tint
|
||||
* and in-use train number.
|
||||
*/
|
||||
private async loadActiveScheduleByTrain(
|
||||
trainIds: string[],
|
||||
): Promise<Map<string, ActiveScheduleRef>> {
|
||||
if (!trainIds.length) return new Map();
|
||||
const rows: (ActiveScheduleRef & { trainId: string })[] = await this.dataSource.query(
|
||||
`SELECT DISTINCT ON (tset.train_id)
|
||||
tset.train_id AS "trainId",
|
||||
ts.id,
|
||||
ts.status,
|
||||
ts.reference,
|
||||
ts.direction,
|
||||
ts.train_number AS "trainNumber"
|
||||
FROM freight.train_schedules ts
|
||||
JOIN freight.train_sets tset ON tset.id = ts.train_set_id
|
||||
WHERE tset.train_id = ANY($1)
|
||||
AND ts.deleted_at IS NULL
|
||||
AND ts.status IN ('DRAFT', 'SCHEDULED', 'DISPATCHED')
|
||||
ORDER BY tset.train_id, (ts.status = 'DISPATCHED') DESC, ts.scheduled_departure_date ASC`,
|
||||
[trainIds],
|
||||
);
|
||||
return new Map(rows.map(({ trainId, ...schedule }) => [trainId, schedule]));
|
||||
}
|
||||
|
||||
/** Full consist: yard, ordered locomotives + wagons, totals vs. haul limits. */
|
||||
async getComposition(id: string) {
|
||||
const train = await this.dataSource.getRepository(Train).findOne({
|
||||
@@ -139,17 +199,17 @@ export class TrainBuilderService {
|
||||
});
|
||||
if (!train) throw new NotFoundException(`Train ${id} not found`);
|
||||
|
||||
const schedules: { id: string; status: string; reference: string | null }[] =
|
||||
await this.dataSource.query(
|
||||
`SELECT ts.id, ts.status, ts.reference
|
||||
FROM freight.train_schedules ts
|
||||
JOIN freight.train_sets tset ON tset.id = ts.train_set_id
|
||||
WHERE tset.train_id = $1
|
||||
AND ts.deleted_at IS NULL
|
||||
AND ts.status IN ('DRAFT', 'SCHEDULED', 'DISPATCHED')
|
||||
ORDER BY ts.scheduled_departure_date ASC`,
|
||||
[id],
|
||||
);
|
||||
const schedules: ActiveScheduleRef[] = await this.dataSource.query(
|
||||
`SELECT ts.id, ts.status, ts.reference, ts.direction,
|
||||
ts.train_number AS "trainNumber"
|
||||
FROM freight.train_schedules ts
|
||||
JOIN freight.train_sets tset ON tset.id = ts.train_set_id
|
||||
WHERE tset.train_id = $1
|
||||
AND ts.deleted_at IS NULL
|
||||
AND ts.status IN ('DRAFT', 'SCHEDULED', 'DISPATCHED')
|
||||
ORDER BY ts.scheduled_departure_date ASC`,
|
||||
[id],
|
||||
);
|
||||
|
||||
const locomotives = (train.locomotives ?? [])
|
||||
.filter((link) => link.locomotive)
|
||||
@@ -212,6 +272,8 @@ export class TrainBuilderService {
|
||||
code: train.code,
|
||||
trainName: train.trainName ?? null,
|
||||
status: train.status,
|
||||
importTrainNumber: train.importTrainNumber ?? null,
|
||||
exportTrainNumber: train.exportTrainNumber ?? null,
|
||||
notes: train.notes ?? null,
|
||||
createdAt: train.createdAt,
|
||||
currentYard: train.currentYard
|
||||
@@ -407,7 +469,7 @@ export class TrainBuilderService {
|
||||
|
||||
// ---------------------------------------------------------------- internals
|
||||
|
||||
private mapSummary(train: Train) {
|
||||
private mapSummary(train: Train, activeSchedule: ActiveScheduleRef | null) {
|
||||
const locomotives = [...(train.locomotives ?? [])]
|
||||
.sort((a, b) => a.sequenceNo - b.sequenceNo)
|
||||
.map((link) => link.locomotive)
|
||||
@@ -421,6 +483,9 @@ export class TrainBuilderService {
|
||||
code: train.code,
|
||||
trainName: train.trainName ?? null,
|
||||
status: train.status,
|
||||
importTrainNumber: train.importTrainNumber ?? null,
|
||||
exportTrainNumber: train.exportTrainNumber ?? null,
|
||||
activeSchedule,
|
||||
createdAt: train.createdAt,
|
||||
currentYard: train.currentYard
|
||||
? { id: train.currentYard.id, code: train.currentYard.code, label: train.currentYard.label }
|
||||
|
||||
Reference in New Issue
Block a user