From d3a479c1427d06de07936bf9bed810c4fbdcfaeb Mon Sep 17 00:00:00 2001 From: Marshal Date: Fri, 12 Jun 2026 14:21:07 +0000 Subject: [PATCH] ui fix --- .../bookings/BookingRequestsHeader.tsx | 158 +++++++------- .../bookings/detail/BookingContainersCard.tsx | 2 +- .../bookings/detail/BookingPaymentCard.tsx | 4 +- .../bookings/detail/BookingRequestHero.tsx | 2 +- .../detail/BookingReviewNotesCard.tsx | 2 +- .../bookings/detail/booking-detail.styles.ts | 6 +- .../src/components/common/MiniGraph.tsx | 193 ++++++++++++++++++ .../layout/FreightDashboardHeader.tsx | 8 +- .../layout/FreightDashboardLayout.tsx | 4 +- .../overview/OverviewBookingTrendChart.tsx | 6 +- .../overview/OverviewDonutChart.tsx | 118 ++++++++--- .../overview/OverviewHorizontalBarChart.tsx | 4 +- .../components/overview/OverviewKpiCard.tsx | 129 +++++------- .../components/overview/OverviewKpiStrip.tsx | 20 +- .../overview/OverviewPaymentChart.tsx | 6 +- .../overview/OverviewStatusChart.tsx | 6 +- .../components/overview/overview.styles.ts | 20 +- .../overview/tabs/OverviewBillingTabPanel.tsx | 6 +- .../tabs/OverviewCustomersTabPanel.tsx | 6 +- .../overview/tabs/OverviewStaffTabPanel.tsx | 6 +- .../trainScheduling/scheduleVisuals.tsx | 116 +++++++---- .../pages/trainScheduling/BatchBoardPage.tsx | 35 ++-- .../BatchScheduleDetailPage.tsx | 112 +++++----- .../TrainScheduleV2DetailPage.tsx | 18 +- .../TrainScheduleV2ListPage.tsx | 42 +++- 25 files changed, 656 insertions(+), 373 deletions(-) create mode 100644 apps/edr-freight-web/backoffice/src/components/common/MiniGraph.tsx 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 ( - - - - {clamped != null ? ( - - ) : null} - - - {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 - + <Title order={1} fw={700} style={{ letterSpacing: "-1px", color: "#8A5304" }}> {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 ( + + + + + + + + + {variant === "area" ? ( + + ) : null} + + {variant === "line" ? ( + + ) : null} + + + ); +} + +/** 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 ( + + + + {clamped != null ? ( + + ) : null} + + + {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 */} - + - +