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

@@ -10,6 +10,12 @@ import {
RefreshCw,
} from "lucide-react";
import { MiniRing, MiniSparkline } from "@/components/common/MiniGraph";
import { ACCENT_CHIP_BG } from "@/components/overview/OverviewKpiCard";
import {
overviewAccentGradients,
type OverviewAccent,
} from "@/components/overview/overview.styles";
import type {
BookingListSummaryMetrics,
BookingListSummaryTabs,
@@ -71,14 +77,21 @@ export function BookingRequestsHeader({
</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={LayoutList}
label="In queue"
value={val(metrics?.inQueue)}
hint="Matching current filter"
accent="gold"
variant="area"
/>
<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"
accent="orange"
/>
<HeroStat
icon={AlertTriangle}
@@ -86,110 +99,89 @@ export function BookingRequestsHeader({
value={val(metrics?.urgent)}
hint="High priority score"
ratio={metrics?.inQueue ? (metrics.urgent ?? 0) / metrics.inQueue : 0}
ratioColor="#ef4444"
accent="rose"
/>
<HeroStat
icon={CheckCircle2}
label="Completed"
value={val(tabs?.completed)}
hint="Fully executed"
accent="gold"
variant="line"
/>
<HeroStat icon={CheckCircle2} label="Completed" value={val(tabs?.completed)} hint="Fully executed" />
</Group>
{tabs ? <PipelineBar tabs={tabs} /> : null}
{/* {tabs ? <PipelineBar tabs={tabs} /> : null} */}
</Stack>
);
}
/** Compact ring gauge with the stat icon at its center. */
function MiniDonut({
pct,
color,
children,
size = 52,
stroke = 5,
}: {
pct?: number | null;
color: string;
children: ReactNode;
size?: number;
stroke?: number;
}) {
const radius = (size - stroke) / 2;
const circumference = 2 * Math.PI * radius;
const clamped = pct != null ? Math.min(100, Math.max(0, Math.round(pct))) : null;
const dash = clamped != null ? (clamped / 100) * circumference : 0;
return (
<Box style={{ position: "relative", width: size, height: size, flexShrink: 0 }}>
<svg width={size} height={size} style={{ transform: "rotate(-90deg)", display: "block" }}>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="var(--mantine-color-gray-2)"
strokeWidth={stroke}
/>
{clamped != null ? (
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={color}
strokeWidth={stroke}
strokeLinecap="round"
strokeDasharray={`${dash} ${circumference}`}
style={{ transition: "stroke-dasharray 400ms ease" }}
/>
) : null}
</svg>
<Box
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
color,
}}
>
{children}
</Box>
</Box>
);
}
/**
* KPI card matching the overview style: icon chip + value + label, with a mini
* graph at the bottom. Ratio cards show a ring; the rest show an area/line trend.
*/
function HeroStat({
icon: Icon,
label,
value,
hint,
ratio,
ratioColor = "var(--mantine-color-green-6)",
accent = "emerald",
variant = "area",
}: {
icon: LucideIcon;
label: string;
value: ReactNode;
hint?: string;
ratio?: number;
ratioColor?: string;
accent?: OverviewAccent;
variant?: "area" | "line";
}) {
const [, accentDeep] = overviewAccentGradients[accent] ?? overviewAccentGradients.default;
const chipBg = ACCENT_CHIP_BG[accent] ?? ACCENT_CHIP_BG.default;
const pct = ratio != null ? Math.round(Math.min(1, Math.max(0, ratio)) * 100) : null;
return (
<Paper p="md" radius="lg" style={{ flex: "1 1 180px", minWidth: 160, ...CARD_STYLE }}>
<Group gap="md" wrap="nowrap" align="center">
<MiniDonut pct={pct} color={ratioColor}>
<Icon size={19} />
</MiniDonut>
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
<Text size="xs" fw={600} tt="uppercase" c="dimmed" style={{ letterSpacing: 0.4 }}>
{label}
</Text>
<Text fw={700} size="28px" lh={1} style={{ color: "#0f172a" }}>
{value}
</Text>
<Text size="xs" c="dimmed" truncate>
{pct != null ? `${pct}% of queue` : hint}
</Text>
</Stack>
</Group>
<Stack gap={8}>
<Group gap="sm" wrap="nowrap" align="center">
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 40,
height: 40,
borderRadius: 11,
background: chipBg,
color: accentDeep,
flexShrink: 0,
}}
>
<Icon size={20} strokeWidth={2} />
</Box>
<Stack gap={1} style={{ minWidth: 0, flex: 1 }}>
<Text fw={800} size="24px" lh={1.05} style={{ color: "#0f172a" }} truncate>
{value}
</Text>
<Text size="xs" fw={600} c="dimmed" truncate>
{label}
{hint ? ` · ${pct != null ? `${pct}% of queue` : hint}` : ""}
</Text>
</Stack>
{pct != null ? (
<MiniRing pct={pct} accent={accent} size={44} stroke={5}>
<Text size="10px" fw={800} style={{ color: accentDeep }}>
{pct}%
</Text>
</MiniRing>
) : null}
</Group>
{pct == null ? (
<MiniSparkline variant={variant} accent={accent} baseline={0.5} seed={label} height={22} />
) : null}
</Stack>
</Paper>
);
}

