This commit is contained in:
Marshal
2026-07-20 03:05:28 +00:00
parent de816ea9d4
commit 4969f62896
10 changed files with 456 additions and 271 deletions

View File

@@ -104,6 +104,11 @@ export function ApiErrorModal() {
onClose={close}
centered
radius="md"
// Mounted at the app root, so its portal is FIRST in <body> — at the
// default z-index (200) any page modal opened later (create schedule,
// allocation wizard, …) paints over it and the error hides underneath.
// Hoist above every Mantine overlay and the react-hot-toast layer (9999).
zIndex={10000}
title={
<Group gap="xs">
<AlertTriangle size={18} color="var(--mantine-color-red-6)" />

View File

@@ -66,8 +66,8 @@ import { useToast } from "@/hooks/use-toast";
import type {
BatchBoardBookingDetail,
BatchBoardBookingState,
BatchBoardCounts,
BatchBoardScheduleDetail,
BatchWindowGroup,
BookingAllocationStatus,
} from "@/types/trainScheduling";
@@ -398,7 +398,7 @@ const BookingTable = memo(function BookingTable({
);
});
function WindowCountChips({ counts }: { counts: BatchWindowGroup["counts"] }) {
function WindowCountChips({ counts }: { counts: BatchBoardCounts }) {
const chips: Array<{ value: number; color: string; label: string }> = [
{ value: counts.allocated, color: "edr-green", label: "allocated" },
{ value: counts.selectedForBatch, color: "orange", label: "selected" },
@@ -609,9 +609,7 @@ export default function BatchScheduleDetailPage() {
const hasAssignedWagons = useMemo(
() =>
Boolean(
data?.windows.some((w) =>
w.bookings.some((b) => b.allocationStatus === "ASSIGNED"),
) ||
data?.bookings.some((b) => b.allocationStatus === "ASSIGNED") ||
data?.pendingContract.bookings.some(
(b) => b.allocationStatus === "ASSIGNED",
),
@@ -619,36 +617,33 @@ export default function BatchScheduleDetailPage() {
[data],
);
const [activeTab, setActiveTab] = useState<string | null>("overview");
const scheduleDetailQuery = useQuery(
api.trainScheduling.scheduleDetail.queryOptions({
input: { id: scheduleId ?? "", freightType: "CONTAINER" },
enabled: Boolean(scheduleId),
// The heavy composition graph is only rendered by the composition tab and
// the overview diagram (which needs assigned wagons) — don't fetch it
// until one of them can actually show something.
enabled:
Boolean(scheduleId) &&
(hasAssignedWagons || activeTab === "composition"),
// Composition data only changes through mutations, which invalidate the
// whole train-scheduling root — no need to refetch on remounts in between.
staleTime: 5 * 60_000,
}),
);
// Every booking on this schedule, flattened across windows + pending-contract,
// de-duplicated (a booking only appears once). Feeds the management table.
// Every booking on this schedule: in-window + pending-contract (the two
// buckets are disjoint). Feeds the management table.
const allBookings = useMemo(() => {
if (!data) return [] as BatchBoardBookingDetail[];
const merged = [
...data.windows.flatMap((w) => w.bookings),
...data.pendingContract.bookings,
];
const byId = new Map<string, BatchBoardBookingDetail>();
for (const b of merged) if (!byId.has(b.id)) byId.set(b.id, b);
return [...byId.values()];
return [...data.bookings, ...data.pendingContract.bookings];
}, [data]);
// All bookings that fall inside the schedule's booking window (every window
// cycle, flattened) — the window is one booking day, so these belong to the
// single window panel above.
const windowBookings = useMemo(
() => (data?.windows ?? []).flatMap((w) => w.bookings),
[data?.windows],
);
// Bookings inside the schedule's booking window — they belong to the single
// window panel above.
const windowBookings = data?.bookings ?? [];
const windowCounts = useMemo(() => {
const counts = {
@@ -684,7 +679,6 @@ export default function BatchScheduleDetailPage() {
[data?.status],
);
const [activeTab, setActiveTab] = useState<string | null>("overview");
const [adjustConsistOpen, setAdjustConsistOpen] = useState(false);
const [selectedBookingId, setSelectedBookingId] = useState<string | null>(
null,

View File

@@ -400,23 +400,18 @@ export interface BatchBoardBookingDetail extends BatchBoardBooking {
consolidationPartnerRef: string | null;
}
export interface BatchWindowGroup {
key: string;
label: string;
/** EAT calendar day as ISO `YYYY-MM-DD` (empty for the pending-contract bucket). */
date: string;
/** Human label for the day, e.g. `Thu, 05 Jun` (empty for pending-contract). */
dateLabel: string;
start: string;
end: string;
counts: {
allocated: number;
selectedForBatch: number;
ready: number;
waiting: number;
expired: number;
pendingContract: number;
};
export interface BatchBoardCounts {
allocated: number;
selectedForBatch: number;
ready: number;
waiting: number;
expired: number;
pendingContract: number;
}
/** A booking bucket on the detail board (in-window vs pending-contract). */
export interface BatchBoardBucket {
counts: BatchBoardCounts;
bookings: BatchBoardBookingDetail[];
}
@@ -443,8 +438,9 @@ export interface BatchBoardScheduleDetail {
locomotive: BatchBoardSchedule["locomotive"];
capacity: BatchBoardSchedule["capacity"];
counts: BatchBoardSchedule["counts"];
windows: BatchWindowGroup[];
pendingContract: BatchWindowGroup;
/** Bookings inside the schedule's booking window (fully-executed contracts). */
bookings: BatchBoardBookingDetail[];
pendingContract: BatchBoardBucket;
allocationViolations: string[];
}