mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +00:00
Add booking window management and locomotive scheduling features
- Implemented migration to release stuck assigned locomotives. - Added schedule window phases to train schedules. - Created booking batch offers table for partial capacity bookings. - Developed BookingSplitService to handle partial booking offers and splits. - Introduced BookingWindowService to manage booking window lifecycle and transitions. - Added BookingBatchOffer entity to represent offers made during booking splits. - Enhanced locomotive options with warnings for scheduling. - Created UpcomingWindowsSection component to display upcoming booking windows.
This commit is contained in:
@@ -48,6 +48,7 @@ import type {
|
||||
} from "@/types/trainScheduling";
|
||||
|
||||
import { ContainerPlacementGrid } from "./ContainerPlacementGrid";
|
||||
import { locomotiveOption, showScheduleWarnings } from "./locomotiveOptions";
|
||||
import {
|
||||
autoFillPlacements,
|
||||
mergePlacementsWithSaved,
|
||||
@@ -274,6 +275,7 @@ export function AllocateBookingWizard({
|
||||
const created = await create.mutateAsync({
|
||||
payload: { routeId, scheduleDate, locomotiveIds },
|
||||
});
|
||||
showScheduleWarnings(created.warnings);
|
||||
setSelectedScheduleId(created.id);
|
||||
return created.id;
|
||||
};
|
||||
@@ -536,10 +538,9 @@ export function AllocateBookingWizard({
|
||||
placeholder={
|
||||
routeId ? "Select at least two locomotives" : "Select a route first"
|
||||
}
|
||||
data={(locomotivesQuery.data ?? []).map((l) => ({
|
||||
value: l.id,
|
||||
label: `${l.code}${l.name ? ` · ${l.name}` : ""}`,
|
||||
}))}
|
||||
data={(locomotivesQuery.data ?? []).map((l) =>
|
||||
locomotiveOption(l, " · "),
|
||||
)}
|
||||
value={locomotiveIds}
|
||||
onChange={setLocomotiveIds}
|
||||
searchable
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Box, Group, Progress, Text, Tooltip } from "@mantine/core";
|
||||
|
||||
import type { BookingWindowPhase } from "@/types/trainScheduling";
|
||||
|
||||
import "./batchVisuals.css";
|
||||
|
||||
/**
|
||||
@@ -213,3 +215,69 @@ export function HeroChip({
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
const PHASE_META: Record<
|
||||
BookingWindowPhase,
|
||||
{ color: string; label: string; pulse: boolean }
|
||||
> = {
|
||||
PRE_WINDOW: { color: "gray", label: "Pre-window", pulse: false },
|
||||
OPEN: { color: "edr-green", label: "Booking open", pulse: true },
|
||||
DOC_REVIEW: { color: "yellow", label: "Doc review", pulse: true },
|
||||
PAYMENT: { color: "blue", label: "Payment", pulse: true },
|
||||
CLOSED_FOR_DAY: { color: "dark", label: "Closed for day", pulse: false },
|
||||
DONE: { color: "dark", label: "Done", pulse: false },
|
||||
};
|
||||
|
||||
/**
|
||||
* Import booking-cycle phase pill (OPEN → DOC_REVIEW → PAYMENT → …) with an
|
||||
* optional cycle number. Same visual language as `WindowStatusPill`.
|
||||
*/
|
||||
export function WindowPhasePill({
|
||||
phase,
|
||||
cycleNo,
|
||||
size = "md",
|
||||
}: {
|
||||
phase: BookingWindowPhase;
|
||||
cycleNo?: number;
|
||||
size?: "sm" | "md";
|
||||
}) {
|
||||
const meta = PHASE_META[phase] ?? {
|
||||
color: "gray",
|
||||
label: phase,
|
||||
pulse: false,
|
||||
};
|
||||
const compact = size === "sm";
|
||||
return (
|
||||
<Group
|
||||
gap={6}
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
padding: compact ? "2px 8px" : "4px 11px",
|
||||
borderRadius: 999,
|
||||
background: `var(--mantine-color-${meta.color}-0)`,
|
||||
border: `1px solid var(--mantine-color-${meta.color}-2)`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
w={compact ? 6 : 7}
|
||||
h={compact ? 6 : 7}
|
||||
className={meta.pulse ? "bb-pulse-dot" : undefined}
|
||||
style={{
|
||||
borderRadius: 999,
|
||||
flexShrink: 0,
|
||||
background: `var(--mantine-color-${meta.color}-6)`,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
size="xs"
|
||||
fw={700}
|
||||
c={`${meta.color}.8`}
|
||||
style={{ letterSpacing: 0.3, lineHeight: 1, whiteSpace: "nowrap" }}
|
||||
>
|
||||
{meta.label}
|
||||
{cycleNo && cycleNo > 1 ? ` · cycle ${cycleNo}` : ""}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import hotToast from "react-hot-toast";
|
||||
|
||||
import type { LocomotiveRecord } from "@/types/trainScheduling";
|
||||
|
||||
/**
|
||||
* Locomotives can now be scheduled in advance: not-at-origin-yard or
|
||||
* already-on-future-schedules is allowed with a warning (only OUT_OF_SERVICE
|
||||
* is blocked server-side). This returns the hint to surface in the picker,
|
||||
* or null when the locomotive is ready at the origin yard.
|
||||
*/
|
||||
export function locomotiveWarning(loco: LocomotiveRecord): string | null {
|
||||
const hints: string[] = [];
|
||||
if (loco.atOriginYard === false) hints.push("not at origin yard");
|
||||
const futureCount = loco.futureScheduleCount ?? 0;
|
||||
if (futureCount > 0) {
|
||||
hints.push(`on ${futureCount} future schedule${futureCount === 1 ? "" : "s"}`);
|
||||
}
|
||||
return hints.length ? hints.join(" · ") : null;
|
||||
}
|
||||
|
||||
/** MultiSelect option for the schedule-creation locomotive picker. */
|
||||
export function locomotiveOption(
|
||||
loco: LocomotiveRecord,
|
||||
nameSeparator = " — ",
|
||||
): { value: string; label: string } {
|
||||
const base = `${loco.code}${loco.name ? `${nameSeparator}${loco.name}` : ""}`;
|
||||
const warning = locomotiveWarning(loco);
|
||||
return {
|
||||
value: loco.id,
|
||||
label: warning ? `${base} · ⚠ ${warning}` : base,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Yellow toast listing create-schedule warnings (e.g. locomotive not at the
|
||||
* origin yard yet). The shared `useToast` hook only knows success/error, so
|
||||
* this styles a react-hot-toast directly.
|
||||
*/
|
||||
export function showScheduleWarnings(warnings?: string[] | null): void {
|
||||
if (!warnings?.length) return;
|
||||
hotToast(warnings.join("\n"), {
|
||||
icon: "⚠️",
|
||||
duration: 8000,
|
||||
style: {
|
||||
background: "var(--mantine-color-yellow-0)",
|
||||
color: "var(--mantine-color-yellow-9)",
|
||||
border: "1px solid var(--mantine-color-yellow-4)",
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user