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:
Marshal
2026-07-15 09:13:02 +00:00
parent 9be7f356f0
commit 11771e5f92
39 changed files with 1731 additions and 269 deletions

View File

@@ -2,9 +2,14 @@ import { AllocationLoadType } from '@edr/types';
import { Booking } from '../bookings/entities/booking.entity';
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
import { sortBookingsForScheduling, type DeferredBookingRow } from './fleet-plan.util';
import {
sortBookingsForScheduling,
type BookingWagonShortage,
type DeferredBookingRow,
} from './fleet-plan.util';
import {
MAX_TEU_SLOTS_PER_WAGON,
containerWagonsForLines,
expandBookingContainerUnits,
roundTons,
tareTonsOf,
@@ -55,7 +60,12 @@ type OpenSlot = {
freeCapacityTons: number;
};
type PlacementProblem = { kind: 'config' | 'stock'; message: string };
type PlacementProblem = {
kind: 'config' | 'stock';
message: string;
/** Wagon types the failing placement could have used (stock problems only). */
candidates?: WagonType[];
};
const slotFromWagonType = (wagonType: WagonType, kind: SlotLoadType): WagonPlanSlot => ({
sequenceNo: 0, // stamped at the end
@@ -69,6 +79,38 @@ const slotFromWagonType = (wagonType: WagonType, kind: SlotLoadType): WagonPlanS
slotLoadType: kind,
});
/**
* Booking-level shortage against the wagon types the failing placement could
* use: wagons the whole booking needs vs stock left for those types. Container
* counts are TEU-packed per booking; bulk divides by the largest candidate.
*/
const shortageFor = (
booking: Booking,
candidates: WagonType[],
remaining: Map<string, number>,
): BookingWagonShortage => {
const wagonsNeeded =
booking.freightType === 'BULK'
? Math.max(
1,
Math.ceil(
Number(booking.cargoTotalWeightVgm ?? 0) /
Math.max(1, ...candidates.map((wt) => Number(wt.capacityTons))),
),
)
: Math.max(1, containerWagonsForLines(booking.bookingContainers ?? []));
const wagonsAvailable = candidates.reduce(
(sum, wt) => sum + (remaining.get(wt.id) ?? 0),
0,
);
return {
wagonTypeCodes: [...new Set(candidates.map((wt) => wt.code))].join('/'),
wagonsNeeded,
wagonsAvailable,
wagonsShort: Math.max(1, wagonsNeeded - wagonsAvailable),
};
};
const addAllocation = (
slot: WagonPlanSlot,
bookingId: string,
@@ -120,7 +162,9 @@ export function planWagonsWithStock(params: {
cargoTypeId: string | null,
): OpenSlot | PlacementProblem => {
const inStock = candidates.filter((wt) => (remaining.get(wt.id) ?? 0) > 0);
if (!inStock.length) return { kind: 'stock', message: noStockMessage(candidates) };
if (!inStock.length) {
return { kind: 'stock', message: noStockMessage(candidates), candidates };
}
// Bulk favors the largest wagon (fewest wagons for the tonnage); containers
// favor the deepest stock so the consist drains evenly. Ties keep config order.
const chosen = [...inStock].sort((a, b) =>
@@ -271,7 +315,21 @@ export function planWagonsWithStock(params: {
});
if (problem.kind === 'config') configIssues.add(problem.message);
deferred.push({ id: booking.id, reference: booking.reference, reason: problem.message });
// remaining is rolled back here, so the shortage counts the stock this
// booking actually saw — not what its own partial placement consumed.
const shortage =
problem.kind === 'stock' && problem.candidates?.length
? shortageFor(booking, problem.candidates, remaining)
: null;
deferred.push({
id: booking.id,
reference: booking.reference,
reason: shortage
? `${problem.message} — needs ${shortage.wagonsNeeded} × ${shortage.wagonTypeCodes}, ` +
`${shortage.wagonsAvailable} available (short ${shortage.wagonsShort})`
: problem.message,
shortage,
});
}
return {