remove reopen delay minutes from global rules and update related types

- Removed the  field from  and related components.
- Updated  to reflect the removal of the reopen delay input field.
- Modified  to include new train number fields:  and .
- Added  interface to manage active schedules with trade direction.
- Introduced  interface to track wagon shortages in bookings.
- Updated  logic to ensure consistent UI state representation.
- Created migrations to drop the  column and add  and  columns to the  table.
- Added tests for the new booking window display logic and wagon planning functionality.
This commit is contained in:
Marshal
2026-07-15 09:13:02 +00:00
parent 9be7f356f0
commit 11771e5f92
39 changed files with 1731 additions and 269 deletions

View File

@@ -15,6 +15,8 @@ export function DataTable<TData, TValue>({
data,
status,
onRowClick,
rowStyle,
rowClassName,
tableOptions,
pagination,
footer,
@@ -115,11 +117,15 @@ export function DataTable<TData, TValue>({
onRowClick(row.original);
}}
role={onRowClick ? "button" : ""}
className={
style={rowStyle?.(row.original)}
className={[
onRowClick
? "cursor-pointer hover:bg-accent hover:text-foreground "
: ""
}
? "cursor-pointer hover:bg-accent hover:text-foreground"
: "",
rowClassName?.(row.original) ?? "",
]
.join(" ")
.trim()}
>
{row.getVisibleCells().map((cell) => (
<Table.Td

View File

@@ -21,6 +21,10 @@ export interface DataTableProps<TData, TValue> {
data: TData[];
status?: "loading" | "error" | "success";
onRowClick?: (row: TData) => void;
/** Per-row inline style (e.g. data-driven background tints via CSS variables). */
rowStyle?: (row: TData) => React.CSSProperties | undefined;
/** Per-row extra class, appended after the built-in clickable-row classes. */
rowClassName?: (row: TData) => string | undefined;
tableOptions?: Omit<
TableOptions<TData>,
"data" | "columns" | "getCoreRowModel"

View File

@@ -69,3 +69,10 @@ export * from "./components/select";
export * from "./components/switch";
export * from "./components/separator";
export * from "./components/field";
export { bookingWindowUiState } from "./lib/booking-window-display";
export type {
BookingWindowUiKind,
BookingWindowStateInput,
BookingWindowUiState,
} from "./lib/booking-window-display";

View File

@@ -0,0 +1,108 @@
/**
* Single source of truth for how a booking window row is presented to a
* customer or staff list: which status badge to show and which deadline (if
* any) to count down to.
*
* The badge and the countdown MUST be derived together. They used to be
* computed independently (badge from `isOpenNow`, countdown from
* `windowPhase`), which let them contradict each other — an export train that
* filled mid-window kept `windowPhase='OPEN'` (space can free again if a pay
* window lapses) while `bookingWindowStatus='FULL'`, so the card showed an
* "Upcoming" badge above a live "Window closes in …" countdown.
*
* Phase/status matrix this resolves (server fields on the schedule row):
* - windowPhase: PRE_WINDOW → OPEN → DOC_REVIEW → PAYMENT → DONE
* (export skips the review/payment phases; CLOSED_FOR_DAY is legacy)
* - bookingWindowStatus: OPEN | CLOSED | FULL — whether the booking desk
* actually accepts bookings right now.
*/
export type BookingWindowUiKind =
/** Bookable right now (phase OPEN and the desk flag agrees). */
| "OPEN"
/** Train has no capacity left — not bookable; may reopen if a reservation expires. */
| "FULL"
/** Announced, opens at `countdownTo`. */
| "PRE_WINDOW"
/** Window closed, staff reviewing documents (import cycle). */
| "DOC_REVIEW"
/** Batch ran, selected customers are paying (import cycle). */
| "PAYMENT"
/** Terminal or not bookable for any other reason. */
| "CLOSED";
export interface BookingWindowStateInput {
windowPhase?: string | null;
bookingWindowStatus?: string | null;
windowOpensAt?: string | null;
windowClosesAt?: string | null;
docReviewEndsAt?: string | null;
paymentPhaseEndsAt?: string | null;
}
export interface BookingWindowUiState {
kind: BookingWindowUiKind;
/** ISO deadline a countdown may tick toward; null = show no countdown. */
countdownTo: string | null;
/** True only when the customer can book right now. */
isBookable: boolean;
}
export function bookingWindowUiState(
w: BookingWindowStateInput,
): BookingWindowUiState {
const phase = w.windowPhase ?? null;
const status = w.bookingWindowStatus ?? null;
if (phase === "DONE" || phase === "CLOSED_FOR_DAY") {
return { kind: "CLOSED", countdownTo: null, isBookable: false };
}
// Mid-cycle phases win over the FULL flag: the batch may have tentatively
// filled the train, but an unpaid reservation can still expire and free
// space, so "document review" / "payment" is the truthful state here.
if (phase === "DOC_REVIEW") {
return {
kind: "DOC_REVIEW",
countdownTo: w.docReviewEndsAt ?? null,
isBookable: false,
};
}
if (phase === "PAYMENT") {
return {
kind: "PAYMENT",
countdownTo: w.paymentPhaseEndsAt ?? null,
isBookable: false,
};
}
// Outside the resolving phases a FULL train is simply not bookable — no
// countdown either: ticking toward "closes in" would promise a window the
// customer cannot use.
if (status === "FULL") {
return { kind: "FULL", countdownTo: null, isBookable: false };
}
if (phase === "PRE_WINDOW") {
return {
kind: "PRE_WINDOW",
countdownTo: w.windowOpensAt ?? null,
isBookable: false,
};
}
if (phase === "OPEN") {
if (status === "OPEN") {
return {
kind: "OPEN",
countdownTo: w.windowClosesAt ?? null,
isBookable: true,
};
}
// Phase says OPEN but the desk flag disagrees (CLOSED): not bookable, and
// no countdown that pretends otherwise.
return { kind: "CLOSED", countdownTo: null, isBookable: false };
}
return { kind: "CLOSED", countdownTo: null, isBookable: false };
}