mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +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:
@@ -133,7 +133,7 @@ import { pickLowestFreeNumber, pickTrainNumberPool } from './train-number.util';
|
||||
import {
|
||||
bookingCargoTons,
|
||||
deriveTrainCapacityFromLocomotive,
|
||||
minLocomotiveLimits,
|
||||
combinedLocomotiveLimits,
|
||||
trainSetLocomotiveLimits,
|
||||
wagonTypeDimensionsFromEntity,
|
||||
LocomotiveLimits,
|
||||
@@ -1300,9 +1300,9 @@ export class TrainSchedulingService {
|
||||
.slice()
|
||||
.sort((a, b) => a.sequenceNo - b.sequenceNo)
|
||||
.map((link) => link.locomotiveId);
|
||||
if (locomotiveIds.length < 2) {
|
||||
if (locomotiveIds.length < 1) {
|
||||
throw new BadRequestException(
|
||||
`Train ${builtTrain.code} has fewer than two locomotives; rebuild it before scheduling`,
|
||||
`Train ${builtTrain.code} has no locomotive; rebuild it before scheduling`,
|
||||
);
|
||||
}
|
||||
if (builtTrain.currentYardId !== route.originYardId) {
|
||||
@@ -1323,8 +1323,8 @@ export class TrainSchedulingService {
|
||||
}
|
||||
} else {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1382,7 +1382,7 @@ export class TrainSchedulingService {
|
||||
builtTrain?.id ?? null,
|
||||
);
|
||||
// Effective capacity is capped by the weakest locomotive in the set.
|
||||
const limitLoco = minLocomotiveLimits(lockedLocomotives) ?? undefined;
|
||||
const limitLoco = combinedLocomotiveLimits(lockedLocomotives) ?? undefined;
|
||||
const departure = new Date(dto.scheduleDate);
|
||||
// Every schedule starts with a CLOSED customer window; the window engine opens
|
||||
// it on schedule. DOMESTIC runs the same one-booking-day cycle as IMPORT
|
||||
@@ -1572,7 +1572,7 @@ export class TrainSchedulingService {
|
||||
};
|
||||
|
||||
const setLocomotives = this.locomotivesOfTrainSet(schedule.trainSet);
|
||||
const limitLoco = minLocomotiveLimits(setLocomotives) ?? undefined;
|
||||
const limitLoco = combinedLocomotiveLimits(setLocomotives) ?? undefined;
|
||||
const limits = await this.resolveTrainLimitConfig(previewDto, limitLoco);
|
||||
|
||||
// Callers that add bookings without hand-picking container slots (the
|
||||
@@ -3971,36 +3971,12 @@ export class TrainSchedulingService {
|
||||
const builtTrainId = await this.builtTrainIdOfSchedule(targetScheduleId);
|
||||
|
||||
const originYardId = dto.originStationId;
|
||||
let stock: WagonStock;
|
||||
if (builtTrainId) {
|
||||
stock = await this.builtTrainStock(builtTrainId);
|
||||
} else {
|
||||
// Dynamic consist: a slot's physical wagon may ride from the train's origin
|
||||
// OR already sit at the booking's own boarding yard and attach there — so
|
||||
// the usable fleet is the union across the origin and every boarding yard.
|
||||
const boardYardIds = [
|
||||
...new Set(
|
||||
[originYardId, ...bookings.map((b) => b.originYardId)].filter(Boolean),
|
||||
),
|
||||
];
|
||||
const fleetCountsByYard = await Promise.all(
|
||||
boardYardIds.map((yardId) =>
|
||||
this.countFleetAvailability(yardId, targetScheduleId),
|
||||
),
|
||||
);
|
||||
const remainingByTypeId = new Map<string, number>();
|
||||
const codesByTypeId = new Map<string, string>();
|
||||
for (const rows of fleetCountsByYard) {
|
||||
for (const row of rows) {
|
||||
remainingByTypeId.set(
|
||||
row.wagonTypeId,
|
||||
(remainingByTypeId.get(row.wagonTypeId) ?? 0) + row.available,
|
||||
);
|
||||
codesByTypeId.set(row.wagonTypeId, row.wagonTypeCode);
|
||||
}
|
||||
}
|
||||
stock = { mode: 'YARD', remainingByTypeId, codesByTypeId };
|
||||
}
|
||||
const stock: WagonStock = await this.wagonStockForSchedule(
|
||||
targetScheduleId,
|
||||
originYardId,
|
||||
bookings.map((b) => b.originYardId),
|
||||
builtTrainId,
|
||||
);
|
||||
|
||||
// Leg-aware stock: each booking consumes wagons only on the edges it rides,
|
||||
// so a ride-along on an empty leg never competes with cargo on a full one.
|
||||
@@ -4129,7 +4105,7 @@ export class TrainSchedulingService {
|
||||
// warning (it must arrive before dispatch), but a set too weak to pull the train
|
||||
// is a hard violation.
|
||||
const offYard = assignedLocomotives.find((l) => l.currentYardId !== originYardId);
|
||||
const setLimits = minLocomotiveLimits(assignedLocomotives);
|
||||
const setLimits = combinedLocomotiveLimits(assignedLocomotives);
|
||||
if (offYard) {
|
||||
warnings.push(
|
||||
`Locomotive ${offYard.code} is not at the schedule origin yard yet; it must arrive before dispatch`,
|
||||
@@ -4799,6 +4775,51 @@ export class TrainSchedulingService {
|
||||
* type. This is the whole plannable pool for its schedules — the plan is
|
||||
* full when every consist wagon is allocated.
|
||||
*/
|
||||
/**
|
||||
* The physical wagons a schedule can actually plan against, by wagon type.
|
||||
*
|
||||
* A schedule built from a Train Builder train plans against ONLY that train's
|
||||
* own consist. A legacy/dynamic-consist schedule plans against the boarding
|
||||
* yards' loose pool: a slot's wagon may ride from the train's origin OR
|
||||
* already sit at the booking's own boarding yard and attach there, so the
|
||||
* usable fleet is the union across the origin and every boarding yard.
|
||||
*
|
||||
* Public because batch fill needs the SAME stock the allocator will later
|
||||
* validate against — selecting a booking the allocator cannot place is how
|
||||
* customers ended up paying for wagons that were never there.
|
||||
*/
|
||||
async wagonStockForSchedule(
|
||||
scheduleId: string | undefined,
|
||||
originYardId: string,
|
||||
boardingYardIds: Array<string | null | undefined> = [],
|
||||
preloadedBuiltTrainId?: string | null,
|
||||
): Promise<WagonStock> {
|
||||
const builtTrainId =
|
||||
preloadedBuiltTrainId !== undefined
|
||||
? preloadedBuiltTrainId
|
||||
: await this.builtTrainIdOfSchedule(scheduleId);
|
||||
if (builtTrainId) return this.builtTrainStock(builtTrainId);
|
||||
|
||||
const boardYardIds = [
|
||||
...new Set([originYardId, ...boardingYardIds].filter((id): id is string => Boolean(id))),
|
||||
];
|
||||
const fleetCountsByYard = await Promise.all(
|
||||
boardYardIds.map((yardId) => this.countFleetAvailability(yardId, scheduleId)),
|
||||
);
|
||||
const remainingByTypeId = new Map<string, number>();
|
||||
const codesByTypeId = new Map<string, string>();
|
||||
for (const rows of fleetCountsByYard) {
|
||||
for (const row of rows) {
|
||||
remainingByTypeId.set(
|
||||
row.wagonTypeId,
|
||||
(remainingByTypeId.get(row.wagonTypeId) ?? 0) + row.available,
|
||||
);
|
||||
codesByTypeId.set(row.wagonTypeId, row.wagonTypeCode);
|
||||
}
|
||||
}
|
||||
return { mode: 'YARD', remainingByTypeId, codesByTypeId };
|
||||
}
|
||||
|
||||
private async builtTrainStock(builtTrainId: string): Promise<WagonStock> {
|
||||
const wagons = await this.dataSource.getRepository(Wagon).find({
|
||||
where: { trainId: builtTrainId },
|
||||
@@ -5347,7 +5368,7 @@ export class TrainSchedulingService {
|
||||
* schedule-creation picker. Mirrors the locomotive picker's advance-scheduling
|
||||
* philosophy: nothing serviceable is filtered out — staff see the status,
|
||||
* whether the train sits at the origin yard yet, and its future schedules.
|
||||
* Trains with fewer than two locomotives are omitted (never schedulable).
|
||||
* Trains with no locomotive at all are omitted (never schedulable).
|
||||
*/
|
||||
async getAvailableTrainsForRoute(routeId: string) {
|
||||
const route = await this.getSchedulableRoute(routeId);
|
||||
@@ -5385,7 +5406,7 @@ export class TrainSchedulingService {
|
||||
const futureCounts = new Map(counts.map((c) => [c.train_id, Number(c.future_count)]));
|
||||
|
||||
return trains
|
||||
.filter((train) => (train.locomotives ?? []).length >= 2)
|
||||
.filter((train) => (train.locomotives ?? []).length >= 1)
|
||||
.map((train) => {
|
||||
const wagons = train.wagons ?? [];
|
||||
return {
|
||||
@@ -5417,7 +5438,15 @@ export class TrainSchedulingService {
|
||||
totalLengthMeters: roundTons(
|
||||
wagons.reduce((sum, w) => sum + (Number(w.wagonType?.lengthMeters) || 0), 0),
|
||||
),
|
||||
maxPullWeightTons: roundTons(Number(train.capacityTons)),
|
||||
// Live from the coupled set — `capacity_tons` still holds the old
|
||||
// single-locomotive figure on trains built before pull weight summed.
|
||||
maxPullWeightTons: roundTons(
|
||||
combinedLocomotiveLimits(
|
||||
(train.locomotives ?? [])
|
||||
.map((link) => link.locomotive)
|
||||
.filter((loco): loco is Locomotive => Boolean(loco)),
|
||||
)?.maxPullWeightTons ?? Number(train.capacityTons),
|
||||
),
|
||||
atOriginYard: train.currentYardId === route.originYardId,
|
||||
futureScheduleCount: futureCounts.get(train.id) ?? 0,
|
||||
};
|
||||
@@ -5467,7 +5496,7 @@ export class TrainSchedulingService {
|
||||
);
|
||||
const pinnedToLiveIds = await this.wagonIdsPinnedToLiveSchedules();
|
||||
|
||||
const limits = minLocomotiveLimits(this.locomotivesOfTrainSet(schedule.trainSet));
|
||||
const limits = combinedLocomotiveLimits(this.locomotivesOfTrainSet(schedule.trainSet));
|
||||
const maxPullWeightTons = roundTons(Number(limits?.maxPullWeightTons ?? 0));
|
||||
const overageToleranceTons = roundTons(Number(limits?.overageToleranceTons) || 0);
|
||||
const maxTrainLengthMeters = roundTons(Number(limits?.maxTrainLengthMeters ?? 0));
|
||||
@@ -5590,7 +5619,7 @@ export class TrainSchedulingService {
|
||||
.filter((slot) => slot.physicalWagonId && (slot.allocations?.length ?? 0) > 0)
|
||||
.map((slot) => slot.physicalWagonId as string),
|
||||
);
|
||||
const limits = minLocomotiveLimits(this.locomotivesOfTrainSet(schedule.trainSet));
|
||||
const limits = combinedLocomotiveLimits(this.locomotivesOfTrainSet(schedule.trainSet));
|
||||
const pullCapTons = roundTons(
|
||||
Number(limits?.maxPullWeightTons ?? 0) + (Number(limits?.overageToleranceTons) || 0),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user