From 138ac5b47db61c9a7693c875620916be866a7bed Mon Sep 17 00:00:00 2001 From: Nathnael Date: Sat, 20 Jun 2026 10:20:38 +0000 Subject: [PATCH] style:wip dashboard ui revamp --- .../layout/FreightDashboardHeader.tsx | 19 +- .../src/components/layout/route-meta.ts | 21 + .../src/components/page/KpiStrip.tsx | 94 ++++ .../src/components/page/PageContainer.tsx | 26 + .../src/components/page/PageHeader.tsx | 78 +++ .../backoffice/src/components/page/index.ts | 6 + .../DropdownSettingsPage.tsx | 483 +++++++----------- .../src/pages/payments/PaymentsPage.tsx | 377 ++++++-------- .../warehouses/WarehouseDashboardPage.tsx | 88 ++-- .../pages/warehouses/WarehouseDetailPage.tsx | 124 ++--- 10 files changed, 647 insertions(+), 669 deletions(-) create mode 100644 apps/edr-freight-web/backoffice/src/components/page/KpiStrip.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/page/PageContainer.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/page/PageHeader.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/page/index.ts 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 83c0fac44..8a45c7cf0 100644 --- a/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx +++ b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx @@ -47,7 +47,6 @@ const ISLAND = "flex size-9 shrink-0 items-center justify-center rounded-full border border-edr-border bg-white text-edr-text transition-colors hover:bg-[#F1F4F7]"; const FreightDashboardHeader = ({ - pageMeta, headerRight, enableThemeToggle = false, userName = "User", @@ -86,7 +85,8 @@ const FreightDashboardHeader = ({ }} > - {/* Left: burger (mobile) + page title */} + {/* Left: burger (mobile) + search — the search now occupies the slot + the page title used to hold; each page owns its own title. */} - - {pageMeta.title} - - - - {/* Right: search + actions + avatar */} - - {/* Search pill */} Search bookings, trains… + + {/* Right: actions + avatar */} + diff --git a/apps/edr-freight-web/backoffice/src/components/layout/route-meta.ts b/apps/edr-freight-web/backoffice/src/components/layout/route-meta.ts index 879292143..0c03b21b1 100644 --- a/apps/edr-freight-web/backoffice/src/components/layout/route-meta.ts +++ b/apps/edr-freight-web/backoffice/src/components/layout/route-meta.ts @@ -50,6 +50,27 @@ const ROUTE_META: Array<{ prefix: string; meta: PageMeta }> = [ subtitle: "View booking payment transactions", }, }, + { + prefix: "/dashboard/warehouse-dashboard", + meta: { + title: "Warehouse Dashboard", + subtitle: "Live overview of warehouse capacity and inventory lifecycle", + }, + }, + { + prefix: "/dashboard/warehouses/", + meta: { + title: "Warehouse detail", + subtitle: "Yards, zones, and inventory for this warehouse", + }, + }, + { + prefix: "/dashboard/warehouses", + meta: { + title: "Warehouses", + subtitle: "Manage warehouses, yards and zones", + }, + }, { prefix: "/dashboard/operations/train-scheduling-v2/", meta: { diff --git a/apps/edr-freight-web/backoffice/src/components/page/KpiStrip.tsx b/apps/edr-freight-web/backoffice/src/components/page/KpiStrip.tsx new file mode 100644 index 000000000..81b75966c --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/page/KpiStrip.tsx @@ -0,0 +1,94 @@ +import { Card, Skeleton, Text } from "@mantine/core"; +import type { LucideIcon } from "lucide-react"; +import type { ReactNode } from "react"; + +import { cn } from "@/lib/utils"; + +export interface KpiItem { + label: string; + value: ReactNode; + /** Optional leading icon rendered in a tinted chip. */ + icon?: LucideIcon; + /** Secondary line under the label (e.g. a unit or comparison). */ + hint?: string; + /** + * Mantine color name for the icon chip (e.g. "edr-green", "red", "yellow"). + * Defaults to the brand green so a strip reads as uniform unless a page opts + * into semantic tints. + */ + color?: string; +} + +export interface KpiStripProps { + items: KpiItem[]; + /** Show skeletons in place of values while data loads. */ + loading?: boolean; +} + +/** + * A single bordered card divided into up to five KPI cells: + * `[ kpi | kpi | kpi ]`. Hairline dividers separate cells (vertical on wide + * screens, horizontal when they wrap). Surface, border and shadow all come from + * the theme — no per-cell backgrounds, gradients or custom shadows. + */ +export function KpiStrip({ items, loading = false }: KpiStripProps) { + // The spec caps a strip at five cells; extra items are dropped rather than + // silently overflowing into an unreadable row. + const cells = items.slice(0, 5); + + return ( + +
+ {cells.map((item, index) => { + const Icon = item.icon; + const color = item.color ?? "edr-green"; + return ( +
0 && + "border-t border-edr-border sm:border-l sm:border-t-0", + )} + > + {Icon ? ( +
+ +
+ ) : null} + +
+ {loading ? ( + + ) : ( + + {item.value} + + )} + + {item.label} + {item.hint ? ` · ${item.hint}` : ""} + +
+
+ ); + })} +
+
+ ); +} + +export default KpiStrip; diff --git a/apps/edr-freight-web/backoffice/src/components/page/PageContainer.tsx b/apps/edr-freight-web/backoffice/src/components/page/PageContainer.tsx new file mode 100644 index 000000000..15c599294 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/page/PageContainer.tsx @@ -0,0 +1,26 @@ +import { Box, Stack, type MantineSpacing } from "@mantine/core"; +import type { ReactNode } from "react"; + +export interface PageContainerProps { + children: ReactNode; + /** Drop the max-width cap for full-bleed pages (boards, very wide tables). */ + fluid?: boolean; + /** Vertical gap between the page's stacked sections. */ + gap?: MantineSpacing; +} + +/** + * Standard page shell: one consistent inset + a vertical Stack so every + * dashboard page shares the same outer padding and inter-section rhythm. + * The surrounding AppShell.Main already paints the page background, so this + * never sets its own — pages stay on the shared `edr-bg` surface. + */ +export function PageContainer({ children, fluid = false, gap = "lg" }: PageContainerProps) { + return ( + + {children} + + ); +} + +export default PageContainer; diff --git a/apps/edr-freight-web/backoffice/src/components/page/PageHeader.tsx b/apps/edr-freight-web/backoffice/src/components/page/PageHeader.tsx new file mode 100644 index 000000000..087567585 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/page/PageHeader.tsx @@ -0,0 +1,78 @@ +import { ActionIcon, Group, Stack, Text, Title } from "@mantine/core"; +import { ArrowLeft } from "lucide-react"; +import type { ReactNode } from "react"; +import { useNavigate } from "react-router-dom"; + +import Breadcrumbs, { type BreadcrumbItem } from "@/components/ui/Breadcrumbs"; + +export interface PageHeaderProps { + title: string; + subtitle?: string; + /** Breadcrumb trail — pass only on nested pages (details, sub-resources). */ + breadcrumbs?: BreadcrumbItem[]; + /** Route to return to; renders a back arrow before the title. */ + backTo?: string; + /** Inline content beside the title (e.g. status badges). */ + meta?: ReactNode; + /** Right-aligned actions — the primary CTA lives here. */ + action?: ReactNode; +} + +/** + * Unified page header: optional breadcrumbs, a title (with optional back arrow + * and inline meta), a subtitle, and a right-aligned action slot. Keeps title / + * action placement and spacing identical across every dashboard page. + */ +export function PageHeader({ + title, + subtitle, + breadcrumbs, + backTo, + meta, + action, +}: PageHeaderProps) { + const navigate = useNavigate(); + + return ( + + {breadcrumbs?.length ? : null} + + + + {backTo ? ( + navigate(backTo)} + aria-label="Go back" + > + + + ) : null} + +
+ + + {title} + + {meta} + + {subtitle ? ( + + {subtitle} + + ) : null} +
+
+ + {action ? ( + + {action} + + ) : null} +
+
+ ); +} + +export default PageHeader; diff --git a/apps/edr-freight-web/backoffice/src/components/page/index.ts b/apps/edr-freight-web/backoffice/src/components/page/index.ts new file mode 100644 index 000000000..8d455cf10 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/page/index.ts @@ -0,0 +1,6 @@ +export { PageContainer } from "./PageContainer"; +export type { PageContainerProps } from "./PageContainer"; +export { PageHeader } from "./PageHeader"; +export type { PageHeaderProps } from "./PageHeader"; +export { KpiStrip } from "./KpiStrip"; +export type { KpiItem, KpiStripProps } from "./KpiStrip"; diff --git a/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/DropdownSettingsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/DropdownSettingsPage.tsx index 7edaeb7b1..d2689c750 100644 --- a/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/DropdownSettingsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/DropdownSettingsPage.tsx @@ -1,12 +1,10 @@ -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { - AlertCircle, Boxes, CheckCircle2, Eye, Filter, ListOrdered, - Loader2, MoreHorizontal, Pencil, Plus, @@ -16,8 +14,20 @@ import { Sparkles, Trash2, } from "lucide-react"; +import { + ActionIcon, + Badge, + Button, + Card, + Code, + Group, + Menu, + Text, + TextInput, + ThemeIcon, +} from "@mantine/core"; -import Breadcrumbs from "@/components/ui/Breadcrumbs"; +import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; import EditDropdownSettingDialog from "./EditDropdownSettingDialog"; import ManageDropdownOptionsDialog from "./ManageDropdownOptionsDialog"; import DeleteDropdownSettingDialog from "./DeleteDropdownSettingDialog"; @@ -30,18 +40,6 @@ import { DataTableFooter, type ColumnDef, usePagination, - Button, - Card, - CardHeader, - CardTitle, - CardDescription, - CardContent, - Input, - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, } from "@edr/ui-common"; type ActiveDialog = "edit" | "options" | "delete"; @@ -50,40 +48,17 @@ export default function DropdownSettingsPage() { const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [query, setQuery] = useState(""); + const [createOpen, setCreateOpen] = useState(false); const [activeDialog, setActiveDialog] = useState(null); const [activeSetting, setActiveSetting] = useState( null, ); const openDialogFor = (dialog: ActiveDialog, setting: DropdownSetting) => { - // Defer past the DropdownMenu's close cycle. Radix's modal lock can leave - // `pointer-events: none` on when a menu closes and a dialog opens - // in the same frame — wait two RAFs and then explicitly reset the body - // style so the dialog interior is interactive. - requestAnimationFrame(() => { - requestAnimationFrame(() => { - document.body.style.pointerEvents = ""; - setActiveSetting(setting); - setActiveDialog(dialog); - }); - }); + setActiveSetting(setting); + setActiveDialog(dialog); }; - const closeDialog = () => { - setActiveDialog(null); - // Keep activeSetting briefly so dialog content doesn't flash empty during - // the close animation; cleared on next open. - }; - - // Belt-and-suspenders for the Radix pointer-events leak: any time the active - // dialog changes, schedule a body-style cleanup after the next paint. - useEffect(() => { - const id = requestAnimationFrame(() => { - if (document.body.style.pointerEvents === "none") { - document.body.style.pointerEvents = ""; - } - }); - return () => cancelAnimationFrame(id); - }, [activeDialog]); + const closeDialog = () => setActiveDialog(null); const { data, isLoading, isError, error } = useQuery( api.dropdownSettings.list.queryOptions(), @@ -138,41 +113,38 @@ export default function DropdownSettingsPage() { cell: ({ row }) => { const s = row.original; return ( -
-
- -
-
-

{s.label}

-

+ + + + +

+ + {s.label} + + {s.description ?? "No description"} -

+
-
+ ); }, }, { id: "code", header: "Code", - cell: ({ row }) => ( - - {row.original.code} - - ), + cell: ({ row }) => {row.original.code}, }, { id: "options", header: "Options", - cell: ({ row }) => { - const s = row.original; - return ( -
- - {s.children?.length ?? 0} -
- ); - }, + cell: ({ row }) => ( + + + + {row.original.children?.length ?? 0} + + + ), }, { id: "behavior", @@ -180,15 +152,21 @@ export default function DropdownSettingsPage() { cell: ({ row }) => { const s = row.original; return ( -
- {s.multiple ? ( - - ) : ( - - )} - {s.meta?.searchable ? : null} - {s.meta?.clearable ? : null} -
+ + + {s.multiple ? "Multi" : "Single"} + + {s.meta?.searchable ? ( + + Searchable + + ) : null} + {s.meta?.clearable ? ( + + Clearable + + ) : null} + ); }, }, @@ -196,24 +174,27 @@ export default function DropdownSettingsPage() { id: "permissions", header: "Permissions", cell: ({ row }) => { - const s = row.original; - const perms = s.meta?.permissions ?? []; + const perms = row.original.meta?.permissions ?? []; + if (perms.length === 0) { + return ( + + — + + ); + } return ( -
- {perms.length === 0 ? ( - - ) : ( - perms.map((p) => ( - - - {p} - - )) - )} -
+ + {perms.map((p) => ( + } + > + {p} + + ))} + ); }, }, @@ -223,178 +204,136 @@ export default function DropdownSettingsPage() { cell: ({ row }) => { const setting = row.original; return ( -
e.stopPropagation()} - > - - - - - - - - View - - openDialogFor("options", setting)} + e.stopPropagation()}> + + + + + + + + }>View + } + onClick={() => openDialogFor("options", setting)} > - Options - - - openDialogFor("edit", setting)} + + + } + onClick={() => openDialogFor("edit", setting)} > - Edit - - - openDialogFor("delete", setting)} - variant="destructive" + + + } + color="red" + onClick={() => openDialogFor("delete", setting)} > - Delete - - - -
+ + + + ); }, }, ]; return ( -
-
- - - -
-

- Dropdown Settings -

-

- Manage every dynamic dropdown across the platform — labels, - options, ordering, and permissions. -

-
- -
-
- - { - setQuery(e.target.value); - setPagination({ - pageIndex: 0, - pageSize: pagination.pageSize, - }); - }} - placeholder="Search by code, label, description..." - className="pl-8!" - /> -
- - - - -
-
- -
- } - /> - } - /> - } - /> - } - /> -
- - {isError ? ( - - - - Failed to load dropdown settings.{" "} - {error instanceof Error ? error.message : "Unknown error."} - - - ) : null} - - - -
- Registered Dropdowns - - Every dynamic dropdown the platform reads from. - -
- - -
+ + } + /> - - {isLoading ? ( -
- - Loading dropdown settings… -
- ) : ( - { }} - pagination={{ - pageIndex: pagination.pageIndex, - pageSize: pagination.pageSize, - pageCount: pageCount, - totalCount: total, - }} - tableOptions={{ - state: { pagination }, - onPaginationChange: setPagination, - }} - containerClassName="border-b shadow-none" - footer={DataTableFooter} - /> - )} -
-
-
+ - {/* Controlled dialogs — hoisted out of the DropdownMenu so they can open - reliably after a menu item is selected. */} + + +
+ + Registered Dropdowns + + + Every dynamic dropdown the platform reads from. + +
+ +
+ + +
+ + {/* Create — fully controlled, no shadcn trigger child. */} + + + {/* Controlled row-action dialogs. */} {activeSetting ? ( <> ) : null} -
- ); -} - -function StatCard({ - label, - value, - icon, -}: { - label: string; - value: number; - icon: React.ReactNode; -}) { - return ( - - -
-

{label}

-

{value}

-
-
- {icon} -
-
-
- ); -} - -function BehaviorChip({ - label, - muted = false, -}: { - label: string; - muted?: boolean; -}) { - return ( - - {label} - + ); } diff --git a/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx index af92d38fd..798077020 100644 --- a/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx @@ -1,18 +1,14 @@ -import { useMemo, useState } from "react"; import { ActionIcon, - Box, Card, - Container, Group, - Paper, + Badge as MantineBadge, Select, Stack, Tabs, Text, TextInput, } from "@mantine/core"; -import { Badge as MantineBadge } from "@mantine/core"; import { CheckCircle2, CircleDollarSign, @@ -22,23 +18,18 @@ import { Search, X, XCircle, - type LucideIcon, } from "lucide-react"; +import { useMemo, useState } from "react"; -import Breadcrumbs from "@/components/ui/Breadcrumbs"; -import "@/components/overview/overview.css"; +import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; import { usePaymentList, usePaymentSummary } from "@/hooks/usePayments"; -import type { - PaymentMethod, - PaymentRow, -} from "@/services/payments.service"; -import { cn } from "@/lib/utils"; +import type { PaymentMethod, PaymentRow } from "@/services/payments.service"; import { Badge, DataTable, DataTableFooter, - type ColumnDef, usePagination, + type ColumnDef, } from "@edr/ui-common"; const STATUS_TABS = [ @@ -75,57 +66,6 @@ const STATUS_COLORS: Record = { refunded: "indigo", }; -function StatCard({ - icon: Icon, - label, - value, - accent, -}: { - icon: LucideIcon; - label: string; - value: string | number; - accent: string; -}) { - return ( - - - - - - - - {value} - - - {label} - - - - - ); -} - function formatAmount(amount: number, currency: string): string { return `${currency} ${Number(amount).toLocaleString(undefined, { minimumFractionDigits: 2, @@ -172,8 +112,6 @@ export default function PaymentsPage() { const total = data?.total ?? 0; const pageCount = Math.max(1, Math.ceil(total / pagination.pageSize)); - const val = (n?: number) => (summaryLoading ? "—" : (n ?? 0)); - const tabCounts: Record = { all: summary === undefined @@ -248,177 +186,152 @@ export default function PaymentsPage() { ]; return ( -
- - + + - - - + + { + setStatusTab((value as StatusTabKey) ?? "all"); + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + }} + variant="pills" + color="edr-green" + keepMounted={false} + > + + {STATUS_TABS.map((t) => { + const isActive = statusTab === t.key; + const count = tabCounts[t.key]; + const Icon = t.icon; + return ( + } + rightSection={ + count !== undefined ? ( + + {count} + + ) : undefined + } + > + {t.label} + + ); + })} + + + + + + + } + value={query} + onChange={(e) => { + setQuery(e.target.value); + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + }} + rightSection={ + query && ( + setQuery("")} + > + + + ) } - accent="teal" + style={{ flex: 1, minWidth: "200px" }} /> - - - - { + setMethod(value); + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + }} + style={{ minWidth: 180 }} /> + + {total} record{total !== 1 ? "s" : ""} + - { - setStatusTab((value as StatusTabKey) ?? "all"); - setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); - }} - variant="pills" - color="green" - keepMounted={false} - classNames={{ list: "ov-tablist", tab: "ov-tab" }} - > - - {STATUS_TABS.map((t) => { - const isActive = statusTab === t.key; - const count = tabCounts[t.key]; - const Icon = t.icon; - return ( - } - rightSection={ - count !== undefined ? ( - - {count} - - ) : undefined - } - > - {t.label} - - ); - })} - - - - - - - } - value={query} - onChange={(e) => { - setQuery(e.target.value); - setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); - }} - rightSection={ - query && ( - setQuery("")} - > - - - ) - } - style={{ flex: 1, minWidth: "200px" }} - radius="lg" - /> -