mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
ui fix
This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
import type { ReactNode } from "react";
|
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 type { LucideIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Clock,
|
Clock,
|
||||||
Inbox,
|
|
||||||
LayoutList,
|
LayoutList,
|
||||||
Plus,
|
Plus,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -55,76 +54,45 @@ export function BookingRequestsHeader({
|
|||||||
const val = (n?: number) => (loading ? "—" : (n ?? 0));
|
const val = (n?: number) => (loading ? "—" : (n ?? 0));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Stack gap="lg">
|
||||||
radius="xl"
|
<Group justify="flex-end" gap="sm">
|
||||||
p="xl"
|
<Button color="green" radius="lg" leftSection={<Plus size={18} />} onClick={onCreate}>
|
||||||
style={{
|
Create booking
|
||||||
position: "relative",
|
</Button>
|
||||||
overflow: "hidden",
|
<Button
|
||||||
background: "#ffffff",
|
variant="default"
|
||||||
border: "1px solid var(--mantine-color-gray-2)",
|
radius="lg"
|
||||||
boxShadow: "0 1px 3px rgba(15,23,42,0.04)",
|
leftSection={<RefreshCw size={16} />}
|
||||||
}}
|
loading={isFetching}
|
||||||
>
|
onClick={onRefresh}
|
||||||
<Stack gap="lg" style={{ position: "relative" }}>
|
>
|
||||||
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
Refresh
|
||||||
<Group gap="md" align="center" wrap="nowrap">
|
</Button>
|
||||||
<ThemeIcon size={56} radius="lg" variant="light" color="green">
|
</Group>
|
||||||
<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>
|
|
||||||
|
|
||||||
<Group grow gap="md" align="stretch" wrap="wrap">
|
<Group grow gap="md" align="stretch" wrap="wrap">
|
||||||
<HeroStat icon={LayoutList} label="In queue" value={val(metrics?.inQueue)} hint="Matching current filter" />
|
<HeroStat icon={LayoutList} label="In queue" value={val(metrics?.inQueue)} hint="Matching current filter" />
|
||||||
<HeroStat
|
<HeroStat
|
||||||
icon={Clock}
|
icon={Clock}
|
||||||
label="Needs action"
|
label="Needs action"
|
||||||
value={val(metrics?.needsAction)}
|
value={val(metrics?.needsAction)}
|
||||||
hint="Submitted or pending"
|
hint="Submitted or pending"
|
||||||
ratio={metrics?.inQueue ? (metrics.needsAction ?? 0) / metrics.inQueue : 0}
|
ratio={metrics?.inQueue ? (metrics.needsAction ?? 0) / metrics.inQueue : 0}
|
||||||
ratioColor="#f59e0b"
|
ratioColor="#f59e0b"
|
||||||
/>
|
/>
|
||||||
<HeroStat
|
<HeroStat
|
||||||
icon={AlertTriangle}
|
icon={AlertTriangle}
|
||||||
label="Urgent"
|
label="Urgent"
|
||||||
value={val(metrics?.urgent)}
|
value={val(metrics?.urgent)}
|
||||||
hint="High priority score"
|
hint="High priority score"
|
||||||
ratio={metrics?.inQueue ? (metrics.urgent ?? 0) / metrics.inQueue : 0}
|
ratio={metrics?.inQueue ? (metrics.urgent ?? 0) / metrics.inQueue : 0}
|
||||||
ratioColor="#ef4444"
|
ratioColor="#ef4444"
|
||||||
/>
|
/>
|
||||||
<HeroStat icon={CheckCircle2} label="Completed" value={val(tabs?.completed)} hint="Fully executed" />
|
<HeroStat icon={CheckCircle2} label="Completed" value={val(tabs?.completed)} hint="Fully executed" />
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{tabs ? <PipelineBar tabs={tabs} /> : null}
|
{tabs ? <PipelineBar tabs={tabs} /> : null}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
import {
|
import { ActionIcon, Group, SegmentedControl, Text } from "@mantine/core";
|
||||||
ActionIcon,
|
import { RefreshCw } from "lucide-react";
|
||||||
Box,
|
|
||||||
Group,
|
|
||||||
SegmentedControl,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
ThemeIcon,
|
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { Activity, RefreshCw } from "lucide-react";
|
|
||||||
|
|
||||||
import type { OverviewRange } from "@/types/overview";
|
import type { OverviewRange } from "@/types/overview";
|
||||||
|
|
||||||
@@ -29,40 +20,6 @@ function formatRelativeTime(iso: string | undefined) {
|
|||||||
return new Date(iso).toLocaleString();
|
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 {
|
interface OverviewPageHeaderProps {
|
||||||
range: OverviewRange;
|
range: OverviewRange;
|
||||||
onRangeChange: (range: OverviewRange) => void;
|
onRangeChange: (range: OverviewRange) => void;
|
||||||
@@ -79,59 +36,31 @@ export function OverviewPageHeader({
|
|||||||
isRefreshing,
|
isRefreshing,
|
||||||
}: OverviewPageHeaderProps) {
|
}: OverviewPageHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Group justify="space-between" align="center" wrap="wrap" gap="md">
|
||||||
style={{
|
<Text size="sm" c="dimmed">
|
||||||
position: "relative",
|
Updated {formatRelativeTime(generatedAt)}
|
||||||
overflow: "hidden",
|
</Text>
|
||||||
borderRadius: 20,
|
<Group gap="sm">
|
||||||
padding: "26px 28px",
|
<SegmentedControl
|
||||||
background: "#ffffff",
|
value={range}
|
||||||
border: "1px solid var(--mantine-color-gray-2)",
|
onChange={(value) => onRangeChange(value as OverviewRange)}
|
||||||
boxShadow: "0 1px 3px rgba(15,23,42,0.04)",
|
data={RANGE_OPTIONS}
|
||||||
}}
|
size="sm"
|
||||||
>
|
radius="lg"
|
||||||
<TrainArtwork />
|
color="green"
|
||||||
|
/>
|
||||||
<Group justify="space-between" align="flex-end" wrap="wrap" gap="lg" style={{ position: "relative" }}>
|
<ActionIcon
|
||||||
<Stack gap={6} style={{ minWidth: 0 }}>
|
variant="light"
|
||||||
<Group gap={8} align="center">
|
color="green"
|
||||||
<ThemeIcon size={28} radius="md" variant="light" color="green">
|
size="lg"
|
||||||
<Activity size={16} />
|
radius="lg"
|
||||||
</ThemeIcon>
|
aria-label="Refresh dashboard"
|
||||||
<Text size="xs" fw={700} c="green.7" tt="uppercase" style={{ letterSpacing: 1.2 }}>
|
onClick={onRefresh}
|
||||||
Freight Backoffice · Live
|
loading={isRefreshing}
|
||||||
</Text>
|
>
|
||||||
</Group>
|
<RefreshCw size={18} />
|
||||||
<Title order={1} style={{ letterSpacing: "-0.03em", fontSize: 34, lineHeight: 1.1, color: "#0f172a" }}>
|
</ActionIcon>
|
||||||
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>
|
</Group>
|
||||||
</Box>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
TextInput,
|
TextInput,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Title,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
@@ -380,47 +379,16 @@ export default function NewBookingPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Hero */}
|
<Group justify="flex-end" mt="md">
|
||||||
<Paper
|
<Button
|
||||||
radius="xl"
|
variant="default"
|
||||||
p="xl"
|
radius="lg"
|
||||||
mt="md"
|
leftSection={<ArrowLeft size={16} />}
|
||||||
style={{
|
onClick={() => navigate("/dashboard/booking-requests")}
|
||||||
position: "relative",
|
>
|
||||||
overflow: "hidden",
|
Back to list
|
||||||
background: "#ffffff",
|
</Button>
|
||||||
border: "1px solid var(--mantine-color-gray-2)",
|
</Group>
|
||||||
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>
|
|
||||||
|
|
||||||
<Grid gutter="lg" mt="lg">
|
<Grid gutter="lg" mt="lg">
|
||||||
{/* LEFT — form */}
|
{/* LEFT — form */}
|
||||||
|
|||||||
@@ -13,14 +13,12 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
Inbox,
|
Inbox,
|
||||||
LayoutGrid,
|
|
||||||
Package,
|
Package,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Ruler,
|
Ruler,
|
||||||
@@ -323,82 +321,38 @@ export default function BatchBoardPage() {
|
|||||||
<Container fluid py="lg" px="xl">
|
<Container fluid py="lg" px="xl">
|
||||||
<Breadcrumbs items={[{ label: "Operations" }, { label: "Batch board" }]} />
|
<Breadcrumbs items={[{ label: "Operations" }, { label: "Batch board" }]} />
|
||||||
|
|
||||||
{/* Hero — clean white */}
|
<Group justify="flex-end" mt="md">
|
||||||
<Paper
|
<Button
|
||||||
radius="xl"
|
variant="default"
|
||||||
p="xl"
|
radius="lg"
|
||||||
mt="md"
|
leftSection={<RefreshCw size={16} />}
|
||||||
withBorder
|
loading={isFetching}
|
||||||
style={{
|
onClick={() => void refetch()}
|
||||||
borderColor: "var(--mantine-color-gray-2)",
|
>
|
||||||
background: "white",
|
Refresh
|
||||||
boxShadow: "0 1px 3px rgba(15,23,42,0.04)",
|
</Button>
|
||||||
}}
|
</Group>
|
||||||
>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 1, xs: 3 }} spacing="md" mt="lg">
|
<SimpleGrid cols={{ base: 1, xs: 3 }} spacing="md" mt="md">
|
||||||
<StatTile
|
<StatTile
|
||||||
icon={Train}
|
icon={Train}
|
||||||
label="Active schedules"
|
label="Active schedules"
|
||||||
value={isLoading ? "…" : schedules.length}
|
value={isLoading ? "…" : schedules.length}
|
||||||
hint="on the board right now"
|
hint="on the board right now"
|
||||||
/>
|
/>
|
||||||
<StatTile
|
<StatTile
|
||||||
icon={CalendarDays}
|
icon={CalendarDays}
|
||||||
label="Open windows"
|
label="Open windows"
|
||||||
value={isLoading ? "…" : summary.openWindows}
|
value={isLoading ? "…" : summary.openWindows}
|
||||||
hint="accepting bookings"
|
hint="accepting bookings"
|
||||||
/>
|
/>
|
||||||
<StatTile
|
<StatTile
|
||||||
icon={Package}
|
icon={Package}
|
||||||
label="Bookings in play"
|
label="Bookings in play"
|
||||||
value={isLoading ? "…" : summary.totalBookings}
|
value={isLoading ? "…" : summary.totalBookings}
|
||||||
hint={`${summary.totalWagons} wagons allocated`}
|
hint={`${summary.totalWagons} wagons allocated`}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<SimpleGrid cols={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="lg" mt="lg">
|
<SimpleGrid cols={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="lg" mt="lg">
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import {
|
|||||||
PlayCircle,
|
PlayCircle,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Ruler,
|
Ruler,
|
||||||
Train,
|
|
||||||
TrainFront,
|
TrainFront,
|
||||||
Weight,
|
Weight,
|
||||||
XCircle,
|
XCircle,
|
||||||
@@ -540,69 +539,50 @@ export default function BatchScheduleDetailPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Hero — clean white */}
|
<Stack gap="lg" mt="md">
|
||||||
<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">
|
|
||||||
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
||||||
<Group gap="md" align="flex-start" wrap="nowrap" style={{ minWidth: 0 }}>
|
<Stack gap={9} style={{ minWidth: 0 }}>
|
||||||
<Button
|
<Group gap="sm" wrap="wrap" align="center">
|
||||||
variant="subtle"
|
<Button
|
||||||
color="gray"
|
variant="subtle"
|
||||||
radius="md"
|
color="gray"
|
||||||
px="sm"
|
radius="md"
|
||||||
leftSection={<ArrowLeft size={16} />}
|
px="sm"
|
||||||
onClick={() => navigate("/dashboard/operations/batch-board")}
|
leftSection={<ArrowLeft size={16} />}
|
||||||
>
|
onClick={() => navigate("/dashboard/operations/batch-board")}
|
||||||
Back
|
>
|
||||||
</Button>
|
Back
|
||||||
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
|
</Button>
|
||||||
<ThemeIcon size={52} radius="md" variant="light" color="green">
|
<Title order={2} fw={800} style={{ color: "#0f172a", letterSpacing: 0.2 }}>
|
||||||
<Train size={26} />
|
{data.trainNumber ?? data.routeName ?? "Schedule"}
|
||||||
</ThemeIcon>
|
</Title>
|
||||||
<Stack gap={7} style={{ minWidth: 0 }}>
|
<WindowStatusPill status={data.bookingWindowStatus} />
|
||||||
<Group gap="sm" wrap="wrap">
|
<HeroChip>{data.status}</HeroChip>
|
||||||
<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>
|
|
||||||
</Group>
|
</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">
|
<Group gap="sm">
|
||||||
<Button
|
<Button
|
||||||
@@ -677,8 +657,7 @@ export default function BatchScheduleDetailPage() {
|
|||||||
sub={`${data.counts.allocated} allocated · ${data.counts.expired} expired`}
|
sub={`${data.counts.allocated} allocated · ${data.counts.expired} expired`}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{/* Booking pipeline */}
|
{/* Booking pipeline */}
|
||||||
<Paper
|
<Paper
|
||||||
|
|||||||
@@ -8,14 +8,12 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
Group,
|
Group,
|
||||||
Modal,
|
Modal,
|
||||||
Paper,
|
|
||||||
Select,
|
Select,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { ArrowRight, CalendarClock, Navigation, Send, Train, Weight } from "lucide-react";
|
import { ArrowRight, CalendarClock, Navigation, Send, Train, Weight } from "lucide-react";
|
||||||
|
|
||||||
@@ -345,53 +343,24 @@ export default function TrainScheduleV2ListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
{/* Hero banner */}
|
<Group justify="flex-end">
|
||||||
<Paper
|
<Button
|
||||||
radius="xl"
|
size="md"
|
||||||
p="xl"
|
radius="lg"
|
||||||
style={{
|
color="green"
|
||||||
position: "relative",
|
leftSection={<Train size={18} />}
|
||||||
overflow: "hidden",
|
onClick={() => setCreateOpen(true)}
|
||||||
background: "#ffffff",
|
>
|
||||||
border: "1px solid var(--mantine-color-gray-2)",
|
New schedule
|
||||||
boxShadow: "0 1px 3px rgba(15,23,42,0.04)",
|
</Button>
|
||||||
}}
|
</Group>
|
||||||
>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="md">
|
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="md">
|
||||||
<StatTile icon={Train} label="Total trains" value={stats.total} />
|
<StatTile icon={Train} label="Total trains" value={stats.total} />
|
||||||
<StatTile icon={CalendarClock} label="Scheduled" value={stats.scheduled} />
|
<StatTile icon={CalendarClock} label="Scheduled" value={stats.scheduled} />
|
||||||
<StatTile icon={Send} label="Dispatched" value={stats.dispatched} />
|
<StatTile icon={Send} label="Dispatched" value={stats.dispatched} />
|
||||||
<StatTile icon={Weight} label="Planned load" value={`${Math.round(stats.weight)}T`} />
|
<StatTile icon={Weight} label="Planned load" value={`${Math.round(stats.weight)}T`} />
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Card radius="lg" padding={0} withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
<Card radius="lg" padding={0} withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
|
|||||||
Reference in New Issue
Block a user