style: WIP ui updates

This commit is contained in:
Nathnael
2026-06-22 07:44:03 +00:00
parent e6101c5514
commit 9a224e9baf
14 changed files with 515 additions and 565 deletions

View File

@@ -1,5 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import type { ReactNode } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
Accordion,
@@ -8,13 +7,10 @@ import {
Badge,
Box,
Button,
Container,
Group,
Loader,
Paper,
SimpleGrid,
Stack,
Table,
Tabs,
Text,
ThemeIcon,
@@ -41,8 +37,11 @@ import {
Weight,
XCircle,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { DataTable, type ColumnDef } from "@edr/ui-common";
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";
@@ -52,8 +51,6 @@ import {
totalBookingCount,
WindowStatusPill,
} from "@/components/trainScheduling/batchVisuals";
import { MiniRing, MiniSparkline } from "@/components/common/MiniGraph";
import type { OverviewAccent } from "@/components/overview/overview.styles";
import { RouteCorridor } from "@/components/trainScheduling/scheduleVisuals";
import {
useBatchBoardDetail,
@@ -72,7 +69,7 @@ const STATE_META: Record<
BatchBoardBookingState,
{ label: string; color: string; icon: typeof CheckCircle2 }
> = {
ALLOCATED: { label: "Allocated", color: "green", icon: CheckCircle2 },
ALLOCATED: { label: "Allocated", color: "edr-green", icon: CheckCircle2 },
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 },
@@ -84,7 +81,7 @@ const ALLOC_META: Record<
BookingAllocationStatus,
{ label: string; color: string }
> = {
ASSIGNED: { label: "Wagons assigned", color: "green" },
ASSIGNED: { label: "Wagons assigned", color: "edr-green" },
NOT_ATTEMPTED: { label: "Not allocated", color: "gray" },
DEFERRED: { label: "Deferred", color: "orange" },
FAILED: { label: "Allocation failed", color: "red" },
@@ -151,140 +148,153 @@ function AllocationBadge({
);
}
function BookingTable({ bookings }: { bookings: BatchBoardBookingDetail[] }) {
if (!bookings.length) {
return (
<Text size="sm" c="dimmed" py="sm" ta="center">
No bookings in this batch window.
const bookingCellMeta = {
headerClassName: ruleEngineTable.headerCell,
cellClassName: ruleEngineTable.bodyCell,
};
const BOOKING_COLUMNS: ColumnDef<BatchBoardBookingDetail>[] = [
{
id: "reference",
header: "Reference",
meta: bookingCellMeta,
cell: ({ row }) => {
const b = row.original;
return (
<Group gap={6} wrap="nowrap">
<Text size="sm" fw={700} c="dark.5">
{b.reference}
</Text>
{b.isGovernment ? (
<Badge size="xs" variant="light" color="grape">
Gov
</Badge>
) : null}
</Group>
);
},
},
{
id: "customer",
header: "Customer",
meta: bookingCellMeta,
cell: ({ row }) => {
const b = row.original;
return (
<Group gap={8} wrap="nowrap">
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 26,
height: 26,
borderRadius: 8,
flexShrink: 0,
background: "#FEF1D5",
border: "1px solid #FBD171",
}}
>
<Text size="10px" fw={800} style={{ color: "#B26C09" }}>
{initials(b.company)}
</Text>
</Box>
<Text size="sm" c="gray.7" truncate>
{b.company}
</Text>
</Group>
);
},
},
{
id: "contractSigned",
header: "Contract signed",
meta: bookingCellMeta,
cell: ({ row }) => (
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
{fmtDateTime(row.original.fullyExecutedAt)} EAT
</Text>
);
}
const th = (label: string) => (
<Table.Th
style={{
fontSize: 11,
fontWeight: 700,
textTransform: "uppercase",
letterSpacing: 0.5,
color: "var(--mantine-color-gray-6)",
background: "var(--mantine-color-gray-0)",
whiteSpace: "nowrap",
}}
>
{label}
</Table.Th>
);
),
},
{
id: "selectedForBatch",
header: "Selected for batch",
meta: bookingCellMeta,
cell: ({ row }) => {
const b = row.original;
if (!b.selectedForBatchAt) {
return (
<Text size="sm" c="dimmed">
</Text>
);
}
return (
<>
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
{fmtDateTime(b.selectedForBatchAt)} EAT
</Text>
{b.paymentDeadline ? (
<Text size="xs" c="orange.7" fw={600} style={{ whiteSpace: "nowrap" }}>
Pay by {fmtDateTime(b.paymentDeadline)} EAT
</Text>
) : null}
</>
);
},
},
{
id: "capacity",
header: "Capacity",
meta: bookingCellMeta,
cell: ({ row }) => {
const b = row.original;
return (
<Group gap={4} wrap="nowrap">
<Badge variant="default" radius="sm" size="sm">
{b.wagons}w
</Badge>
<Badge variant="default" radius="sm" size="sm">
{fmtTons(b.weightTons)}
</Badge>
</Group>
);
},
},
{
id: "state",
header: "Batch state",
meta: bookingCellMeta,
cell: ({ row }) => <StateBadge state={row.original.state} />,
},
{
id: "allocation",
header: "Wagon allocation",
meta: bookingCellMeta,
cell: ({ row }) => (
<AllocationBadge
status={row.original.allocationStatus}
issue={row.original.allocationIssue}
/>
),
},
];
function BookingTable({ bookings }: { bookings: BatchBoardBookingDetail[] }) {
return (
<Box
style={{
border: "1px solid var(--mantine-color-gray-2)",
borderRadius: 12,
overflow: "auto",
}}
>
<Table highlightOnHover verticalSpacing="sm" horizontalSpacing="md" miw={760}>
<Table.Thead>
<Table.Tr>
{th("Reference")}
{th("Customer")}
{th("Contract signed")}
{th("Selected for batch")}
{th("Capacity")}
{th("Batch state")}
{th("Wagon allocation")}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{bookings.map((b) => (
<Table.Tr key={b.id}>
<Table.Td>
<Group gap={6} wrap="nowrap">
<Text size="sm" fw={700} c="dark.5">
{b.reference}
</Text>
{b.isGovernment ? (
<Badge size="xs" variant="light" color="grape">
Gov
</Badge>
) : null}
</Group>
</Table.Td>
<Table.Td>
<Group gap={8} wrap="nowrap">
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 26,
height: 26,
borderRadius: 8,
flexShrink: 0,
background: "#FEF1D5",
border: "1px solid #FBD171",
}}
>
<Text size="10px" fw={800} style={{ color: "#B26C09" }}>
{initials(b.company)}
</Text>
</Box>
<Text size="sm" c="gray.7" truncate>
{b.company}
</Text>
</Group>
</Table.Td>
<Table.Td>
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
{fmtDateTime(b.fullyExecutedAt)} EAT
</Text>
</Table.Td>
<Table.Td>
{b.selectedForBatchAt ? (
<>
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
{fmtDateTime(b.selectedForBatchAt)} EAT
</Text>
{b.paymentDeadline ? (
<Text size="xs" c="orange.7" fw={600} style={{ whiteSpace: "nowrap" }}>
Pay by {fmtDateTime(b.paymentDeadline)} EAT
</Text>
) : null}
</>
) : (
<Text size="sm" c="dimmed">
</Text>
)}
</Table.Td>
<Table.Td>
<Group gap={4} wrap="nowrap">
<Badge variant="default" radius="sm" size="sm">
{b.wagons}w
</Badge>
<Badge variant="default" radius="sm" size="sm">
{fmtTons(b.weightTons)}
</Badge>
</Group>
</Table.Td>
<Table.Td>
<StateBadge state={b.state} />
</Table.Td>
<Table.Td>
<AllocationBadge status={b.allocationStatus} issue={b.allocationIssue} />
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Box>
<DataTable
columns={BOOKING_COLUMNS}
data={bookings}
status="success"
emptyMessage="No bookings in this batch window."
containerClassName="overflow-x-auto rounded-lg border border-edr-border"
/>
);
}
function WindowCountChips({ counts }: { counts: BatchWindowGroup["counts"] }) {
const chips: Array<{ value: number; color: string; label: string }> = [
{ value: counts.allocated, color: "green", label: "allocated" },
{ value: counts.allocated, color: "edr-green", label: "allocated" },
{ value: counts.selectedForBatch, color: "orange", label: "selected" },
{ value: counts.ready, color: "teal", label: "ready" },
{ value: counts.waiting, color: "blue", label: "waiting" },
@@ -415,66 +425,6 @@ function WindowAccordionItem({ window }: { window: BatchWindowGroup }) {
);
}
/**
* Light stat card matching the overview look — keeps the explicit numbers.
* Capacity cards (pct set) show a ring; count cards show an area/line trend.
*/
function StatCard({
icon: Icon,
label,
value,
sub,
pct,
variant = "area",
}: {
icon: LucideIcon;
label: string;
value: ReactNode;
sub?: string;
pct?: number | null;
variant?: "area" | "line";
}) {
const ringAccent: OverviewAccent =
pct == null ? "gold" : pct >= 100 ? "rose" : pct >= 85 ? "orange" : "gold";
return (
<Paper
p="md"
radius="lg"
withBorder
style={{ borderColor: "var(--mantine-color-gray-2)", background: "white" }}
>
<Stack gap={8}>
<Group gap="sm" wrap="nowrap" align="center">
<ThemeIcon size={38} radius="md" variant="light" color="#F2A516">
<Icon size={19} />
</ThemeIcon>
<Stack gap={1} style={{ minWidth: 0, flex: 1 }}>
<Text fw={800} size="lg" lh={1.05} c="dark.5" style={{ whiteSpace: "nowrap" }}>
{value}
</Text>
<Text size="xs" fw={600} c="dimmed" truncate>
{label}
{sub ? ` · ${sub}` : ""}
</Text>
</Stack>
{pct != null ? (
<MiniRing pct={pct} accent={ringAccent} size={42} stroke={5}>
<Text size="9px" fw={800} c="dark.4">
{Math.round(pct)}%
</Text>
</MiniRing>
) : null}
</Group>
{pct == null ? (
<MiniSparkline variant={variant} accent="gold" baseline={0.5} seed={label} height={20} />
) : null}
</Stack>
</Paper>
);
}
export default function BatchScheduleDetailPage() {
const { scheduleId } = useParams<{ scheduleId: string }>();
const navigate = useNavigate();
@@ -621,11 +571,11 @@ export default function BatchScheduleDetailPage() {
if (isLoading || !data) {
return (
<Container fluid py="lg" px="xl">
<PageContainer fluid>
<Group justify="center" py="xl">
<Loader color="green" />
<Loader color="edr-green" />
</Group>
</Container>
</PageContainer>
);
}
@@ -641,7 +591,7 @@ export default function BatchScheduleDetailPage() {
const totalBookings = totalBookingCount(data.counts);
return (
<Container fluid py="lg" px="xl">
<PageContainer fluid>
<Breadcrumbs
items={[
{ label: "Operations" },
@@ -650,7 +600,7 @@ export default function BatchScheduleDetailPage() {
]}
/>
<Tabs value={activeTab} onChange={setActiveTab} mt="md">
<Tabs value={activeTab} onChange={setActiveTab}>
<Tabs.List>
<Tabs.Tab value="overview">Overview</Tabs.Tab>
<Tabs.Tab value="composition">
@@ -673,7 +623,7 @@ export default function BatchScheduleDetailPage() {
>
Back
</Button>
<Title order={2} fw={800} style={{ color: "#0f172a", letterSpacing: 0.2 }}>
<Title order={2} fw={800}>
{data.trainNumber ?? data.routeName ?? "Schedule"}
</Title>
<WindowStatusPill status={data.bookingWindowStatus} />
@@ -715,7 +665,7 @@ export default function BatchScheduleDetailPage() {
Refresh
</Button>
<Button
color="green"
color="edr-green"
radius="md"
leftSection={<PlayCircle size={16} />}
loading={runAllocation.isPending}
@@ -725,7 +675,7 @@ export default function BatchScheduleDetailPage() {
</Button>
<Button
variant="light"
color="green"
color="edr-green"
radius="md"
leftSection={<Layers size={16} />}
onClick={() =>
@@ -743,41 +693,36 @@ export default function BatchScheduleDetailPage() {
</Alert>
) : null}
<SimpleGrid cols={{ base: 1, xs: 2, md: 4 }} spacing="md">
<StatCard
icon={Boxes}
label="Allocated wagons"
value={data.capacity.allocatedWagons}
sub="on this train"
/>
<StatCard
icon={Ruler}
label="Train length"
value={
data.capacity.maxLengthMeters
<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)
}
pct={lengthPct}
/>
<StatCard
icon={Weight}
label="Weight"
value={
data.capacity.maxWeightTons
: fmtMeters(data.capacity.allocatedLengthMeters),
icon: Ruler,
},
{
label: "Weight",
value: data.capacity.maxWeightTons
? `${fmtTons(data.capacity.usedWeightTons)} / ${fmtTons(data.capacity.maxWeightTons)}`
: fmtTons(data.capacity.usedWeightTons)
}
pct={weightPct}
/>
<StatCard
icon={Package}
label="Bookings"
value={totalBookings}
sub={`${data.counts.allocated} allocated · ${data.counts.expired} expired`}
variant="line"
/>
</SimpleGrid>
: fmtTons(data.capacity.usedWeightTons),
icon: Weight,
},
{
label: "Bookings",
value: totalBookings,
hint: `${data.counts.allocated} allocated · ${data.counts.expired} expired`,
icon: Package,
},
]}
/>
{/* Booking pipeline */}
<Paper
@@ -828,21 +773,9 @@ export default function BatchScheduleDetailPage() {
style={{ borderColor: "var(--mantine-color-gray-2)" }}
>
<Group gap="sm" mb={4} wrap="nowrap" align="flex-start">
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 38,
height: 38,
borderRadius: 10,
background: "linear-gradient(135deg, #FBD171, #F2A516)",
color: "white",
flexShrink: 0,
}}
>
<ThemeIcon size={38} radius="md" variant="light" color="#F2A516">
<Clock size={19} />
</Box>
</ThemeIcon>
<Box>
<Title order={4}>Batch windows (EAT)</Title>
<Text size="sm" c="dimmed">
@@ -1054,7 +987,7 @@ export default function BatchScheduleDetailPage() {
style={{ borderColor: "var(--mantine-color-gray-2)" }}
>
<Group justify="center">
<Loader color="green" size="sm" />
<Loader color="edr-green" size="sm" />
<Text size="sm" c="dimmed">
Loading train composition
</Text>
@@ -1063,6 +996,6 @@ export default function BatchScheduleDetailPage() {
)}
</Tabs.Panel>
</Tabs>
</Container>
</PageContainer>
);
}

View File

@@ -24,6 +24,7 @@ import {
Title,
} from "@mantine/core";
import { PageContainer } from "@/components/page";
import { RouteCorridorTrack } from "@/components/trainScheduling/RouteCorridorTrack";
import { RouteCorridor, StatusPill } from "@/components/trainScheduling/scheduleVisuals";
import { freightBrand } from "@/theme/freight-brand";
@@ -62,7 +63,7 @@ function MetaStat({
}) {
return (
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
<ThemeIcon size={32} radius="md" variant="light" color="green">
<ThemeIcon size={32} radius="md" variant="light" color="edr-green">
{icon}
</ThemeIcon>
<Stack gap={0} style={{ minWidth: 0 }}>
@@ -85,18 +86,22 @@ export default function TrainScheduleTrackPage() {
if (trackQuery.isLoading) {
return (
<Group justify="center" py="xl">
<Loader size="sm" color="green" />
</Group>
<PageContainer>
<Group justify="center" py="xl">
<Loader size="sm" color="edr-green" />
</Group>
</PageContainer>
);
}
const track = trackQuery.data;
if (!track || !scheduleId) {
return (
<Text c="dimmed" py="xl">
Tracking data not found.
</Text>
<PageContainer>
<Text c="dimmed" py="xl">
Tracking data not found.
</Text>
</PageContainer>
);
}
@@ -130,7 +135,7 @@ export default function TrainScheduleTrackPage() {
};
return (
<Stack gap="md" px={{ base: "xs", sm: 0 }} py="md" maw={1080} mx="auto" w="100%">
<PageContainer>
<Button
component={Link}
to={`/dashboard/operations/train-scheduling-v2/${scheduleId}`}
@@ -144,7 +149,7 @@ export default function TrainScheduleTrackPage() {
</Button>
{/* Header */}
<Paper radius="lg" withBorder p="lg" style={{ borderColor: "var(--mantine-color-gray-2)" }}>
<Paper radius="lg" withBorder p="lg">
<Stack gap="lg">
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
<Group gap="md" align="flex-start" wrap="nowrap" style={{ minWidth: 0 }}>
@@ -169,7 +174,7 @@ export default function TrainScheduleTrackPage() {
Train tracking
</Title>
{track.trainNumber ? (
<Badge variant="light" color="green" radius="sm">
<Badge variant="light" color="edr-green" radius="sm">
{track.trainNumber}
</Badge>
) : null}
@@ -193,7 +198,7 @@ export default function TrainScheduleTrackPage() {
<Text size="xs" fw={700} c="gray.7" tt="uppercase" style={{ letterSpacing: 0.4 }}>
Journey progress
</Text>
<Text size="xs" fw={700} c="green.8">
<Text size="xs" fw={700} c="edr-green.8">
{reached} / {totalStations} stations · {Math.round(clampedPct)}%
</Text>
</Group>
@@ -201,7 +206,7 @@ export default function TrainScheduleTrackPage() {
value={clampedPct}
size="lg"
radius="xl"
color="green"
color="edr-green"
striped={track.status === "DISPATCHED"}
animated={track.status === "DISPATCHED"}
/>
@@ -230,10 +235,10 @@ export default function TrainScheduleTrackPage() {
</Paper>
{/* Corridor */}
<Paper radius="lg" p="lg" withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
<Paper radius="lg" p="lg" withBorder>
<Stack gap="md">
<Group gap="sm" align="center" wrap="nowrap">
<ThemeIcon size={34} radius="md" variant="light" color="green">
<ThemeIcon size={34} radius="md" variant="light" color="edr-green">
<Navigation size={17} />
</ThemeIcon>
<Stack gap={0}>
@@ -264,9 +269,9 @@ export default function TrainScheduleTrackPage() {
</Paper>
{/* Checkpoint log */}
<Paper radius="lg" p="lg" withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
<Paper radius="lg" p="lg" withBorder>
<Group gap="sm" align="center" wrap="nowrap" mb="md">
<ThemeIcon size={34} radius="md" variant="light" color="green">
<ThemeIcon size={34} radius="md" variant="light" color="edr-green">
<CheckCircle2 size={17} />
</ThemeIcon>
<Stack gap={0}>
@@ -292,7 +297,7 @@ export default function TrainScheduleTrackPage() {
</Text>
</Stack>
) : (
<Timeline active={track.checkpoints.length} bulletSize={22} lineWidth={2} color="green">
<Timeline active={track.checkpoints.length} bulletSize={22} lineWidth={2} color="edr-green">
{track.checkpoints.map((cp) => (
<Timeline.Item
key={cp.id}
@@ -306,7 +311,7 @@ export default function TrainScheduleTrackPage() {
size="xs"
radius="sm"
variant="light"
color={cp.kind === "ARRIVED" ? "teal" : cp.kind === "DEPARTED" ? "blue" : "green"}
color={cp.kind === "ARRIVED" ? "teal" : cp.kind === "DEPARTED" ? "blue" : "edr-green"}
>
{cp.kind}
</Badge>
@@ -326,6 +331,6 @@ export default function TrainScheduleTrackPage() {
</Timeline>
)}
</Paper>
</Stack>
</PageContainer>
);
}

View File

@@ -1,5 +1,17 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Link, useParams } from "react-router-dom";
import {
Badge,
Box,
Button,
Checkbox,
Group,
Loader,
Paper,
RingProgress,
Stack,
Text,
ThemeIcon,
Title,
} from "@mantine/core";
import { isAxiosError } from "axios";
import {
ArrowLeft,
@@ -15,48 +27,35 @@ import {
Train,
Weight,
} from "lucide-react";
import {
Badge,
Box,
Button,
Checkbox,
Group,
Loader,
Paper,
RingProgress,
SimpleGrid,
Stack,
Text,
ThemeIcon,
Title,
} from "@mantine/core";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Link, useParams } from "react-router-dom";
import { ContainerPlacementGrid } from "@/components/trainScheduling/ContainerPlacementGrid";
import { KpiStrip, PageContainer } from "@/components/page";
import {
autoFillPlacements,
mergePlacementsWithSaved,
placementsFromScheduleWagons,
validateLocalPlacements,
} from "@/components/trainScheduling/containerPlacement.util";
import { ContainerPlacementGrid } from "@/components/trainScheduling/ContainerPlacementGrid";
import { FleetAvailabilitySummary } from "@/components/trainScheduling/FleetAvailabilitySummary";
import { RescheduleTrainDialog } from "@/components/trainScheduling/RescheduleTrainDialog";
import { ScheduleBatchPanel } from "@/components/trainScheduling/ScheduleBatchPanel";
import { ScheduleBookingsStep } from "@/components/trainScheduling/ScheduleBookingsStep";
import { FreightTypeBadge } from "@/components/trainScheduling/ScheduleStatusBadge";
import {
RouteCorridor,
StatusPill,
scheduleBrand,
} from "@/components/trainScheduling/scheduleVisuals";
import {
PreviewSummary,
ScheduleWarningsAlert,
} from "@/components/trainScheduling/ScheduleWarningsAlert";
import { FreightTypeBadge } from "@/components/trainScheduling/ScheduleStatusBadge";
import {
RouteCorridor,
StatTile,
StatusPill,
scheduleBrand,
} from "@/components/trainScheduling/scheduleVisuals";
import { RescheduleTrainDialog } from "@/components/trainScheduling/RescheduleTrainDialog";
import { ScheduleBatchPanel } from "@/components/trainScheduling/ScheduleBatchPanel";
import { WorkflowRail, WorkflowStep } from "@/components/trainScheduling/WorkflowStep";
import { shouldShowContainerPlacementStep } from "@/components/trainScheduling/schedulingContainerStep.util";
import { WagonPlanGrid } from "@/components/trainScheduling/WagonPlanGrid";
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
import { WagonPlanGrid } from "@/components/trainScheduling/WagonPlanGrid";
import { WorkflowRail, WorkflowStep } from "@/components/trainScheduling/WorkflowStep";
import {
useEligibleBookings,
useScheduleDetail,
@@ -267,17 +266,21 @@ export default function TrainScheduleV2DetailPage() {
if (detailQuery.isLoading) {
return (
<Group justify="center" py="xl">
<Loader size="sm" />
</Group>
<PageContainer>
<Group justify="center" py="xl">
<Loader size="sm" />
</Group>
</PageContainer>
);
}
if (!schedule || !scheduleId) {
return (
<Text c="dimmed" py="xl">
Schedule not found
</Text>
<PageContainer>
<Text c="dimmed" py="xl">
Schedule not found
</Text>
</PageContainer>
);
}
@@ -405,7 +408,7 @@ export default function TrainScheduleV2DetailPage() {
return (
<Badge
variant="light"
color={previewResult.valid ? "green" : "red"}
color={previewResult.valid ? "edr-green" : "red"}
radius="sm"
>
{previewResult.valid ? "Plan valid" : "Has issues"}
@@ -413,14 +416,14 @@ export default function TrainScheduleV2DetailPage() {
);
}
return allSelectedIds.length ? (
<Badge variant="light" color="green" radius="sm">
<Badge variant="light" color="edr-green" radius="sm">
{allSelectedIds.length} selected
</Badge>
) : null;
}
if (key === "wagon" && displayWagonPlan.length) {
return (
<Badge variant="light" color="green" radius="sm">
<Badge variant="light" color="edr-green" radius="sm">
{displayWagonPlan.length} wagons
</Badge>
);
@@ -429,7 +432,7 @@ export default function TrainScheduleV2DetailPage() {
return (
<Badge
variant="light"
color={containerComplete ? "green" : "yellow"}
color={containerComplete ? "edr-green" : "yellow"}
radius="sm"
>
{containerUnits.length} units
@@ -485,7 +488,7 @@ export default function TrainScheduleV2DetailPage() {
size="sm"
/>
<Button
color="green"
color="edr-green"
radius="md"
leftSection={<Eye size={16} />}
loading={preview.isPending}
@@ -532,7 +535,7 @@ export default function TrainScheduleV2DetailPage() {
<Group>
{!hasContainerStep ? (
<Button
color="green"
color="edr-green"
radius="md"
loading={assign.isPending}
onClick={handleAssign}
@@ -541,7 +544,7 @@ export default function TrainScheduleV2DetailPage() {
</Button>
) : (
<Button
color="green"
color="edr-green"
radius="md"
rightSection={<ContainerIcon size={16} />}
onClick={() => setActiveStep(2)}
@@ -578,7 +581,7 @@ export default function TrainScheduleV2DetailPage() {
{canEditBookings ? (
<Group>
<Button
color="green"
color="edr-green"
radius="md"
loading={assign.isPending}
onClick={handleAssign}
@@ -629,7 +632,7 @@ export default function TrainScheduleV2DetailPage() {
<Text fw={600}>Ready to depart</Text>
<Text size="sm" c="dimmed">
Finalizing locks the plan and moves the schedule to{" "}
<Text span fw={600} c="green.7">
<Text span fw={600} c="edr-green.7">
SCHEDULED
</Text>
. Dispatch then begins rail movement and notifies the yard.
@@ -640,7 +643,7 @@ export default function TrainScheduleV2DetailPage() {
<Group>
{canFinalize ? (
<Button
color="green"
color="edr-green"
size="md"
radius="md"
leftSection={<CheckCircle2 size={18} />}
@@ -663,7 +666,7 @@ export default function TrainScheduleV2DetailPage() {
) : null}
{canDispatch ? (
<Button
color="green"
color="edr-green"
size="md"
radius="md"
leftSection={<Send size={18} />}
@@ -695,7 +698,7 @@ export default function TrainScheduleV2DetailPage() {
};
return (
<Stack gap="lg">
<PageContainer>
<Button
component={Link}
to="/dashboard/operations/train-scheduling-v2"
@@ -711,13 +714,7 @@ export default function TrainScheduleV2DetailPage() {
<Paper
radius="xl"
p="xl"
style={{
position: "relative",
overflow: "hidden",
background: "#ffffff",
border: "1px solid var(--mantine-color-gray-2)",
boxShadow: "0 1px 3px rgba(15,23,42,0.04)",
}}
style={{ position: "relative", overflow: "hidden" }}
>
<Stack gap="lg" style={{ position: "relative" }}>
<Group justify="space-between" align="flex-start" wrap="wrap">
@@ -758,7 +755,7 @@ export default function TrainScheduleV2DetailPage() {
<Button
component={Link}
to={`/dashboard/operations/train-scheduling-v2/${scheduleId}/track`}
color="green"
color="edr-green"
radius="lg"
size="sm"
leftSection={<Navigation size={16} />}
@@ -779,63 +776,12 @@ export default function TrainScheduleV2DetailPage() {
</Group>
</Group>
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="md">
<StatTile
icon={Train}
label="Locomotive"
value={schedule.trainSet?.locomotive?.code ?? "—"}
hint={
schedule.trainSet?.locomotive?.currentYardId
? schedule.trainSet.locomotive.currentYardId === schedule.originStation?.id
? `At ${schedule.originStation?.label ?? schedule.originStation?.code ?? "origin yard"}`
: "Not at schedule origin yard"
: "No current yard set"
}
accent="#F2A516"
graph="area"
graphAccent="gold"
/>
<StatTile
icon={Package}
label="Bookings"
value={schedule.bookings?.length ?? 0}
accent="#FB8C2E"
graph="line"
graphAccent="orange"
/>
<StatTile
icon={Weight}
label="Wagons / load"
value={`${schedule.trainSet?.wagonCount ?? displayWagonPlan.length} · ${
schedule.trainSet?.totalWeightTons ?? 0
}T`}
accent="#F2A516"
graph="area"
graphAccent="gold"
/>
<StatTile
icon={CalendarClock}
label="Departure"
value={new Date(schedule.scheduledDepartureDate).toLocaleDateString(
"en",
{ month: "short", day: "2-digit" },
)}
hint={new Date(schedule.scheduledDepartureDate).toLocaleTimeString("en", {
hour: "2-digit",
minute: "2-digit",
})}
accent="#FB8C2E"
graph="line"
graphAccent="orange"
/>
</SimpleGrid>
{previewResult ? (
<Badge
size="lg"
radius="sm"
variant="light"
color={previewResult.valid ? "green" : "red"}
color={previewResult.valid ? "edr-green" : "red"}
leftSection={
<Box
w={8}
@@ -843,7 +789,7 @@ export default function TrainScheduleV2DetailPage() {
style={{
borderRadius: 999,
background: previewResult.valid
? "var(--mantine-color-green-6)"
? "var(--mantine-color-edr-green-6)"
: "var(--mantine-color-red-6)",
}}
/>
@@ -855,17 +801,51 @@ export default function TrainScheduleV2DetailPage() {
</Stack>
</Paper>
<Paper radius="xl" p="lg" withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
<KpiStrip
items={[
{
label: "Locomotive",
value: schedule.trainSet?.locomotive?.code ?? "—",
hint: schedule.trainSet?.locomotive?.currentYardId
? schedule.trainSet.locomotive.currentYardId === schedule.originStation?.id
? `At ${schedule.originStation?.label ?? schedule.originStation?.code ?? "origin yard"}`
: "Not at schedule origin yard"
: "No current yard set",
icon: Train,
},
{
label: "Bookings",
value: schedule.bookings?.length ?? 0,
icon: Package,
},
{
label: "Wagons / load",
value: `${schedule.trainSet?.wagonCount ?? displayWagonPlan.length} · ${
schedule.trainSet?.totalWeightTons ?? 0
}T`,
icon: Weight,
},
{
label: "Departure",
value: new Date(schedule.scheduledDepartureDate).toLocaleDateString("en", {
month: "short",
day: "2-digit",
}),
hint: new Date(schedule.scheduledDepartureDate).toLocaleTimeString("en", {
hour: "2-digit",
minute: "2-digit",
}),
icon: CalendarClock,
},
]}
/>
<Paper radius="xl" p="lg">
<Stack gap="lg">
{/* Workflow header with ring progress */}
<Group justify="space-between" align="center" wrap="nowrap">
<Group gap="md" align="center" wrap="nowrap">
<ThemeIcon
size={44}
radius="md"
variant="gradient"
gradient={{ from: "green", to: "teal", deg: 135 }}
>
<ThemeIcon size={44} radius="md" variant="light" color="edr-green">
<RouteIcon size={22} />
</ThemeIcon>
<Stack gap={2}>
@@ -882,9 +862,9 @@ export default function TrainScheduleV2DetailPage() {
size={64}
thickness={6}
roundCaps
sections={[{ value: progressPct, color: "green" }]}
sections={[{ value: progressPct, color: "edr-green" }]}
label={
<Text ta="center" size="xs" fw={700} c="green.7">
<Text ta="center" size="xs" fw={700} c="edr-green.7">
{progressPct}%
</Text>
}
@@ -928,6 +908,6 @@ export default function TrainScheduleV2DetailPage() {
onComplete={() => void detailQuery.refetch()}
/>
) : null}
</Stack>
</PageContainer>
);
}