This commit is contained in:
Marshal
2026-06-12 14:21:07 +00:00
parent 6b79348074
commit d3a479c142
25 changed files with 656 additions and 373 deletions

View File

@@ -58,10 +58,10 @@ const fmtScheduleDate = (iso: string | null) =>
}).format(new Date(iso)) + " EAT"
: "No date";
/** Capacity ring color: gold normally, red once over capacity. */
function ringColor(pct: number) {
if (pct >= 100) return "red";
if (pct >= 85) return "orange";
return "green";
if (pct >= 100) return "#fa5252";
return "#F2A516";
}
/** Capacity ring that keeps the explicit allocated/max numbers underneath. */
@@ -88,7 +88,7 @@ function CapacityRing({
rootColor="var(--mantine-color-gray-1)"
label={
<Stack gap={0} align="center">
<Text ta="center" size="lg" fw={800} c={`${color}.7`} lh={1}>
<Text ta="center" size="lg" fw={800} lh={1} style={{ color }}>
{Math.round(pct)}%
</Text>
<Text ta="center" size="9px" c="dimmed" fw={600}>
@@ -141,19 +141,11 @@ function ScheduleCard({ schedule }: { schedule: BatchBoardSchedule }) {
navigate(`/dashboard/operations/batch-board/${schedule.scheduleId}`)
}
>
{/* thin brand accent line */}
<Box
style={{
height: 4,
background: `linear-gradient(90deg, ${FREIGHT_BRAND}, var(--mantine-color-teal-6))`,
}}
/>
<Stack gap="md" p="lg" style={{ flex: 1 }}>
{/* header */}
<Group justify="space-between" align="flex-start" wrap="nowrap">
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
<ThemeIcon size={44} radius="md" variant="light" color="green">
<ThemeIcon size={44} radius="md" variant="light" color="#F2A516">
<Train size={22} />
</ThemeIcon>
<Box style={{ minWidth: 0 }}>
@@ -224,7 +216,7 @@ function ScheduleCard({ schedule }: { schedule: BatchBoardSchedule }) {
) : null}
<Stack gap={0} align="center" style={{ flex: 1 }}>
<ThemeIcon size={34} radius="md" variant="light" color="green">
<ThemeIcon size={34} radius="md" variant="light" color="gray">
<Package size={17} />
</ThemeIcon>
<Text fw={800} size="26px" c="dark.5" lh={1.1} mt={6}>
@@ -289,7 +281,6 @@ function ScheduleCard({ schedule }: { schedule: BatchBoardSchedule }) {
function CardSkeleton() {
return (
<Paper radius="lg" withBorder style={{ overflow: "hidden" }}>
<Skeleton height={4} radius={0} />
<Stack gap="md" p="lg">
<Group justify="space-between">
<Group gap="sm">
@@ -339,18 +330,32 @@ export default function BatchBoardPage() {
label="Active schedules"
value={isLoading ? "…" : schedules.length}
hint="on the board right now"
accent="#F2A516"
graph="area"
graphAccent="gold"
/>
<StatTile
icon={CalendarDays}
label="Open windows"
value={isLoading ? "…" : summary.openWindows}
hint="accepting bookings"
accent="#FB8C2E"
graph="line"
graphAccent="orange"
/>
<StatTile
icon={Package}
label="Bookings in play"
value={isLoading ? "…" : summary.totalBookings}
hint={`${summary.totalWagons} wagons allocated`}
accent="#F2A516"
graph="ring"
graphAccent="gold"
graphPct={
schedules.length
? Math.min(100, Math.round((summary.openWindows / schedules.length) * 100))
: 0
}
/>
</SimpleGrid>

View File

@@ -47,7 +47,9 @@ import {
totalBookingCount,
WindowStatusPill,
} from "@/components/trainScheduling/batchVisuals";
import { RouteCorridor, scheduleBrand } from "@/components/trainScheduling/scheduleVisuals";
import { MiniRing, MiniSparkline } from "@/components/common/MiniGraph";
import type { OverviewAccent } from "@/components/overview/overview.styles";
import { RouteCorridor } from "@/components/trainScheduling/scheduleVisuals";
import {
useBatchBoardDetail,
useRunAllocation,
@@ -215,11 +217,11 @@ function BookingTable({ bookings }: { bookings: BatchBoardBookingDetail[] }) {
height: 26,
borderRadius: 8,
flexShrink: 0,
background: "var(--mantine-color-green-0)",
border: "1px solid var(--mantine-color-green-1)",
background: "#FEF1D5",
border: "1px solid #FBD171",
}}
>
<Text size="10px" fw={800} c="green.8">
<Text size="10px" fw={800} style={{ color: "#B26C09" }}>
{initials(b.company)}
</Text>
</Box>
@@ -336,15 +338,9 @@ function WindowAccordionItem({ window }: { window: BatchWindowGroup }) {
height: 34,
borderRadius: 10,
flexShrink: 0,
background: total
? "var(--mantine-color-green-0)"
: "var(--mantine-color-gray-0)",
border: total
? "1px solid var(--mantine-color-green-2)"
: "1px solid var(--mantine-color-gray-2)",
color: total
? "var(--mantine-color-green-7)"
: "var(--mantine-color-gray-5)",
background: total ? "#FEF1D5" : "var(--mantine-color-gray-0)",
border: total ? "1px solid #FBD171" : "1px solid var(--mantine-color-gray-2)",
color: total ? "#B26C09" : "var(--mantine-color-gray-5)",
}}
>
<Clock size={16} />
@@ -380,28 +376,28 @@ function WindowAccordionItem({ window }: { window: BatchWindowGroup }) {
);
}
/** Light stat card with an optional capacity bar — keeps the explicit numbers. */
/**
* 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 barColor =
pct == null
? "green"
: pct >= 100
? "red"
: pct >= 85
? "orange"
: "green";
const ringAccent: OverviewAccent =
pct == null ? "gold" : pct >= 100 ? "rose" : pct >= 85 ? "orange" : "gold";
return (
<Paper
p="md"
@@ -409,50 +405,33 @@ function StatCard({
withBorder
style={{ borderColor: "var(--mantine-color-gray-2)", background: "white" }}
>
<Group gap="sm" wrap="nowrap" align="flex-start">
<ThemeIcon size={38} radius="md" variant="light" color="green">
<Icon size={19} />
</ThemeIcon>
<Stack gap={5} style={{ minWidth: 0, flex: 1 }}>
<Text
size="xs"
fw={600}
tt="uppercase"
style={{ letterSpacing: 0.4 }}
c="dimmed"
>
{label}
</Text>
<Text fw={800} size="lg" lh={1.1} c="dark.5" style={{ whiteSpace: "nowrap" }}>
{value}
</Text>
{pct != null ? (
<Box
style={{
height: 6,
borderRadius: 999,
background: "var(--mantine-color-gray-1)",
overflow: "hidden",
}}
>
<Box
style={{
width: `${Math.min(100, Math.max(0, pct))}%`,
height: "100%",
borderRadius: 999,
background: `var(--mantine-color-${barColor}-5)`,
transition: "width 0.4s ease",
}}
/>
</Box>
) : null}
{sub ? (
<Text size="xs" c="dimmed">
{sub}
<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}
</Stack>
</Group>
</Group>
{pct == null ? (
<MiniSparkline variant={variant} accent="gold" baseline={0.5} seed={label} height={20} />
) : null}
</Stack>
</Paper>
);
}
@@ -655,6 +634,7 @@ export default function BatchScheduleDetailPage() {
label="Bookings"
value={totalBookings}
sub={`${data.counts.allocated} allocated · ${data.counts.expired} expired`}
variant="line"
/>
</SimpleGrid>
</Stack>
@@ -669,7 +649,7 @@ export default function BatchScheduleDetailPage() {
>
<Group justify="space-between" mb="sm">
<Group gap={8} wrap="nowrap">
<ThemeIcon size={32} radius="md" variant="light" color="green">
<ThemeIcon size={32} radius="md" variant="light" color="#F2A516">
<Package size={16} />
</ThemeIcon>
<Text fw={700}>Booking pipeline</Text>
@@ -716,7 +696,7 @@ export default function BatchScheduleDetailPage() {
width: 38,
height: 38,
borderRadius: 10,
background: scheduleBrand.heroGradient,
background: "linear-gradient(135deg, #FBD171, #F2A516)",
color: "white",
flexShrink: 0,
}}

View File

@@ -622,7 +622,7 @@ export default function TrainScheduleV2DetailPage() {
}}
>
<Group gap="md" align="flex-start" wrap="nowrap">
<ThemeIcon size={44} radius="md" variant="light" color="green">
<ThemeIcon size={44} radius="md" variant="light" color="#F2A516">
<CheckCircle2 size={22} />
</ThemeIcon>
<Stack gap={2}>
@@ -722,7 +722,7 @@ export default function TrainScheduleV2DetailPage() {
<Stack gap="lg" style={{ position: "relative" }}>
<Group justify="space-between" align="flex-start" wrap="wrap">
<Group gap="md" align="flex-start" wrap="nowrap">
<ThemeIcon size={56} radius="lg" variant="light" color="green">
<ThemeIcon size={56} radius="lg" variant="light" color="#F2A516">
<Train size={28} />
</ThemeIcon>
<Stack gap={6}>
@@ -731,7 +731,7 @@ export default function TrainScheduleV2DetailPage() {
{schedule.route?.name ?? "Train schedule"}
</Title>
{schedule.trainNumber ? (
<Badge variant="light" color="green" radius="sm" style={{ fontWeight: 600 }}>
<Badge variant="light" color="#F2A516" radius="sm" style={{ fontWeight: 600 }}>
{schedule.trainNumber}
</Badge>
) : null}
@@ -791,11 +791,17 @@ export default function TrainScheduleV2DetailPage() {
? "Import-ready"
: undefined
}
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}
@@ -803,6 +809,9 @@ export default function TrainScheduleV2DetailPage() {
value={`${schedule.trainSet?.wagonCount ?? displayWagonPlan.length} · ${
schedule.trainSet?.totalWeightTons ?? 0
}T`}
accent="#F2A516"
graph="area"
graphAccent="gold"
/>
<StatTile
icon={CalendarClock}
@@ -815,6 +824,9 @@ export default function TrainScheduleV2DetailPage() {
hour: "2-digit",
minute: "2-digit",
})}
accent="#FB8C2E"
graph="line"
graphAccent="orange"
/>
</SimpleGrid>

View File

@@ -356,10 +356,40 @@ export default function TrainScheduleV2ListPage() {
</Group>
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="md">
<StatTile icon={Train} label="Total trains" value={stats.total} />
<StatTile icon={CalendarClock} label="Scheduled" value={stats.scheduled} />
<StatTile icon={Send} label="Dispatched" value={stats.dispatched} />
<StatTile icon={Weight} label="Planned load" value={`${Math.round(stats.weight)}T`} />
<StatTile
icon={Train}
label="Total trains"
value={stats.total}
accent="#F2A516"
graph="area"
graphAccent="gold"
/>
<StatTile
icon={CalendarClock}
label="Scheduled"
value={stats.scheduled}
accent="#FB8C2E"
graph="line"
graphAccent="orange"
graphPct={stats.total ? Math.round((stats.scheduled / stats.total) * 100) : 0}
/>
<StatTile
icon={Send}
label="Dispatched"
value={stats.dispatched}
accent="#F2A516"
graph="ring"
graphAccent="gold"
graphPct={stats.total ? Math.round((stats.dispatched / stats.total) * 100) : 0}
/>
<StatTile
icon={Weight}
label="Planned load"
value={`${Math.round(stats.weight)}T`}
accent="#FB8C2E"
graph="area"
graphAccent="orange"
/>
</SimpleGrid>
<Card radius="lg" padding={0} withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
@@ -607,11 +637,11 @@ function ScheduleCard({
}}
>
{/* accent strip */}
<Box style={{ height: 4, background: scheduleBrand.heroGradient }} />
<Box style={{ height: 4, background: "linear-gradient(90deg, #FBD171, #F2A516)" }} />
<Stack gap="sm" p="md">
<Group justify="space-between" align="flex-start" wrap="nowrap">
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
<ThemeIcon size={38} radius="md" variant="light" color="green">
<ThemeIcon size={38} radius="md" variant="light" color="#F2A516">
<Train size={18} />
</ThemeIcon>
<Stack gap={0} style={{ minWidth: 0 }}>