-
-
-
-
-
{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()}>
+
-
- 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.
+
+
+ }>
+ Filter
+
+
+
+
+
+
+ {/* 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 (
-
-
-
-
- {icon}
-
-
-
- );
-}
-
-function BehaviorChip({
- label,
- muted = false,
-}: {
- label: string;
- muted?: boolean;
-}) {
- return (
-
- {label}
-
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx
index 48ab5e429..9206372e4 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx
@@ -751,7 +751,7 @@ export function WagonTypesCrudPage() {
{type.lengthMeters}
{type.supportedLoadTypes?.join(', ') || '-'}
-
+
{type.isActive === false ? 'Inactive' : 'Active'}
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx
index f246ad194..6292a1764 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx
@@ -1,7 +1,18 @@
-import { useEffect, useMemo, useState } from "react";
-import { Navigate, useLocation } from "react-router-dom";
import type { ColumnDef } from "@edr/ui-common";
import { Box, Button, Card, Group, Modal, Select, Stack, Text } from "@mantine/core";
+import {
+ Archive,
+ Circle,
+ CircleCheck,
+ CircleSlash,
+ Layers,
+ Link2,
+ Plus,
+ Wrench,
+ type LucideIcon,
+} from "lucide-react";
+import { useEffect, useMemo, useState } from "react";
+import { Navigate, useLocation } from "react-router-dom";
import FleetCardGrid from "@/components/fleet/FleetCardGrid";
import FleetFormDialog from "@/components/fleet/FleetFormDialog";
@@ -9,16 +20,16 @@ import FleetRecordActions from "@/components/fleet/FleetRecordActions";
import FleetToolbar from "@/components/fleet/FleetToolbar";
import { formatFleetCell, registerFleetOptionLabels } from "@/components/fleet/fleetFormat";
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode";
+import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
+import { useFleetList, useFleetMutations } from "@/hooks/fleet/useFleet";
import { useCargoTypes } from "@/hooks/use-cargo-types";
import { useContainerTypes } from "@/hooks/use-container-types";
-import { useWagonTypes } from "@/hooks/use-wagon-types";
-import { useFleetList, useFleetMutations } from "@/hooks/fleet/useFleet";
-import { useContainers } from "@/hooks/useContainers";
import { useToast } from "@/hooks/use-toast";
+import { useWagonTypes } from "@/hooks/use-wagon-types";
+import { useContainers } from "@/hooks/useContainers";
import { useRouteYards } from "@/hooks/useRoutes";
import { useWagons } from "@/hooks/useWagons";
-import type { FleetListFilters } from "@/services/fleet/fleet.service";
import {
FLEET_SELECT_NONE,
getFleetResource,
@@ -26,11 +37,27 @@ import {
type FleetFormFieldDef,
type FleetResourceSlug,
} from "@/pages/fleet/config/resources";
-import type { FleetRecord } from "@/services/fleet/fleet.service";
+import type { FleetListFilters, FleetRecord } from "@/services/fleet/fleet.service";
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
const DEFAULT_SLUG: FleetResourceSlug = "locomotives";
+const FLEET_STATUS_META: Record<
+ string,
+ { label: string; icon: LucideIcon; color: string }
+> = {
+ AVAILABLE: { label: "Available", icon: CircleCheck, color: "edr-green" },
+ ASSIGNED: { label: "Assigned", icon: Link2, color: "blue" },
+ MAINTENANCE: { label: "Maintenance", icon: Wrench, color: "yellow" },
+ OUT_OF_SERVICE: { label: "Out of service", icon: CircleSlash, color: "red" },
+ RETIRED: { label: "Retired", icon: Archive, color: "gray" },
+};
+
+const humanizeStatus = (status: string) => {
+ const text = status.replace(/_/g, " ").toLowerCase();
+ return text.charAt(0).toUpperCase() + text.slice(1);
+};
+
const FleetResourcePage = () => {
const location = useLocation();
const slug = getFleetSlugFromPath(location.pathname) ?? DEFAULT_SLUG;
@@ -249,6 +276,39 @@ const FleetResourcePage = () => {
const tableStatus = isLoading ? "loading" : isError ? "error" : "success";
+ const kpiItems = useMemo(() => {
+ const items = [
+ {
+ label: `Total ${config?.label.toLowerCase() ?? ""}`,
+ value: allRows.length,
+ icon: Layers,
+ color: "edr-green",
+ },
+ ];
+ if (hasStatusColumn) {
+ const counts = new Map
();
+ for (const row of allRows) {
+ const status = String(
+ (row as unknown as Record).status ?? "",
+ );
+ if (status) counts.set(status, (counts.get(status) ?? 0) + 1);
+ }
+ const top = [...counts.entries()]
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 4);
+ for (const [status, count] of top) {
+ const meta = FLEET_STATUS_META[status];
+ items.push({
+ label: meta?.label ?? humanizeStatus(status),
+ value: count,
+ icon: meta?.icon ?? Circle,
+ color: meta?.color ?? "gray",
+ });
+ }
+ }
+ return items.slice(0, 5);
+ }, [allRows, hasStatusColumn, config?.label]);
+
if (!config) {
return ;
}
@@ -291,8 +351,26 @@ const FleetResourcePage = () => {
const itemLabel = config.label.toLowerCase();
return (
-
-
+
+ }
+ onClick={() => {
+ setEditing(null);
+ setFormOpen(true);
+ }}
+ >
+ {config.addLabel}
+
+ }
+ />
+
+
+
+
{
onSearchChange={setSearch}
searchPlaceholder={config.searchPlaceholder}
showSearch={config.supportsSearch}
- addLabel={config.addLabel}
- onAdd={() => {
- setEditing(null);
- setFormOpen(true);
- }}
viewMode={viewMode}
onViewModeChange={setViewMode}
filters={
@@ -315,7 +388,6 @@ const FleetResourcePage = () => {
key={filter.key}
size="sm"
radius="lg"
- label={filter.label}
value={filter.value}
onChange={(v) => {
if (!v) return;
@@ -435,7 +507,7 @@ const FleetResourcePage = () => {
-
+
);
};
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx
index bd4a3364c..543ae26db 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx
@@ -1,5 +1,5 @@
import { FormEvent, useMemo, useState } from "react";
-import { Edit, Eye, Trash2 } from "lucide-react";
+import { Ban, CircleCheck, Edit, Eye, Plus, Route as RouteIcon, Trash2 } from "lucide-react";
import type { ColumnDef } from "@edr/ui-common";
import {
ActionIcon,
@@ -17,6 +17,7 @@ import {
Tooltip,
} from "@mantine/core";
+import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
import FleetToolbar from "@/components/fleet/FleetToolbar";
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode";
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
@@ -101,6 +102,9 @@ export default function RoutesPage() {
return filteredRoutes.slice(start, start + pagination.pageSize);
}, [filteredRoutes, pagination.pageIndex, pagination.pageSize]);
+ const allRoutes = routesQuery.data ?? [];
+ const activeCount = allRoutes.filter((route) => route.isActive).length;
+
const yardOptions = useMemo(
() =>
(yardsQuery.data ?? []).map((yard) => ({
@@ -241,7 +245,7 @@ export default function RoutesPage() {
header: "Status",
meta: { headerClassName, cellClassName },
cell: ({ row }) => (
-
+
{row.original.isActive ? "Active" : "Inactive"}
),
@@ -279,16 +283,38 @@ export default function RoutesPage() {
}, [deactivateMutation.isPending]);
return (
-
-
+
+ } onClick={openCreate}>
+ Add route
+
+ }
+ />
+
+
+
+
@@ -338,7 +364,7 @@ export default function RoutesPage() {
{route.name}
-
+
{route.isActive ? "Active" : "Inactive"}
@@ -431,7 +457,7 @@ export default function RoutesPage() {
Cancel
-
+
Save
@@ -484,6 +510,6 @@ export default function RoutesPage() {
) : null}
-
+
);
}
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..e5a490927 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,15 @@
-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 +19,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 = [
@@ -67,7 +59,7 @@ const METHOD_OPTIONS: { value: PaymentMethod; label: string }[] = [
];
const STATUS_COLORS: Record = {
- success: "green",
+ success: "edr-green",
processing: "yellow",
"action-required": "yellow",
failed: "red",
@@ -75,57 +67,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 +113,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 +187,155 @@ export default function PaymentsPage() {
];
return (
-
-
-
+
+
-
-
-
-
-
-
-
-
+
- {
- 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}
-
- );
- })}
-
-
+ {
+ 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("")}
- >
-
-
- )
- }
- style={{ flex: 1, minWidth: "200px" }}
- radius="lg"
- />
-
+
+
+
+
+ }
+ value={query}
+ onChange={(e) => {
+ setQuery(e.target.value);
+ setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
+ }}
+ rightSection={
+ query && (
+ setQuery("")}
+ >
+
+
+ )
+ }
+ style={{ flex: 1, minWidth: "200px" }}
+ />
+
+
-
-
-
-
-
+
+
+
-
-
+
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx
index f03fb199e..783c0ffd4 100644
--- a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx
@@ -3,9 +3,10 @@ import { Navigate, useLocation, useParams } from "react-router-dom";
import { useAuth } from "@/auth/useAuth";
import { canAccessRuleEngineResource } from "@/lib/permissions";
import type { ColumnDef } from "@tanstack/react-table";
-import { Loader2 } from "lucide-react";
-import { Card, Button, Modal, Stack, Group, Text, List } from "@mantine/core";
+import { Box, Card, Button, Modal, Stack, Group, Text, List, Loader } from "@mantine/core";
+import { Plus } from "lucide-react";
+import { PageContainer, PageHeader } from "@/components/page";
import RuleEngineCardGrid from "@/components/ruleEngine/RuleEngineCardGrid";
import RuleEngineFormDialog from "@/components/ruleEngine/RuleEngineFormDialog";
import ManageRuleEngineOrderDialog from "@/components/ruleEngine/ManageRuleEngineOrderDialog";
@@ -325,31 +326,44 @@ const RuleEngineResourcePage = () => {
};
const itemLabel = config.label.toLowerCase();
+ const addLabel = `Add ${config.label.replace(/s$/, "")}`;
return (
-
-
-
- {
- setSearch(v);
- setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
- }
- : undefined
- }
- showSearch={Boolean(config.supportsSearch)}
- searchPlaceholder={config.searchPlaceholder}
- onAdd={canManage ? openCreate : undefined}
- addLabel={`Add ${config.label.replace(/s$/, "")}`}
- onManageOrder={
- canManage && config.orderConfig ? () => setOrderDialogOpen(true) : undefined
- }
- viewMode={viewMode}
- onViewModeChange={setViewMode}
- />
+
+ } onClick={openCreate}>
+ {addLabel}
+
+ ) : undefined
+ }
+ />
+
+
+
+
+ {
+ setSearch(v);
+ setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
+ }
+ : undefined
+ }
+ showSearch={Boolean(config.supportsSearch)}
+ searchPlaceholder={config.searchPlaceholder}
+ onManageOrder={
+ canManage && config.orderConfig ? () => setOrderDialogOpen(true) : undefined
+ }
+ viewMode={viewMode}
+ onViewModeChange={setViewMode}
+ />
+
{viewMode === "table" ? (
{
state: { pagination },
onPaginationChange: setPagination,
}}
- containerClassName="border-0 shadow-none [&_[data-slot=table-row]]:border-border"
- footerClassName="border-t border-border bg-card"
+ containerClassName="border-0 shadow-none bg-transparent"
footer={({ table, pagination: footerPagination }) => (
{
{
if (!deleteTarget) return;
remove.mutate(deleteTarget.id, {
onSuccess: () => setDeleteTarget(null),
});
}}
- leftSection={remove.isPending && }
>
- {remove.isPending ? "Deleting..." : "Delete"}
+ Delete
@@ -498,7 +510,7 @@ const RuleEngineResourcePage = () => {
{chainLoading ? (
-
+
) : (
<>
@@ -524,7 +536,7 @@ const RuleEngineResourcePage = () => {
)}
-
+
);
};
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 4ee2a7d96..bc6e5bb15 100644
--- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchBoardPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchBoardPage.tsx
@@ -1,12 +1,13 @@
import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
+ ActionIcon,
Alert,
Box,
Button,
Card,
- Container,
Group,
+ Menu,
Paper,
RingProgress,
Select,
@@ -21,7 +22,9 @@ import {
ArrowRight,
CalendarClock,
CalendarDays,
+ Eye,
Inbox,
+ MoreHorizontal,
Package,
Ruler,
Train,
@@ -31,7 +34,7 @@ import {
import type { ColumnDef } from "@edr/ui-common";
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
-import Breadcrumbs from "@/components/ui/Breadcrumbs";
+import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
import FleetToolbar from "@/components/fleet/FleetToolbar";
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode";
import {
@@ -40,7 +43,7 @@ import {
totalBookingCount,
WindowStatusPill,
} from "@/components/trainScheduling/batchVisuals";
-import { RouteCorridor, StatTile } from "@/components/trainScheduling/scheduleVisuals";
+import { RouteCorridor } from "@/components/trainScheduling/scheduleVisuals";
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
import { FREIGHT_BRAND, FREIGHT_BRAND_DARK } from "@/theme/freight-brand";
import { useBatchBoard } from "@/hooks/trainScheduling/useTrainScheduling";
@@ -365,7 +368,7 @@ function CardSkeleton() {
export default function BatchBoardPage() {
const navigate = useNavigate();
- const { data, isLoading, isFetching, refetch } = useBatchBoard();
+ const { data, isLoading, isError, isFetching, refetch } = useBatchBoard();
const { viewMode, setViewMode } = useFleetViewMode("batch-board");
const { pagination, setPagination } = usePagination({ pageSize: 10 });
const [search, setSearch] = useState("");
@@ -513,12 +516,12 @@ export default function BatchBoardPage() {
style={{
padding: "2px 8px",
borderRadius: 8,
- background: "var(--mantine-color-green-0)",
- border: "1px solid var(--mantine-color-green-1)",
+ background: "var(--mantine-color-edr-green-0)",
+ border: "1px solid var(--mantine-color-edr-green-1)",
}}
>
-
-
+
+
{row.original.capacity.allocatedWagons}
@@ -549,72 +552,69 @@ export default function BatchBoardPage() {
header: "",
meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` },
cell: ({ row }) => (
-
- }
- onClick={() =>
- navigate(`/dashboard/operations/batch-board/${row.original.scheduleId}`)
- }
- >
- View windows
-
+ e.stopPropagation()}>
+
),
},
];
}, [navigate]);
- const tableStatus = isLoading ? "loading" : "success";
+ const tableStatus = isLoading ? "loading" : isError ? "error" : "success";
return (
-
-
+
+ void refetch()}>
+ Refresh
+
+ }
+ />
-
-
-
-
-
+
-
+
+ navigate(`/dashboard/operations/batch-board/${schedule.scheduleId}`)
+ }
+ error={
+ isError
+ ? {
+ message: "Failed to load the batch board.",
+ onRetry: () => void refetch(),
+ }
+ : undefined
+ }
emptyMessage="No active schedules"
pagination={{
pageIndex: pagination.pageIndex,
@@ -688,7 +699,6 @@ export default function BatchBoardPage() {
borderRadius: 20,
background: "white",
border: "1px solid var(--mantine-color-gray-2)",
- boxShadow: "0 4px 12px rgba(15,23,42,0.06)",
}}
>
@@ -711,18 +721,6 @@ export default function BatchBoardPage() {
)}
-
-
- void refetch()}
- >
- Refresh
-
-
-
+
);
}
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 311f76124..f78639359 100644
--- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchScheduleDetailPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchScheduleDetailPage.tsx
@@ -1,5 +1,4 @@
import { useEffect, useMemo, useState } from "react";
-import type { ReactNode } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
Accordion,
@@ -8,13 +7,10 @@ import {
Badge,
Box,
Button,
- Container,
Group,
Loader,
Paper,
- SimpleGrid,
Stack,
- Table,
Tabs,
Text,
ThemeIcon,
@@ -41,8 +37,11 @@ import {
Weight,
XCircle,
} from "lucide-react";
-import type { LucideIcon } from "lucide-react";
+import { DataTable, type ColumnDef } from "@edr/ui-common";
+
+import { KpiStrip, PageContainer } from "@/components/page";
+import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
import { TrainConsistView, CompositionBookingTabs } from "@/components/trainScheduling/compositionEditor";
@@ -52,8 +51,6 @@ import {
totalBookingCount,
WindowStatusPill,
} from "@/components/trainScheduling/batchVisuals";
-import { MiniRing, MiniSparkline } from "@/components/common/MiniGraph";
-import type { OverviewAccent } from "@/components/overview/overview.styles";
import { RouteCorridor } from "@/components/trainScheduling/scheduleVisuals";
import {
useBatchBoardDetail,
@@ -72,7 +69,7 @@ const STATE_META: Record<
BatchBoardBookingState,
{ label: string; color: string; icon: typeof CheckCircle2 }
> = {
- ALLOCATED: { label: "Allocated", color: "green", icon: CheckCircle2 },
+ ALLOCATED: { label: "Allocated", color: "edr-green", icon: CheckCircle2 },
SELECTED_FOR_BATCH: { label: "Selected for batch", color: "orange", icon: Clock },
READY: { label: "Ready for batch", color: "teal", icon: Hourglass },
WAITING: { label: "Paid · waiting", color: "blue", icon: Hourglass },
@@ -84,7 +81,7 @@ const ALLOC_META: Record<
BookingAllocationStatus,
{ label: string; color: string }
> = {
- ASSIGNED: { label: "Wagons assigned", color: "green" },
+ ASSIGNED: { label: "Wagons assigned", color: "edr-green" },
NOT_ATTEMPTED: { label: "Not allocated", color: "gray" },
DEFERRED: { label: "Deferred", color: "orange" },
FAILED: { label: "Allocation failed", color: "red" },
@@ -151,140 +148,153 @@ function AllocationBadge({
);
}
-function BookingTable({ bookings }: { bookings: BatchBoardBookingDetail[] }) {
- if (!bookings.length) {
- return (
-
- No bookings in this batch window.
+const bookingCellMeta = {
+ headerClassName: ruleEngineTable.headerCell,
+ cellClassName: ruleEngineTable.bodyCell,
+};
+
+const BOOKING_COLUMNS: ColumnDef[] = [
+ {
+ id: "reference",
+ header: "Reference",
+ meta: bookingCellMeta,
+ cell: ({ row }) => {
+ const b = row.original;
+ return (
+
+
+ {b.reference}
+
+ {b.isGovernment ? (
+
+ Gov
+
+ ) : null}
+
+ );
+ },
+ },
+ {
+ id: "customer",
+ header: "Customer",
+ meta: bookingCellMeta,
+ cell: ({ row }) => {
+ const b = row.original;
+ return (
+
+
+
+ {initials(b.company)}
+
+
+
+ {b.company}
+
+
+ );
+ },
+ },
+ {
+ id: "contractSigned",
+ header: "Contract signed",
+ meta: bookingCellMeta,
+ cell: ({ row }) => (
+
+ {fmtDateTime(row.original.fullyExecutedAt)} EAT
- );
- }
-
- const th = (label: string) => (
-
- {label}
-
- );
+ ),
+ },
+ {
+ id: "selectedForBatch",
+ header: "Selected for batch",
+ meta: bookingCellMeta,
+ cell: ({ row }) => {
+ const b = row.original;
+ if (!b.selectedForBatchAt) {
+ return (
+
+ —
+
+ );
+ }
+ return (
+ <>
+
+ {fmtDateTime(b.selectedForBatchAt)} EAT
+
+ {b.paymentDeadline ? (
+
+ Pay by {fmtDateTime(b.paymentDeadline)} EAT
+
+ ) : null}
+ >
+ );
+ },
+ },
+ {
+ id: "capacity",
+ header: "Capacity",
+ meta: bookingCellMeta,
+ cell: ({ row }) => {
+ const b = row.original;
+ return (
+
+
+ {b.wagons}w
+
+
+ {fmtTons(b.weightTons)}
+
+
+ );
+ },
+ },
+ {
+ id: "state",
+ header: "Batch state",
+ meta: bookingCellMeta,
+ cell: ({ row }) => ,
+ },
+ {
+ id: "allocation",
+ header: "Wagon allocation",
+ meta: bookingCellMeta,
+ cell: ({ row }) => (
+
+ ),
+ },
+];
+function BookingTable({ bookings }: { bookings: BatchBoardBookingDetail[] }) {
return (
-
-
-
-
- {th("Reference")}
- {th("Customer")}
- {th("Contract signed")}
- {th("Selected for batch")}
- {th("Capacity")}
- {th("Batch state")}
- {th("Wagon allocation")}
-
-
-
- {bookings.map((b) => (
-
-
-
-
- {b.reference}
-
- {b.isGovernment ? (
-
- Gov
-
- ) : null}
-
-
-
-
-
-
- {initials(b.company)}
-
-
-
- {b.company}
-
-
-
-
-
- {fmtDateTime(b.fullyExecutedAt)} EAT
-
-
-
- {b.selectedForBatchAt ? (
- <>
-
- {fmtDateTime(b.selectedForBatchAt)} EAT
-
- {b.paymentDeadline ? (
-
- Pay by {fmtDateTime(b.paymentDeadline)} EAT
-
- ) : null}
- >
- ) : (
-
- —
-
- )}
-
-
-
-
- {b.wagons}w
-
-
- {fmtTons(b.weightTons)}
-
-
-
-
-
-
-
-
-
-
- ))}
-
-
-
+
);
}
function WindowCountChips({ counts }: { counts: BatchWindowGroup["counts"] }) {
const chips: Array<{ value: number; color: string; label: string }> = [
- { value: counts.allocated, color: "green", label: "allocated" },
+ { value: counts.allocated, color: "edr-green", label: "allocated" },
{ value: counts.selectedForBatch, color: "orange", label: "selected" },
{ value: counts.ready, color: "teal", label: "ready" },
{ value: counts.waiting, color: "blue", label: "waiting" },
@@ -415,66 +425,6 @@ function WindowAccordionItem({ window }: { window: BatchWindowGroup }) {
);
}
-/**
- * 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 ringAccent: OverviewAccent =
- pct == null ? "gold" : pct >= 100 ? "rose" : pct >= 85 ? "orange" : "gold";
-
- return (
-
-
-
-
-
-
-
-
- {value}
-
-
- {label}
- {sub ? ` · ${sub}` : ""}
-
-
- {pct != null ? (
-
-
- {Math.round(pct)}%
-
-
- ) : null}
-
-
- {pct == null ? (
-
- ) : null}
-
-
- );
-}
-
export default function BatchScheduleDetailPage() {
const { scheduleId } = useParams<{ scheduleId: string }>();
const navigate = useNavigate();
@@ -621,11 +571,11 @@ export default function BatchScheduleDetailPage() {
if (isLoading || !data) {
return (
-
+
-
+
-
+
);
}
@@ -641,7 +591,7 @@ export default function BatchScheduleDetailPage() {
const totalBookings = totalBookingCount(data.counts);
return (
-
+
-
+
Overview
@@ -673,7 +623,7 @@ export default function BatchScheduleDetailPage() {
>
Back
-
+
{data.trainNumber ?? data.routeName ?? "Schedule"}
@@ -715,7 +665,7 @@ export default function BatchScheduleDetailPage() {
Refresh
}
loading={runAllocation.isPending}
@@ -725,7 +675,7 @@ export default function BatchScheduleDetailPage() {
}
onClick={() =>
@@ -743,41 +693,36 @@ export default function BatchScheduleDetailPage() {
) : null}
-
-
-
-
-
-
+ : fmtTons(data.capacity.usedWeightTons),
+ icon: Weight,
+ },
+ {
+ label: "Bookings",
+ value: totalBookings,
+ hint: `${data.counts.allocated} allocated · ${data.counts.expired} expired`,
+ icon: Package,
+ },
+ ]}
+ />
{/* Booking pipeline */}
-
+
-
+
Batch windows (EAT)
@@ -1054,7 +987,7 @@ export default function BatchScheduleDetailPage() {
style={{ borderColor: "var(--mantine-color-gray-2)" }}
>
-
+
Loading train composition…
@@ -1063,6 +996,6 @@ export default function BatchScheduleDetailPage() {
)}
-
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx
index 6f85b82d6..caed00435 100644
--- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx
@@ -24,6 +24,7 @@ import {
Title,
} from "@mantine/core";
+import { PageContainer } from "@/components/page";
import { RouteCorridorTrack } from "@/components/trainScheduling/RouteCorridorTrack";
import { RouteCorridor, StatusPill } from "@/components/trainScheduling/scheduleVisuals";
import { freightBrand } from "@/theme/freight-brand";
@@ -62,7 +63,7 @@ function MetaStat({
}) {
return (
-
+
{icon}
@@ -85,18 +86,22 @@ export default function TrainScheduleTrackPage() {
if (trackQuery.isLoading) {
return (
-
-
-
+
+
+
+
+
);
}
const track = trackQuery.data;
if (!track || !scheduleId) {
return (
-
- Tracking data not found.
-
+
+
+ Tracking data not found.
+
+
);
}
@@ -130,7 +135,7 @@ export default function TrainScheduleTrackPage() {
};
return (
-
+
{/* Header */}
-
+
@@ -169,7 +174,7 @@ export default function TrainScheduleTrackPage() {
Train tracking
{track.trainNumber ? (
-
+
{track.trainNumber}
) : null}
@@ -193,7 +198,7 @@ export default function TrainScheduleTrackPage() {
Journey progress
-
+
{reached} / {totalStations} stations · {Math.round(clampedPct)}%
@@ -201,7 +206,7 @@ export default function TrainScheduleTrackPage() {
value={clampedPct}
size="lg"
radius="xl"
- color="green"
+ color="edr-green"
striped={track.status === "DISPATCHED"}
animated={track.status === "DISPATCHED"}
/>
@@ -230,10 +235,10 @@ export default function TrainScheduleTrackPage() {
{/* Corridor */}
-
+
-
+
@@ -264,9 +269,9 @@ export default function TrainScheduleTrackPage() {
{/* Checkpoint log */}
-
+
-
+
@@ -292,7 +297,7 @@ export default function TrainScheduleTrackPage() {
) : (
-
+
{track.checkpoints.map((cp) => (
{cp.kind}
@@ -326,6 +331,6 @@ export default function TrainScheduleTrackPage() {
)}
-
+
);
}
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 834066e2f..e2618510a 100644
--- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx
@@ -1,5 +1,17 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { Link, useParams } from "react-router-dom";
+import {
+ Badge,
+ Box,
+ Button,
+ Checkbox,
+ Group,
+ Loader,
+ Paper,
+ RingProgress,
+ Stack,
+ Text,
+ ThemeIcon,
+ Title,
+} from "@mantine/core";
import { isAxiosError } from "axios";
import {
ArrowLeft,
@@ -15,48 +27,35 @@ import {
Train,
Weight,
} from "lucide-react";
-import {
- Badge,
- Box,
- Button,
- Checkbox,
- Group,
- Loader,
- Paper,
- RingProgress,
- SimpleGrid,
- Stack,
- Text,
- ThemeIcon,
- Title,
-} from "@mantine/core";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { Link, useParams } from "react-router-dom";
-import { ContainerPlacementGrid } from "@/components/trainScheduling/ContainerPlacementGrid";
+import { KpiStrip, PageContainer } from "@/components/page";
import {
autoFillPlacements,
mergePlacementsWithSaved,
placementsFromScheduleWagons,
validateLocalPlacements,
} from "@/components/trainScheduling/containerPlacement.util";
+import { ContainerPlacementGrid } from "@/components/trainScheduling/ContainerPlacementGrid";
import { FleetAvailabilitySummary } from "@/components/trainScheduling/FleetAvailabilitySummary";
+import { RescheduleTrainDialog } from "@/components/trainScheduling/RescheduleTrainDialog";
+import { ScheduleBatchPanel } from "@/components/trainScheduling/ScheduleBatchPanel";
import { ScheduleBookingsStep } from "@/components/trainScheduling/ScheduleBookingsStep";
+import { FreightTypeBadge } from "@/components/trainScheduling/ScheduleStatusBadge";
+import {
+ RouteCorridor,
+ StatusPill,
+ scheduleBrand,
+} from "@/components/trainScheduling/scheduleVisuals";
import {
PreviewSummary,
ScheduleWarningsAlert,
} from "@/components/trainScheduling/ScheduleWarningsAlert";
-import { FreightTypeBadge } from "@/components/trainScheduling/ScheduleStatusBadge";
-import {
- RouteCorridor,
- StatTile,
- StatusPill,
- scheduleBrand,
-} from "@/components/trainScheduling/scheduleVisuals";
-import { RescheduleTrainDialog } from "@/components/trainScheduling/RescheduleTrainDialog";
-import { ScheduleBatchPanel } from "@/components/trainScheduling/ScheduleBatchPanel";
-import { WorkflowRail, WorkflowStep } from "@/components/trainScheduling/WorkflowStep";
import { shouldShowContainerPlacementStep } from "@/components/trainScheduling/schedulingContainerStep.util";
-import { WagonPlanGrid } from "@/components/trainScheduling/WagonPlanGrid";
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
+import { WagonPlanGrid } from "@/components/trainScheduling/WagonPlanGrid";
+import { WorkflowRail, WorkflowStep } from "@/components/trainScheduling/WorkflowStep";
import {
useEligibleBookings,
useScheduleDetail,
@@ -267,17 +266,21 @@ export default function TrainScheduleV2DetailPage() {
if (detailQuery.isLoading) {
return (
-
-
-
+
+
+
+
+
);
}
if (!schedule || !scheduleId) {
return (
-
- Schedule not found
-
+
+
+ Schedule not found
+
+
);
}
@@ -405,7 +408,7 @@ export default function TrainScheduleV2DetailPage() {
return (
{previewResult.valid ? "Plan valid" : "Has issues"}
@@ -413,14 +416,14 @@ export default function TrainScheduleV2DetailPage() {
);
}
return allSelectedIds.length ? (
-
+
{allSelectedIds.length} selected
) : null;
}
if (key === "wagon" && displayWagonPlan.length) {
return (
-
+
{displayWagonPlan.length} wagons
);
@@ -429,7 +432,7 @@ export default function TrainScheduleV2DetailPage() {
return (
{containerUnits.length} units
@@ -485,7 +488,7 @@ export default function TrainScheduleV2DetailPage() {
size="sm"
/>
}
loading={preview.isPending}
@@ -532,7 +535,7 @@ export default function TrainScheduleV2DetailPage() {
{!hasContainerStep ? (
) : (
}
onClick={() => setActiveStep(2)}
@@ -578,7 +581,7 @@ export default function TrainScheduleV2DetailPage() {
{canEditBookings ? (
Ready to depart
Finalizing locks the plan and moves the schedule to{" "}
-
+
SCHEDULED
. Dispatch then begins rail movement and notifies the yard.
@@ -640,7 +643,7 @@ export default function TrainScheduleV2DetailPage() {
{canFinalize ? (
}
@@ -663,7 +666,7 @@ export default function TrainScheduleV2DetailPage() {
) : null}
{canDispatch ? (
}
@@ -695,7 +698,7 @@ export default function TrainScheduleV2DetailPage() {
};
return (
-
+
@@ -758,7 +755,7 @@ export default function TrainScheduleV2DetailPage() {
}
@@ -779,63 +776,12 @@ export default function TrainScheduleV2DetailPage() {
-
-
-
-
-
-
-
{previewResult ? (
@@ -855,17 +801,51 @@ export default function TrainScheduleV2DetailPage() {
-
+
+
+
{/* Workflow header with ring progress */}
-
+
@@ -882,9 +862,9 @@ export default function TrainScheduleV2DetailPage() {
size={64}
thickness={6}
roundCaps
- sections={[{ value: progressPct, color: "green" }]}
+ sections={[{ value: progressPct, color: "edr-green" }]}
label={
-
+
{progressPct}%
}
@@ -928,6 +908,6 @@ export default function TrainScheduleV2DetailPage() {
onComplete={() => void detailQuery.refetch()}
/>
) : null}
-
+
);
}
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 6a2e77287..d389009aa 100644
--- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx
@@ -1,12 +1,11 @@
-import { useEffect, useMemo, useState } from "react";
-import { useNavigate } from "react-router-dom";
-import { isAxiosError } from "axios";
import type { ColumnDef } from "@edr/ui-common";
import {
+ ActionIcon,
Box,
Button,
Card,
Group,
+ Menu,
Modal,
Select,
SimpleGrid,
@@ -15,26 +14,38 @@ import {
TextInput,
ThemeIcon,
} from "@mantine/core";
-import { ArrowRight, CalendarClock, Navigation, Send, Train, Weight } from "lucide-react";
+import { isAxiosError } from "axios";
+import {
+ ArrowRight,
+ Ban,
+ CalendarClock,
+ Eye,
+ MoreHorizontal,
+ Navigation,
+ Send,
+ Train,
+ Weight,
+} from "lucide-react";
+import { useEffect, useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
import FleetToolbar from "@/components/fleet/FleetToolbar";
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode";
+import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
+import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
+import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
import { FreightTypeBadge } from "@/components/trainScheduling/ScheduleStatusBadge";
import {
RouteCorridor,
- StatTile,
StatusPill,
- scheduleBrand,
} from "@/components/trainScheduling/scheduleVisuals";
-import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
-import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
-import { useRoutes } from "@/hooks/useRoutes";
import {
useAvailableLocomotives,
useScheduleList,
useScheduleMutations,
} from "@/hooks/trainScheduling/useTrainScheduling";
import { useToast } from "@/hooks/use-toast";
+import { useRoutes } from "@/hooks/useRoutes";
import type { TrainScheduleListItem } from "@/types/trainScheduling";
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
@@ -169,8 +180,8 @@ export default function TrainScheduleV2ListPage() {
width: 34,
height: 34,
borderRadius: 9,
- background: "var(--mantine-color-green-0)",
- color: "var(--mantine-color-green-7)",
+ background: "var(--mantine-color-edr-green-0)",
+ color: "var(--mantine-color-edr-green-7)",
flexShrink: 0,
}}
>
@@ -251,63 +262,67 @@ export default function TrainScheduleV2ListPage() {
},
{
id: "actions",
- header: "Actions",
+ size:32,
meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` },
- cell: ({ row }) => (
-
- }
- onClick={() =>
- navigate(`/dashboard/operations/train-scheduling-v2/${row.original.id}`)
- }
- >
- Open
-
- {["DISPATCHED", "ARRIVED"].includes(row.original.status) ? (
- }
- onClick={() =>
- navigate(
- `/dashboard/operations/train-scheduling-v2/${row.original.id}/track`,
- )
- }
- >
- Track
-
- ) : null}
- {["DRAFT", "SCHEDULED"].includes(row.original.status) ? (
- {
- try {
- await cancel.mutateAsync({
- id: row.original.id,
- freightType: row.original.freightType ?? "CONTAINER",
- });
- toast({ title: "Schedule cancelled" });
- } catch (err) {
- toast({
- title: "Cancel failed",
- description: parseError(err, "Could not cancel"),
- variant: "destructive",
- });
- }
- }}
- >
- Cancel
-
- ) : null}
-
- ),
+ cell: ({ row }) => {
+ const schedule = row.original;
+ return (
+ e.stopPropagation()}>
+
+
+ );
+ },
},
];
}, [navigate, cancel.isPending, cancel, toast]);
@@ -340,65 +355,33 @@ export default function TrainScheduleV2ListPage() {
: "success";
return (
-
-
- }
- onClick={() => setCreateOpen(true)}
- >
- New schedule
-
-
+
+ } onClick={() => setCreateOpen(true)}>
+ New schedule
+
+ }
+ />
-
-
-
-
-
-
+
-
+
setCreateOpen(true)}
viewMode={viewMode}
onViewModeChange={setViewMode}
filters={
@@ -442,6 +425,17 @@ export default function TrainScheduleV2ListPage() {
columns={columns}
data={paged}
status={tableStatus}
+ onRowClick={(schedule) =>
+ navigate(`/dashboard/operations/train-scheduling-v2/${schedule.id}`)
+ }
+ error={
+ schedulesQuery.isError
+ ? {
+ message: "Failed to load train schedules.",
+ onRetry: () => void schedulesQuery.refetch(),
+ }
+ : undefined
+ }
emptyMessage="No train schedules found"
pagination={{
pageIndex: pagination.pageIndex,
@@ -554,13 +548,13 @@ export default function TrainScheduleV2ListPage() {
setCreateOpen(false)}>
Cancel
-
+
Create
-
+
);
}
@@ -582,13 +576,13 @@ function MetricChip({
borderRadius: 8,
background: subtle
? "var(--mantine-color-gray-1)"
- : "var(--mantine-color-green-0)",
+ : "var(--mantine-color-edr-green-0)",
border: `1px solid ${
- subtle ? "var(--mantine-color-gray-2)" : "var(--mantine-color-green-1)"
+ subtle ? "var(--mantine-color-gray-2)" : "var(--mantine-color-edr-green-1)"
}`,
}}
>
-
+
{value}
{label ? (
@@ -613,31 +607,15 @@ function ScheduleCard({
const canTrack = ["DISPATCHED", "ARRIVED"].includes(schedule.status);
return (
{
- e.currentTarget.style.boxShadow = scheduleBrand.shadowSm;
- e.currentTarget.style.transform = "translateY(-2px)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.boxShadow = "";
- e.currentTarget.style.transform = "";
- }}
+ className="cursor-pointer overflow-hidden transition-[transform,border-color] duration-150 hover:-translate-y-0.5 hover:border-edr-primary!"
>
- {/* accent strip */}
-
-
+
@@ -675,7 +653,7 @@ function ScheduleCard({
-
- Train scheduling rules
-
- Global limits applied when previewing and assigning bookings to trains.
-
-
+
+
-
+
- void handleSave()}>
+ void handleSave()}>
Save rules
-
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/ArrivalQueuePage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/ArrivalQueuePage.tsx
index c05f2c626..0b52d1388 100644
--- a/apps/edr-freight-web/backoffice/src/pages/warehouses/ArrivalQueuePage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/ArrivalQueuePage.tsx
@@ -4,20 +4,16 @@ import {
Badge,
Button,
Card,
- Container,
Group,
- Loader,
- Stack,
- Table,
Text,
} from '@mantine/core';
import { ClipboardList, Eye, PackageOpen, Truck } from 'lucide-react';
+import { DataTable, type ColumnDef } from '@edr/ui-common';
-import Breadcrumbs from '@/components/ui/Breadcrumbs';
+import { PageContainer, PageHeader } from '@/components/page';
import {
InspectionReportModal,
VisualEmptyState,
- WarehouseHero,
formatDate,
} from '@/components/warehouses';
import { useArrivalQueue, useAutoUnloadArrived, useUnloadBooking } from '@/hooks/useWarehouses';
@@ -26,7 +22,7 @@ import type { ArrivalQueueItem } from '@/types/warehouse';
function inspectionBadge(status: string | null) {
if (!status) return Not inspected;
- const color = status === 'PASSED' ? 'green' : status === 'FAILED' ? 'red' : 'orange';
+ const color = status === 'PASSED' ? 'edr-green' : status === 'FAILED' ? 'red' : 'orange';
return {status.replace(/_/g, ' ')};
}
@@ -63,138 +59,138 @@ export default function ArrivalQueuePage() {
}
};
- return (
-
-
-
-
-
-
-
-
- {items.length} arrived booking(s)
- }
- loading={autoUnload.isPending}
- onClick={handleAutoUnload}
- >
- Auto Unload Arrived Bookings
-
+ const columns: ColumnDef[] = [
+ {
+ id: 'booking',
+ header: 'Booking',
+ cell: ({ row }) => (
+
+ {row.original.bookingReference}
+
+ ),
+ },
+ { id: 'customer', header: 'Customer', cell: ({ row }) => row.original.customer ?? '—' },
+ {
+ id: 'cargo',
+ header: 'Cargo / Container',
+ cell: ({ row }) => row.original.container ?? row.original.cargo ?? '—',
+ },
+ {
+ id: 'arrival',
+ header: 'Arrival',
+ cell: ({ row }) => {formatDate(row.original.arrivalDate)},
+ },
+ { id: 'facility', header: 'Facility', cell: ({ row }) => row.original.facility ?? '—' },
+ { id: 'warehouse', header: 'Warehouse', cell: ({ row }) => row.original.warehouse ?? '—' },
+ { id: 'yard', header: 'Yard', cell: ({ row }) => row.original.yard ?? '—' },
+ { id: 'zone', header: 'Zone', cell: ({ row }) => row.original.zone ?? '—' },
+ {
+ id: 'status',
+ header: 'Status',
+ cell: ({ row }) =>
+ row.original.unloaded ? (
+
+ {row.original.currentStatus ?? 'RECEIVED'}
+
+ ) : (
+
+ Not unloaded
+
+ ),
+ },
+ {
+ id: 'inspection',
+ header: 'Inspection',
+ cell: ({ row }) => inspectionBadge(row.original.inspectionStatus),
+ },
+ {
+ id: 'actions',
+ header: '',
+ cell: ({ row }) => {
+ const item = row.original;
+ return (
+ e.stopPropagation()}>
+ {!item.unloaded && (
+ }
+ loading={unloadOne.isPending}
+ onClick={() => handleUnloadOne(item)}
+ >
+ Unload
+
+ )}
+ {item.inventoryId && (
+ }
+ onClick={() => setInspectInventoryId(item.inventoryId)}
+ >
+ Inspect
+
+ )}
+ {item.inventoryId && (
+ }
+ onClick={() => navigate('/dashboard/warehouse-inventory')}
+ >
+ Inventory
+
+ )}
+ );
+ },
+ },
+ ];
- {isLoading ? (
-
-
-
- ) : items.length === 0 ? (
-
- ) : (
-
-
-
-
- Booking
- Customer
- Cargo / Container
- Arrival
- Facility
- Warehouse
- Yard
- Zone
- Status
- Inspection
- Actions
-
-
-
- {items.map((item) => (
-
-
- {item.bookingReference}
-
- {item.customer ?? '—'}
- {item.container ?? item.cargo ?? '—'}
-
- {formatDate(item.arrivalDate)}
-
- {item.facility ?? '—'}
- {item.warehouse ?? '—'}
- {item.yard ?? '—'}
- {item.zone ?? '—'}
-
- {item.unloaded ? (
-
- {item.currentStatus ?? 'RECEIVED'}
-
- ) : (
-
- Not unloaded
-
- )}
-
- {inspectionBadge(item.inspectionStatus)}
-
-
- {!item.unloaded && (
- }
- loading={unloadOne.isPending}
- onClick={() => handleUnloadOne(item)}
- >
- Unload
-
- )}
- {item.inventoryId && (
- }
- onClick={() => setInspectInventoryId(item.inventoryId)}
- >
- Inspect
-
- )}
- {item.inventoryId && (
- }
- onClick={() => navigate('/dashboard/warehouse-inventory')}
- >
- Inventory
-
- )}
-
-
-
- ))}
-
-
-
- )}
-
-
+ return (
+
+ }
+ loading={autoUnload.isPending}
+ onClick={handleAutoUnload}
+ >
+ Auto Unload Arrived Bookings
+
+ }
+ />
+
+
+
+ {items.length} arrived booking(s)
+
+
+ {!isLoading && items.length === 0 ? (
+
+ ) : (
+
+ )}
+
setInspectInventoryId(null)}
inventoryId={inspectInventoryId}
/>
-
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/DispatchQueuePage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/DispatchQueuePage.tsx
index 58e630fbf..1776ac8e0 100644
--- a/apps/edr-freight-web/backoffice/src/pages/warehouses/DispatchQueuePage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/DispatchQueuePage.tsx
@@ -1,7 +1,7 @@
-import { Card, Container, Stack } from '@mantine/core';
+import { Card } from '@mantine/core';
-import Breadcrumbs from '@/components/ui/Breadcrumbs';
-import { InventoryWorkbench, VisualEmptyState, WarehouseHero } from '@/components/warehouses';
+import { PageContainer, PageHeader } from '@/components/page';
+import { InventoryWorkbench, VisualEmptyState } from '@/components/warehouses';
import { useWarehouseInventory } from '@/hooks/useWarehouses';
/** Items that are LOADED and awaiting dispatch (train departure). */
@@ -10,29 +10,23 @@ export default function DispatchQueuePage() {
const items = data ?? [];
return (
-
-
+
+
-
-
-
-
- {!isLoading && items.length === 0 ? (
-
- ) : (
-
- )}
-
-
-
+
+ {!isLoading && items.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/InventoryInquiryPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/InventoryInquiryPage.tsx
index 0995796da..eeaaa8848 100644
--- a/apps/edr-freight-web/backoffice/src/pages/warehouses/InventoryInquiryPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/InventoryInquiryPage.tsx
@@ -1,8 +1,8 @@
import { useMemo, useState } from 'react';
-import { Button, Card, Center, Container, Group, Loader, Select, Stack, Text, TextInput, Title } from '@mantine/core';
+import { Button, Card, Center, Group, Loader, Select, Stack, TextInput } from '@mantine/core';
import { Search } from 'lucide-react';
-import Breadcrumbs from '@/components/ui/Breadcrumbs';
+import { PageContainer, PageHeader } from '@/components/page';
import { VisualEmptyState, WarehouseInquiryTable, inventoryStatusOptions } from '@/components/warehouses';
import {
useInventoryInquiry,
@@ -43,113 +43,107 @@ export default function InventoryInquiryPage() {
};
return (
-
-
+
+
-
-
-
Inventory Inquiry
-
- Locate any cargo, container or goods inside the warehouse network.
-
-
-
-
-
-
- { const v = e.currentTarget.value; setDraft((f) => ({ ...f, bookingNumber: v || undefined })); }}
- w={200}
- />
- { const v = e.currentTarget.value; setDraft((f) => ({ ...f, containerNumber: v || undefined })); }}
- w={200}
- />
- { const v = e.currentTarget.value; setDraft((f) => ({ ...f, goodsName: v || undefined })); }}
- w={180}
- />
-
-
-
- } onClick={runSearch}>
- Search
-
-
- Reset
-
-
-
-
-
-
- {isFetching ? (
-
-
-
- ) : results.length === 0 ? (
-
+
+
+ { const v = e.currentTarget.value; setDraft((f) => ({ ...f, bookingNumber: v || undefined })); }}
+ w={200}
/>
- ) : (
-
- )}
-
-
-
+ { const v = e.currentTarget.value; setDraft((f) => ({ ...f, containerNumber: v || undefined })); }}
+ w={200}
+ />
+ { const v = e.currentTarget.value; setDraft((f) => ({ ...f, goodsName: v || undefined })); }}
+ w={180}
+ />
+
+ setDraft((f) => ({ ...f, warehouseId: value ?? undefined, yardId: undefined, zoneId: undefined }))
+ }
+ w={200}
+ />
+ setDraft((f) => ({ ...f, yardId: value ?? undefined, zoneId: undefined }))}
+ w={180}
+ />
+ setDraft((f) => ({ ...f, zoneId: value ?? undefined }))}
+ w={180}
+ />
+ setDraft((f) => ({ ...f, status: (value as InventoryStatus) ?? undefined }))}
+ w={180}
+ />
+
+
+
+ } onClick={runSearch}>
+ Search
+
+
+ Reset
+
+
+
+
+
+
+ {isFetching ? (
+
+
+
+ ) : results.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadedInventoryPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadedInventoryPage.tsx
index 5cea3de20..d5dbd0b10 100644
--- a/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadedInventoryPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadedInventoryPage.tsx
@@ -1,86 +1,91 @@
-import { Badge, Card, Container, Group, Loader, Stack, Table, Text } from '@mantine/core';
+import { Badge, Card, Group, Text } from '@mantine/core';
+import { DataTable, type ColumnDef } from '@edr/ui-common';
-import Breadcrumbs from '@/components/ui/Breadcrumbs';
-import { FreightVisual, VisualEmptyState, WarehouseHero, formatDate, formatNumber } from '@/components/warehouses';
+import { PageContainer, PageHeader } from '@/components/page';
+import { FreightVisual, VisualEmptyState, formatDate, formatNumber } from '@/components/warehouses';
import { useWarehouseLoadings } from '@/hooks/useWarehouses';
+type Loading = NonNullable['data']>[number];
+
+const columns: ColumnDef[] = [
+ {
+ id: 'wagon',
+ header: 'Wagon',
+ cell: ({ row }) => (
+
+
+
+ {row.original.wagonNumber ?? row.original.wagonId.slice(0, 8)}
+
+
+ ),
+ },
+ {
+ id: 'warehouse',
+ header: 'Warehouse',
+ cell: ({ row }) =>
+ row.original.inventory?.warehouse
+ ? `${row.original.inventory.warehouse.name} (${row.original.inventory.warehouse.code})`
+ : '—',
+ },
+ {
+ id: 'zone',
+ header: 'Zone',
+ cell: ({ row }) => (row.original.inventory?.zone ? row.original.inventory.zone.name : '—'),
+ },
+ {
+ id: 'weight',
+ header: 'Loaded Weight (kg)',
+ cell: ({ row }) => formatNumber(row.original.loadedWeight),
+ },
+ {
+ id: 'loadedAt',
+ header: 'Loaded At',
+ cell: ({ row }) => formatDate(row.original.loadedAt),
+ },
+ {
+ id: 'status',
+ header: 'Status',
+ cell: ({ row }) => (
+
+ {row.original.inventory?.status ?? 'LOADED'}
+
+ ),
+ },
+];
+
/** Record of every inventory item loaded onto a wagon. */
export default function LoadedInventoryPage() {
const { data, isLoading } = useWarehouseLoadings();
const loadings = data ?? [];
return (
-
-
+
+
-
-
-
-
- {isLoading ? (
-
-
-
- ) : loadings.length === 0 ? (
-
- ) : (
-
-
-
-
- Wagon
- Warehouse
- Zone
- Loaded Weight (kg)
- Loaded At
- Status
-
-
-
- {loadings.map((l) => (
-
-
-
-
-
- {l.wagonNumber ?? l.wagonId.slice(0, 8)}
-
-
-
-
- {l.inventory?.warehouse
- ? `${l.inventory.warehouse.name} (${l.inventory.warehouse.code})`
- : '—'}
-
- {l.inventory?.zone ? l.inventory.zone.name : '—'}
- {formatNumber(l.loadedWeight)}
- {formatDate(l.loadedAt)}
-
-
- {l.inventory?.status ?? 'LOADED'}
-
-
-
- ))}
-
-
-
- )}
-
-
-
+
+ {!isLoading && loadings.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadingQueuePage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadingQueuePage.tsx
index 7bc363c09..bb65d8fe6 100644
--- a/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadingQueuePage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadingQueuePage.tsx
@@ -1,13 +1,13 @@
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
-import { Badge, Button, Card, Container, Group, Stack, Table, Tabs, Text } from '@mantine/core';
+import { Badge, Button, Card, Group, Tabs, Text } from '@mantine/core';
import { CreditCard, Eye, Truck } from 'lucide-react';
+import { DataTable, type ColumnDef } from '@edr/ui-common';
-import Breadcrumbs from '@/components/ui/Breadcrumbs';
+import { PageContainer, PageHeader } from '@/components/page';
import {
InventoryWorkbench,
VisualEmptyState,
- WarehouseHero,
formatNumber,
} from '@/components/warehouses';
import { useAutoLoadReady, useWarehouseInventory } from '@/hooks/useWarehouses';
@@ -53,35 +53,28 @@ export default function LoadingQueuePage() {
const unpaidItems = useMemo(() => readyItems.filter((i) => !isPaid(i)), [readyItems]);
return (
-
-
-
-
-
-
-
+
+ }
loading={autoLoad.isPending}
onClick={handleAutoLoad}
>
Auto Load Ready Items
-
+ }
+ />
-
-
+
+
+
{paidItems.length}
}
@@ -171,10 +164,9 @@ export default function LoadingQueuePage() {
)}
-
-
-
-
+
+
+
);
}
@@ -185,63 +177,67 @@ interface PendingPaymentTableProps {
/** Read-only view of unpaid ready-for-loading items. No Mark-as-Loaded action. */
function PendingPaymentTable({ items, onNavigate }: PendingPaymentTableProps) {
+ const columns: ColumnDef[] = [
+ {
+ id: 'booking',
+ header: 'Booking',
+ cell: ({ row }) => (
+
+ {row.original.booking?.reference ?? row.original.bookingId?.slice(0, 8) ?? '—'}
+
+ ),
+ },
+ { id: 'warehouse', header: 'Warehouse', cell: ({ row }) => row.original.warehouse?.code ?? '—' },
+ { id: 'zone', header: 'Zone', cell: ({ row }) => row.original.zone?.code ?? '—' },
+ { id: 'weight', header: 'Weight (kg)', cell: ({ row }) => formatNumber(row.original.weight) },
+ {
+ id: 'payment',
+ header: 'Payment',
+ cell: ({ row }) => (
+
+ {row.original.booking?.status ?? row.original.booking?.paymentStatus ?? 'UNPAID'}
+
+ ),
+ },
+ {
+ id: 'actions',
+ header: '',
+ cell: ({ row }) => {
+ const item = row.original;
+ return (
+ e.stopPropagation()}>
+ }
+ disabled={!item.bookingId}
+ onClick={() => item.bookingId && onNavigate(`/dashboard/booking-requests/${item.bookingId}`)}
+ >
+ Booking
+
+ }
+ disabled={!item.bookingId}
+ onClick={() => item.bookingId && onNavigate(`/dashboard/booking-requests/${item.bookingId}`)}
+ >
+ Payment
+
+
+ );
+ },
+ },
+ ];
+
return (
-
-
-
-
- Booking
- Warehouse
- Zone
- Weight (kg)
- Payment
- Actions
-
-
-
- {items.map((item) => (
-
-
-
- {item.booking?.reference ?? item.bookingId?.slice(0, 8) ?? '—'}
-
-
- {item.warehouse?.code ?? '—'}
- {item.zone?.code ?? '—'}
- {formatNumber(item.weight)}
-
-
- {item.booking?.status ?? item.booking?.paymentStatus ?? 'UNPAID'}
-
-
-
-
- }
- disabled={!item.bookingId}
- onClick={() => item.bookingId && onNavigate(`/dashboard/booking-requests/${item.bookingId}`)}
- >
- Booking
-
- }
- disabled={!item.bookingId}
- onClick={() => item.bookingId && onNavigate(`/dashboard/booking-requests/${item.bookingId}`)}
- >
- Payment
-
-
-
-
- ))}
-
-
-
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx
index b85227152..be8cbf7b4 100644
--- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx
@@ -1,5 +1,5 @@
import { useNavigate } from 'react-router-dom';
-import { Card, Center, Container, Group, Loader, SimpleGrid, Stack, Text, ThemeIcon } from '@mantine/core';
+import { Card, Center, Group, Loader, SimpleGrid, Text, ThemeIcon } from '@mantine/core';
import {
ClipboardCheck,
ClipboardList,
@@ -15,22 +15,17 @@ import {
Layers,
} from 'lucide-react';
-import Breadcrumbs from '@/components/ui/Breadcrumbs';
-import { WarehouseDashboardCharts, WarehouseHero } from '@/components/warehouses';
+import { PageContainer, PageHeader } from '@/components/page';
+import { WarehouseDashboardCharts } from '@/components/warehouses';
import { useWarehouseDashboard } from '@/hooks/useWarehouses';
import type { WarehouseDashboard } from '@/types/warehouse';
-/** Brand palette: alternating orange + light green. */
-const ORANGE = { solid: '#f08c00', soft: '#fff4e6', border: '#ffd8a8', text: '#e8590c' };
-const GREEN = { solid: '#5bbf4a', soft: '#ebfbee', border: '#b2f2bb', text: '#2f9e44' };
-
interface Metric {
key: keyof WarehouseDashboard;
label: string;
icon: React.ReactNode;
/** Route to navigate to when the card is clicked. */
to: string;
- theme: typeof ORANGE;
}
const METRICS: Metric[] = [
@@ -53,60 +48,36 @@ export default function WarehouseDashboardPage() {
const { data, isLoading } = useWarehouseDashboard();
return (
-
-
+
+
-
-
-
- {isLoading ? (
-
-
-
- ) : (
- <>
+ {isLoading ? (
+
+
+
+ ) : (
+ <>
{METRICS.map((metric) => (
navigate(metric.to)}
- style={{
- cursor: 'pointer',
- background: `linear-gradient(135deg, ${metric.theme.soft} 0%, #ffffff 75%)`,
- border: `1px solid ${metric.theme.border}`,
- transition: 'box-shadow 150ms ease, transform 150ms ease',
- }}
- onMouseEnter={(e) => {
- e.currentTarget.style.boxShadow = `0 10px 24px -12px ${metric.theme.solid}`;
- e.currentTarget.style.transform = 'translateY(-3px)';
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.boxShadow = '';
- e.currentTarget.style.transform = '';
- }}
+ className="cursor-pointer transition-[transform,border-color] duration-150 hover:-translate-y-0.5 hover:border-edr-primary!"
>
-
+
{metric.label}
-
+
{data ? data[metric.key] : 0}
-
+
{metric.icon}
@@ -115,9 +86,8 @@ export default function WarehouseDashboardPage() {
- >
- )}
-
-
+ >
+ )}
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDetailPage.tsx
index 0d352d205..145d3579d 100644
--- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDetailPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDetailPage.tsx
@@ -8,17 +8,16 @@ import {
Container,
Group,
Loader,
- SimpleGrid,
Stack,
Select,
- Table,
Tabs,
Text,
- Title,
} from '@mantine/core';
import { ArrowLeft, Boxes, LayoutGrid, Package, Pencil, Plus } from 'lucide-react';
-import Breadcrumbs from '@/components/ui/Breadcrumbs';
+import { DataTable, type ColumnDef } from '@edr/ui-common';
+
+import { KpiStrip, PageContainer, PageHeader } from '@/components/page';
import {
CreateYardModal,
CreateZoneModal,
@@ -36,19 +35,6 @@ import {
} from '@/hooks/useWarehouses';
import type { WarehouseYard, WarehouseZone } from '@/types/warehouse';
-function StatCard({ label, value }: { label: string; value: string }) {
- return (
-
-
- {label}
-
-
- {value}
-
-
- );
-}
-
export default function WarehouseDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
@@ -73,6 +59,90 @@ export default function WarehouseDetailPage() {
[yards],
);
+ const yardColumns = useMemo[]>(
+ () => [
+ { id: 'name', header: 'Name', cell: ({ row }) => row.original.name },
+ { id: 'code', header: 'Code', cell: ({ row }) => row.original.code },
+ { id: 'type', header: 'Type', cell: ({ row }) => humanizeEnum(row.original.type) },
+ {
+ id: 'weight',
+ header: 'Weight (cur / cap)',
+ cell: ({ row }) => formatCapacity(row.original.currentWeight, row.original.capacityWeight),
+ },
+ {
+ id: 'containers',
+ header: 'Containers (cur / cap)',
+ cell: ({ row }) =>
+ formatCapacity(row.original.currentContainers, row.original.capacityContainers),
+ },
+ {
+ id: 'status',
+ header: 'Status',
+ cell: ({ row }) => ,
+ },
+ {
+ id: 'actions',
+ header: 'Actions',
+ meta: { headerClassName: 'text-right', cellClassName: 'text-right' },
+ cell: ({ row }) => (
+ {
+ setEditingYard(row.original);
+ setYardModalOpen(true);
+ }}
+ >
+
+
+ ),
+ },
+ ],
+ [],
+ );
+
+ const zoneColumns = useMemo[]>(
+ () => [
+ { id: 'name', header: 'Name', cell: ({ row }) => row.original.name },
+ { id: 'code', header: 'Code', cell: ({ row }) => row.original.code },
+ { id: 'type', header: 'Type', cell: ({ row }) => humanizeEnum(row.original.type) },
+ {
+ id: 'weight',
+ header: 'Weight (cur / cap)',
+ cell: ({ row }) => formatCapacity(row.original.currentWeight, row.original.capacityWeight),
+ },
+ {
+ id: 'containers',
+ header: 'Containers (cur / cap)',
+ cell: ({ row }) =>
+ formatCapacity(row.original.currentContainers, row.original.capacityContainers),
+ },
+ {
+ id: 'status',
+ header: 'Status',
+ cell: ({ row }) => ,
+ },
+ {
+ id: 'actions',
+ header: 'Actions',
+ meta: { headerClassName: 'text-right', cellClassName: 'text-right' },
+ cell: ({ row }) => (
+ {
+ setEditingZone(row.original);
+ setZoneModalOpen(true);
+ }}
+ >
+
+
+ ),
+ },
+ ],
+ [],
+ );
+
if (isLoading) {
return (
@@ -95,65 +165,61 @@ export default function WarehouseDetailPage() {
}
return (
-
-
+
+
+
+
+ }
/>
-
-
-
- navigate('/dashboard/warehouses')}>
-
-
-
-
- {warehouse.name}
-
-
-
-
- {warehouse.code}
- {warehouse.locationName ? ` · ${warehouse.locationName}` : ''}
-
-
-
-
+
+
+ }>
+ Overview
+
+ }>
+ Yards
+
+ }>
+ Zones
+
+ }>
+ Inventory
+
+
-
-
- }>
- Overview
-
- }>
- Yards
-
- }>
- Zones
-
- }>
- Inventory
-
-
-
- {/* OVERVIEW */}
-
-
-
-
-
-
-
-
+ {/* OVERVIEW */}
+
+
+
{/* YARDS */}
@@ -173,51 +239,27 @@ export default function WarehouseDetailPage() {
- {yards.length === 0 ? (
-
- No yards yet.
-
- ) : (
-
-
-
- Name
- Code
- Type
- Weight (cur / cap)
- Containers (cur / cap)
- Status
- Actions
-
-
-
- {yards.map((yard) => (
-
- {yard.name}
- {yard.code}
- {humanizeEnum(yard.type)}
- {formatCapacity(yard.currentWeight, yard.capacityWeight)}
- {formatCapacity(yard.currentContainers, yard.capacityContainers)}
-
-
-
-
- {
- setEditingYard(yard);
- setYardModalOpen(true);
- }}
- >
-
-
-
-
- ))}
-
-
- )}
+ void yardsQuery.refetch(),
+ }
+ : undefined
+ }
+ />
@@ -253,50 +295,28 @@ export default function WarehouseDetailPage() {
Select a yard to view its zones.
- ) : (zonesQuery.data ?? []).length === 0 ? (
-
- No zones in this yard yet.
-
) : (
-
-
-
- Name
- Code
- Type
- Weight (cur / cap)
- Containers (cur / cap)
- Status
- Actions
-
-
-
- {(zonesQuery.data ?? []).map((zone) => (
-
- {zone.name}
- {zone.code}
- {humanizeEnum(zone.type)}
- {formatCapacity(zone.currentWeight, zone.capacityWeight)}
- {formatCapacity(zone.currentContainers, zone.capacityContainers)}
-
-
-
-
- {
- setEditingZone(zone);
- setZoneModalOpen(true);
- }}
- >
-
-
-
-
- ))}
-
-
+ void zonesQuery.refetch(),
+ }
+ : undefined
+ }
+ />
)}
@@ -308,8 +328,7 @@ export default function WarehouseDetailPage() {
-
-
+
{id && (
)}
-
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInventoryPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInventoryPage.tsx
index 8831037d1..234c37826 100644
--- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInventoryPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInventoryPage.tsx
@@ -1,10 +1,10 @@
import { useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
-import { Button, Card, Container, Group, Select, Stack, Text, TextInput, Title } from '@mantine/core';
+import { Button, Card, Group, Select, Stack, TextInput } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { PackagePlus, Search } from 'lucide-react';
-import Breadcrumbs from '@/components/ui/Breadcrumbs';
+import { PageContainer, PageHeader } from '@/components/page';
import {
InventoryWorkbench,
ReceiveInventoryModal,
@@ -52,79 +52,73 @@ export default function WarehouseInventoryPage() {
);
return (
-
-
-
-
-
-
-
Warehouse Inventory
-
- Track received items through the storage, reservation, loading and dispatch lifecycle.
-
-
+
+ } onClick={() => setModalOpen(true)}>
Receive Inventory
-
+ }
+ />
-
-
-
- }
- value={search}
- onChange={(e) => setSearch(e.currentTarget.value)}
- w={220}
- />
-
- setFilter((f) => ({ ...f, warehouseId: value ?? undefined, yardId: undefined, zoneId: undefined }))
- }
- w={220}
- />
- setFilter((f) => ({ ...f, yardId: value ?? undefined, zoneId: undefined }))}
- w={200}
- />
- setFilter((f) => ({ ...f, zoneId: value ?? undefined }))}
- w={200}
- />
- setFilter((f) => ({ ...f, status: (value as InventoryStatus) ?? undefined }))}
- w={200}
- />
-
+
+
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ w={220}
+ />
+
+ setFilter((f) => ({ ...f, warehouseId: value ?? undefined, yardId: undefined, zoneId: undefined }))
+ }
+ w={220}
+ />
+ setFilter((f) => ({ ...f, yardId: value ?? undefined, zoneId: undefined }))}
+ w={200}
+ />
+ setFilter((f) => ({ ...f, zoneId: value ?? undefined }))}
+ w={200}
+ />
+ setFilter((f) => ({ ...f, status: (value as InventoryStatus) ?? undefined }))}
+ w={200}
+ />
+
-
-
-
-
+
+
+
setModalOpen(false)} />
-
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInvoicesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInvoicesPage.tsx
index 3f680bf0f..7b1ae4d30 100644
--- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInvoicesPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInvoicesPage.tsx
@@ -4,7 +4,6 @@ import {
Badge,
Button,
Card,
- Container,
Divider,
Group,
Loader,
@@ -17,9 +16,9 @@ import {
TextInput,
} from '@mantine/core';
import { Ban, CreditCard, Eye, Search } from 'lucide-react';
+import { DataTable, type ColumnDef } from '@edr/ui-common';
-import Breadcrumbs from '@/components/ui/Breadcrumbs';
-import { WarehouseHero } from '@/components/warehouses';
+import { PageContainer, PageHeader } from '@/components/page';
import { useToast } from '@/hooks/use-toast';
import {
useCancelInvoice,
@@ -37,7 +36,7 @@ const STATUS_COLOR: Record = {
DRAFT: 'gray',
ISSUED: 'orange',
PARTIALLY_PAID: 'yellow',
- PAID: 'green',
+ PAID: 'edr-green',
CANCELLED: 'gray',
};
@@ -58,19 +57,65 @@ export default function WarehouseInvoicesPage() {
return invoices.filter((i) => [i.invoiceNumber, i.bookingId, i.customerId].join(' ').toLowerCase().includes(q));
}, [invoices, search]);
- return (
-
-
-
-
+ const invoiceColumns: ColumnDef[] = [
+ {
+ id: 'invoiceNumber',
+ header: 'Invoice No',
+ cell: ({ row }) => (
+
+ {row.original.invoiceNumber}
+
+ ),
+ },
+ { id: 'type', header: 'Type', cell: ({ row }) => row.original.invoiceType.replace(/_/g, ' ') },
+ { id: 'total', header: 'Total', cell: ({ row }) => fmt(row.original.totalAmount, row.original.currency) },
+ { id: 'paid', header: 'Paid', cell: ({ row }) => fmt(row.original.paidAmount, row.original.currency) },
+ {
+ id: 'balance',
+ header: 'Balance',
+ cell: ({ row }) => fmt(row.original.balanceAmount, row.original.currency),
+ },
+ {
+ id: 'status',
+ header: 'Status',
+ cell: ({ row }) => (
+
+ {row.original.status.replace(/_/g, ' ')}
+
+ ),
+ },
+ {
+ id: 'issued',
+ header: 'Issued',
+ cell: ({ row }) => {fmtDate(row.original.issuedAt)},
+ },
+ {
+ id: 'actions',
+ header: '',
+ cell: ({ row }) => (
+ e.stopPropagation()}>
+ setDetailId(row.original.id)}
+ title="View"
+ >
+
+
+
+ ),
+ },
+ ];
-
-
+ return (
+
+
+
+
+
}
@@ -88,46 +133,17 @@ export default function WarehouseInvoicesPage() {
/>
- {isLoading ? (
-
- ) : filtered.length === 0 ? (
- No invoices found.
- ) : (
-
-
-
-
- Invoice NoTypeTotal
- PaidBalanceStatus
- IssuedActions
-
-
-
- {filtered.map((inv) => (
-
- {inv.invoiceNumber}
- {inv.invoiceType.replace(/_/g, ' ')}
- {fmt(inv.totalAmount, inv.currency)}
- {fmt(inv.paidAmount, inv.currency)}
- {fmt(inv.balanceAmount, inv.currency)}
- {inv.status.replace(/_/g, ' ')}
- {fmtDate(inv.issuedAt)}
-
- setDetailId(inv.id)} title="View">
-
-
-
-
- ))}
-
-
-
- )}
-
-
+
+
setDetailId(null)} />
-
+
);
}
@@ -217,7 +233,7 @@ function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () =>
onChange={(v) => setPayAmount(v === '' ? '' : Number(v))}
style={{ flex: 1 }}
/>
- } loading={pay.isPending} onClick={handlePay}>
+ } loading={pay.isPending} onClick={handlePay}>
Pay
diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseListPage.tsx
index b0d71834e..3f9df44f6 100644
--- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseListPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseListPage.tsx
@@ -1,10 +1,10 @@
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
-import { Button, Card, Center, Container, Group, Loader, Stack, Text, Title } from '@mantine/core';
+import { Button, Card, Center, Loader, Stack, Text } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { Plus } from 'lucide-react';
-import Breadcrumbs from '@/components/ui/Breadcrumbs';
+import { PageContainer, PageHeader } from '@/components/page';
import {
CreateWarehouseModal,
WarehouseCardView,
@@ -42,44 +42,38 @@ export default function WarehouseListPage() {
const openDetail = (warehouse: Warehouse) => navigate(`/dashboard/warehouses/${warehouse.id}`);
return (
-
-
-
-
-
-
-
Warehouses
-
- Manage warehouses, yards and zones.
-
-
+
+ } onClick={openCreate}>
Create Warehouse
-
+ }
+ />
-
-
-
+
+
+
- {isLoading ? (
-
-
-
- ) : isError ? (
-
- Failed to load warehouses.
-
- ) : view === 'table' ? (
-
- ) : (
-
- )}
-
-
-
+ {isLoading ? (
+
+
+
+ ) : isError ? (
+
+ Failed to load warehouses.
+
+ ) : view === 'table' ? (
+
+ ) : (
+
+ )}
+
+
setModalOpen(false)} warehouse={editing} />
-
+
);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseRulesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseRulesPage.tsx
index 1bfce1de1..af5adb83d 100644
--- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseRulesPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseRulesPage.tsx
@@ -4,22 +4,19 @@ import {
Badge,
Button,
Card,
- Container,
Group,
- Loader,
Modal,
NumberInput,
Select,
Stack,
- Table,
Tabs,
Text,
TextInput,
} from '@mantine/core';
import { Plus, Trash2 } from 'lucide-react';
+import { DataTable, type ColumnDef } from '@edr/ui-common';
-import Breadcrumbs from '@/components/ui/Breadcrumbs';
-import { WarehouseHero } from '@/components/warehouses';
+import { PageContainer, PageHeader } from '@/components/page';
import { useToast } from '@/hooks/use-toast';
import {
useAllocationRules,
@@ -45,31 +42,26 @@ const clean = (s: string) => s.trim() || undefined;
export default function WarehouseRulesPage() {
return (
-
-
-
-
-
-
-
- Allocation Rules
- Storage / Demurrage Fees
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ Allocation Rules
+ Storage / Demurrage Fees
+
+
+
+
+
+
+
+
+
+
);
}
@@ -112,45 +104,52 @@ function AllocationRules() {
setForm({ name: '', priority: 100, freightType: '', tradeDirection: '', cargoTypeCode: '', containerStatus: '', targetYardCode: '', storageType: '' });
};
+ const columns: ColumnDef<(typeof rules)[number]>[] = [
+ { id: 'priority', header: 'Priority', cell: ({ row }) => row.original.priority },
+ { id: 'name', header: 'Name', cell: ({ row }) => row.original.name },
+ { id: 'freight', header: 'Freight', cell: ({ row }) => row.original.freightType ?? '—' },
+ { id: 'trade', header: 'Trade', cell: ({ row }) => row.original.tradeDirection ?? '—' },
+ { id: 'cargo', header: 'Cargo code', cell: ({ row }) => row.original.cargoTypeCode ?? '—' },
+ {
+ id: 'targetYard',
+ header: 'Target yard',
+ cell: ({ row }) => {row.original.targetYardCode},
+ },
+ {
+ id: 'active',
+ header: 'Active',
+ cell: ({ row }) => (
+
+ {row.original.isActive ? 'Yes' : 'No'}
+
+ ),
+ },
+ {
+ id: 'actions',
+ header: '',
+ cell: ({ row }) => (
+ e.stopPropagation()}>
+ remove.mutate(row.original.id)} title="Delete">
+
+
+
+ ),
+ },
+ ];
+
return (
<>
{rules.length} rule(s) — matched by ascending priority
- } onClick={() => setOpen(true)}>New allocation rule
+ } onClick={() => setOpen(true)}>New allocation rule
- {isLoading ? (
-
- ) : (
-
-
-
-
- PriorityNameFreight
- TradeCargo codeTarget yard
- ActiveActions
-
-
-
- {rules.map((r) => (
-
- {r.priority}
- {r.name}
- {r.freightType ?? '—'}
- {r.tradeDirection ?? '—'}
- {r.cargoTypeCode ?? '—'}
- {r.targetYardCode}
- {r.isActive ? 'Yes' : 'No'}
-
- remove.mutate(r.id)} title="Delete">
-
-
-
-
- ))}
-
-
-
- )}
+
setOpen(false)} title="New allocation rule" centered size="lg">
@@ -172,7 +171,7 @@ function AllocationRules() {
setOpen(false)}>Cancel
- Create
+ Create
@@ -218,45 +217,60 @@ function FeeRules() {
setOpen(false);
};
+ const columns: ColumnDef<(typeof rules)[number]>[] = [
+ {
+ id: 'type',
+ header: 'Type',
+ cell: ({ row }) => (
+
+ {row.original.ruleType === 'DEMURRAGE_FEE' ? 'Demurrage' : 'Storage'}
+
+ ),
+ },
+ { id: 'name', header: 'Name', cell: ({ row }) => row.original.name },
+ { id: 'freight', header: 'Freight', cell: ({ row }) => row.original.freightType ?? '—' },
+ { id: 'trade', header: 'Trade', cell: ({ row }) => row.original.tradeDirection ?? '—' },
+ { id: 'freeDays', header: 'Free days', cell: ({ row }) => row.original.freeDays },
+ {
+ id: 'rate',
+ header: 'Rate / day',
+ cell: ({ row }) => `${Number(row.original.ratePerDay).toLocaleString()} ${row.original.currency}`,
+ },
+ {
+ id: 'active',
+ header: 'Active',
+ cell: ({ row }) => (
+
+ {row.original.isActive ? 'Yes' : 'No'}
+
+ ),
+ },
+ {
+ id: 'actions',
+ header: '',
+ cell: ({ row }) => (
+ e.stopPropagation()}>
+ remove.mutate(row.original.id)} title="Delete">
+
+
+
+ ),
+ },
+ ];
+
return (
<>
{rules.length} rule(s) — most specific match applies
- } onClick={() => setOpen(true)}>New fee rule
+ } onClick={() => setOpen(true)}>New fee rule
- {isLoading ? (
-
- ) : (
-
-
-
-
- TypeNameFreight
- TradeFree daysRate / day
- ActiveActions
-
-
-
- {rules.map((r) => (
-
- {r.ruleType === 'DEMURRAGE_FEE' ? 'Demurrage' : 'Storage'}
- {r.name}
- {r.freightType ?? '—'}
- {r.tradeDirection ?? '—'}
- {r.freeDays}
- {Number(r.ratePerDay).toLocaleString()} {r.currency}
- {r.isActive ? 'Yes' : 'No'}
-
- remove.mutate(r.id)} title="Delete">
-
-
-
-
- ))}
-
-
-
- )}
+
setOpen(false)} title="New fee rule" centered size="lg">
@@ -276,7 +290,7 @@ function FeeRules() {
setOpen(false)}>Cancel
- Create
+ Create
diff --git a/apps/edr-freight-web/backoffice/src/theme/freight-brand.ts b/apps/edr-freight-web/backoffice/src/theme/freight-brand.ts
index 32192dd36..6c1efd3f3 100644
--- a/apps/edr-freight-web/backoffice/src/theme/freight-brand.ts
+++ b/apps/edr-freight-web/backoffice/src/theme/freight-brand.ts
@@ -1,4 +1,8 @@
-import { createTheme, type MantineColorsTuple } from "@mantine/core";
+import {
+ colorsTuple,
+ createTheme,
+ type MantineColorsTuple,
+} from "@mantine/core";
/** EDR Freight primary brand green */
export const FREIGHT_BRAND = "#1B9E7A";
@@ -17,24 +21,155 @@ export const freightBrand = {
mutedBorder: "#B7EBDC",
} as const;
-/** Mantine green scale with #1B9E7A at index 6 (filled buttons, nav active). */
-const freightGreen: MantineColorsTuple = [
- "#E7F8F2",
- "#C5EFE1",
- "#9CE4CD",
- "#6BD6B5",
- "#41C8A0",
- "#25B58C",
- FREIGHT_BRAND,
- FREIGHT_BRAND_DARK,
- "#105F47",
- "#0A3D2E",
+const edrGreen: MantineColorsTuple = [
+ "#ecfdf5",
+ "#d1fae5",
+ "#a7f3d0",
+ "#6ee7b7",
+ "#34d399",
+ "#0EA371",
+ "#0A8A5F",
+ "#0A6F4D",
+ "#065f46",
+ "#064e3b",
+];
+
+const neutral: MantineColorsTuple = [
+ "#F4F7FA",
+ "#eef2f7",
+ "#E6ECF2",
+ "#d0dae6",
+ "#b0bfce",
+ "#8fa0b2",
+ "#6B7C8E",
+ "#4b5a6a",
+ "#10202F",
+ "#0C1A2B",
];
export const freightMantineTheme = createTheme({
- primaryColor: "green",
colors: {
- green: freightGreen,
+ "edr-green": edrGreen,
+ gray: neutral,
+
+ // Brand surface + text tokens (single-value semantic colors).
+ // Each generates --mantine-color-{name}-{0..9} CSS variables.
+ "edr-bg": colorsTuple("#F7FAFC"),
+ "edr-card": colorsTuple("#FFFFFF"),
+ "edr-border": colorsTuple("#E6ECF2"),
+ "edr-divider": colorsTuple("#EEF1F5"),
+ "edr-text": colorsTuple("#10202F"),
+ "edr-muted": colorsTuple("#6B7C8E"),
+ "edr-soft": colorsTuple("#ECF6F1"),
+ "edr-ink": colorsTuple("#0C1A2B"),
+ "edr-accent": colorsTuple("#F2A516"),
+
+ // Semantic status shades (from the design system).
+ "edr-amber-soft": colorsTuple("#FDF3E0"),
+ "edr-amber-text": colorsTuple("#9A5B00"),
+ "edr-blue": colorsTuple("#2E5B96"),
+ "edr-blue-soft": colorsTuple("#E9F0F8"),
+ "edr-blue-dot": colorsTuple("#3B6FB0"),
+ "edr-red": colorsTuple("#C0392B"),
+ "edr-red-soft": colorsTuple("#FBEAE7"),
+ "edr-slate": colorsTuple("#475569"),
+ "edr-slate-soft": colorsTuple("#F1F4F7"),
+ "edr-slate-soft2": colorsTuple("#EEF2F6"),
+ "edr-step": colorsTuple("#94A3B8"),
+ "edr-step-idle": colorsTuple("#D5DBE2"),
+ "edr-conn-idle": colorsTuple("#E6EAEF"),
+ },
+ primaryColor: "edr-green",
+ primaryShade: { light: 5, dark: 4 },
+
+ white: "#ffffff",
+ black: "#10202F",
+
+ fontFamily:
+ '"Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
+
+ defaultRadius: "md",
+
+ radius: {
+ xs: "4px",
+ sm: "6px",
+ md: "8px",
+ lg: "12px",
+ xl: "18px",
+ },
+
+ spacing: {
+ xs: "8px",
+ sm: "12px",
+ md: "16px",
+ lg: "24px",
+ xl: "32px",
+ },
+
+ fontSizes: {
+ xs: "12px",
+ sm: "14px",
+ md: "16px",
+ lg: "20px",
+ xl: "24px",
+ },
+
+ lineHeights: {
+ xs: "1.4",
+ sm: "1.45",
+ md: "1.55",
+ lg: "1.55",
+ xl: "1.5",
+ },
+
+ headings: {
+ fontFamily: '"Inter", var(--mantine-font-family)',
+ fontWeight: "700",
+ sizes: {
+ h1: { fontSize: "36px", lineHeight: "1.1", fontWeight: "800" },
+ h2: { fontSize: "28px", lineHeight: "1.2", fontWeight: "700" },
+ h3: { fontSize: "22px", lineHeight: "1.3", fontWeight: "600" },
+ h4: { fontSize: "18px", lineHeight: "1.4", fontWeight: "600" },
+ h5: { fontSize: "15px", lineHeight: "1.45", fontWeight: "600" },
+ h6: { fontSize: "13px", lineHeight: "1.45", fontWeight: "600" },
+ },
+ },
+
+ shadows: {
+ xs: "0 1px 2px rgba(16, 24, 40, 0.04)",
+ sm: "0 1px 3px rgba(16, 24, 40, 0.06)",
+ md: "0 4px 12px rgba(16, 24, 40, 0.06)",
+ },
+
+ components: {
+ Card: {
+ defaultProps: {
+ radius: "xl",
+ withBorder: true,
+ shadow: "none",
+ padding: "lg",
+ },
+ styles: {
+ root: { borderColor: "#E6ECF2" },
+ },
+ },
+ Button: {
+ defaultProps: { radius: "md" },
+ styles: { root: { fontWeight: 600 } },
+ },
+ Badge: {
+ defaultProps: { radius: "xl", variant: "light" },
+ styles: { root: { fontWeight: 600, textTransform: "none" } },
+ },
+ Paper: {
+ defaultProps: { radius: "xl", shadow: "none", withBorder: true },
+ styles: { root: { borderColor: "#E6ECF2" } },
+ },
+ Table: {
+ defaultProps: { verticalSpacing: "sm", horizontalSpacing: "md" },
+ },
+ Title: {
+ styles: { root: { letterSpacing: "-0.01em" } },
+ },
},
- fontFamily: "'Outfit', var(--font-sans)",
});
diff --git a/apps/edr-freight-web/portal/src/components/auth/AuthShell.tsx b/apps/edr-freight-web/portal/src/components/auth/AuthShell.tsx
index 96d55bdd0..5a4adf587 100644
--- a/apps/edr-freight-web/portal/src/components/auth/AuthShell.tsx
+++ b/apps/edr-freight-web/portal/src/components/auth/AuthShell.tsx
@@ -11,14 +11,24 @@ export const primaryButtonClass =
"h-11 w-full rounded-full bg-primary text-sm font-semibold text-primary-foreground shadow-[0_8px_20px_-6px_rgba(16,94,52,0.5)] transition-all duration-200 hover:bg-primary/90 hover:shadow-[0_10px_24px_-6px_rgba(16,94,52,0.55)] active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-60 disabled:shadow-none";
const LeftPanelDecor = () => (
-
+
@@ -26,12 +36,23 @@ const LeftPanelDecor = () => (
);
const RightPanelDecor = () => (
-
+
-