diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/BookingRequestsHeader.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/BookingRequestsHeader.tsx
index 85243be46..ca4fca5a5 100644
--- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingRequestsHeader.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingRequestsHeader.tsx
@@ -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({
-
+
+
-
- {tabs ? : null}
+ {/* {tabs ? : null} */}
);
}
-/** 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 (
-
-
-
- {children}
-
-
- );
-}
-
+/**
+ * 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 (
-
-
-
-
-
-
- {label}
-
-
- {value}
-
-
- {pct != null ? `${pct}% of queue` : hint}
-
-
-
+
+
+
+
+
+
+
+ {value}
+
+
+ {label}
+ {hint ? ` · ${pct != null ? `${pct}% of queue` : hint}` : ""}
+
+
+ {pct != null ? (
+
+
+ {pct}%
+
+
+ ) : null}
+
+
+ {pct == null ? (
+
+ ) : null}
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingContainersCard.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingContainersCard.tsx
index 2ed87baf3..3048b705b 100644
--- a/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingContainersCard.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingContainersCard.tsx
@@ -42,7 +42,7 @@ export function BookingContainersCard({ containers }: BookingContainersCardProps
{container.quantity}
{container.vgmPerUnitTons} t
-
+
{(container.quantity * container.vgmPerUnitTons).toFixed(2)} t
diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingPaymentCard.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingPaymentCard.tsx
index 90440dfae..7d0535bd4 100644
--- a/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingPaymentCard.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingPaymentCard.tsx
@@ -21,10 +21,10 @@ export function BookingPaymentCard({
Total Amount
-
+
{totalAmount.toLocaleString(undefined, { minimumFractionDigits: 2 })}
-
+
{currency}
diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingRequestHero.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingRequestHero.tsx
index e396ec4c2..94648a76a 100644
--- a/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingRequestHero.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingRequestHero.tsx
@@ -83,7 +83,7 @@ export function BookingRequestHero({
-
+
Booking reference
diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingReviewNotesCard.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingReviewNotesCard.tsx
index 218f0c7c5..87e9097fa 100644
--- a/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingReviewNotesCard.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingReviewNotesCard.tsx
@@ -25,7 +25,7 @@ export function BookingReviewNotesCard({ notes }: BookingReviewNotesCardProps) {
{notes.map((note) => (
-
+
diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/booking-detail.styles.ts b/apps/edr-freight-web/backoffice/src/components/bookings/detail/booking-detail.styles.ts
index 4a6f79661..e12b7220b 100644
--- a/apps/edr-freight-web/backoffice/src/components/bookings/detail/booking-detail.styles.ts
+++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/booking-detail.styles.ts
@@ -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 = {
diff --git a/apps/edr-freight-web/backoffice/src/components/common/MiniGraph.tsx b/apps/edr-freight-web/backoffice/src/components/common/MiniGraph.tsx
new file mode 100644
index 000000000..0aa322702
--- /dev/null
+++ b/apps/edr-freight-web/backoffice/src/components/common/MiniGraph.tsx
@@ -0,0 +1,193 @@
+import { Box } from "@mantine/core";
+
+import {
+ overviewAccentGradients,
+ type OverviewAccent,
+} from "@/components/overview/overview.styles";
+
+/**
+ * Tiny inline-SVG mini graphs shared by every KPI / stat card across the app.
+ * No charting dependency — crisp and cheap to render in long card grids.
+ *
+ * - `MiniSparkline` (variant "area" | "line") draws a smooth decorative trend.
+ * - `MiniRing` draws a circular percentage gauge.
+ *
+ * The sparkline series is deterministic (seeded from a string) so a given card
+ * always renders the same shape — purely decorative, not a real time-series.
+ */
+
+/** Deterministic, gently-rising series seeded by a string (purely decorative). */
+export function sparkHeights(seed: string, baseline: number, count = 9): number[] {
+ let h = 2166136261;
+ for (let i = 0; i < seed.length; i += 1) {
+ h ^= seed.charCodeAt(i);
+ h = Math.imul(h, 16777619) >>> 0;
+ }
+ const out: number[] = [];
+ let v = 0.35 + (baseline > 0 ? Math.min(0.35, baseline * 0.35) : 0.15);
+ for (let i = 0; i < count; i += 1) {
+ h = (Math.imul(h, 1103515245) + 12345) >>> 0;
+ const delta = ((h % 1000) / 1000 - 0.42) * 0.28;
+ v = Math.min(1, Math.max(0.22, v + delta));
+ out.push(v);
+ }
+ // Nudge the final point up so the series reads as an upward trend.
+ out[out.length - 1] = Math.min(1, out[out.length - 1] + 0.15);
+ return out;
+}
+
+/** Build a smooth (Catmull-Rom → bezier) path string through y-points in [0,1]. */
+function smoothPath(values: number[], width: number, height: number, pad = 2): string {
+ const n = values.length;
+ if (n === 0) return "";
+ const innerW = width - pad * 2;
+ const innerH = height - pad * 2;
+ const pts = values.map((v, i) => ({
+ x: pad + (n === 1 ? 0 : (i / (n - 1)) * innerW),
+ y: pad + (1 - v) * innerH,
+ }));
+ if (n === 1) return `M ${pts[0].x} ${pts[0].y}`;
+ let d = `M ${pts[0].x} ${pts[0].y}`;
+ for (let i = 0; i < n - 1; i += 1) {
+ const p0 = pts[i - 1] ?? pts[i];
+ const p1 = pts[i];
+ const p2 = pts[i + 1];
+ const p3 = pts[i + 2] ?? p2;
+ const c1x = p1.x + (p2.x - p0.x) / 6;
+ const c1y = p1.y + (p2.y - p0.y) / 6;
+ const c2x = p2.x - (p3.x - p1.x) / 6;
+ const c2y = p2.y - (p3.y - p1.y) / 6;
+ d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${p2.x} ${p2.y}`;
+ }
+ return d;
+}
+
+export type SparklineVariant = "area" | "line";
+
+/**
+ * Smooth decorative trend. `area` fills under the curve with an accent gradient;
+ * `line` is a clean stroke with a highlighted dot at the latest point.
+ */
+export function MiniSparkline({
+ variant,
+ accent = "default",
+ seed,
+ baseline = 0,
+ height = 38,
+}: {
+ variant: SparklineVariant;
+ accent?: OverviewAccent;
+ seed: string;
+ baseline?: number;
+ height?: number;
+}) {
+ const [light, deep] = overviewAccentGradients[accent] ?? overviewAccentGradients.default;
+ const values = sparkHeights(seed, baseline);
+ const width = 120;
+ const pad = 3;
+ const line = smoothPath(values, width, height, pad);
+ const last = values[values.length - 1];
+ const lastX = pad + ((values.length - 1) / (values.length - 1 || 1)) * (width - pad * 2);
+ const lastY = pad + (1 - last) * (height - pad * 2);
+ // Unique gradient id per seed+accent so multiple cards don't collide.
+ const gid = `spark-${variant}-${accent}-${seed.replace(/[^a-zA-Z0-9]/g, "")}`;
+
+ return (
+
+
+
+ );
+}
+
+/** Circular percentage gauge with an accent stroke. */
+export function MiniRing({
+ pct,
+ accent = "default",
+ size = 52,
+ stroke = 5,
+ children,
+}: {
+ pct: number | null | undefined;
+ accent?: OverviewAccent;
+ size?: number;
+ stroke?: number;
+ children?: React.ReactNode;
+}) {
+ const [, deep] = overviewAccentGradients[accent] ?? overviewAccentGradients.default;
+ 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 (
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx
index 14336e70a..cc6454f60 100644
--- a/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx
@@ -78,8 +78,8 @@ const FreightDashboardHeader = ({
-
- Freight Backoffice
+ {/*
+ Freight Backoffice */}
{pageMeta.title}
-
+ {/*
{pageMeta.subtitle}
-
+ */}
diff --git a/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardLayout.tsx b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardLayout.tsx
index 8417065cf..a8e0b0aa6 100644
--- a/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardLayout.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardLayout.tsx
@@ -79,11 +79,11 @@ const FreightDashboardLayout = ({
height: "100dvh",
overflow: "hidden",
background: "var(--mantine-color-gray-1)",
- padding: "8px",
+ padding: "0px",
fontFamily: "'Outfit', var(--font-sans)",
}}
>
-
+
point.count > 0);
return (
-
-
+
+
Booking trend
{!hasData ? (
No bookings in this period
) : (
-
+
diff --git a/apps/edr-freight-web/backoffice/src/components/overview/OverviewDonutChart.tsx b/apps/edr-freight-web/backoffice/src/components/overview/OverviewDonutChart.tsx
index 494b3d653..6b333d6ea 100644
--- a/apps/edr-freight-web/backoffice/src/components/overview/OverviewDonutChart.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/overview/OverviewDonutChart.tsx
@@ -5,7 +5,7 @@ import {
ResponsiveContainer,
Tooltip,
} from "recharts";
-import { Paper, Stack, Text } from "@mantine/core";
+import { Box, Group, Paper, Stack, Text } from "@mantine/core";
import { overviewChartColors } from "./overview.styles";
@@ -27,42 +27,100 @@ export function OverviewDonutChart({
}: OverviewDonutChartProps) {
const filtered = data.filter((item) => item.value > 0);
const hasData = filtered.length > 0;
+ const total = filtered.reduce((sum, item) => sum + item.value, 0);
return (
-
-
- {title}
+
+
+
+ {title}
+
{!hasData ? (
-
+
{emptyMessage}
) : (
-
-
-
+ {/* Compact donut with the total in its center */}
+
+
+
+
+ {filtered.map((entry, index) => (
+ |
+ ))}
+
+ [value, "Count"]} />
+
+
+
- {filtered.map((entry, index) => (
- |
- ))}
-
- [value, "Count"]} />
-
-
+
+ {total}
+
+
+ Total
+
+
+
+
+ {/* Legend fills the space — color, name, count and share */}
+
+ {filtered.map((entry, index) => {
+ const color =
+ overviewChartColors.pipeline[index % overviewChartColors.pipeline.length];
+ const pct = total > 0 ? Math.round((entry.value / total) * 100) : 0;
+ return (
+
+
+
+
+ {entry.name}
+
+
+
+
+ {entry.value}
+
+
+ {pct}%
+
+
+
+ );
+ })}
+
+
)}
diff --git a/apps/edr-freight-web/backoffice/src/components/overview/OverviewHorizontalBarChart.tsx b/apps/edr-freight-web/backoffice/src/components/overview/OverviewHorizontalBarChart.tsx
index e5b5d3f8e..2de3eb20b 100644
--- a/apps/edr-freight-web/backoffice/src/components/overview/OverviewHorizontalBarChart.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/overview/OverviewHorizontalBarChart.tsx
@@ -36,8 +36,8 @@ export function OverviewHorizontalBarChart({
const hasData = chartData.length > 0;
return (
-
-
+
+
{title}
{!hasData ? (
diff --git a/apps/edr-freight-web/backoffice/src/components/overview/OverviewKpiCard.tsx b/apps/edr-freight-web/backoffice/src/components/overview/OverviewKpiCard.tsx
index 43108ffd7..f986f622e 100644
--- a/apps/edr-freight-web/backoffice/src/components/overview/OverviewKpiCard.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/overview/OverviewKpiCard.tsx
@@ -1,19 +1,25 @@
import type { LucideIcon } from "lucide-react";
import { Card, Group, Stack, Text } from "@mantine/core";
+import { MiniRing, MiniSparkline } from "@/components/common/MiniGraph";
import {
overviewAccentGradients,
type OverviewAccent,
} from "./overview.styles";
+/** Mini-graph type rendered at the bottom of a KPI card. */
+export type KpiGraphVariant = "area" | "line" | "ring";
+
/** Pale chip background per accent — matches the Pencil "tinted icon chip" look. */
-const ACCENT_CHIP_BG: Record = {
+export const ACCENT_CHIP_BG: Record = {
default: "#F1F5F4",
emerald: "#E7F8F2",
amber: "#FEF9C3",
rose: "#FFE4E6",
sky: "#E0F2FE",
violet: "#EDE9FE",
+ gold: "#FEF1D5",
+ orange: "#FEEAD7",
};
export interface OverviewKpiItem {
@@ -22,67 +28,10 @@ export interface OverviewKpiItem {
hint?: string;
icon: LucideIcon;
accent?: keyof typeof ACCENT_CHIP_BG;
- /** Share 0..1 used as a baseline for the decorative trend sparkline. */
+ /** Share 0..1 used as a baseline for the decorative trend + ring gauge. */
progress?: number;
-}
-
-/** Deterministic, gently-rising series seeded by the KPI label (purely decorative). */
-function sparkHeights(seed: string, baseline: number, count = 9): number[] {
- let h = 2166136261;
- for (let i = 0; i < seed.length; i += 1) {
- h ^= seed.charCodeAt(i);
- h = Math.imul(h, 16777619) >>> 0;
- }
- const out: number[] = [];
- let v = 0.35 + (baseline > 0 ? Math.min(0.35, baseline * 0.35) : 0.15);
- for (let i = 0; i < count; i += 1) {
- h = (Math.imul(h, 1103515245) + 12345) >>> 0;
- const delta = ((h % 1000) / 1000 - 0.42) * 0.28;
- v = Math.min(1, Math.max(0.22, v + delta));
- out.push(v);
- }
- // Nudge the final bar up so the series reads as an upward trend.
- out[out.length - 1] = Math.min(1, out[out.length - 1] + 0.15);
- return out;
-}
-
-/** Compact bar sparkline; last bar highlighted in the accent colour. */
-function KpiSparkline({
- accent,
- baseline,
- seed,
-}: {
- accent: OverviewAccent;
- baseline: number;
- seed: string;
-}) {
- const [light, deep] = overviewAccentGradients[accent] ?? overviewAccentGradients.default;
- const bars = sparkHeights(seed, baseline);
-
- return (
-
- {bars.map((value, index) => (
-
- ))}
-
- );
+ /** Mini-graph type; defaults to "area" when not provided. */
+ variant?: KpiGraphVariant;
}
export function OverviewKpiCard({ item }: { item: OverviewKpiItem }) {
@@ -91,10 +40,11 @@ export function OverviewKpiCard({ item }: { item: OverviewKpiItem }) {
const accentKey = accent as OverviewAccent;
const [, accentDeep] = overviewAccentGradients[accentKey] ?? overviewAccentGradients.default;
const chipBg = ACCENT_CHIP_BG[accent] ?? ACCENT_CHIP_BG.default;
+ const variant = item.variant ?? "area";
return (
-
-
+
+
+
+
+ {item.value}
+
+
+ {item.label}
+ {item.hint ? ` · ${item.hint}` : ""}
+
+
+ {variant === "ring" ? (
+
+
+ {Math.round((item.progress ?? 0) * 100)}%
+
+
+ ) : null}
-
-
- {item.value}
-
-
-
- {item.label}
-
- {item.hint && (
-
- · {item.hint}
-
- )}
-
-
-
-
+ {variant !== "ring" ? (
+
+ ) : null}
);
diff --git a/apps/edr-freight-web/backoffice/src/components/overview/OverviewKpiStrip.tsx b/apps/edr-freight-web/backoffice/src/components/overview/OverviewKpiStrip.tsx
index 79132cd64..04740585b 100644
--- a/apps/edr-freight-web/backoffice/src/components/overview/OverviewKpiStrip.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/overview/OverviewKpiStrip.tsx
@@ -1,6 +1,20 @@
import { Group, Paper, Text } from "@mantine/core";
-import { OverviewKpiCard, type OverviewKpiItem } from "./OverviewKpiCard";
+import {
+ OverviewKpiCard,
+ type KpiGraphVariant,
+ type OverviewKpiItem,
+} from "./OverviewKpiCard";
+
+/** Rotate mini-graph types per card so each strip reads as a lively mix. */
+const VARIANT_CYCLE: KpiGraphVariant[] = ["area", "line", "ring"];
+
+/** Cohesive accent rotation — gold-forward with an orange and neutral break. */
+const ACCENT_CYCLE: NonNullable[] = [
+ "gold",
+ "orange",
+ "default",
+];
interface OverviewKpiStripProps {
title?: string;
@@ -33,11 +47,13 @@ export function OverviewKpiStrip({ title, items }: OverviewKpiStripProps) {
)}
- {items.map((item) => (
+ {items.map((item, index) => (
0 ? toNumber(item.value) / max : 0),
}}
diff --git a/apps/edr-freight-web/backoffice/src/components/overview/OverviewPaymentChart.tsx b/apps/edr-freight-web/backoffice/src/components/overview/OverviewPaymentChart.tsx
index 58b90d02a..f1a74ac4e 100644
--- a/apps/edr-freight-web/backoffice/src/components/overview/OverviewPaymentChart.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/overview/OverviewPaymentChart.tsx
@@ -30,15 +30,15 @@ export function OverviewPaymentChart({ data }: { data: IOverviewPaymentTrendPoin
const hasData = data.some((point) => point.amountEtb > 0 || point.amountUsd > 0);
return (
-
-
+
+
Payment trend
{!hasData ? (
No successful payments in this period
) : (
-
+
item.count > 0);
return (
-
-
+
+
Pipeline by stage
{!hasData ? (
No bookings in pipeline
) : (
-
+
-
-
+
+
Payments by method
{methodChartData.length === 0 ? (
No payment methods recorded
) : (
-
+
diff --git a/apps/edr-freight-web/backoffice/src/components/overview/tabs/OverviewCustomersTabPanel.tsx b/apps/edr-freight-web/backoffice/src/components/overview/tabs/OverviewCustomersTabPanel.tsx
index 3788dfed6..dea4c9921 100644
--- a/apps/edr-freight-web/backoffice/src/components/overview/tabs/OverviewCustomersTabPanel.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/overview/tabs/OverviewCustomersTabPanel.tsx
@@ -48,15 +48,15 @@ export function OverviewCustomersTabPanel({ data }: OverviewCustomersTabPanelPro
-
-
+
+
Customer growth
{!hasGrowth ? (
No new customers in this period
) : (
-
+
diff --git a/apps/edr-freight-web/backoffice/src/components/overview/tabs/OverviewStaffTabPanel.tsx b/apps/edr-freight-web/backoffice/src/components/overview/tabs/OverviewStaffTabPanel.tsx
index 27f0b1271..1293cba95 100644
--- a/apps/edr-freight-web/backoffice/src/components/overview/tabs/OverviewStaffTabPanel.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/overview/tabs/OverviewStaffTabPanel.tsx
@@ -47,15 +47,15 @@ export function OverviewStaffTabPanel({ data }: OverviewStaffTabPanelProps) {
-
-
+
+
Employee onboarding trend
{!hasGrowth ? (
No new employees in this period
) : (
-
+
diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/scheduleVisuals.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/scheduleVisuals.tsx
index 1079d299c..ff7417a94 100644
--- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/scheduleVisuals.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/scheduleVisuals.tsx
@@ -3,8 +3,13 @@ import { Box, Group, Paper, Stack, Text } from "@mantine/core";
import type { LucideIcon } from "lucide-react";
import { MapPin } from "lucide-react";
+import { MiniRing, MiniSparkline } from "@/components/common/MiniGraph";
+import type { OverviewAccent } from "@/components/overview/overview.styles";
import { freightBrand } from "@/theme/freight-brand";
+/** Mini-graph variants a StatTile can render at its bottom. */
+export type StatTileGraph = "area" | "line" | "ring" | "none";
+
/**
* Shared visual building blocks for the Train Scheduling V2 surfaces.
* Everything keys off the freight brand green so the list + detail pages
@@ -95,6 +100,9 @@ export function StatTile({
hint,
onDark = false,
accent = freightBrand.primary,
+ graph = "none",
+ graphAccent = "emerald",
+ graphPct,
}: {
icon?: LucideIcon;
label: string;
@@ -102,7 +110,15 @@ export function StatTile({
hint?: ReactNode;
onDark?: boolean;
accent?: string;
+ /** Optional decorative mini-graph at the bottom (ignored on dark tiles). */
+ graph?: StatTileGraph;
+ graphAccent?: OverviewAccent;
+ /** Percentage 0..100 for the ring variant. */
+ graphPct?: number | null;
}) {
+ // Mini-graphs only render on light tiles (the gradient reads poorly on dark).
+ const showGraph = graph !== "none" && !onDark;
+
return (
-
- {Icon ? (
-
-
-
- ) : null}
-
-
- {label}
-
-
- {value}
-
- {hint ? (
-
- {hint}
-
+
+
+ {Icon ? (
+
+
+
) : null}
-
-
+
+
+ {value}
+
+
+ {label}
+ {hint ? · {hint} : null}
+
+
+ {showGraph && graph === "ring" ? (
+
+
+ {Math.round(graphPct ?? 0)}%
+
+
+ ) : null}
+
+
+ {showGraph && graph !== "ring" ? (
+
+ ) : null}
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchBoardPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchBoardPage.tsx
index 029c33ae2..0dec57af5 100644
--- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchBoardPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchBoardPage.tsx
@@ -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={
-
+
{Math.round(pct)}%
@@ -141,19 +141,11 @@ function ScheduleCard({ schedule }: { schedule: BatchBoardSchedule }) {
navigate(`/dashboard/operations/batch-board/${schedule.scheduleId}`)
}
>
- {/* thin brand accent line */}
-
-
{/* header */}
-
+
@@ -224,7 +216,7 @@ function ScheduleCard({ schedule }: { schedule: BatchBoardSchedule }) {
) : null}
-
+
@@ -289,7 +281,6 @@ function ScheduleCard({ schedule }: { schedule: BatchBoardSchedule }) {
function CardSkeleton() {
return (
-
@@ -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"
/>
diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchScheduleDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchScheduleDetailPage.tsx
index 772bd4d32..91849ee3e 100644
--- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchScheduleDetailPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchScheduleDetailPage.tsx
@@ -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",
}}
>
-
+
{initials(b.company)}
@@ -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)",
}}
>
@@ -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 (
-
-
-
-
-
-
- {label}
-
-
- {value}
-
- {pct != null ? (
-
-
-
- ) : null}
- {sub ? (
-
- {sub}
+
+
+
+
+
+
+
+ {value}
+
+ {label}
+ {sub ? ` · ${sub}` : ""}
+
+
+ {pct != null ? (
+
+
+ {Math.round(pct)}%
+
+
) : null}
-
-
+
+
+ {pct == null ? (
+
+ ) : null}
+
);
}
@@ -655,6 +634,7 @@ export default function BatchScheduleDetailPage() {
label="Bookings"
value={totalBookings}
sub={`${data.counts.allocated} allocated · ${data.counts.expired} expired`}
+ variant="line"
/>
@@ -669,7 +649,7 @@ export default function BatchScheduleDetailPage() {
>
-
+
Booking pipeline
@@ -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,
}}
diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx
index ac9d85b76..0543ae0ba 100644
--- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx
@@ -622,7 +622,7 @@ export default function TrainScheduleV2DetailPage() {
}}
>
-
+
@@ -722,7 +722,7 @@ export default function TrainScheduleV2DetailPage() {
-
+
@@ -731,7 +731,7 @@ export default function TrainScheduleV2DetailPage() {
{schedule.route?.name ?? "Train schedule"}
{schedule.trainNumber ? (
-
+
{schedule.trainNumber}
) : null}
@@ -791,11 +791,17 @@ export default function TrainScheduleV2DetailPage() {
? "Import-ready"
: undefined
}
+ accent="#F2A516"
+ graph="area"
+ graphAccent="gold"
/>
diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx
index 850bfa03d..52930b86b 100644
--- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx
@@ -356,10 +356,40 @@ export default function TrainScheduleV2ListPage() {
-
-
-
-
+
+
+
+
@@ -607,11 +637,11 @@ function ScheduleCard({
}}
>
{/* accent strip */}
-
+
-
+