From 053b4f35c50d037d037a236277ad7d2a42da03e3 Mon Sep 17 00:00:00 2001 From: Marshal Date: Sat, 6 Jun 2026 06:32:04 +0000 Subject: [PATCH] changes --- apps/edr-freight-web/backoffice/package.json | 3 + .../bookings/BookingPriorityBadge.tsx | 14 +- .../components/bookings/BookingStatGrid.tsx | 83 +++-- .../bookings/BookingStatusBadge.tsx | 40 ++- .../components/bookings/BookingStatusTabs.tsx | 141 ++++---- .../ruleEngine/RuleEngineCardGrid.tsx | 271 ++++++++++------ .../ruleEngine/RuleEngineFormDialog.tsx | 302 ++++++++++-------- .../ruleEngine/RuleEngineRecordActions.tsx | 96 +++--- .../ruleEngine/RuleEngineToolbar.tsx | 122 ++++--- .../ruleEngine/ruleEngineFormat.tsx | 103 ++++-- .../components/ruleEngine/ruleEngineStyles.ts | 26 +- apps/edr-freight-web/backoffice/src/main.tsx | 21 +- .../ruleEngine/RuleEngineResourcePage.tsx | 236 +++++++------- .../src/pages/ruleEngine/config/resources.ts | 2 + pnpm-lock.yaml | 95 +++++- 15 files changed, 894 insertions(+), 661 deletions(-) diff --git a/apps/edr-freight-web/backoffice/package.json b/apps/edr-freight-web/backoffice/package.json index 7499e38a2..af6eea3b0 100644 --- a/apps/edr-freight-web/backoffice/package.json +++ b/apps/edr-freight-web/backoffice/package.json @@ -14,6 +14,9 @@ "dependencies": { "@edr/types": "workspace:*", "@edr/ui-common": "workspace:*", + "@mantine/core": "^9.3.0", + "@mantine/hooks": "^9.3.0", + "@tabler/icons-react": "^3.44.0", "@tanstack/react-query": "^5.100.11", "@tria-plc/iamui-common": "1.1.1", "axios": "^1.7.7", diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/BookingPriorityBadge.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/BookingPriorityBadge.tsx index 9202977c6..5a17e4568 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingPriorityBadge.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingPriorityBadge.tsx @@ -1,21 +1,23 @@ +import { Badge } from "@mantine/core"; + export function BookingPriorityBadge({ score }: { score: number }) { if (score >= 1000) { return ( - + Urgent - + ); } if (score >= 500) { return ( - + High - + ); } return ( - + Normal - + ); } diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatGrid.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatGrid.tsx index a216e927d..010e05c17 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatGrid.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatGrid.tsx @@ -1,6 +1,5 @@ import type { LucideIcon } from "lucide-react"; -import { bookingGlass } from "./booking-ui.styles"; -import { cn } from "@/lib/utils"; +import { Card, Group, Stack, Text, SimpleGrid } from "@mantine/core"; export interface StatItem { label: string; @@ -10,54 +9,76 @@ export interface StatItem { accent?: "default" | "amber" | "emerald" | "rose"; } -const iconAccentStyles = { - default: "text-foreground/70", - amber: "text-amber-600 dark:text-amber-400", - emerald: "text-emerald-600 dark:text-emerald-400", - rose: "text-rose-600 dark:text-rose-400", +const accentColors = { + default: { bg: "var(--mantine-color-gray-1)", color: "var(--mantine-color-gray-6)" }, + amber: { bg: "var(--mantine-color-yellow-1)", color: "var(--mantine-color-yellow-6)" }, + emerald: { bg: "var(--mantine-color-green-1)", color: "var(--mantine-color-green-6)" }, + rose: { bg: "var(--mantine-color-red-1)", color: "var(--mantine-color-red-6)" }, }; export function BookingStatGrid({ items }: { items: StatItem[] }) { return ( -
+ {items.map((item) => { const Icon = item.icon; const accent = item.accent ?? "default"; + const accentStyle = accentColors[accent]; + return ( -
{ + 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)"; + }} > -
-
-

+ + + {item.label} -

-

+ + {item.value} -

+ {item.hint && ( -

+ {item.hint} -

+ )} -
+
- +
-
-
+ + ); })} -
+ ); } diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusBadge.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusBadge.tsx index 25c3363c3..6d8ce3a99 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusBadge.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusBadge.tsx @@ -1,19 +1,43 @@ -import { Badge } from "@edr/ui-common"; -import { cn } from "@/lib/utils"; +import { Badge } from "@mantine/core"; import { BOOKING_STATUS_STYLES } from "@/features/bookings/booking-status.config"; +const statusColorMap: Record = { + DRAFT: "gray", + SUBMITTED: "yellow", + CHANGES_REQUESTED: "orange", + PENDING_APPROVAL: "yellow", + APPROVED_PENDING_SIGNATURE: "cyan", + APPROVED: "green", + CONTRACT_READY: "indigo", + SIGNED_CUSTOMER: "cyan", + FULLY_EXECUTED: "indigo", + PNR_GENERATED: "violet", + PAYMENT_VERIFICATION_IN_PROGRESS: "yellow", + PAID: "green", + IN_TRANSIT: "cyan", + COMPLETED: "indigo", + REJECTED: "red", + CANCELLED: "red", + PENDING_CONSOLIDATION: "yellow", + CONSOLIDATED: "indigo", +}; + export function BookingStatusBadge({ status }: { status: string }) { const style = BOOKING_STATUS_STYLES[status] ?? { label: status, - color: "bg-muted text-muted-foreground border-border", + color: "gray", }; + const color = statusColorMap[status] ?? "gray"; + return ( {style.label} 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 bbbf373bc..e09bab140 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusTabs.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusTabs.tsx @@ -8,27 +8,24 @@ import { Wallet, XCircle, } from "lucide-react"; +import { Group, Badge, UnstyledButton, Stack, Text } from "@mantine/core"; import { BOOKING_LIST_TABS, type BookingStatusTabKey, } from "@/features/bookings/booking-status.config"; -import { bookingGlass } from "./booking-ui.styles"; -import { cn } from "@/lib/utils"; const TAB_ICONS: Record = { - all: , - intake: , - in_approval: , - approved_contract: , - payment: , - operations: , - completed: , - closed: , + all: , + intake: , + in_approval: , + approved_contract: , + payment: , + operations: , + completed: , + closed: , }; -const activeTabText = "text-black"; - interface BookingStatusTabsProps { active: BookingStatusTabKey; onChange: (tab: BookingStatusTabKey) => void; @@ -41,66 +38,68 @@ export function BookingStatusTabs({ counts, }: BookingStatusTabsProps) { return ( -
-
- {BOOKING_LIST_TABS.map((tab) => { - const isActive = active === tab.key; - const count = counts?.[tab.key]; - return ( - - ); - })} -
-
+ {TAB_ICONS[tab.key]} + + + {tab.label} + + + {count !== undefined && count > 0 && ( + + {count} + + )} + + + ); + })} + ); } 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 e57a48ff4..a953f47e6 100644 --- a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx +++ b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx @@ -1,12 +1,11 @@ +import { Stack, Group, Text, Pagination, Card, SimpleGrid } from "@mantine/core"; + import type { RuleEngineResourceConfig } from "@/pages/ruleEngine/config/resources"; import type { RuleEngineRecord } from "@/types/rule-engine"; -import { DataTableFooter } from "@edr/ui-common"; -import type { Table } from "@edr/ui-common"; import RuleEngineRecordActions from "./RuleEngineRecordActions"; import { cardInitials, resolveCardPresentation } from "./ruleEngineCardMeta"; import { formatCell } from "./ruleEngineFormat"; -import { ruleEngineCard } from "./ruleEngineStyles"; export interface RuleEngineCardGridProps { config: RuleEngineResourceConfig; @@ -14,7 +13,6 @@ export interface RuleEngineCardGridProps { status: "loading" | "error" | "success"; emptyMessage: string; itemLabel: string; - table: Table; pagination: { pageIndex: number; pageSize: number; @@ -29,13 +27,49 @@ export interface RuleEngineCardGridProps { onApproveRate?: (record: RuleEngineRecord) => void; } +const extractLabel = (value: unknown): string => { + if (!value || typeof value !== "object") { + return String(value || ""); + } + + const obj = value as Record; + return ( + (typeof obj.label === "string" ? obj.label : null) || + (typeof obj.cargoTypeName === "string" ? obj.cargoTypeName : null) || + (typeof obj.serviceName === "string" ? obj.serviceName : null) || + (typeof obj.code === "string" ? obj.code : null) || + (typeof obj.name === "string" ? obj.name : null) || + (typeof obj.actionLabel === "string" ? obj.actionLabel : null) || + String(value) + ); +}; + +const getSmartValue = (record: RuleEngineRecord, key: string): unknown => { + const value = record[key as keyof RuleEngineRecord]; + + // If the value is already an object, use it directly + if (value && typeof value === "object") { + return value; + } + + // If the key ends with "Id" and there's a corresponding non-Id key, use that + if (typeof key === "string" && key.endsWith("Id")) { + const relatedKey = key.slice(0, -2); // Remove "Id" suffix + const relatedValue = record[relatedKey as keyof RuleEngineRecord]; + if (relatedValue && typeof relatedValue === "object") { + return relatedValue; + } + } + + return value; +}; + const RuleEngineCardGrid = ({ config, rows, status, emptyMessage, itemLabel, - table, pagination, onEdit, onDelete, @@ -48,109 +82,156 @@ const RuleEngineCardGrid = ({ if (status === "error") { return ( -
-

Failed to load data

-

+ + Failed to load data + Please refresh the page or try again later. -

-
+ + ); } if (status === "loading") { return ( -
- {Array.from({ length: 6 }).map((_, index) => ( -
-
-
-
-
-
+ + + {Array.from({ length: 6 }).map((_, index) => ( + +
+
+
+
+
-
-
-
-
-
-
- ))} -
+ + ))} + + ); } if (status === "success" && rows.length === 0) { return ( -
-

{emptyMessage}

-

+ + {emptyMessage} + Try adjusting your search or add a new record. -

-
+ + ); } + const avatarColors = ["blue", "cyan", "grape", "green", "lime", "orange", "pink", "red", "teal", "violet", "yellow"]; + const getAvatarColor = (title: string) => avatarColors[title.charCodeAt(0) % avatarColors.length]; + return ( - <> -
+ + {rows.map((record) => { - const title = String(record[presentation.titleKey] ?? "Untitled"); - const subtitle = presentation.subtitleKey - ? String(record[presentation.subtitleKey] ?? "") - : ""; - const code = presentation.codeKey - ? String(record[presentation.codeKey] ?? "") - : ""; + const titleValue = getSmartValue(record, presentation.titleKey); + const title = extractLabel(titleValue); + + const subtitleValue = presentation.subtitleKey + ? getSmartValue(record, presentation.subtitleKey) + : null; + const subtitle = subtitleValue ? extractLabel(subtitleValue) : ""; + + const codeValue = presentation.codeKey + ? getSmartValue(record, presentation.codeKey) + : null; + const code = codeValue ? extractLabel(codeValue) : ""; + const statusValue = presentation.statusKey ? record[presentation.statusKey] : undefined; return ( -
-
-
-
+ { + e.currentTarget.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.1)"; + e.currentTarget.style.borderColor = "var(--mantine-color-blue-3)"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.boxShadow = "none"; + e.currentTarget.style.borderColor = "var(--mantine-color-gray-2)"; + }} + > + + +
{cardInitials(title)}
-
-
-

{title}

- {presentation.statusKey - ? formatCell(statusValue, "activeBadge") - : null} -
- {(code || subtitle) && ( -
- {code ? formatCell(code, "code") : null} - {subtitle ? ( - - {presentation.subtitleKey === "stepOrder" - ? `Step ${subtitle}` - : subtitle} - - ) : null} -
+
+ + {title} + + {code && ( + + {code} + )}
-
-
+ + {presentation.statusKey && ( +
{formatCell(statusValue, "activeBadge")}
+ )} + - {presentation.detailColumns.length > 0 ? ( -
- {presentation.detailColumns.map((col) => ( -
-
{col.header}
-
- {formatCell(record[col.accessorKey], col.format)} -
-
- ))} -
- ) : ( -
+ {(subtitle || presentation.detailColumns.length > 0) && ( + + {subtitle && ( + + + {presentation.subtitleKey === "stepOrder" ? "Step" : "Type"}: + + + {subtitle} + + + )} + {presentation.detailColumns.map((col) => { + const displayValue = getSmartValue(record, col.accessorKey); + return ( + + + {col.header}: + +
+ {formatCell(displayValue, col.format)} +
+
+ ); + })} +
)} -
+ -
-
+ + ); })} -
+ -
- -
- + {pagination.pageCount > 1 && ( + + + Showing {Math.min(rows.length, pagination.pageSize)} of {pagination.totalCount} {itemLabel} + + + + )} + ); }; diff --git a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineFormDialog.tsx b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineFormDialog.tsx index 2628eadb8..2cb57d8bc 100644 --- a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineFormDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineFormDialog.tsx @@ -1,27 +1,18 @@ import { useEffect, useState } from "react"; import { Loader2 } from "lucide-react"; - import { + Modal, Button, - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - Field, - FieldContent, - FieldLabel, - Input, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - Separator, - Switch, + TextInput, Textarea, -} from "@edr/ui-common"; + Select, + Switch, + Stack, + Group, + Divider, + Text, + Box, +} from "@mantine/core"; import { RULE_ENGINE_SELECT_NONE, @@ -29,8 +20,6 @@ import { } from "@/pages/ruleEngine/config/resources"; import type { RuleEngineRecord } from "@/types/rule-engine"; -import { ruleEngineField, ruleEngineSurface } from "./ruleEngineStyles"; - export interface RuleEngineFormDialogProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -142,133 +131,174 @@ const RuleEngineFormDialog = ({ }; return ( - - - - {title} - {description} - + onOpenChange(false)} + title={title} + centered + size="md" + radius="lg" + styles={{ + header: { + paddingBottom: "20px", + borderBottom: "1px solid var(--mantine-color-gray-2)", + }, + body: { + paddingTop: "24px", + paddingBottom: "24px", + }, + title: { + fontSize: "1.25rem", + fontWeight: 700, + color: "var(--mantine-color-gray-9)", + }, + }} + > +
+ + + {description} + - -
- {fields.map((field) => ( - - {field.type === "boolean" ? ( -
-
- - {field.label} - -

- {Boolean(values[field.name]) ? "Enabled" : "Disabled"} -

-
- setField(field.name, checked)} +
+ + {fields.map((field) => ( + + {field.type === "boolean" ? ( + +
+ + {field.label} + + + {Boolean(values[field.name]) ? "✓ Enabled" : "○ Disabled"} + +
+ setField(field.name, e.currentTarget.checked)} + size="lg" + /> +
+ ) : field.type === "select" ? ( + - setField( - field.name, - v === RULE_ENGINE_SELECT_NONE ? "" : v, - ) - } - disabled={selectOptionsLoading} - > - - - - - {(field.options ?? []) - .filter((opt) => opt.value !== "") - .map((opt) => ( - - {opt.label} - - ))} - - - ) : field.type === "textarea" ? ( -