Files
edr-platform/apps/edr-freight-api/src/modules/train-scheduling/fleet-plan.util.ts
Marshal b6d5047d27 feat: Implement general contract booking orders functionality
- Add DTOs for creating booking orders and viewing contract quantities.
- Create entities for booking orders and booking order lines.
- Implement service for managing general contract operations, including activation after payment and retrieving quantity lines.
- Develop UI components for contract detail and list pages, including order placement dialog.
- Integrate API service for booking orders, enabling listing and creating orders against contracts.
- Enhance contract status display and quantity pool visualization in the UI.
2026-06-20 19:31:51 +00:00

171 lines
5.2 KiB
TypeScript

import type { Booking } from '../bookings/entities/booking.entity';
import type { WagonType } from '../wagon-types/entities/wagon-type.entity';
import {
buildBulkWagonPlan,
buildContainerWagonPlan,
buildMixedWagonPlan,
roundTons,
type WagonPlanSlot,
} from './wagon-plan.util';
export type FleetAvailabilityRow = {
wagonTypeId: string;
wagonTypeCode: string;
needed: number;
available: number;
shortfall: number;
};
export type DeferredBookingRow = {
id: string;
reference: string;
reason: string;
};
export function sortBookingsForScheduling(bookings: Booking[]): Booking[] {
return [...bookings].sort((a, b) => {
const govDiff = Number(Boolean(b.isGovernment)) - Number(Boolean(a.isGovernment));
if (govDiff !== 0) return govDiff;
const priorityDiff = (b.priorityScore ?? 0) - (a.priorityScore ?? 0);
if (priorityDiff !== 0) return priorityDiff;
const aTime = a.scheduledDate ? new Date(a.scheduledDate).getTime() : 0;
const bTime = b.scheduledDate ? new Date(b.scheduledDate).getTime() : 0;
return aTime - bTime;
});
}
export function wagonsRequiredForBooking(booking: Booking, bulkWagonCapacity?: number): number {
if (booking.freightType === 'BULK') {
const weight = Number(booking.cargoTotalWeightVgm ?? 0);
const capacity = bulkWagonCapacity && bulkWagonCapacity > 0 ? bulkWagonCapacity : 1;
return Math.max(1, Math.ceil(weight / capacity));
}
const lineSlots = (booking.bookingContainers ?? []).reduce(
(sum, line) => sum + Number(line.wagonsRequired ?? 0),
0,
);
return Math.max(1, lineSlots);
}
export function countSlotsByType(wagonPlan: WagonPlanSlot[]): Map<string, { code: string; count: number }> {
const map = new Map<string, { code: string; count: number }>();
for (const slot of wagonPlan) {
const existing = map.get(slot.wagonTypeId) ?? { code: slot.wagonTypeCode, count: 0 };
existing.count += 1;
map.set(slot.wagonTypeId, existing);
}
return map;
}
export function computeFleetAvailability(
demandPlan: WagonPlanSlot[],
fleetByTypeId: Map<string, number>,
fleetTypeCodes: Map<string, string>,
): FleetAvailabilityRow[] {
const neededByType = countSlotsByType(demandPlan);
const typeIds = new Set([...neededByType.keys(), ...fleetByTypeId.keys()]);
return [...typeIds].map((wagonTypeId) => {
const needed = neededByType.get(wagonTypeId)?.count ?? 0;
const available = fleetByTypeId.get(wagonTypeId) ?? 0;
return {
wagonTypeId,
wagonTypeCode:
neededByType.get(wagonTypeId)?.code ??
fleetTypeCodes.get(wagonTypeId) ??
wagonTypeId,
needed,
available,
shortfall: Math.max(0, needed - available),
};
}).filter((row) => row.needed > 0 || row.available > 0);
}
export function selectBookingsWithinFleetCap(
bookings: Booking[],
fleetByTypeId: Map<string, number>,
resolveWagonTypeId: (booking: Booking) => string,
bulkWagonCapacity?: number,
): { fitting: Booking[]; deferred: DeferredBookingRow[] } {
const remaining = new Map(fleetByTypeId);
const fitting: Booking[] = [];
const deferred: DeferredBookingRow[] = [];
for (const booking of sortBookingsForScheduling(bookings)) {
const typeId = resolveWagonTypeId(booking);
const needed = wagonsRequiredForBooking(booking, bulkWagonCapacity);
const available = remaining.get(typeId) ?? 0;
if (available >= needed) {
remaining.set(typeId, available - needed);
fitting.push(booking);
continue;
}
deferred.push({
id: booking.id,
reference: booking.reference,
reason:
available > 0
? `Needs ${needed} wagons but only ${available} available for this type`
: `No available wagons for required type (${needed} needed)`,
});
}
return { fitting, deferred };
}
export function buildCappedWagonPlan(params: {
bookings: Booking[];
resolvedMode: 'CONTAINER' | 'BULK' | 'MIXED';
containerWagonType: WagonType;
bulkWagonType: WagonType;
}): WagonPlanSlot[] {
const { bookings, resolvedMode, containerWagonType, bulkWagonType } = params;
if (resolvedMode === 'MIXED') {
const containerBookings = bookings.filter((b) => b.freightType === 'CONTAINER');
const bulkBookings = bookings.filter((b) => b.freightType === 'BULK');
return buildMixedWagonPlan(
containerBookings,
bulkBookings,
containerWagonType,
bulkWagonType,
);
}
if (resolvedMode === 'BULK') {
return buildBulkWagonPlan(bookings, bulkWagonType);
}
return buildContainerWagonPlan(bookings, containerWagonType);
}
export function summarizeFleetWarnings(
fleetAvailability: FleetAvailabilityRow[],
deferred: DeferredBookingRow[],
): string[] {
const warnings: string[] = [];
for (const row of fleetAvailability.filter((r) => r.shortfall > 0)) {
warnings.push(
`Fleet shortage: need ${row.needed} ${row.wagonTypeCode}, only ${row.available} available (short ${row.shortfall})`,
);
}
if (deferred.length) {
warnings.push(
`${deferred.length} booking(s) deferred to next train due to insufficient fleet wagons`,
);
}
return warnings;
}
export function totalAssignedWeight(bookings: Booking[]): number {
return roundTons(bookings.reduce((sum, b) => sum + Number(b.cargoTotalWeightVgm ?? 0), 0));
}