diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusTabs.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusTabs.tsx index 217a24163..c0eaaaace 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusTabs.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusTabs.tsx @@ -1,3 +1,4 @@ +import { Badge, ScrollArea, Tabs } from "@mantine/core"; import { CheckCircle, ClipboardCheck, @@ -8,13 +9,12 @@ import { Wallet, XCircle, } from "lucide-react"; -import { Badge, Tabs } from "@mantine/core"; +import "@/components/overview/overview.css"; import { BOOKING_LIST_TABS, type BookingStatusTabKey, } from "@/features/bookings/booking-status.config"; -import "@/components/overview/overview.css"; const TAB_ICONS: Record = { all: , @@ -47,8 +47,9 @@ export function BookingStatusTabs({ keepMounted={false} classNames={{ list: "ov-tablist", tab: "ov-tab" }} > - - {BOOKING_LIST_TABS.map((tab) => { + + + {BOOKING_LIST_TABS.map((tab) => { const isActive = active === tab.key; const count = counts?.[tab.key]; return ( @@ -56,6 +57,7 @@ export function BookingStatusTabs({ key={tab.key} value={tab.key} leftSection={TAB_ICONS[tab.key]} + size={"sm"} rightSection={ count !== undefined ? ( ); - })} - + })} + + ); } diff --git a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx index 9e8fb72c2..0310ca328 100644 --- a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx +++ b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx @@ -1,5 +1,5 @@ import type { OnChangeFn, PaginationState } from "@tanstack/react-table"; -import { Stack, Group, Text, Card, SimpleGrid } from "@mantine/core"; +import { Stack, Group, Text, Card, SimpleGrid, Skeleton } from "@mantine/core"; import type { RuleEngineResourceConfig } from "@/pages/ruleEngine/config/resources"; import type { RuleEngineRecord } from "@/types/rule-engine"; @@ -99,13 +99,16 @@ const RuleEngineCardGrid = ({ {Array.from({ length: 6 }).map((_, index) => ( - -
-
-
-
-
-
+ + + + + + + + + + ))} @@ -124,8 +127,8 @@ const RuleEngineCardGrid = ({ ); } - const avatarBg = "#f1f5f9"; - const avatarText = "#475569"; + const avatarBg = "var(--mantine-color-gray-1)"; + const avatarText = "var(--mantine-color-gray-7)"; return ( @@ -152,24 +155,7 @@ const RuleEngineCardGrid = ({ { - e.currentTarget.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.08)"; - e.currentTarget.style.borderColor = "var(--mantine-color-gray-3)"; - }} - onMouseLeave={(e) => { - e.currentTarget.style.boxShadow = "none"; - e.currentTarget.style.borderColor = "var(--mantine-color-gray-2)"; - }} + style={{ display: "flex", flexDirection: "column" }} > @@ -207,7 +193,7 @@ const RuleEngineCardGrid = ({ {(subtitle || presentation.detailColumns.length > 0) && ( - + {subtitle && ( @@ -234,7 +220,7 @@ const RuleEngineCardGrid = ({ )} - + ( @@ -228,8 +219,8 @@ const RuleEngineFormDialog = ({ px="md" style={{ minHeight: 42, - background: "#f8fafc", - border: "1px solid #e2e8f0", + background: "var(--mantine-color-gray-0)", + border: "1px solid var(--mantine-color-gray-3)", borderRadius: "var(--mantine-radius-md)", }} > diff --git a/apps/edr-freight-web/backoffice/src/components/ruleEngine/ruleEngineStyles.ts b/apps/edr-freight-web/backoffice/src/components/ruleEngine/ruleEngineStyles.ts index 7be1692c2..fb9dc8b24 100644 --- a/apps/edr-freight-web/backoffice/src/components/ruleEngine/ruleEngineStyles.ts +++ b/apps/edr-freight-web/backoffice/src/components/ruleEngine/ruleEngineStyles.ts @@ -40,7 +40,7 @@ export const ruleEngineCard = { "group flex flex-col overflow-hidden rounded-lg border border-border bg-card shadow-sm transition-shadow duration-200 hover:shadow-md", header: "border-b border-border bg-muted/25 px-3 py-2.5 sm:px-4 sm:py-3.5", avatar: - "flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-[#f1f5f9] text-xs font-semibold text-slate-600 sm:h-10 sm:w-10 sm:text-sm", + "flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted text-xs font-semibold text-muted-foreground sm:h-10 sm:w-10 sm:text-sm", title: "truncate text-sm font-semibold text-foreground sm:text-[15px]", meta: "text-xs text-muted-foreground", detailLabel: diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx index 6cbef4b33..ecf08bdfc 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx @@ -1,5 +1,7 @@ import { ActionIcon, + Box, + Button, Card, Group, Stack, @@ -7,14 +9,26 @@ import { Text, TextInput, } from "@mantine/core"; -import { ArrowRight, Calendar, Package, Search, User, X } from "lucide-react"; +import { + AlertTriangle, + ArrowRight, + Calendar, + CheckCircle2, + Clock, + LayoutList, + Package, + Plus, + RefreshCw, + Search, + User, + X, +} from "lucide-react"; import { useCallback, useMemo, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { BookingActionsMenu } from "@/components/bookings/BookingActionsMenu"; import { BookingApprovalProgressCell } from "@/components/bookings/BookingApprovalProgressCell"; import { BookingPriorityBadge } from "@/components/bookings/BookingPriorityBadge"; -import { BookingRequestsHeader } from "@/components/bookings/BookingRequestsHeader"; import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge"; import { BookingStatusTabs, @@ -24,7 +38,7 @@ import { BookingTableEmpty } from "@/components/bookings/BookingTableEmpty"; import { OperationsBookingQueue } from "@/components/bookings/OperationsBookingQueue"; import { OperationsScheduledBookings } from "@/components/bookings/OperationsScheduledBookings"; import { bookingTable } from "@/components/bookings/booking-ui.styles"; -import { PageContainer } from "@/components/page"; +import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; import { AllocateBookingWizard } from "@/components/trainScheduling/AllocateBookingWizard"; import { BOOKING_LIST_TABS } from "@/features/bookings/booking-status.config"; import { toBookingListRow } from "@/features/bookings/mapBookingListRow"; @@ -33,7 +47,6 @@ import { useBookingList, useBookingListSummary, } from "@/hooks/bookings/useBookings"; -import { cn } from "@/lib/utils"; import type { BookingListFilter } from "@/services/bookings.service"; import type { BookingListRow } from "@/types/booking"; import { @@ -50,13 +63,25 @@ function getStatusesForTab(tab: BookingStatusTabKey): string | undefined { return match.statuses.join(","); } +function formatDate(value: string | null | undefined): string { + if (!value) return "—"; + const d = new Date(value); + return Number.isNaN(d.getTime()) + ? "—" + : d.toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); +} + type OperationsSubTab = "ready" | "scheduled"; export default function BookingRequestsPage() { const navigate = useNavigate(); const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [query, setQuery] = useState(""); - const [activeTab, setActiveTab] = useState("in_approval"); + const [activeTab, setActiveTab] = useState("all"); const [operationsSubTab, setOperationsSubTab] = useState("ready"); const [allocateOpen, setAllocateOpen] = useState(false); const [allocateIds, setAllocateIds] = useState([]); @@ -251,7 +276,7 @@ export default function BookingRequestsPage() { cell: ({ row }) => ( - {row.original.scheduledDate} + {formatDate(row.original.scheduledDate)} ), }, @@ -262,29 +287,9 @@ export default function BookingRequestsPage() { ), }, - { - id: "amount", - header: () => ( - Amount - ), - cell: ({ row }) => { - const b = row.original; - return ( - - {b.paymentCurrency}{" "} - {b.totalAmount.toLocaleString(undefined, { - minimumFractionDigits: 2, - })} - - ); - }, - }, { id: "actions", size: 140, - header: () => ( - Actions - ), cell: ({ row }) => ( - + + + + } + /> + + navigate("/dashboard/booking-requests/new")} - onRefresh={handleRefresh} + items={[ + { + label: "In queue", + value: metrics?.inQueue ?? 0, + icon: LayoutList, + color: "edr-green", + }, + { + label: "Needs action", + value: metrics?.needsAction ?? 0, + icon: Clock, + color: "yellow", + }, + { + label: "Urgent", + value: metrics?.urgent ?? 0, + icon: AlertTriangle, + color: "red", + }, + { + label: "Completed", + value: tabCounts?.completed ?? 0, + icon: CheckCircle2, + color: "edr-green", + }, + ]} /> - - - - } - value={query} - onChange={(e) => setQuery(e.target.value)} - rightSection={ - query && ( - setQuery("")} - > - - - ) - } - style={{ flex: 1, minWidth: "200px" }} - radius="lg" - /> - - {total} record{total !== 1 ? "s" : ""} - - + + + + + } + value={query} + onChange={(e) => setQuery(e.target.value)} + rightSection={ + query && ( + setQuery("")} + > + + + ) + } + style={{ flex: 1, minWidth: "200px" }} + radius="lg" + /> + + {total} record{total !== 1 ? "s" : ""} + + + {isOperationsTab ? ( - - - setOperationsSubTab((value as OperationsSubTab) ?? "ready") - } - > - - Ready to allocate - On train / scheduled - - - {isError ? ( - - ) : operationsSubTab === "ready" ? ( - - ) : ( - - )} - + + + + setOperationsSubTab((value as OperationsSubTab) ?? "ready") + } + > + + Ready to allocate + On train / scheduled + + + {isError ? ( + + ) : operationsSubTab === "ready" ? ( + + ) : ( + + )} + + ) : showEmpty ? ( - + + + ) : ( -
+ -
+ )}
diff --git a/apps/edr-freight-web/backoffice/src/pages/documents/FileUploadSettingsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/documents/FileUploadSettingsPage.tsx index 9cb0e94e3..acc9b91c0 100644 --- a/apps/edr-freight-web/backoffice/src/pages/documents/FileUploadSettingsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/documents/FileUploadSettingsPage.tsx @@ -1,33 +1,47 @@ import { useMemo, useState } from "react"; import { - AlertCircle, - Filter, + ActionIcon, + Badge, + Box, + Button, + Card, + Code, + Group, + Stack, + Table, + Text, + TextInput, + ThemeIcon, +} from "@mantine/core"; +import { FileUp, HardDrive, Layers, - Loader2, Paperclip, Pencil, Plus, Search, Settings, Trash2, + X, } from "lucide-react"; +import { useQuery } from "@tanstack/react-query"; + +import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; +import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; +import { api } from "@/services/api"; +import { useDeleteFileUploadSetting } from "@/hooks/useFileUploadSettings"; +import { getMinFiles, type FileUploadSetting } from "@/types/fileUploadSettings"; +import { DataTable, type ColumnDef } from "@edr/ui-common"; -// import Breadcrumbs from "@/components/Breadcrumbs"; import EditFileUploadSettingDialog from "./EditFileUploadSettingDialog"; import ManageFileUploadFieldsDialog from "./ManageFileUploadFieldsDialog"; import DeleteFileUploadSettingDialog from "./DeleteFileUploadSettingDialog"; -import Breadcrumbs from "@/components/ui/Breadcrumbs"; -import { getMinFiles } from "@/types/fileUploadSettings"; -import { useQuery } from "@tanstack/react-query"; -import { api } from "@/services/api"; -import { useDeleteFileUploadSetting } from "@/hooks/useFileUploadSettings"; export default function FileUploadSettingsPage() { const [query, setQuery] = useState(""); - const { data, isLoading, isError, error } = useQuery( + const { data, isLoading, isError, error, refetch } = useQuery( api.fileUploadSettings.list.queryOptions(), ); const deleteMutation = useDeleteFileUploadSetting(); @@ -66,397 +80,298 @@ export default function FileUploadSettingsPage() { 0, ); - return ( -
-
- + const columns = useMemo[]>(() => { + const headerClassName = ruleEngineTable.headerCell; + const cellClassName = ruleEngineTable.bodyCell; + return [ + { + id: "setting", + header: "Setting", + meta: { headerClassName, cellClassName }, + cell: ({ row }) => { + const s = row.original; + return ( + + + + + + + {s.label} + + + {s.description ?? "No description"} + + + + ); + }, + }, + { + id: "code", + header: "Code", + meta: { headerClassName, cellClassName }, + cell: ({ row }) => {row.original.code}, + }, + { + id: "entity", + header: "Entity", + meta: { headerClassName, cellClassName }, + cell: ({ row }) => ( + + {row.original.entity ?? "—"} + + ), + }, + { + id: "fields", + header: "Fields", + meta: { headerClassName, cellClassName }, + cell: ({ row }) => ( + + + + {row.original.fields.length} + + + ), + }, + { + id: "requiredMulti", + header: "Required / Multi", + meta: { headerClassName, cellClassName }, + cell: ({ row }) => { + const required = row.original.fields.filter((f) => f.isRequired).length; + const multi = row.original.fields.filter((f) => f.isMultiple).length; + return ( + + + {required} required + + + {multi} multi + + + ); + }, + }, + { + id: "maxSize", + header: "Max size", + meta: { headerClassName, cellClassName }, + cell: ({ row }) => { + const maxSize = Math.max( + 0, + ...row.original.fields.map((f) => f.maxSizeMb), + ); + return ( + + + {maxSize ? `${maxSize} MB` : "—"} + + ); + }, + }, + { + id: "actions", + header: "Actions", + meta: { + headerClassName, + cellClassName: `${cellClassName} whitespace-nowrap`, + }, + cell: ({ row }) => { + const setting = row.original; + return ( + + + + - {/* Header */} -
-
-

- File Upload Settings -

-

- Define the file inputs every form in the platform should render — - required/optional, single/multiple, allowed types and size. -

-
+ + + + + -
-
- - setQuery(e.target.value)} - placeholder="Search by code, label, or file key..." - className="h-10 w-full rounded-2xl border border-slate-200 bg-white pl-10 pr-4 text-sm text-slate-700 outline-none transition placeholder:text-slate-400 focus:border-[#10B981]/50 focus:ring-2 focus:ring-[#10B981]/20" - /> -
- - - - -
-
+ + + + +
+ ); + }, + }, + ]; + }, [deleteMutation]); - {/* Stats */} -
- } - /> - } - /> - } - /> - } - /> -
+ const tableStatus = isLoading ? "loading" : isError ? "error" : "success"; - {/* Table */} -
-
-
-

- Registered File Upload Groups -

-

- Every group a form can reference by code. -

-
+ return ( + + + + + } + /> - -
+ -
- - - - - - - - - - - - + + + + } + value={query} + onChange={(e) => setQuery(e.currentTarget.value)} + rightSection={ + query ? ( + setQuery("")} + aria-label="Clear search" + > + + + ) : null + } + style={{ maxWidth: 420 }} + /> + - - {isLoading ? ( - - - - ) : isError ? ( - - - - ) : filtered.length === 0 ? ( - - - - ) : ( - filtered.map((setting) => { - const required = setting.fields.filter( - (f: any) => f.isRequired, - ).length; - const multi = setting.fields.filter( - (f: any) => f.isMultiple, - ).length; - const maxSize = Math.max( - 0, - ...setting.fields.map((f) => f.maxSizeMb), - ); + + void refetch(), + } + : undefined + } + emptyMessage={ + query.trim() + ? "No file upload settings match your search." + : 'No file upload settings yet. Click "New setting" to add one.' + } + containerClassName="border-0 shadow-none bg-transparent min-w-[920px]" + /> + + + - return ( - - + + + ); +} - +/** + * Static reference: how `min_files` / `max_files` are derived from the + * Required × Multiple toggles. Documentation aid for whoever wires uploaders. + */ +function BehaviorReferenceCard() { + const rows: { required: boolean; multiple: boolean; min: string; max: string }[] = + [ + { required: false, multiple: false, min: "0", max: "1" }, + { required: true, multiple: false, min: "1", max: "1" }, + { required: false, multiple: true, min: "0", max: "field.maxFiles" }, + { required: true, multiple: true, min: "1", max: "field.maxFiles" }, + ]; - + return ( + + + Required × Multiple behavior + + Min and max file counts are derived from these two toggles. The + "Max files" you set on a field is only used when{" "} + + Multiple + {" "} + is on. + - +
SettingCodeEntityFieldsRequired / MultiMax SizeActions
- -

- Loading file upload settings… -

-
- -

- Failed to load settings.{" "} - {error instanceof Error ? error.message : "Unknown error."} -

-
- {fileUploadSettings.length === 0 - ? "No file upload settings yet. Click \"New Setting\" to add one." - : "No file upload settings match your search."} -
-
-
- -
-
-

- {setting.label} -

-

- {setting.description ?? "No description"} -

-
-
-
- - {setting.code} - - - - {setting.entity ?? "—"} - - -
- - - {setting.fields.length} - -
-
+ + + Required + Multiple + min_files + max_files + + + + {rows.map((r) => ( + + + + {r.required ? "Required" : "Optional"} + + + + + {r.multiple ? "Multiple" : "Single"} + + + + {r.min} + + + {r.max} + + + ))} + +
- -
- {required} required - {multi} multi -
- - - -
- - {maxSize ? `${maxSize} MB` : "—"} -
- - - -
- - - - - - - - - - deleteMutation.mutate(setting.id) - } - > - - -
- - - ); - }) - )} - - -
-
- - {/* Behavior reference card */} -
-

