mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
ui fix
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Box, Button, Group, Paper, Stack, Text, ThemeIcon, Title } from "@mantine/core";
|
||||
import { Box, Button, Group, Paper, Stack, Text } from "@mantine/core";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Inbox,
|
||||
LayoutList,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
@@ -55,76 +54,45 @@ export function BookingRequestsHeader({
|
||||
const val = (n?: number) => (loading ? "—" : (n ?? 0));
|
||||
|
||||
return (
|
||||
<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)",
|
||||
}}
|
||||
>
|
||||
<Stack gap="lg" style={{ position: "relative" }}>
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
||||
<Group gap="md" align="center" wrap="nowrap">
|
||||
<ThemeIcon size={56} radius="lg" variant="light" color="green">
|
||||
<Inbox size={28} />
|
||||
</ThemeIcon>
|
||||
<Stack gap={4}>
|
||||
<Text size="xs" fw={700} c="green.7" tt="uppercase" style={{ letterSpacing: 1 }}>
|
||||
Operations
|
||||
</Text>
|
||||
<Title order={2} fw={700} style={{ color: "#0f172a" }}>
|
||||
Booking Requests
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" maw={520}>
|
||||
Track every booking from submission through approval, payment, and
|
||||
dispatch — prioritize what needs action.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group gap="sm">
|
||||
<Button color="green" radius="lg" leftSection={<Plus size={18} />} onClick={onCreate}>
|
||||
Create booking
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
radius="lg"
|
||||
leftSection={<RefreshCw size={16} />}
|
||||
loading={isFetching}
|
||||
onClick={onRefresh}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
<Stack gap="lg">
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button color="green" radius="lg" leftSection={<Plus size={18} />} onClick={onCreate}>
|
||||
Create booking
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
radius="lg"
|
||||
leftSection={<RefreshCw size={16} />}
|
||||
loading={isFetching}
|
||||
onClick={onRefresh}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Group grow gap="md" align="stretch" wrap="wrap">
|
||||
<HeroStat icon={LayoutList} label="In queue" value={val(metrics?.inQueue)} hint="Matching current filter" />
|
||||
<HeroStat
|
||||
icon={Clock}
|
||||
label="Needs action"
|
||||
value={val(metrics?.needsAction)}
|
||||
hint="Submitted or pending"
|
||||
ratio={metrics?.inQueue ? (metrics.needsAction ?? 0) / metrics.inQueue : 0}
|
||||
ratioColor="#f59e0b"
|
||||
/>
|
||||
<HeroStat
|
||||
icon={AlertTriangle}
|
||||
label="Urgent"
|
||||
value={val(metrics?.urgent)}
|
||||
hint="High priority score"
|
||||
ratio={metrics?.inQueue ? (metrics.urgent ?? 0) / metrics.inQueue : 0}
|
||||
ratioColor="#ef4444"
|
||||
/>
|
||||
<HeroStat icon={CheckCircle2} label="Completed" value={val(tabs?.completed)} hint="Fully executed" />
|
||||
</Group>
|
||||
<Group grow gap="md" align="stretch" wrap="wrap">
|
||||
<HeroStat icon={LayoutList} label="In queue" value={val(metrics?.inQueue)} hint="Matching current filter" />
|
||||
<HeroStat
|
||||
icon={Clock}
|
||||
label="Needs action"
|
||||
value={val(metrics?.needsAction)}
|
||||
hint="Submitted or pending"
|
||||
ratio={metrics?.inQueue ? (metrics.needsAction ?? 0) / metrics.inQueue : 0}
|
||||
ratioColor="#f59e0b"
|
||||
/>
|
||||
<HeroStat
|
||||
icon={AlertTriangle}
|
||||
label="Urgent"
|
||||
value={val(metrics?.urgent)}
|
||||
hint="High priority score"
|
||||
ratio={metrics?.inQueue ? (metrics.urgent ?? 0) / metrics.inQueue : 0}
|
||||
ratioColor="#ef4444"
|
||||
/>
|
||||
<HeroStat icon={CheckCircle2} label="Completed" value={val(tabs?.completed)} hint="Fully executed" />
|
||||
</Group>
|
||||
|
||||
{tabs ? <PipelineBar tabs={tabs} /> : null}
|
||||
</Stack>
|
||||
</Paper>
|
||||
{tabs ? <PipelineBar tabs={tabs} /> : null}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Group,
|
||||
SegmentedControl,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { Activity, RefreshCw } from "lucide-react";
|
||||
import { ActionIcon, Group, SegmentedControl, Text } from "@mantine/core";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
|
||||
import type { OverviewRange } from "@/types/overview";
|
||||
|
||||
@@ -29,40 +20,6 @@ function formatRelativeTime(iso: string | undefined) {
|
||||
return new Date(iso).toLocaleString();
|
||||
}
|
||||
|
||||
/** Decorative line-art locomotive + rails — faint brand tint on the right. */
|
||||
function TrainArtwork() {
|
||||
return (
|
||||
<Box
|
||||
aria-hidden
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: -10,
|
||||
bottom: -8,
|
||||
width: 360,
|
||||
height: 200,
|
||||
opacity: 0.06,
|
||||
pointerEvents: "none",
|
||||
color: "var(--mantine-color-green-7)",
|
||||
}}
|
||||
>
|
||||
<svg viewBox="0 0 360 200" fill="none" width="100%" height="100%">
|
||||
<path d="M0 168 H360" stroke="currentColor" strokeWidth="2" strokeDasharray="2 10" strokeLinecap="round" />
|
||||
<path d="M0 180 H360" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M70 60 H250 a14 14 0 0 1 14 14 V150 H56 V94 a34 34 0 0 1 14-28 Z" stroke="currentColor" strokeWidth="3" />
|
||||
<rect x="84" y="80" width="40" height="30" rx="6" stroke="currentColor" strokeWidth="3" />
|
||||
<rect x="140" y="80" width="44" height="30" rx="6" stroke="currentColor" strokeWidth="3" />
|
||||
<rect x="200" y="80" width="44" height="30" rx="6" stroke="currentColor" strokeWidth="3" />
|
||||
<path d="M56 130 H264" stroke="currentColor" strokeWidth="3" />
|
||||
<circle cx="96" cy="150" r="16" stroke="currentColor" strokeWidth="3" />
|
||||
<circle cx="150" cy="150" r="16" stroke="currentColor" strokeWidth="3" />
|
||||
<circle cx="214" cy="150" r="16" stroke="currentColor" strokeWidth="3" />
|
||||
<path d="M264 120 H300 a8 8 0 0 1 8 8 V150 H300" stroke="currentColor" strokeWidth="3" />
|
||||
<path d="M56 100 l-28 -10 M56 112 l-30 0 M56 124 l-28 10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface OverviewPageHeaderProps {
|
||||
range: OverviewRange;
|
||||
onRangeChange: (range: OverviewRange) => void;
|
||||
@@ -79,59 +36,31 @@ export function OverviewPageHeader({
|
||||
isRefreshing,
|
||||
}: OverviewPageHeaderProps) {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
borderRadius: 20,
|
||||
padding: "26px 28px",
|
||||
background: "#ffffff",
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
boxShadow: "0 1px 3px rgba(15,23,42,0.04)",
|
||||
}}
|
||||
>
|
||||
<TrainArtwork />
|
||||
|
||||
<Group justify="space-between" align="flex-end" wrap="wrap" gap="lg" style={{ position: "relative" }}>
|
||||
<Stack gap={6} style={{ minWidth: 0 }}>
|
||||
<Group gap={8} align="center">
|
||||
<ThemeIcon size={28} radius="md" variant="light" color="green">
|
||||
<Activity size={16} />
|
||||
</ThemeIcon>
|
||||
<Text size="xs" fw={700} c="green.7" tt="uppercase" style={{ letterSpacing: 1.2 }}>
|
||||
Freight Backoffice · Live
|
||||
</Text>
|
||||
</Group>
|
||||
<Title order={1} style={{ letterSpacing: "-0.03em", fontSize: 34, lineHeight: 1.1, color: "#0f172a" }}>
|
||||
Operations Overview
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
Real-time freight performance · updated {formatRelativeTime(generatedAt)}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Group gap="sm" style={{ position: "relative" }}>
|
||||
<SegmentedControl
|
||||
value={range}
|
||||
onChange={(value) => onRangeChange(value as OverviewRange)}
|
||||
data={RANGE_OPTIONS}
|
||||
size="sm"
|
||||
radius="lg"
|
||||
color="green"
|
||||
/>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="green"
|
||||
size="lg"
|
||||
radius="lg"
|
||||
aria-label="Refresh dashboard"
|
||||
onClick={onRefresh}
|
||||
loading={isRefreshing}
|
||||
>
|
||||
<RefreshCw size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<Group justify="space-between" align="center" wrap="wrap" gap="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
Updated {formatRelativeTime(generatedAt)}
|
||||
</Text>
|
||||
<Group gap="sm">
|
||||
<SegmentedControl
|
||||
value={range}
|
||||
onChange={(value) => onRangeChange(value as OverviewRange)}
|
||||
data={RANGE_OPTIONS}
|
||||
size="sm"
|
||||
radius="lg"
|
||||
color="green"
|
||||
/>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="green"
|
||||
size="lg"
|
||||
radius="lg"
|
||||
aria-label="Refresh dashboard"
|
||||
onClick={onRefresh}
|
||||
loading={isRefreshing}
|
||||
>
|
||||
<RefreshCw size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Box>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
Textarea,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
@@ -380,47 +379,16 @@ export default function NewBookingPage() {
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Hero */}
|
||||
<Paper
|
||||
radius="xl"
|
||||
p="xl"
|
||||
mt="md"
|
||||
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)",
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap" style={{ position: "relative" }}>
|
||||
<Group gap="md" align="center" wrap="nowrap">
|
||||
<ThemeIcon size={54} radius="lg" variant="light" color="green">
|
||||
<Package size={26} />
|
||||
</ThemeIcon>
|
||||
<Stack gap={2}>
|
||||
<Text size="xs" fw={700} c="green.7" tt="uppercase" style={{ letterSpacing: 1 }}>
|
||||
New booking · Staff
|
||||
</Text>
|
||||
<Title order={2} fw={700} style={{ color: "#0f172a" }}>
|
||||
Create freight booking
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" maw={520}>
|
||||
Capture cargo details, container lines, and routing — saved as a draft and pushed
|
||||
into the approval workflow.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Button
|
||||
variant="default"
|
||||
radius="lg"
|
||||
leftSection={<ArrowLeft size={16} />}
|
||||
onClick={() => navigate("/dashboard/booking-requests")}
|
||||
>
|
||||
Back to list
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button
|
||||
variant="default"
|
||||
radius="lg"
|
||||
leftSection={<ArrowLeft size={16} />}
|
||||
onClick={() => navigate("/dashboard/booking-requests")}
|
||||
>
|
||||
Back to list
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Grid gutter="lg" mt="lg">
|
||||
{/* LEFT — form */}
|
||||
|
||||
@@ -13,14 +13,12 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowRight,
|
||||
CalendarDays,
|
||||
Inbox,
|
||||
LayoutGrid,
|
||||
Package,
|
||||
RefreshCw,
|
||||
Ruler,
|
||||
@@ -323,82 +321,38 @@ export default function BatchBoardPage() {
|
||||
<Container fluid py="lg" px="xl">
|
||||
<Breadcrumbs items={[{ label: "Operations" }, { label: "Batch board" }]} />
|
||||
|
||||
{/* Hero — clean white */}
|
||||
<Paper
|
||||
radius="xl"
|
||||
p="xl"
|
||||
mt="md"
|
||||
withBorder
|
||||
style={{
|
||||
borderColor: "var(--mantine-color-gray-2)",
|
||||
background: "white",
|
||||
boxShadow: "0 1px 3px rgba(15,23,42,0.04)",
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
||||
<Group gap="md" align="center" wrap="nowrap">
|
||||
<ThemeIcon size={56} radius="lg" variant="light" color="green">
|
||||
<LayoutGrid size={28} />
|
||||
</ThemeIcon>
|
||||
<Stack gap={2}>
|
||||
<Group gap={8}>
|
||||
<Text
|
||||
size="xs"
|
||||
fw={700}
|
||||
c="green.7"
|
||||
tt="uppercase"
|
||||
style={{ letterSpacing: 1.2 }}
|
||||
>
|
||||
Allocation · Live
|
||||
</Text>
|
||||
<Box
|
||||
w={7}
|
||||
h={7}
|
||||
className="bb-pulse-dot"
|
||||
style={{ borderRadius: 999, background: FREIGHT_BRAND }}
|
||||
/>
|
||||
</Group>
|
||||
<Title order={2} fw={800} style={{ color: "#0f172a", letterSpacing: 0.2 }}>
|
||||
Batch board
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" maw={580}>
|
||||
Active schedules — open a card to see EAT 3-hour batch windows, bookings,
|
||||
and live wagon allocation.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Button
|
||||
variant="default"
|
||||
radius="lg"
|
||||
leftSection={<RefreshCw size={16} />}
|
||||
loading={isFetching}
|
||||
onClick={() => void refetch()}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Group>
|
||||
<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="lg">
|
||||
<StatTile
|
||||
icon={Train}
|
||||
label="Active schedules"
|
||||
value={isLoading ? "…" : schedules.length}
|
||||
hint="on the board right now"
|
||||
/>
|
||||
<StatTile
|
||||
icon={CalendarDays}
|
||||
label="Open windows"
|
||||
value={isLoading ? "…" : summary.openWindows}
|
||||
hint="accepting bookings"
|
||||
/>
|
||||
<StatTile
|
||||
icon={Package}
|
||||
label="Bookings in play"
|
||||
value={isLoading ? "…" : summary.totalBookings}
|
||||
hint={`${summary.totalWagons} wagons allocated`}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
<SimpleGrid cols={{ base: 1, xs: 3 }} spacing="md" mt="md">
|
||||
<StatTile
|
||||
icon={Train}
|
||||
label="Active schedules"
|
||||
value={isLoading ? "…" : schedules.length}
|
||||
hint="on the board right now"
|
||||
/>
|
||||
<StatTile
|
||||
icon={CalendarDays}
|
||||
label="Open windows"
|
||||
value={isLoading ? "…" : summary.openWindows}
|
||||
hint="accepting bookings"
|
||||
/>
|
||||
<StatTile
|
||||
icon={Package}
|
||||
label="Bookings in play"
|
||||
value={isLoading ? "…" : summary.totalBookings}
|
||||
hint={`${summary.totalWagons} wagons allocated`}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
{isLoading ? (
|
||||
<SimpleGrid cols={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="lg" mt="lg">
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
PlayCircle,
|
||||
RefreshCw,
|
||||
Ruler,
|
||||
Train,
|
||||
TrainFront,
|
||||
Weight,
|
||||
XCircle,
|
||||
@@ -540,69 +539,50 @@ export default function BatchScheduleDetailPage() {
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Hero — clean white */}
|
||||
<Paper
|
||||
radius="xl"
|
||||
p="xl"
|
||||
mt="md"
|
||||
withBorder
|
||||
style={{
|
||||
borderColor: "var(--mantine-color-gray-2)",
|
||||
background: "white",
|
||||
boxShadow: "0 1px 3px rgba(15,23,42,0.04)",
|
||||
}}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<Stack gap="lg" mt="md">
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
||||
<Group gap="md" align="flex-start" wrap="nowrap" style={{ minWidth: 0 }}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
radius="md"
|
||||
px="sm"
|
||||
leftSection={<ArrowLeft size={16} />}
|
||||
onClick={() => navigate("/dashboard/operations/batch-board")}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
|
||||
<ThemeIcon size={52} radius="md" variant="light" color="green">
|
||||
<Train size={26} />
|
||||
</ThemeIcon>
|
||||
<Stack gap={7} style={{ minWidth: 0 }}>
|
||||
<Group gap="sm" wrap="wrap">
|
||||
<Title order={2} fw={800} style={{ color: "#0f172a", letterSpacing: 0.2 }}>
|
||||
{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",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
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
|
||||
</HeroChip>
|
||||
) : null}
|
||||
</Group>
|
||||
</Stack>
|
||||
<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} style={{ color: "#0f172a", letterSpacing: 0.2 }}>
|
||||
{data.trainNumber ?? data.routeName ?? "Schedule"}
|
||||
</Title>
|
||||
<WindowStatusPill status={data.bookingWindowStatus} />
|
||||
<HeroChip>{data.status}</HeroChip>
|
||||
</Group>
|
||||
</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",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
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
|
||||
</HeroChip>
|
||||
) : null}
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Group gap="sm">
|
||||
<Button
|
||||
@@ -677,8 +657,7 @@ export default function BatchScheduleDetailPage() {
|
||||
sub={`${data.counts.allocated} allocated · ${data.counts.expired} expired`}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
{/* Booking pipeline */}
|
||||
<Paper
|
||||
|
||||
@@ -8,14 +8,12 @@ import {
|
||||
Card,
|
||||
Group,
|
||||
Modal,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { ArrowRight, CalendarClock, Navigation, Send, Train, Weight } from "lucide-react";
|
||||
|
||||
@@ -345,53 +343,24 @@ export default function TrainScheduleV2ListPage() {
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{/* Hero banner */}
|
||||
<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)",
|
||||
}}
|
||||
>
|
||||
<Stack gap="lg" style={{ position: "relative" }}>
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap">
|
||||
<Group gap="md" align="center" wrap="nowrap">
|
||||
<ThemeIcon size={56} radius="lg" variant="light" color="green">
|
||||
<Train size={28} />
|
||||
</ThemeIcon>
|
||||
<Stack gap={4}>
|
||||
<Title order={2} fw={700} style={{ color: "#0f172a" }}>
|
||||
Train Schedules
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" maw={520}>
|
||||
Plan departures, allocate bookings, and dispatch trains across
|
||||
every corridor.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Button
|
||||
size="md"
|
||||
radius="lg"
|
||||
color="green"
|
||||
leftSection={<Train size={18} />}
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
New schedule
|
||||
</Button>
|
||||
</Group>
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
size="md"
|
||||
radius="lg"
|
||||
color="green"
|
||||
leftSection={<Train size={18} />}
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
New schedule
|
||||
</Button>
|
||||
</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`} />
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<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`} />
|
||||
</SimpleGrid>
|
||||
|
||||
<Card radius="lg" padding={0} withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
||||
<Stack gap={0}>
|
||||
|
||||
Reference in New Issue
Block a user