This commit is contained in:
Marshal
2026-06-12 12:59:01 +00:00
parent db39e77e90
commit 6b79348074
6 changed files with 166 additions and 399 deletions

View File

@@ -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>
);
}

View File

@@ -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>
);
}