mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 01:48:12 +00:00
implement contract cancellation feature and update contract statuses
- Added functionality to cancel contracts, allowing users to provide a reason for cancellation. - Updated contract statuses to include SUSPENDED and changed CLOSED to COMPLETED. - Enhanced the UI to reflect the new cancellation option and updated messaging for contract statuses. - Refactored contract booking actions to accommodate changes in booking logic for ONE_TIME and GENERAL contracts. - Removed clearance document management from the contract detail page, as it is now handled per booking. - Introduced a SQL script to reset bookings and train schedules for development purposes.
This commit is contained in:
@@ -33,10 +33,10 @@ export class BuildTrainDto {
|
||||
@ApiProperty({
|
||||
type: [String],
|
||||
format: 'uuid',
|
||||
description: 'Locomotives pulling the train (minimum 2 — front and back), in consist order',
|
||||
description: 'Locomotives pulling the train (minimum 1), in consist order',
|
||||
})
|
||||
@IsArray()
|
||||
@ArrayMinSize(2, { message: 'A train must be pulled by at least two locomotives' })
|
||||
@ArrayMinSize(1, { message: 'A train must be pulled by at least one locomotive' })
|
||||
@IsUUID('all', { each: true })
|
||||
locomotiveIds!: string[];
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ export class UpdateTrainLocomotivesDto {
|
||||
@ApiProperty({
|
||||
type: [String],
|
||||
format: 'uuid',
|
||||
description: 'Full replacement locomotive set (minimum 2), in consist order',
|
||||
description: 'Full replacement locomotive set (minimum 1), in consist order',
|
||||
})
|
||||
@IsArray()
|
||||
@ArrayMinSize(2, { message: 'A train must be pulled by at least two locomotives' })
|
||||
@ArrayMinSize(1, { message: 'A train must be pulled by at least one locomotive' })
|
||||
@IsUUID('all', { each: true })
|
||||
locomotiveIds!: string[];
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Train } from './train.entity';
|
||||
|
||||
/**
|
||||
* Link row joining a built train to one of its locomotives. A train must be
|
||||
* pulled by at least two locomotives (front + back); `sequenceNo` is the order
|
||||
* pulled by at least one locomotive; `sequenceNo` is the order
|
||||
* in the consist — 0 is the lead locomotive.
|
||||
*
|
||||
* Mirrors `train_set_locomotives`, but for the persistent fleet `Train` built
|
||||
|
||||
@@ -80,7 +80,7 @@ export class Train extends BaseEntity {
|
||||
@OneToMany(() => Wagon, (wagon) => wagon.train)
|
||||
wagons!: Wagon[];
|
||||
|
||||
/** Locomotives pulling this train (minimum 2), ordered by sequenceNo. */
|
||||
/** Locomotives pulling this train (minimum 1), ordered by sequenceNo. */
|
||||
@OneToMany(() => TrainLocomotive, (link) => link.train)
|
||||
locomotives?: TrainLocomotive[];
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export class TrainBuilderController {
|
||||
|
||||
@Put(':id/locomotives')
|
||||
@FleetManage(FREIGHT_PERMS.trains.update)
|
||||
@ApiOperation({ summary: 'Replace the locomotive set (minimum 2, same yard)' })
|
||||
@ApiOperation({ summary: 'Replace the locomotive set (minimum 1, same yard)' })
|
||||
setLocomotives(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() dto: UpdateTrainLocomotivesDto,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { DataSource, EntityManager, ILike, In } from 'typeorm';
|
||||
@@ -10,7 +11,7 @@ import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity
|
||||
|
||||
import { Locomotive } from '../locomotives/entities/locomotive.entity';
|
||||
import { Yard } from '../rule-engine/entities/yard.entity';
|
||||
import { minLocomotiveLimits } from '../train-scheduling/train-capacity.util';
|
||||
import { combinedLocomotiveLimits } from '../train-scheduling/train-capacity.util';
|
||||
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
||||
import { WagonMovement } from '../wagons/entities/wagon-movement.entity';
|
||||
import { Wagon } from '../wagons/entities/wagon.entity';
|
||||
@@ -55,12 +56,14 @@ export interface ActiveScheduleRef {
|
||||
*/
|
||||
@Injectable()
|
||||
export class TrainBuilderService {
|
||||
private readonly logger = new Logger(TrainBuilderService.name);
|
||||
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
|
||||
async buildTrain(dto: BuildTrainDto) {
|
||||
const locomotiveIds = [...new Set(dto.locomotiveIds)];
|
||||
if (locomotiveIds.length < 2) {
|
||||
throw new BadRequestException('A train must be pulled by at least two locomotives');
|
||||
if (locomotiveIds.length < 1) {
|
||||
throw new BadRequestException('A train must be pulled by at least one locomotive');
|
||||
}
|
||||
|
||||
const trainId = await this.dataSource.transaction(async (manager) => {
|
||||
@@ -96,7 +99,7 @@ export class TrainBuilderService {
|
||||
);
|
||||
|
||||
// Effective haul capacity is capped by the weakest locomotive in the set.
|
||||
const limits = minLocomotiveLimits(locomotives);
|
||||
const limits = combinedLocomotiveLimits(locomotives);
|
||||
const train = await manager.getRepository(Train).save(
|
||||
manager.getRepository(Train).create({
|
||||
code,
|
||||
@@ -283,7 +286,7 @@ export class TrainBuilderService {
|
||||
: null,
|
||||
}));
|
||||
|
||||
const limits = minLocomotiveLimits(
|
||||
const limits = combinedLocomotiveLimits(
|
||||
(train.locomotives ?? [])
|
||||
.map((link) => link.locomotive)
|
||||
.filter((loco): loco is Locomotive => Boolean(loco)),
|
||||
@@ -339,11 +342,11 @@ export class TrainBuilderService {
|
||||
};
|
||||
}
|
||||
|
||||
/** Replace the locomotive set (still minimum 2, same-yard rule applies). */
|
||||
/** Replace the locomotive set (minimum 1, same-yard rule applies). */
|
||||
async setLocomotives(id: string, dto: UpdateTrainLocomotivesDto) {
|
||||
const locomotiveIds = [...new Set(dto.locomotiveIds)];
|
||||
if (locomotiveIds.length < 2) {
|
||||
throw new BadRequestException('A train must be pulled by at least two locomotives');
|
||||
if (locomotiveIds.length < 1) {
|
||||
throw new BadRequestException('A train must be pulled by at least one locomotive');
|
||||
}
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
const train = await this.getEditableTrain(manager, id);
|
||||
@@ -360,7 +363,7 @@ export class TrainBuilderService {
|
||||
train.id,
|
||||
);
|
||||
await this.replaceLocomotiveLinks(manager, train.id, locomotiveIds);
|
||||
const limits = minLocomotiveLimits(locomotives);
|
||||
const limits = combinedLocomotiveLimits(locomotives);
|
||||
await manager
|
||||
.getRepository(Train)
|
||||
.update(train.id, { capacityTons: round(limits?.maxPullWeightTons ?? 0) });
|
||||
@@ -520,6 +523,28 @@ export class TrainBuilderService {
|
||||
sequenceNumber: null,
|
||||
status: WagonStatus.Maintenance,
|
||||
});
|
||||
// Audit row: which train it came off and when. The wagon does not change
|
||||
// yard here, so from/to are the same — the ledger is the wagon's history
|
||||
// surface, and a maintenance detach has to be in it.
|
||||
const yardId = wagon.currentYardId ?? train.currentYardId ?? null;
|
||||
if (yardId) {
|
||||
await manager.getRepository(WagonMovement).save(
|
||||
manager.getRepository(WagonMovement).create({
|
||||
wagonId: wagon.id,
|
||||
fromYardId: yardId,
|
||||
toYardId: yardId,
|
||||
kind: WagonMovementKind.Maintenance,
|
||||
note: `Sent to maintenance from train ${train.trainNumber ?? train.code}`,
|
||||
occurredAt: new Date(),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// to_yard_id is NOT NULL — a yard-less wagon still goes to maintenance,
|
||||
// it just cannot carry a ledger row.
|
||||
this.logger.warn(
|
||||
`Wagon ${wagon.wagonNumber} sent to maintenance with no yard — ledger row skipped`,
|
||||
);
|
||||
}
|
||||
await this.resequenceWagons(manager, train.id);
|
||||
});
|
||||
return this.getComposition(id);
|
||||
@@ -743,7 +768,16 @@ export class TrainBuilderService {
|
||||
totalLengthMeters: round(
|
||||
wagons.reduce((sum, w) => sum + (Number(w.wagonType?.lengthMeters) || 0), 0),
|
||||
),
|
||||
maxPullWeightTons: round(train.capacityTons),
|
||||
// Derived live from the coupled set, NOT from the stored capacity_tons.
|
||||
// That column is written at build/re-couple time, so every train built
|
||||
// before pull weight became additive still holds the old single-locomotive
|
||||
// figure. Computing it here keeps the board honest without a backfill;
|
||||
// the column self-heals the next time the locomotive set is saved.
|
||||
maxPullWeightTons: round(
|
||||
combinedLocomotiveLimits(locomotives)?.maxPullWeightTons ??
|
||||
Number(train.capacityTons) ??
|
||||
0,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -878,7 +912,7 @@ export class TrainBuilderService {
|
||||
where: { trainId: train.id },
|
||||
relations: { locomotive: true },
|
||||
});
|
||||
const limits = minLocomotiveLimits(
|
||||
const limits = combinedLocomotiveLimits(
|
||||
links
|
||||
.map((link) => link.locomotive)
|
||||
.filter((loco): loco is Locomotive => Boolean(loco)),
|
||||
|
||||
Reference in New Issue
Block a user