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 { const map = new Map(); 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, fleetTypeCodes: Map, ): 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, 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)); }