mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 18:48:11 +00:00
add detail batch and allocation monitoring page
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Group,
|
||||
Paper,
|
||||
RingProgress,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stack,
|
||||
@@ -17,17 +19,21 @@ import {
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowRight,
|
||||
CalendarClock,
|
||||
CalendarDays,
|
||||
Inbox,
|
||||
Package,
|
||||
RefreshCw,
|
||||
Ruler,
|
||||
Train,
|
||||
TrainFront,
|
||||
Weight,
|
||||
} from "lucide-react";
|
||||
import type { ColumnDef } from "@edr/ui-common";
|
||||
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
|
||||
|
||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||
import FleetToolbar from "@/components/fleet/FleetToolbar";
|
||||
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode";
|
||||
import {
|
||||
BookingPipeline,
|
||||
HeroChip,
|
||||
@@ -35,6 +41,7 @@ import {
|
||||
WindowStatusPill,
|
||||
} from "@/components/trainScheduling/batchVisuals";
|
||||
import { RouteCorridor, StatTile } from "@/components/trainScheduling/scheduleVisuals";
|
||||
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
||||
import { FREIGHT_BRAND, FREIGHT_BRAND_DARK } from "@/theme/freight-brand";
|
||||
import { useBatchBoard } from "@/hooks/trainScheduling/useTrainScheduling";
|
||||
import type { BatchBoardSchedule } from "@/types/trainScheduling";
|
||||
@@ -58,6 +65,27 @@ const fmtScheduleDate = (iso: string | null) =>
|
||||
}).format(new Date(iso)) + " EAT"
|
||||
: "No date";
|
||||
|
||||
const splitDate = (iso: string | null) => {
|
||||
if (!iso) return { day: "—", time: "" };
|
||||
const date = new Date(iso);
|
||||
if (Number.isNaN(date.getTime())) return { day: "—", time: "" };
|
||||
return {
|
||||
day: new Intl.DateTimeFormat("en-GB", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
timeZone: "Africa/Addis_Ababa",
|
||||
}).format(date),
|
||||
time:
|
||||
new Intl.DateTimeFormat("en-GB", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
timeZone: "Africa/Addis_Ababa",
|
||||
}).format(date) + " EAT",
|
||||
};
|
||||
};
|
||||
|
||||
/** Capacity ring color: gold normally, red once over capacity. */
|
||||
function ringColor(pct: number) {
|
||||
if (pct >= 100) return "#fa5252";
|
||||
@@ -109,18 +137,56 @@ function CapacityRing({
|
||||
);
|
||||
}
|
||||
|
||||
/** Small percent chip used in the table's capacity column. */
|
||||
function CapacityChip({
|
||||
icon: Icon,
|
||||
pct,
|
||||
text,
|
||||
}: {
|
||||
icon: typeof Weight;
|
||||
pct: number | null;
|
||||
text: string;
|
||||
}) {
|
||||
const over = pct != null && pct >= 100;
|
||||
return (
|
||||
<Group
|
||||
gap={4}
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
padding: "2px 8px",
|
||||
borderRadius: 8,
|
||||
background: over ? "var(--mantine-color-red-0)" : "var(--mantine-color-gray-1)",
|
||||
border: `1px solid ${over ? "var(--mantine-color-red-2)" : "var(--mantine-color-gray-2)"}`,
|
||||
}}
|
||||
>
|
||||
<Icon size={12} color={over ? "var(--mantine-color-red-6)" : "var(--mantine-color-gray-6)"} />
|
||||
<Text size="xs" fw={700} c={over ? "red.7" : "gray.7"} lh={1.2}>
|
||||
{pct != null ? `${Math.round(pct)}%` : "—"}
|
||||
</Text>
|
||||
<Text size="10px" c="dimmed" lh={1.2}>
|
||||
{text}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
function weightPctOf(s: BatchBoardSchedule) {
|
||||
return s.capacity.maxWeightTons && s.capacity.maxWeightTons > 0
|
||||
? (s.capacity.usedWeightTons / s.capacity.maxWeightTons) * 100
|
||||
: null;
|
||||
}
|
||||
function lengthPctOf(s: BatchBoardSchedule) {
|
||||
return s.capacity.maxLengthMeters && s.capacity.maxLengthMeters > 0
|
||||
? (s.capacity.allocatedLengthMeters / s.capacity.maxLengthMeters) * 100
|
||||
: null;
|
||||
}
|
||||
|
||||
function ScheduleCard({ schedule }: { schedule: BatchBoardSchedule }) {
|
||||
const navigate = useNavigate();
|
||||
const { capacity, counts, locomotive } = schedule;
|
||||
|
||||
const lengthPct =
|
||||
capacity.maxLengthMeters && capacity.maxLengthMeters > 0
|
||||
? (capacity.allocatedLengthMeters / capacity.maxLengthMeters) * 100
|
||||
: null;
|
||||
const weightPct =
|
||||
capacity.maxWeightTons && capacity.maxWeightTons > 0
|
||||
? (capacity.usedWeightTons / capacity.maxWeightTons) * 100
|
||||
: null;
|
||||
const lengthPct = lengthPctOf(schedule);
|
||||
const weightPct = weightPctOf(schedule);
|
||||
|
||||
const totalBookings = totalBookingCount(counts);
|
||||
|
||||
@@ -298,7 +364,13 @@ function CardSkeleton() {
|
||||
}
|
||||
|
||||
export default function BatchBoardPage() {
|
||||
const navigate = useNavigate();
|
||||
const { data, isLoading, isFetching, refetch } = useBatchBoard();
|
||||
const { viewMode, setViewMode } = useFleetViewMode("batch-board");
|
||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||
const [search, setSearch] = useState("");
|
||||
const [windowFilter, setWindowFilter] = useState("ALL");
|
||||
|
||||
const schedules = data ?? [];
|
||||
|
||||
const summary = useMemo(() => {
|
||||
@@ -308,22 +380,199 @@ export default function BatchBoardPage() {
|
||||
return { openWindows, totalBookings, totalWagons };
|
||||
}, [schedules]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const query = search.trim().toLowerCase();
|
||||
return schedules.filter((s) => {
|
||||
if (windowFilter !== "ALL" && s.bookingWindowStatus !== windowFilter) return false;
|
||||
if (!query) return true;
|
||||
const haystack = [
|
||||
s.trainNumber,
|
||||
s.routeName,
|
||||
s.origin,
|
||||
s.destination,
|
||||
s.locomotive?.code,
|
||||
s.status,
|
||||
s.bookingWindowStatus,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return haystack.includes(query);
|
||||
});
|
||||
}, [schedules, search, windowFilter]);
|
||||
|
||||
const pageCount = Math.max(1, Math.ceil(filtered.length / pagination.pageSize));
|
||||
const paged = useMemo(() => {
|
||||
const start = pagination.pageIndex * pagination.pageSize;
|
||||
return filtered.slice(start, start + pagination.pageSize);
|
||||
}, [filtered, pagination]);
|
||||
|
||||
const columns = useMemo((): ColumnDef<BatchBoardSchedule>[] => {
|
||||
const headerClassName = ruleEngineTable.headerCell;
|
||||
const cellClassName = ruleEngineTable.bodyCell;
|
||||
return [
|
||||
{
|
||||
id: "train",
|
||||
header: "Train / Route",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => (
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<ThemeIcon size={34} radius="md" variant="light" color="#F2A516">
|
||||
<Train size={17} />
|
||||
</ThemeIcon>
|
||||
<Stack gap={2} style={{ minWidth: 0 }}>
|
||||
<Text size="sm" fw={700} lh={1.2} truncate>
|
||||
{row.original.trainNumber ?? row.original.routeName ?? "Schedule"}
|
||||
</Text>
|
||||
<Box maw={220}>
|
||||
<RouteCorridor
|
||||
origin={row.original.origin}
|
||||
destination={row.original.destination}
|
||||
variant="compact"
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "date",
|
||||
header: "Departure",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => {
|
||||
const { day, time } = splitDate(row.original.scheduleDate);
|
||||
return (
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: 9,
|
||||
background: "var(--mantine-color-orange-0)",
|
||||
color: "#B26C09",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<CalendarClock size={16} />
|
||||
</Box>
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" fw={600} lh={1.2}>
|
||||
{day}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" lh={1.2}>
|
||||
{time || "—"}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "window",
|
||||
header: "Window",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => <WindowStatusPill status={row.original.bookingWindowStatus} />,
|
||||
},
|
||||
{
|
||||
id: "loco",
|
||||
header: "Locomotive",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) =>
|
||||
row.original.locomotive ? (
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<TrainFront size={14} color="var(--mantine-color-gray-5)" />
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" fw={600} lh={1.2}>
|
||||
{row.original.locomotive.code}
|
||||
</Text>
|
||||
<Text size="10px" c="dimmed" lh={1.2}>
|
||||
{fmtTons(row.original.locomotive.maxPullWeightTons)} pull
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
) : (
|
||||
<Text size="xs" c="red.6" fw={600}>
|
||||
No loco
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "capacity",
|
||||
header: "Capacity",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => (
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<CapacityChip icon={Weight} pct={weightPctOf(row.original)} text="wt" />
|
||||
<CapacityChip icon={Ruler} pct={lengthPctOf(row.original)} text="len" />
|
||||
<Group
|
||||
gap={4}
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
padding: "2px 8px",
|
||||
borderRadius: 8,
|
||||
background: "var(--mantine-color-green-0)",
|
||||
border: "1px solid var(--mantine-color-green-1)",
|
||||
}}
|
||||
>
|
||||
<Package size={12} color="var(--mantine-color-green-7)" />
|
||||
<Text size="xs" fw={700} c="green.8" lh={1.2}>
|
||||
{row.original.capacity.allocatedWagons}
|
||||
</Text>
|
||||
<Text size="10px" c="dimmed" lh={1.2}>
|
||||
wgn
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "bookings",
|
||||
header: "Bookings",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => {
|
||||
const total = totalBookingCount(row.original.counts);
|
||||
return (
|
||||
<Stack gap={4} style={{ minWidth: 130 }}>
|
||||
<Text size="sm" fw={700} c="dark.4" lh={1.2}>
|
||||
{total} booking{total === 1 ? "" : "s"}
|
||||
</Text>
|
||||
<BookingPipeline counts={row.original.counts} size={10} />
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "",
|
||||
meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` },
|
||||
cell: ({ row }) => (
|
||||
<Group justify="flex-end" wrap="nowrap">
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
size="compact-sm"
|
||||
rightSection={<ArrowRight size={14} />}
|
||||
onClick={() =>
|
||||
navigate(`/dashboard/operations/batch-board/${row.original.scheduleId}`)
|
||||
}
|
||||
>
|
||||
View windows
|
||||
</Button>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [navigate]);
|
||||
|
||||
const tableStatus = isLoading ? "loading" : "success";
|
||||
|
||||
return (
|
||||
<Container fluid py="lg" px="xl">
|
||||
<Breadcrumbs items={[{ label: "Operations" }, { label: "Batch board" }]} />
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button
|
||||
variant="default"
|
||||
radius="lg"
|
||||
leftSection={<RefreshCw size={16} />}
|
||||
loading={isFetching}
|
||||
onClick={() => void refetch()}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, xs: 3 }} spacing="md" mt="md">
|
||||
<StatTile
|
||||
icon={Train}
|
||||
@@ -359,46 +608,121 @@ export default function BatchBoardPage() {
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
{isLoading ? (
|
||||
<SimpleGrid cols={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="lg" mt="lg">
|
||||
<CardSkeleton />
|
||||
<CardSkeleton />
|
||||
<CardSkeleton />
|
||||
</SimpleGrid>
|
||||
) : schedules.length === 0 ? (
|
||||
<Paper radius="lg" withBorder p={48} mt="lg" bg="gray.0">
|
||||
<Stack align="center" gap="sm">
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 20,
|
||||
background: "white",
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
boxShadow: "0 4px 12px rgba(15,23,42,0.06)",
|
||||
<Card
|
||||
radius="lg"
|
||||
padding={0}
|
||||
withBorder
|
||||
mt="lg"
|
||||
style={{ borderColor: "var(--mantine-color-gray-2)" }}
|
||||
>
|
||||
<Stack gap={0}>
|
||||
<Box px="md" pt="md" pb="sm" w="100%">
|
||||
<FleetToolbar
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
searchPlaceholder="Search schedules…"
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
filters={
|
||||
<Select
|
||||
size="sm"
|
||||
radius="lg"
|
||||
value={windowFilter}
|
||||
onChange={(v) => v && setWindowFilter(v)}
|
||||
data={[
|
||||
{ value: "ALL", label: "All windows" },
|
||||
{ value: "OPEN", label: "Open" },
|
||||
{ value: "FULL", label: "Full" },
|
||||
{ value: "CLOSED", label: "Closed" },
|
||||
]}
|
||||
w={150}
|
||||
styles={{ input: { borderColor: "var(--mantine-color-gray-3)" } }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{viewMode === "table" ? (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={paged}
|
||||
status={tableStatus}
|
||||
emptyMessage="No active schedules"
|
||||
pagination={{
|
||||
pageIndex: pagination.pageIndex,
|
||||
pageSize: pagination.pageSize,
|
||||
pageCount,
|
||||
totalCount: filtered.length,
|
||||
}}
|
||||
>
|
||||
<Inbox size={28} color="var(--mantine-color-gray-5)" />
|
||||
</Box>
|
||||
<Text fw={700} c="gray.7">
|
||||
No active schedules
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" ta="center" maw={380}>
|
||||
Schedules with an open booking window appear here as cards. Create or activate
|
||||
a schedule to get started.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="lg" mt="lg">
|
||||
{schedules.map((s) => (
|
||||
<ScheduleCard key={s.scheduleId} schedule={s} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
tableOptions={{
|
||||
manualPagination: true,
|
||||
pageCount,
|
||||
state: { pagination },
|
||||
onPaginationChange: setPagination,
|
||||
}}
|
||||
containerClassName="border-0 shadow-none bg-transparent"
|
||||
footer={({ table, pagination: footerPagination }) => (
|
||||
<DataTableFooter
|
||||
table={table}
|
||||
pagination={footerPagination}
|
||||
options={{ labels: { items: "schedules" } }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : isLoading ? (
|
||||
<SimpleGrid cols={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="lg" p="md">
|
||||
<CardSkeleton />
|
||||
<CardSkeleton />
|
||||
<CardSkeleton />
|
||||
</SimpleGrid>
|
||||
) : filtered.length === 0 ? (
|
||||
<Paper radius="lg" p={48} m="md" bg="gray.0">
|
||||
<Stack align="center" gap="sm">
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 20,
|
||||
background: "white",
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
boxShadow: "0 4px 12px rgba(15,23,42,0.06)",
|
||||
}}
|
||||
>
|
||||
<Inbox size={28} color="var(--mantine-color-gray-5)" />
|
||||
</Box>
|
||||
<Text fw={700} c="gray.7">
|
||||
No active schedules
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" ta="center" maw={380}>
|
||||
Schedules with an open booking window appear here. Create or activate a
|
||||
schedule to get started.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="lg" p="md">
|
||||
{filtered.map((s) => (
|
||||
<ScheduleCard key={s.scheduleId} schedule={s} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="xs"
|
||||
loading={isFetching}
|
||||
onClick={() => void refetch()}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Group>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Table,
|
||||
Tabs,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
@@ -44,6 +45,7 @@ import type { LucideIcon } from "lucide-react";
|
||||
|
||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
|
||||
import { TrainConsistView, CompositionBookingTabs } from "@/components/trainScheduling/compositionEditor";
|
||||
import {
|
||||
BookingPipeline,
|
||||
HeroChip,
|
||||
@@ -491,10 +493,20 @@ export default function BatchScheduleDetailPage() {
|
||||
[data],
|
||||
);
|
||||
|
||||
const scheduleDetailQuery = useScheduleDetail(
|
||||
hasAssignedWagons ? scheduleId : undefined,
|
||||
"CONTAINER",
|
||||
);
|
||||
const scheduleDetailQuery = useScheduleDetail(scheduleId, "CONTAINER");
|
||||
|
||||
// Batch bookings by state for the composition side panel (payment / expired lists).
|
||||
const batchBookings = useMemo(() => {
|
||||
if (!data) return { awaitingPayment: [], expired: [] };
|
||||
const all = [
|
||||
...data.windows.flatMap((w) => w.bookings),
|
||||
...data.pendingContract.bookings,
|
||||
];
|
||||
return {
|
||||
awaitingPayment: all.filter((b) => b.state === "SELECTED_FOR_BATCH"),
|
||||
expired: all.filter((b) => b.state === "EXPIRED"),
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
// Group the flat window list into per-day sections (one per EAT calendar date).
|
||||
const dayGroups = useMemo(() => {
|
||||
@@ -568,6 +580,8 @@ export default function BatchScheduleDetailPage() {
|
||||
// Date-stepper: which day is currently shown. Default to today, else the first
|
||||
// day with bookings, else the first day. Keep the selection if still valid.
|
||||
const [selectedDate, setSelectedDate] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<string | null>("overview");
|
||||
const [selectedBookingId, setSelectedBookingId] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (!dayGroups.length) return;
|
||||
if (selectedDate && dayGroups.some((d) => d.date === selectedDate)) return;
|
||||
@@ -636,8 +650,17 @@ export default function BatchScheduleDetailPage() {
|
||||
]}
|
||||
/>
|
||||
|
||||
<Stack gap="lg" mt="md">
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
||||
<Tabs value={activeTab} onChange={setActiveTab} mt="md">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="overview">Overview</Tabs.Tab>
|
||||
<Tabs.Tab value="composition">
|
||||
Train Composition {scheduleDetailQuery.data?.trainSet?.wagons && scheduleDetailQuery.data.trainSet.wagons.length > 0 && `(${scheduleDetailQuery.data.trainSet.wagons.length})`}
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="overview" pt="lg">
|
||||
<Stack gap="lg">
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
||||
<Stack gap={9} style={{ minWidth: 0 }}>
|
||||
<Group gap="sm" wrap="wrap" align="center">
|
||||
<Button
|
||||
@@ -755,7 +778,6 @@ export default function BatchScheduleDetailPage() {
|
||||
variant="line"
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
{/* Booking pipeline */}
|
||||
<Paper
|
||||
@@ -987,7 +1009,7 @@ export default function BatchScheduleDetailPage() {
|
||||
) : null}
|
||||
</Paper>
|
||||
|
||||
{/* Train composition */}
|
||||
{/* Train composition diagram */}
|
||||
{hasAssignedWagons && scheduleDetailQuery.data ? (
|
||||
<Box mt="lg">
|
||||
<TrainCompositionDiagram
|
||||
@@ -999,6 +1021,48 @@ export default function BatchScheduleDetailPage() {
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="composition" pt="lg">
|
||||
{scheduleDetailQuery.data && scheduleDetailQuery.data.trainSet ? (
|
||||
<Group align="stretch" gap="md" wrap="nowrap">
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<TrainConsistView
|
||||
scheduleDetail={scheduleDetailQuery.data}
|
||||
scheduleId={scheduleId ?? ""}
|
||||
maxWagons={53}
|
||||
highlightBookingId={selectedBookingId}
|
||||
/>
|
||||
</Box>
|
||||
<Box style={{ width: 380, flexShrink: 0, minHeight: 520 }}>
|
||||
<CompositionBookingTabs
|
||||
scheduleDetail={scheduleDetailQuery.data}
|
||||
scheduleId={scheduleId ?? ""}
|
||||
awaitingPayment={batchBookings.awaitingPayment}
|
||||
expired={batchBookings.expired}
|
||||
selectedBookingId={selectedBookingId}
|
||||
onSelectBooking={setSelectedBookingId}
|
||||
/>
|
||||
</Box>
|
||||
</Group>
|
||||
) : (
|
||||
<Paper
|
||||
radius="lg"
|
||||
withBorder
|
||||
py={64}
|
||||
style={{ borderColor: "var(--mantine-color-gray-2)" }}
|
||||
>
|
||||
<Group justify="center">
|
||||
<Loader color="green" size="sm" />
|
||||
<Text size="sm" c="dimmed">
|
||||
Loading train composition…
|
||||
</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user