mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +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:
@@ -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