- Required × Multiple behavior -

-

- Min and max file counts are derived from these two toggles. The - "Max Files" you set on a field is only used when{" "} - Multiple is on. -

-
- - - - - - - - - - - - - - - -
RequiredMultiplemin_filesmax_files
-
-

- Helpers getMinFiles and{" "} - getEffectiveMaxFiles live in{" "} - @/types/fileUploadSettings — use - them when wiring real uploaders. Example: a field with{" "} - isRequired=false,{" "} - isMultiple=true,{" "} - maxFiles=5 gives{" "} - {getMinFiles({ - id: "demo", - fileKey: "demo", - fileLabel: "demo", + + Helpers getMinFiles and getEffectiveMaxFiles{" "} + live in @/types/fileUploadSettings — use them when wiring + real uploaders. Example: a field with isRequired=false,{" "} + isMultiple=true, maxFiles=5 gives min{" "} + + {getMinFiles({ isRequired: false, - isMultiple: true, - maxFiles: 5, - allowedExtensions: [], - maxSizeMb: 1, - })} - …5. -

-
-
-
- ); -} - -function BehaviorRow({ - required, - multiple, - min, - max, -}: { - required: boolean; - multiple: boolean; - min: string; - max: string; -}) { - return ( - - - {required ? "Required" : "Optional"} - - - {multiple ? "Multiple" : "Single"} - - {min} - {max} - - ); -} - -function Chip({ - children, - muted = false, -}: { - children: React.ReactNode; - muted?: boolean; -}) { - return ( - - {children} - - ); -} - -function StatCard({ - title, - value, - icon, -}: { - title: string; - value: string; - icon: React.ReactNode; -}) { - return ( -
-
-
-

{title}

-

{value}

-
-
- {icon} -
-
-
+ })} +
{" "} + …5. + +
+
); } 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 dfaf52fac..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,25 +1,35 @@ -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 { PageContainer } from "@/components/page"; import FleetCardGrid from "@/components/fleet/FleetCardGrid"; import FleetFormDialog from "@/components/fleet/FleetFormDialog"; 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, @@ -27,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; @@ -250,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 ; } @@ -293,6 +352,24 @@ const FleetResourcePage = () => { return ( + } + onClick={() => { + setEditing(null); + setFormOpen(true); + }} + > + {config.addLabel} + + } + /> + + + @@ -301,11 +378,6 @@ const FleetResourcePage = () => { onSearchChange={setSearch} searchPlaceholder={config.searchPlaceholder} showSearch={config.supportsSearch} - addLabel={config.addLabel} - onAdd={() => { - setEditing(null); - setFormOpen(true); - }} viewMode={viewMode} onViewModeChange={setViewMode} filters={ @@ -316,7 +388,6 @@ const FleetResourcePage = () => { key={filter.key} size="sm" radius="lg" - label={filter.label} value={filter.value} onChange={(v) => { if (!v) return; 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 72ef9c965..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,7 +17,7 @@ import { Tooltip, } from "@mantine/core"; -import { PageContainer } from "@/components/page"; +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"; @@ -102,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) => ({ @@ -281,6 +284,30 @@ export default function RoutesPage() { return ( + } onClick={openCreate}> + Add route + + } + /> + + + @@ -288,8 +315,6 @@ export default function RoutesPage() { search={search} onSearchChange={setSearch} searchPlaceholder="Search routes…" - addLabel="Add Route" - onAdd={openCreate} viewMode={viewMode} onViewModeChange={setViewMode} /> 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 0c6c4b284..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,5 +1,6 @@ import { ActionIcon, + Box, Card, Group, Badge as MantineBadge, @@ -268,49 +269,51 @@ export default function PaymentsPage() { - - - - } - value={query} - onChange={(e) => { - setQuery(e.target.value); - setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); - }} - rightSection={ - query && ( - setQuery("")} - > - - - ) - } - style={{ flex: 1, minWidth: "200px" }} - /> - { + setMethod(value); + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + }} + style={{ minWidth: 180 }} + /> + + {total} record{total !== 1 ? "s" : ""} + + + -
+ -
+
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 }) => ( {
@@ -498,7 +510,7 @@ const RuleEngineResourcePage = () => { {chainLoading ? ( - + ) : ( <> @@ -524,7 +536,7 @@ const RuleEngineResourcePage = () => { )} -
+ ); }; 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 5586357c1..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,6 +1,3 @@ -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, @@ -17,6 +14,7 @@ import { TextInput, ThemeIcon, } from "@mantine/core"; +import { isAxiosError } from "axios"; import { ArrowRight, Ban, @@ -28,24 +26,26 @@ import { Train, Weight, } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; -import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; 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, StatusPill, } 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"; @@ -262,7 +262,7 @@ export default function TrainScheduleV2ListPage() { }, { id: "actions", - header: "Actions", + size:32, meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` }, cell: ({ row }) => { const schedule = row.original;