mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
ui fix
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
Reference in New Issue
Block a user