mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 18:20:57 +00:00
fix issues
This commit is contained in:
@@ -230,14 +230,13 @@ export function listBatchWindowsForBookings(
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Board-display windows: full-day, midnight-based 3h slots over a date range.
|
||||
// These are used ONLY for the batch-board UI grouping (not persisted, and
|
||||
// independent of the cron intake hours above).
|
||||
// Board-display windows: the REAL booking-window cycles derived from the
|
||||
// train_scheduling_global_rules config (window open hour, lead days, duration,
|
||||
// reopen delay) — NOT a fixed clock grid. Import shows each booking-window cycle
|
||||
// (opens at windowOpenHour EAT, lasts windowDurationHours, reopens after
|
||||
// reopenDelayMinutes until departure). Export shows the single FCFS lead window.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Midnight-based 3-hour slot starts (00–03, 03–06, … 21–24). */
|
||||
export const BOARD_WINDOW_HOURS = [0, 3, 6, 9, 12, 15, 18, 21] as const;
|
||||
|
||||
/** A board window carries an EAT calendar date in addition to the slot times. */
|
||||
export interface BoardWindow extends BatchWindow {
|
||||
/** EAT calendar day as ISO `YYYY-MM-DD`. */
|
||||
@@ -246,6 +245,15 @@ export interface BoardWindow extends BatchWindow {
|
||||
dateLabel: string;
|
||||
}
|
||||
|
||||
/** Config fields the board needs to reconstruct booking-window cycles. */
|
||||
export interface BoardWindowConfig {
|
||||
importWindowLeadDays: number;
|
||||
windowOpenHour: number;
|
||||
windowDurationHours: number;
|
||||
reopenDelayMinutes: number;
|
||||
exportBookingLeadHours: number;
|
||||
}
|
||||
|
||||
const dayLabelFmt = new Intl.DateTimeFormat('en-GB', {
|
||||
weekday: 'short',
|
||||
day: '2-digit',
|
||||
@@ -257,119 +265,124 @@ function pad2(n: number): string {
|
||||
return String(n).padStart(2, '0');
|
||||
}
|
||||
|
||||
/** Build a midnight-based 3h board window for an EAT calendar day + slot start hour. */
|
||||
function boardWindowFromEatStart(
|
||||
year: number,
|
||||
month: number,
|
||||
day: number,
|
||||
startHour: number,
|
||||
): BoardWindow {
|
||||
const start = eatToUtc(year, month, day, startHour);
|
||||
const endHour = startHour + 3; // 21 -> 24 (handled by Date.UTC roll-over)
|
||||
const end = eatToUtc(year, month, day, endHour);
|
||||
const endLabel = endHour >= 24 ? '24:00' : `${pad2(endHour)}:00`;
|
||||
/** Wrap a [start, end] interval as a labelled BoardWindow keyed on its EAT day. */
|
||||
function boardWindowFromInterval(start: Date, end: Date): BoardWindow {
|
||||
const { year, month, day } = eatParts(start);
|
||||
return {
|
||||
key: start.toISOString(),
|
||||
start,
|
||||
end,
|
||||
label: formatWindowLabel(start, end, endLabel),
|
||||
label: formatWindowLabel(start, end),
|
||||
date: `${year}-${pad2(month)}-${pad2(day)}`,
|
||||
dateLabel: dayLabelFmt.format(start),
|
||||
};
|
||||
}
|
||||
|
||||
/** Which midnight-based 3h EAT slot a timestamp falls in. */
|
||||
export function boardWindowForTimestamp(date: Date): BoardWindow {
|
||||
const { year, month, day, hour } = eatParts(date);
|
||||
let startHour: (typeof BOARD_WINDOW_HOURS)[number] = 0;
|
||||
for (const h of BOARD_WINDOW_HOURS) {
|
||||
if (hour >= h) startHour = h;
|
||||
}
|
||||
return boardWindowFromEatStart(year, month, day, startHour);
|
||||
}
|
||||
|
||||
/**
|
||||
* Continuous list of board windows from `openDate` to `departureDate` (inclusive),
|
||||
* clamped to the slot containing `openDate` on the first day and the slot
|
||||
* containing `departureDate` on the last day. Returned in chronological order.
|
||||
* The real booking-window cycles for a schedule, straight from config.
|
||||
*
|
||||
* IMPORT: first window opens at `windowOpenHour` EAT on `departure − importWindowLeadDays`
|
||||
* for `windowDurationHours`; if the train isn't full it reopens `reopenDelayMinutes`
|
||||
* after each close, on the same booking day, until departure. This mirrors
|
||||
* `computeImportWindowTimes` + `concludeCycle`'s reopen math so the board shows the
|
||||
* exact windows the engine runs.
|
||||
* EXPORT: a single FCFS window from `departure − exportBookingLeadHours` to departure.
|
||||
*/
|
||||
export function listBoardWindowsForRange(
|
||||
openDate: Date,
|
||||
departureDate: Date,
|
||||
export function listConfigBookingWindows(
|
||||
direction: string | null | undefined,
|
||||
departure: Date,
|
||||
cfg: BoardWindowConfig,
|
||||
): BoardWindow[] {
|
||||
const startWin = boardWindowForTimestamp(openDate);
|
||||
const endWin = boardWindowForTimestamp(departureDate);
|
||||
// Guard against an inverted range (departure before open).
|
||||
if (endWin.start.getTime() < startWin.start.getTime()) {
|
||||
return [startWin];
|
||||
if (direction === 'EXPORT') {
|
||||
const start = new Date(departure.getTime() - cfg.exportBookingLeadHours * 3_600_000);
|
||||
return [boardWindowFromInterval(start, departure)];
|
||||
}
|
||||
|
||||
const windows: BoardWindow[] = [];
|
||||
const seen = new Set<string>();
|
||||
// Walk day-by-day in EAT, emitting each day's slots, stepping via UTC noon to
|
||||
// avoid any boundary ambiguity, then filter to [startWin.start, endWin.start].
|
||||
let cursor = new Date(eatToUtc(
|
||||
Number(startWin.date.slice(0, 4)),
|
||||
Number(startWin.date.slice(5, 7)),
|
||||
Number(startWin.date.slice(8, 10)),
|
||||
12,
|
||||
));
|
||||
const lastDayMs = eatToUtc(
|
||||
Number(endWin.date.slice(0, 4)),
|
||||
Number(endWin.date.slice(5, 7)),
|
||||
Number(endWin.date.slice(8, 10)),
|
||||
12,
|
||||
).getTime();
|
||||
const durationMs = cfg.windowDurationHours * 3_600_000;
|
||||
const reopenMs = cfg.reopenDelayMinutes * 60_000;
|
||||
const windowDay = shiftEatDay(eatDay(departure), -cfg.importWindowLeadDays);
|
||||
|
||||
while (cursor.getTime() <= lastDayMs) {
|
||||
const { year, month, day } = eatParts(cursor);
|
||||
for (const h of BOARD_WINDOW_HOURS) {
|
||||
const w = boardWindowFromEatStart(year, month, day, h);
|
||||
if (
|
||||
w.start.getTime() >= startWin.start.getTime() &&
|
||||
w.start.getTime() <= endWin.start.getTime() &&
|
||||
!seen.has(w.key)
|
||||
) {
|
||||
seen.add(w.key);
|
||||
windows.push(w);
|
||||
}
|
||||
let opensAt = eatDayToUtc(windowDay, cfg.windowOpenHour);
|
||||
// Reopen stays on the same EAT booking day and before departure; cap at 12 cycles.
|
||||
for (let cycle = 0; cycle < 12; cycle += 1) {
|
||||
if (opensAt.getTime() >= departure.getTime()) break;
|
||||
let closesAt = new Date(opensAt.getTime() + durationMs);
|
||||
if (closesAt.getTime() > departure.getTime()) closesAt = departure;
|
||||
windows.push(boardWindowFromInterval(opensAt, closesAt));
|
||||
|
||||
const nextOpensAt = new Date(closesAt.getTime() + reopenMs);
|
||||
if (
|
||||
nextOpensAt.getTime() >= departure.getTime() ||
|
||||
eatDay(nextOpensAt) !== eatDay(opensAt)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
cursor = new Date(cursor.getTime() + 24 * 60 * 60 * 1000);
|
||||
opensAt = nextOpensAt;
|
||||
}
|
||||
|
||||
windows.sort(compareBatchWindows);
|
||||
// Degenerate config (no window before departure) — surface a single window
|
||||
// clamped to departure so the board still renders something meaningful.
|
||||
if (windows.length === 0) {
|
||||
windows.push(boardWindowFromInterval(new Date(departure.getTime() - durationMs), departure));
|
||||
}
|
||||
return windows;
|
||||
}
|
||||
|
||||
/** Which config booking-window a timestamp falls in; null if before/after all of them. */
|
||||
function configWindowForTimestamp(
|
||||
windows: BoardWindow[],
|
||||
date: Date,
|
||||
): BoardWindow | null {
|
||||
const ms = date.getTime();
|
||||
for (const w of windows) {
|
||||
if (ms >= w.start.getTime() && ms < w.end.getTime()) return w;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group items into board windows spanning [openDate, departureDate]. Empty
|
||||
* windows are kept so the UI shows every slot. Items whose timestamp falls
|
||||
* outside the range still get their own window (nothing hidden). Items without
|
||||
* a timestamp go to `pendingKey`.
|
||||
* Group items into the real config booking-window cycles for a schedule. Empty
|
||||
* windows are kept so the UI shows every cycle. Items whose timestamp falls
|
||||
* outside every window (e.g. a booking created before the window opened) are
|
||||
* attached to the nearest window by start time so nothing is hidden. Items
|
||||
* without a timestamp go to `pendingKey`.
|
||||
*/
|
||||
export function groupBookingsIntoBoardWindows<T>(
|
||||
items: T[],
|
||||
getTimestamp: (item: T) => Date | null | undefined,
|
||||
openDate: Date,
|
||||
departureDate: Date,
|
||||
direction: string | null | undefined,
|
||||
departure: Date,
|
||||
cfg: BoardWindowConfig,
|
||||
pendingKey = 'pending-contract',
|
||||
): Map<string, { window: BoardWindow | null; items: T[] }> {
|
||||
const windows = listConfigBookingWindows(direction, departure, cfg);
|
||||
const map = new Map<string, { window: BoardWindow | null; items: T[] }>();
|
||||
|
||||
for (const w of listBoardWindowsForRange(openDate, departureDate)) {
|
||||
for (const w of windows) {
|
||||
map.set(w.key, { window: w, items: [] });
|
||||
}
|
||||
map.set(pendingKey, { window: null, items: [] });
|
||||
|
||||
const firstWindow = windows[0] ?? null;
|
||||
const lastWindow = windows[windows.length - 1] ?? null;
|
||||
|
||||
for (const item of items) {
|
||||
const ts = getTimestamp(item);
|
||||
if (!ts) {
|
||||
map.get(pendingKey)!.items.push(item);
|
||||
continue;
|
||||
}
|
||||
const w = boardWindowForTimestamp(ts);
|
||||
if (!map.has(w.key)) {
|
||||
map.set(w.key, { window: w, items: [] });
|
||||
let w = configWindowForTimestamp(windows, ts);
|
||||
if (!w) {
|
||||
// Booked before the window opened → first cycle; after it closed → last cycle.
|
||||
w =
|
||||
firstWindow && ts.getTime() < firstWindow.start.getTime()
|
||||
? firstWindow
|
||||
: lastWindow;
|
||||
}
|
||||
if (!w) {
|
||||
map.get(pendingKey)!.items.push(item);
|
||||
continue;
|
||||
}
|
||||
map.get(w.key)!.items.push(item);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user