mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 16:00:56 +00:00
add lashing surcharge for cargo types with hasLashing flag
add lashing surcharge for cargo types with hasLashing flag
This commit is contained in:
@@ -266,6 +266,29 @@ export function clampCloseToOfficeHours(
|
||||
return closesAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* The instant a schedule stops accepting bookings. By default that is departure,
|
||||
* but a configured close offset (import/export, minutes) pulls it earlier:
|
||||
* `departure − offset`. This is the single bound every window close, reopen
|
||||
* cycle and export FCFS close is capped at — swap it in wherever the logic used
|
||||
* to cap at departure. A non-positive/absent offset yields departure unchanged.
|
||||
*/
|
||||
export function bookingCloseCutoff(
|
||||
departure: Date,
|
||||
direction: string | null | undefined,
|
||||
cfg: {
|
||||
importCloseOffsetMinutes?: number | null;
|
||||
exportCloseOffsetMinutes?: number | null;
|
||||
},
|
||||
): Date {
|
||||
const offsetMinutes =
|
||||
direction === 'EXPORT'
|
||||
? cfg.exportCloseOffsetMinutes
|
||||
: cfg.importCloseOffsetMinutes;
|
||||
if (offsetMinutes == null || !(offsetMinutes > 0)) return departure;
|
||||
return new Date(departure.getTime() - offsetMinutes * 60_000);
|
||||
}
|
||||
|
||||
export interface InitialWindowTimes {
|
||||
windowOpensAt: Date;
|
||||
windowClosesAt: Date;
|
||||
@@ -296,9 +319,13 @@ export function computeImportWindowTimes(
|
||||
windowOpenHour: number;
|
||||
windowCloseHour: number;
|
||||
windowDurationHours: number;
|
||||
importCloseOffsetMinutes?: number | null;
|
||||
},
|
||||
now: Date,
|
||||
): InitialWindowTimes {
|
||||
// The window opens off the REAL departure (open day = departure − leadDays),
|
||||
// but shuts at the configured cutoff (departure − closeOffset, or departure).
|
||||
const cutoff = bookingCloseCutoff(departure, 'IMPORT', cfg);
|
||||
const windowDay = shiftEatDay(eatDay(departure), -cfg.importWindowLeadDays);
|
||||
const anchor = eatDayToUtc(windowDay, cfg.windowOpenHour);
|
||||
|
||||
@@ -324,8 +351,8 @@ export function computeImportWindowTimes(
|
||||
windowOpenHour: cfg.windowOpenHour,
|
||||
windowCloseHour: cfg.windowCloseHour,
|
||||
});
|
||||
if (closesAt.getTime() > departure.getTime()) {
|
||||
closesAt = departure;
|
||||
if (closesAt.getTime() > cutoff.getTime()) {
|
||||
closesAt = cutoff;
|
||||
}
|
||||
return { windowOpensAt: opensAt, windowClosesAt: closesAt };
|
||||
}
|
||||
@@ -344,8 +371,12 @@ export function computeExportWindowTimes(
|
||||
exportBookingLeadHours: number;
|
||||
windowOpenHour: number;
|
||||
windowCloseHour: number;
|
||||
exportCloseOffsetMinutes?: number | null;
|
||||
},
|
||||
): InitialWindowTimes {
|
||||
// Opens off the real departure (lead hours), shuts at the cutoff
|
||||
// (departure − closeOffset, or departure when no offset is set).
|
||||
const cutoff = bookingCloseCutoff(departure, 'EXPORT', cfg);
|
||||
const rawOpen = new Date(
|
||||
departure.getTime() - cfg.exportBookingLeadHours * 3_600_000,
|
||||
);
|
||||
@@ -353,10 +384,12 @@ export function computeExportWindowTimes(
|
||||
windowOpenHour: cfg.windowOpenHour,
|
||||
windowCloseHour: cfg.windowCloseHour,
|
||||
});
|
||||
if (opensAt.getTime() > departure.getTime()) {
|
||||
opensAt = departure;
|
||||
// Open can't outlive the cutoff (a huge offset would otherwise leave a
|
||||
// negative-length window); clamp to a zero-length window at the cutoff.
|
||||
if (opensAt.getTime() > cutoff.getTime()) {
|
||||
opensAt = cutoff;
|
||||
}
|
||||
return { windowOpensAt: opensAt, windowClosesAt: departure };
|
||||
return { windowOpensAt: opensAt, windowClosesAt: cutoff };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -457,6 +490,10 @@ export interface BoardWindowConfig {
|
||||
*/
|
||||
reopenGapMinutes: number;
|
||||
exportBookingLeadHours: number;
|
||||
/** Minutes before departure the import window shuts; NULL/0 ⇒ close at departure. */
|
||||
importCloseOffsetMinutes?: number | null;
|
||||
/** Minutes before departure the export window shuts; NULL/0 ⇒ close at departure. */
|
||||
exportCloseOffsetMinutes?: number | null;
|
||||
}
|
||||
|
||||
const dayLabelFmt = new Intl.DateTimeFormat('en-GB', {
|
||||
@@ -507,10 +544,15 @@ export function listConfigBookingWindows(
|
||||
cfg: BoardWindowConfig,
|
||||
anchorOpensAt?: Date | null,
|
||||
): BoardWindow[] {
|
||||
// Bookings shut at the cutoff (departure − closeOffset), not departure. The
|
||||
// window opens still key off the real departure below; only closes are capped
|
||||
// here, so the board draws the exact windows the engine runs.
|
||||
const cutoff = bookingCloseCutoff(departure, direction, cfg);
|
||||
|
||||
if (direction === 'EXPORT') {
|
||||
const start =
|
||||
anchorOpensAt ?? computeExportWindowTimes(departure, cfg).windowOpensAt;
|
||||
return [boardWindowFromInterval(start, departure)];
|
||||
return [boardWindowFromInterval(start, cutoff)];
|
||||
}
|
||||
|
||||
const windows: BoardWindow[] = [];
|
||||
@@ -527,29 +569,29 @@ export function listConfigBookingWindows(
|
||||
let opensAt: Date | null = anchorOpensAt ?? eatDayToUtc(windowDay, cfg.windowOpenHour);
|
||||
// The loop terminates naturally: every cycle advances opensAt by at least
|
||||
// (duration + reopen) > 0, and nextCycleOpensAt returns null once opensAt would
|
||||
// reach departure. maxCycles is a derived runaway backstop sized to the real
|
||||
// span (first open → departure) over the smallest possible advance, so a
|
||||
// reach the cutoff. maxCycles is a derived runaway backstop sized to the real
|
||||
// span (first open → cutoff) over the smallest possible advance, so a
|
||||
// legitimate config is never silently truncated — only a pathological
|
||||
// zero-length one would hit it.
|
||||
const spanMs = departure.getTime() - opensAt.getTime();
|
||||
const spanMs = cutoff.getTime() - opensAt.getTime();
|
||||
const minAdvanceMs = Math.max(durationMs + reopenMs, 60_000);
|
||||
const maxCycles = Math.ceil(spanMs / minAdvanceMs) + 2;
|
||||
for (let cycle = 0; cycle < maxCycles; cycle += 1) {
|
||||
if (opensAt.getTime() >= departure.getTime()) break;
|
||||
if (opensAt.getTime() >= cutoff.getTime()) break;
|
||||
let closesAt = new Date(opensAt.getTime() + durationMs);
|
||||
closesAt = clampCloseToOfficeHours(opensAt, closesAt, officeHours);
|
||||
if (closesAt.getTime() > departure.getTime()) closesAt = departure;
|
||||
if (closesAt.getTime() > cutoff.getTime()) closesAt = cutoff;
|
||||
windows.push(boardWindowFromInterval(opensAt, closesAt));
|
||||
|
||||
const earliestNextOpen = new Date(closesAt.getTime() + reopenMs);
|
||||
opensAt = nextCycleOpensAt(earliestNextOpen, officeHours, departure);
|
||||
opensAt = nextCycleOpensAt(earliestNextOpen, officeHours, cutoff);
|
||||
if (opensAt == null) break;
|
||||
}
|
||||
|
||||
// Degenerate config (no window before departure) — surface a single window
|
||||
// clamped to departure so the board still renders something meaningful.
|
||||
// Degenerate config (no window before the cutoff) — surface a single window
|
||||
// clamped to the cutoff so the board still renders something meaningful.
|
||||
if (windows.length === 0) {
|
||||
windows.push(boardWindowFromInterval(new Date(departure.getTime() - durationMs), departure));
|
||||
windows.push(boardWindowFromInterval(new Date(cutoff.getTime() - durationMs), cutoff));
|
||||
}
|
||||
return windows;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user