View File

@@ -42,7 +42,7 @@ export function BookingContainersCard({ containers }: BookingContainersCardProps
<Table.Td>{container.quantity}</Table.Td>
<Table.Td>{container.vgmPerUnitTons} t</Table.Td>
<Table.Td>
<Text fw={600} c="green.7" size="sm">
<Text fw={600} size="sm" style={{ color: "#B26C09" }}>
{(container.quantity * container.vgmPerUnitTons).toFixed(2)} t
</Text>
</Table.Td>

View File

@@ -21,10 +21,10 @@ export function BookingPaymentCard({
Total Amount
</Text>
<Group align="flex-end" gap="xs">
<Title order={1} fw={700} c="green.9" style={{ letterSpacing: "-1px" }}>
<Title order={1} fw={700} style={{ letterSpacing: "-1px", color: "#8A5304" }}>
{totalAmount.toLocaleString(undefined, { minimumFractionDigits: 2 })}
</Title>
<Text fw={600} c="green.7" mb={6}>
<Text fw={600} mb={6} style={{ color: "#B26C09" }}>
{currency}
</Text>
</Group>

View File

@@ -83,7 +83,7 @@ export function BookingRequestHero({
<Group justify="space-between" align="flex-start" wrap="wrap" gap="lg">
<Stack gap="sm" style={{ flex: 1, minWidth: 0 }}>
<Text size="xs" c="green.7" fw={700} tt="uppercase" style={{ letterSpacing: 1 }}>
<Text size="xs" fw={700} tt="uppercase" style={{ letterSpacing: 1, color: "#B26C09" }}>
Booking reference
</Text>
<Group gap="sm" align="center" wrap="wrap">

View File

@@ -25,7 +25,7 @@ export function BookingReviewNotesCard({ notes }: BookingReviewNotesCardProps) {
<Stack gap="md">
{notes.map((note) => (
<Group key={note.id} align="flex-start" gap="md" wrap="nowrap">
<ThemeIcon size={34} radius="xl" variant="light" color="green">
<ThemeIcon size={34} radius="xl" variant="light" color="#F2A516">
<FileText size={16} />
</ThemeIcon>
<Stack gap={2} style={{ flex: 1 }}>

View File

@@ -1,9 +1,7 @@
import type { CSSProperties } from "react";
import { FREIGHT_BRAND } from "@/theme/freight-brand";
/** Single brand accent. Minimal design uses solid green sparingly, no gradients. */
export const BRAND_GREEN = FREIGHT_BRAND;
/** Single brand accent (gold). Used sparingly for icons/highlights, no gradients. */
export const BRAND_GREEN = "#F2A516";
/** Centralised style tokens for the booking detail page + cards. */
export const detailStyles = {