mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
- 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.
171 lines
5.2 KiB
TypeScript
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));
|
|
}
|