Merge branch 'freight_feature/profile' of github.com:Tria-plc/edr-platform into freight_feature/profile

This commit is contained in:
Marshal
2026-06-24 23:56:07 +00:00
88 changed files with 4215 additions and 3624 deletions

View File

@@ -44,7 +44,10 @@ import { KpiStrip, PageContainer } from "@/components/page";
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
import { TrainConsistView, CompositionBookingTabs } from "@/components/trainScheduling/compositionEditor";
import {
TrainConsistView,
CompositionBookingTabs,
} from "@/components/trainScheduling/compositionEditor";
import {
BookingPipeline,
HeroChip,
@@ -67,10 +70,18 @@ const STATE_META: Record<
{ label: string; color: string; icon: typeof CheckCircle2 }
> = {
ALLOCATED: { label: "Allocated", color: "edr-green", icon: CheckCircle2 },
SELECTED_FOR_BATCH: { label: "Selected for batch", color: "orange", icon: Clock },
SELECTED_FOR_BATCH: {
label: "Selected for batch",
color: "orange",
icon: Clock,
},
READY: { label: "Ready for batch", color: "teal", icon: Hourglass },
WAITING: { label: "Paid · waiting", color: "blue", icon: Hourglass },
PENDING_CONTRACT: { label: "Pending contract", color: "gray", icon: Hourglass },
PENDING_CONTRACT: {
label: "Pending contract",
color: "gray",
icon: Hourglass,
},
EXPIRED: { label: "Expired", color: "red", icon: XCircle },
};
@@ -93,13 +104,13 @@ const fmtMeters = (n: number) =>
const fmtDateTime = (iso: string | null) =>
iso
? new Intl.DateTimeFormat("en-GB", {
day: "2-digit",
month: "short",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "Africa/Addis_Ababa",
}).format(new Date(iso))
day: "2-digit",
month: "short",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "Africa/Addis_Ababa",
}).format(new Date(iso))
: "—";
const initials = (name: string) =>
@@ -115,7 +126,12 @@ function StateBadge({ state }: { state: BatchBoardBookingState }) {
const meta = STATE_META[state];
const Icon = meta.icon;
return (
<Badge variant="light" color={meta.color} radius="sm" leftSection={<Icon size={11} />}>
<Badge
variant="light"
color={meta.color}
radius="sm"
leftSection={<Icon size={11} />}
>
{meta.label}
</Badge>
);
@@ -232,7 +248,12 @@ const BOOKING_COLUMNS: ColumnDef<BatchBoardBookingDetail>[] = [
{fmtDateTime(b.selectedForBatchAt)} EAT
</Text>
{b.paymentDeadline ? (
<Text size="xs" c="orange.7" fw={600} style={{ whiteSpace: "nowrap" }}>
<Text
size="xs"
c="orange.7"
fw={600}
style={{ whiteSpace: "nowrap" }}
>
Pay by {fmtDateTime(b.paymentDeadline)} EAT
</Text>
) : null}
@@ -385,7 +406,9 @@ function WindowAccordionItem({ window }: { window: BatchWindowGroup }) {
borderRadius: 10,
flexShrink: 0,
background: total ? "#FEF1D5" : "var(--mantine-color-gray-0)",
border: total ? "1px solid #FBD171" : "1px solid var(--mantine-color-gray-2)",
border: total
? "1px solid #FBD171"
: "1px solid var(--mantine-color-gray-2)",
color: total ? "#B26C09" : "var(--mantine-color-gray-5)",
}}
>
@@ -396,7 +419,9 @@ function WindowAccordionItem({ window }: { window: BatchWindowGroup }) {
{timeLabelOf(window.label)}
</Text>
<Text size="xs" c="dimmed">
{total ? `${total} booking${total === 1 ? "" : "s"}` : "Empty window"}
{total
? `${total} booking${total === 1 ? "" : "s"}`
: "Empty window"}
</Text>
</Box>
</Group>
@@ -443,7 +468,9 @@ export default function BatchScheduleDetailPage() {
data?.windows.some((w) =>
w.bookings.some((b) => b.allocationStatus === "ASSIGNED"),
) ||
data?.pendingContract.bookings.some((b) => b.allocationStatus === "ASSIGNED"),
data?.pendingContract.bookings.some(
(b) => b.allocationStatus === "ASSIGNED",
),
),
[data],
);
@@ -514,7 +541,9 @@ export default function BatchScheduleDetailPage() {
group.hasIssues =
group.hasIssues ||
w.bookings.some(
(b) => b.allocationStatus === "FAILED" || b.allocationStatus === "DEFERRED",
(b) =>
b.allocationStatus === "FAILED" ||
b.allocationStatus === "DEFERRED",
);
}
return [...byDate.values()];
@@ -522,7 +551,10 @@ export default function BatchScheduleDetailPage() {
// Windows with bookings open by default (inside an expanded day).
const openWindowKeys = useMemo(
() => (data ? data.windows.filter((w) => w.bookings.length > 0).map((w) => w.key) : []),
() =>
data
? data.windows.filter((w) => w.bookings.length > 0).map((w) => w.key)
: [],
[data],
);
@@ -541,7 +573,9 @@ export default function BatchScheduleDetailPage() {
// 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);
const [selectedBookingId, setSelectedBookingId] = useState<string | null>(
null,
);
useEffect(() => {
if (!dayGroups.length) return;
if (selectedDate && dayGroups.some((d) => d.date === selectedDate)) return;
@@ -562,7 +596,9 @@ export default function BatchScheduleDetailPage() {
runAllocation
.mutateAsync({ scheduleId: scheduleId ?? "" })
.then((result) => {
const failed = result.issues.filter((i) => i.status === "FAILED").length;
const failed = result.issues.filter(
(i) => i.status === "FAILED",
).length;
const deferred = result.deferred.length;
toast({
title: "Allocation run complete",
@@ -589,15 +625,6 @@ export default function BatchScheduleDetailPage() {
);
}
const lengthPct =
data.capacity.maxLengthMeters && data.capacity.maxLengthMeters > 0
? (data.capacity.allocatedLengthMeters / data.capacity.maxLengthMeters) * 100
: null;
const weightPct =
data.capacity.maxWeightTons && data.capacity.maxWeightTons > 0
? (data.capacity.usedWeightTons / data.capacity.maxWeightTons) * 100
: null;
const totalBookings = totalBookingCount(data.counts);
return (
@@ -614,36 +641,49 @@ export default function BatchScheduleDetailPage() {
<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})`}
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
variant="subtle"
color="gray"
radius="md"
px="sm"
leftSection={<ArrowLeft size={16} />}
onClick={() => navigate("/dashboard/operations/batch-board")}
>
Back
</Button>
<Title order={2} fw={800}>
{data.trainNumber ?? data.routeName ?? "Schedule"}
</Title>
<WindowStatusPill status={data.bookingWindowStatus} />
<HeroChip>{data.status}</HeroChip>
</Group>
<RouteCorridor origin={data.origin} destination={data.destination} />
<Group gap={6} wrap="wrap">
<HeroChip icon={<CalendarDays size={12} />}>
{data.scheduleDate
? new Intl.DateTimeFormat("en-GB", {
<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
variant="subtle"
color="gray"
radius="md"
px="sm"
leftSection={<ArrowLeft size={16} />}
onClick={() =>
navigate("/dashboard/operations/batch-board")
}
>
Back
</Button>
<Title order={2} fw={800}>
{data.trainNumber ?? data.routeName ?? "Schedule"}
</Title>
<WindowStatusPill status={data.bookingWindowStatus} />
<HeroChip>{data.status}</HeroChip>
</Group>
<RouteCorridor
origin={data.origin}
destination={data.destination}
/>
<Group gap={6} wrap="wrap">
<HeroChip icon={<CalendarDays size={12} />}>
{data.scheduleDate
? new Intl.DateTimeFormat("en-GB", {
weekday: "short",
day: "2-digit",
month: "short",
@@ -653,317 +693,360 @@ export default function BatchScheduleDetailPage() {
hour12: false,
timeZone: "Africa/Addis_Ababa",
}).format(new Date(data.scheduleDate)) + " EAT"
: "No date"}
</HeroChip>
{data.locomotive ? (
<HeroChip icon={<TrainFront size={12} />}>
Loco {data.locomotive.code} · {fmtTons(data.locomotive.maxPullWeightTons)} ·{" "}
{data.locomotive.maxTrainLengthMeters} m
: "No date"}
</HeroChip>
) : null}
</Group>
</Stack>
<Group gap="sm">
<Button
variant="default"
radius="md"
leftSection={<RefreshCw size={16} />}
loading={isFetching}
onClick={() => void refetch()}
>
Refresh
</Button>
<Button
color="edr-green"
radius="md"
leftSection={<PlayCircle size={16} />}
loading={runAllocation.isPending}
onClick={handleRunAllocation}
>
Run allocation
</Button>
<Button
variant="light"
color="edr-green"
radius="md"
leftSection={<Layers size={16} />}
onClick={() =>
navigate(`/dashboard/operations/train-scheduling-v2/${data.scheduleId}`)
}
>
Open schedule
</Button>
</Group>
</Group>
{!data.locomotive ? (
<Alert color="red" radius="md" icon={<AlertTriangle size={16} />}>
No locomotive assigned wagon allocation cannot run.
</Alert>
) : null}
<KpiStrip
items={[
{
label: "Allocated wagons",
value: data.capacity.allocatedWagons,
hint: "on this train",
icon: Boxes,
},
{
label: "Train length",
value: data.capacity.maxLengthMeters
? `${fmtMeters(data.capacity.allocatedLengthMeters)} / ${fmtMeters(data.capacity.maxLengthMeters)}`
: fmtMeters(data.capacity.allocatedLengthMeters),
icon: Ruler,
},
{
label: "Weight",
value: data.capacity.maxWeightTons
? `${fmtTons(data.capacity.usedWeightTons)} / ${fmtTons(data.capacity.maxWeightTons)}`
: fmtTons(data.capacity.usedWeightTons),
icon: Weight,
},
{
label: "Bookings",
value: totalBookings,
hint: `${data.counts.allocated} allocated · ${data.counts.expired} expired`,
icon: Package,
},
]}
/>
{/* Booking pipeline */}
<Paper
radius="lg"
withBorder
p="lg"
mt="lg"
style={{ borderColor: "var(--mantine-color-gray-2)" }}
>
<Group justify="space-between" mb="sm">
<Group gap={8} wrap="nowrap">
<ThemeIcon size={32} radius="md" variant="light" color="#F2A516">
<Package size={16} />
</ThemeIcon>
<Text fw={700}>Booking pipeline</Text>
</Group>
<Text size="sm" fw={700} c="dark.4">
{totalBookings} booking{totalBookings === 1 ? "" : "s"}
</Text>
</Group>
<BookingPipeline counts={data.counts} size={14} />
</Paper>
{data.allocationViolations.length ? (
<Alert
color="red"
mt="lg"
radius="lg"
icon={<AlertTriangle size={16} />}
title="Allocation constraints"
>
<Stack gap={4}>
{data.allocationViolations.map((v) => (
<Text key={v} size="sm">
{v}
</Text>
))}
</Stack>
</Alert>
) : null}
{/* Batch windows */}
<Paper
radius="lg"
withBorder
p="lg"
mt="lg"
style={{ borderColor: "var(--mantine-color-gray-2)" }}
>
<Group gap="sm" mb={4} wrap="nowrap" align="flex-start">
<ThemeIcon size={38} radius="md" variant="light" color="#F2A516">
<Clock size={19} />
</ThemeIcon>
<Box>
<Title order={4}>Batch windows (EAT)</Title>
<Text size="sm" c="dimmed">
3-hour windows for every day from when the booking window opened through the
departure date. Bookings appear under the date their contract was signed open a
day to see its windows.
</Text>
</Box>
</Group>
{dayGroups.length && selectedDay ? (
<>
{/* Date stepper — page back/forward through each day in the range */}
<Group justify="center" align="center" wrap="nowrap" gap="md" mt="md">
<ActionIcon
variant="light"
color="#F2A516"
size="xl"
radius="xl"
aria-label="Previous day"
disabled={selectedIndex <= 0}
onClick={() => setSelectedDate(dayGroups[selectedIndex - 1]?.date ?? null)}
>
<ChevronLeft size={20} />
</ActionIcon>
<Paper
withBorder
radius="xl"
px="xl"
py="xs"
style={{
flex: 1,
maxWidth: 360,
textAlign: "center",
background: selectedDay.totalBookings ? "#FEF1D5" : "white",
borderColor: selectedDay.totalBookings
? "#FBD171"
: "var(--mantine-color-gray-2)",
}}
>
<Group justify="center" gap={8} wrap="nowrap">
<CalendarDays size={15} color="#B26C09" />
<Text
fw={800}
style={{ color: selectedDay.totalBookings ? "#8A5304" : "#0f172a" }}
>
{selectedDay.dateLabel}
</Text>
{selectedDay.date === todayEat ? (
<Badge size="xs" variant="light" color="#F2A516">
Today
</Badge>
{data.locomotive ? (
<HeroChip icon={<TrainFront size={12} />}>
Loco {data.locomotive.code} ·{" "}
{fmtTons(data.locomotive.maxPullWeightTons)} ·{" "}
{data.locomotive.maxTrainLengthMeters} m
</HeroChip>
) : null}
</Group>
<Text size="xs" c="dimmed" mt={2}>
{selectedDay.totalBookings
? `${selectedDay.totalBookings} booking${selectedDay.totalBookings === 1 ? "" : "s"} · ${selectedDay.windows.length} windows`
: `${selectedDay.windows.length} windows · no bookings`}
</Text>
</Paper>
</Stack>
<ActionIcon
variant="light"
color="#F2A516"
size="xl"
radius="xl"
aria-label="Next day"
disabled={selectedIndex >= dayGroups.length - 1}
onClick={() => setSelectedDate(dayGroups[selectedIndex + 1]?.date ?? null)}
>
<ChevronRight size={20} />
</ActionIcon>
</Group>
<Group justify="space-between" align="center" mt="sm">
<Text size="xs" c="dimmed">
Day {selectedIndex + 1} of {dayGroups.length}
</Text>
<Group gap={6} wrap="nowrap">
{selectedDay.hasIssues ? (
<Badge
variant="light"
color="red"
size="sm"
leftSection={<AlertTriangle size={10} />}
>
Issues
</Badge>
) : null}
<WindowCountChips counts={selectedDay.counts} />
<Group gap="sm">
<Button
variant="default"
radius="md"
leftSection={<RefreshCw size={16} />}
loading={isFetching}
onClick={() => void refetch()}
>
Refresh
</Button>
<Button
color="edr-green"
radius="md"
leftSection={<PlayCircle size={16} />}
loading={runAllocation.isPending}
onClick={handleRunAllocation}
>
Run allocation
</Button>
<Button
variant="light"
color="edr-green"
radius="md"
leftSection={<Layers size={16} />}
onClick={() =>
navigate(
`/dashboard/operations/train-scheduling-v2/${data.scheduleId}`,
)
}
>
Open schedule
</Button>
</Group>
</Group>
<Accordion
key={selectedDay.date}
multiple
defaultValue={openWindowKeys}
variant="separated"
radius="md"
mt="md"
className="bb-window-accordion"
>
{selectedDay.windows.map((window) => (
<WindowAccordionItem key={window.key} window={window} />
))}
</Accordion>
</>
) : (
<Text size="sm" c="dimmed" ta="center" py="lg">
No batch windows for this schedule.
</Text>
)}
{!data.locomotive ? (
<Alert color="red" radius="md" icon={<AlertTriangle size={16} />}>
No locomotive assigned wagon allocation cannot run.
</Alert>
) : null}
{data.pendingContract.bookings.length ? (
<Accordion
multiple
defaultValue={["pending-contract"]}
variant="separated"
radius="md"
mt="md"
className="bb-window-accordion"
>
<Accordion.Item value="pending-contract">
<Accordion.Control>
<Group justify="space-between" wrap="nowrap" pr="md" gap="sm">
<Group gap="sm" wrap="nowrap">
<Box
<KpiStrip
items={[
{
label: "Allocated wagons",
value: data.capacity.allocatedWagons,
hint: "on this train",
icon: Boxes,
},
{
label: "Train length",
value: data.capacity.maxLengthMeters
? `${fmtMeters(data.capacity.allocatedLengthMeters)} / ${fmtMeters(data.capacity.maxLengthMeters)}`
: fmtMeters(data.capacity.allocatedLengthMeters),
icon: Ruler,
},
{
label: "Weight",
value: data.capacity.maxWeightTons
? `${fmtTons(data.capacity.usedWeightTons)} / ${fmtTons(data.capacity.maxWeightTons)}`
: fmtTons(data.capacity.usedWeightTons),
icon: Weight,
},
{
label: "Bookings",
value: totalBookings,
hint: `${data.counts.allocated} allocated · ${data.counts.expired} expired`,
icon: Package,
},
]}
/>
{/* Booking pipeline */}
<Paper
radius="lg"
withBorder
p="lg"
mt="lg"
style={{ borderColor: "var(--mantine-color-gray-2)" }}
>
<Group justify="space-between" mb="sm">
<Group gap={8} wrap="nowrap">
<ThemeIcon
size={32}
radius="md"
variant="light"
color="#F2A516"
>
<Package size={16} />
</ThemeIcon>
<Text fw={700}>Booking pipeline</Text>
</Group>
<Text size="sm" fw={700} c="dark.4">
{totalBookings} booking{totalBookings === 1 ? "" : "s"}
</Text>
</Group>
<BookingPipeline counts={data.counts} size={14} />
</Paper>
{data.allocationViolations.length ? (
<Alert
color="red"
mt="lg"
radius="lg"
icon={<AlertTriangle size={16} />}
title="Allocation constraints"
>
<Stack gap={4}>
{data.allocationViolations.map((v) => (
<Text key={v} size="sm">
{v}
</Text>
))}
</Stack>
</Alert>
) : null}
{/* Batch windows */}
<Paper
radius="lg"
withBorder
p="lg"
mt="lg"
style={{ borderColor: "var(--mantine-color-gray-2)" }}
>
<Group gap="sm" mb={4} wrap="nowrap" align="flex-start">
<ThemeIcon
size={38}
radius="md"
variant="light"
color="#F2A516"
>
<Clock size={19} />
</ThemeIcon>
<Box>
<Title order={4}>Batch windows (EAT)</Title>
<Text size="sm" c="dimmed">
3-hour windows for every day from when the booking window
opened through the departure date. Bookings appear under the
date their contract was signed open a day to see its
windows.
</Text>
</Box>
</Group>
{dayGroups.length && selectedDay ? (
<>
{/* Date stepper — page back/forward through each day in the range */}
<Group
justify="center"
align="center"
wrap="nowrap"
gap="md"
mt="md"
>
<ActionIcon
variant="light"
color="#F2A516"
size="xl"
radius="xl"
aria-label="Previous day"
disabled={selectedIndex <= 0}
onClick={() =>
setSelectedDate(
dayGroups[selectedIndex - 1]?.date ?? null,
)
}
>
<ChevronLeft size={20} />
</ActionIcon>
<Paper
withBorder
radius="xl"
px="xl"
py="xs"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 34,
height: 34,
borderRadius: 10,
flexShrink: 0,
background: "var(--mantine-color-gray-0)",
border: "1px solid var(--mantine-color-gray-2)",
color: "var(--mantine-color-gray-6)",
flex: 1,
maxWidth: 360,
textAlign: "center",
background: selectedDay.totalBookings
? "#FEF1D5"
: "white",
borderColor: selectedDay.totalBookings
? "#FBD171"
: "var(--mantine-color-gray-2)",
}}
>
<FileSignature size={16} />
</Box>
<Box>
<Text fw={700} size="sm">
Pending contract
<Group justify="center" gap={8} wrap="nowrap">
<CalendarDays size={15} color="#B26C09" />
<Text
fw={800}
style={{
color: selectedDay.totalBookings
? "#8A5304"
: "#0f172a",
}}
>
{selectedDay.dateLabel}
</Text>
{selectedDay.date === todayEat ? (
<Badge size="xs" variant="light" color="#F2A516">
Today
</Badge>
) : null}
</Group>
<Text size="xs" c="dimmed" mt={2}>
{selectedDay.totalBookings
? `${selectedDay.totalBookings} booking${selectedDay.totalBookings === 1 ? "" : "s"} · ${selectedDay.windows.length} windows`
: `${selectedDay.windows.length} windows · no bookings`}
</Text>
<Text size="xs" c="dimmed">
Contract not signed yet not in any window
</Text>
</Box>
</Group>
<Badge variant="outline" color="gray" size="sm">
{data.pendingContract.bookings.length} booking
{data.pendingContract.bookings.length === 1 ? "" : "s"}
</Badge>
</Group>
</Accordion.Control>
<Accordion.Panel>
<BookingTable bookings={data.pendingContract.bookings} />
</Accordion.Panel>
</Accordion.Item>
</Accordion>
) : null}
</Paper>
</Paper>
{/* Train composition diagram */}
{hasAssignedWagons && scheduleDetailQuery.data ? (
<Box mt="lg">
<TrainCompositionDiagram
locomotive={scheduleDetailQuery.data.trainSet?.locomotive}
wagons={scheduleDetailQuery.data.trainSet?.wagons ?? []}
freightType="CONTAINER"
trainNumber={scheduleDetailQuery.data.trainNumber}
totalLengthMeters={scheduleDetailQuery.data.trainSet?.totalLengthMeters}
/>
</Box>
) : null}
<ActionIcon
variant="light"
color="#F2A516"
size="xl"
radius="xl"
aria-label="Next day"
disabled={selectedIndex >= dayGroups.length - 1}
onClick={() =>
setSelectedDate(
dayGroups[selectedIndex + 1]?.date ?? null,
)
}
>
<ChevronRight size={20} />
</ActionIcon>
</Group>
<Group justify="space-between" align="center" mt="sm">
<Text size="xs" c="dimmed">
Day {selectedIndex + 1} of {dayGroups.length}
</Text>
<Group gap={6} wrap="nowrap">
{selectedDay.hasIssues ? (
<Badge
variant="light"
color="red"
size="sm"
leftSection={<AlertTriangle size={10} />}
>
Issues
</Badge>
) : null}
<WindowCountChips counts={selectedDay.counts} />
</Group>
</Group>
<Accordion
key={selectedDay.date}
multiple
defaultValue={openWindowKeys}
variant="separated"
radius="md"
mt="md"
className="bb-window-accordion"
>
{selectedDay.windows.map((window) => (
<WindowAccordionItem key={window.key} window={window} />
))}
</Accordion>
</>
) : (
<Text size="sm" c="dimmed" ta="center" py="lg">
No batch windows for this schedule.
</Text>
)}
{data.pendingContract.bookings.length ? (
<Accordion
multiple
defaultValue={["pending-contract"]}
variant="separated"
radius="md"
mt="md"
className="bb-window-accordion"
>
<Accordion.Item value="pending-contract">
<Accordion.Control>
<Group
justify="space-between"
wrap="nowrap"
pr="md"
gap="sm"
>
<Group gap="sm" wrap="nowrap">
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 34,
height: 34,
borderRadius: 10,
flexShrink: 0,
background: "var(--mantine-color-gray-0)",
border: "1px solid var(--mantine-color-gray-2)",
color: "var(--mantine-color-gray-6)",
}}
>
<FileSignature size={16} />
</Box>
<Box>
<Text fw={700} size="sm">
Pending contract
</Text>
<Text size="xs" c="dimmed">
Contract not signed yet not in any window
</Text>
</Box>
</Group>
<Badge variant="outline" color="gray" size="sm">
{data.pendingContract.bookings.length} booking
{data.pendingContract.bookings.length === 1
? ""
: "s"}
</Badge>
</Group>
</Accordion.Control>
<Accordion.Panel>
<BookingTable bookings={data.pendingContract.bookings} />
</Accordion.Panel>
</Accordion.Item>
</Accordion>
) : null}
</Paper>
{/* Train composition diagram */}
{hasAssignedWagons && scheduleDetailQuery.data ? (
<Box mt="lg">
<TrainCompositionDiagram
locomotive={scheduleDetailQuery.data.trainSet?.locomotive}
wagons={scheduleDetailQuery.data.trainSet?.wagons ?? []}
freightType="CONTAINER"
trainNumber={scheduleDetailQuery.data.trainNumber}
totalLengthMeters={
scheduleDetailQuery.data.trainSet?.totalLengthMeters
}
/>
</Box>
) : null}
</Stack>
</Tabs.Panel>