mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 01:18:18 +00:00
changes
This commit is contained in:
@@ -14,6 +14,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edr/types": "workspace:*",
|
"@edr/types": "workspace:*",
|
||||||
"@edr/ui-common": "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",
|
"@tanstack/react-query": "^5.100.11",
|
||||||
"@tria-plc/iamui-common": "1.1.1",
|
"@tria-plc/iamui-common": "1.1.1",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
|
import { Badge } from "@mantine/core";
|
||||||
|
|
||||||
export function BookingPriorityBadge({ score }: { score: number }) {
|
export function BookingPriorityBadge({ score }: { score: number }) {
|
||||||
if (score >= 1000) {
|
if (score >= 1000) {
|
||||||
return (
|
return (
|
||||||
<span className="rounded-full bg-red-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-red-700">
|
<Badge color="red" variant="filled" size="sm" radius="lg" tt="uppercase">
|
||||||
Urgent
|
Urgent
|
||||||
</span>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (score >= 500) {
|
if (score >= 500) {
|
||||||
return (
|
return (
|
||||||
<span className="rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-amber-700">
|
<Badge color="yellow" variant="filled" size="sm" radius="lg" tt="uppercase">
|
||||||
High
|
High
|
||||||
</span>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-slate-600">
|
<Badge color="gray" variant="light" size="sm" radius="lg" tt="uppercase">
|
||||||
Normal
|
Normal
|
||||||
</span>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { bookingGlass } from "./booking-ui.styles";
|
import { Card, Group, Stack, Text, SimpleGrid } from "@mantine/core";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export interface StatItem {
|
export interface StatItem {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -10,54 +9,76 @@ export interface StatItem {
|
|||||||
accent?: "default" | "amber" | "emerald" | "rose";
|
accent?: "default" | "amber" | "emerald" | "rose";
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconAccentStyles = {
|
const accentColors = {
|
||||||
default: "text-foreground/70",
|
default: { bg: "var(--mantine-color-gray-1)", color: "var(--mantine-color-gray-6)" },
|
||||||
amber: "text-amber-600 dark:text-amber-400",
|
amber: { bg: "var(--mantine-color-yellow-1)", color: "var(--mantine-color-yellow-6)" },
|
||||||
emerald: "text-emerald-600 dark:text-emerald-400",
|
emerald: { bg: "var(--mantine-color-green-1)", color: "var(--mantine-color-green-6)" },
|
||||||
rose: "text-rose-600 dark:text-rose-400",
|
rose: { bg: "var(--mantine-color-red-1)", color: "var(--mantine-color-red-6)" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function BookingStatGrid({ items }: { items: StatItem[] }) {
|
export function BookingStatGrid({ items }: { items: StatItem[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="md">
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const accent = item.accent ?? "default";
|
const accent = item.accent ?? "default";
|
||||||
|
const accentStyle = accentColors[accent];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Card
|
||||||
key={item.label}
|
key={item.label}
|
||||||
className={cn(
|
p="lg"
|
||||||
"group relative overflow-hidden rounded-xl p-5 transition-all duration-200 hover:shadow-md",
|
radius="lg"
|
||||||
bookingGlass.card,
|
withBorder
|
||||||
)}
|
style={{
|
||||||
|
background: "white",
|
||||||
|
border: "1px solid var(--mantine-color-gray-2)",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
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)";
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3">
|
<Group justify="space-between" align="flex-start">
|
||||||
<div className="min-w-0 flex-1">
|
<Stack gap="xs" style={{ flex: 1 }}>
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
<Text size="xs" fw={600} c="dimmed" tt="uppercase">
|
||||||
{item.label}
|
{item.label}
|
||||||
</p>
|
</Text>
|
||||||
<p className="mt-2 text-3xl font-semibold tabular-nums tracking-tight text-foreground">
|
<Text size="32px" fw={700} style={{ lineHeight: 1, letterSpacing: "-0.02em" }}>
|
||||||
{item.value}
|
{item.value}
|
||||||
</p>
|
</Text>
|
||||||
{item.hint && (
|
{item.hint && (
|
||||||
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
<Text size="xs" c="dimmed">
|
||||||
{item.hint}
|
{item.hint}
|
||||||
</p>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Stack>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
style={{
|
||||||
"flex size-10 shrink-0 items-center justify-center rounded-xl transition-transform duration-200 group-hover:scale-[1.02]",
|
display: "flex",
|
||||||
bookingGlass.iconWellGreen,
|
alignItems: "center",
|
||||||
iconAccentStyles[accent],
|
justifyContent: "center",
|
||||||
)}
|
width: "44px",
|
||||||
|
height: "44px",
|
||||||
|
borderRadius: "10px",
|
||||||
|
background: accentStyle.bg,
|
||||||
|
color: accentStyle.color,
|
||||||
|
flexShrink: 0,
|
||||||
|
transition: "transform 0.2s ease",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Icon className="size-[18px]" strokeWidth={1.75} />
|
<Icon size={22} strokeWidth={1.75} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Group>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</SimpleGrid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,43 @@
|
|||||||
import { Badge } from "@edr/ui-common";
|
import { Badge } from "@mantine/core";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { BOOKING_STATUS_STYLES } from "@/features/bookings/booking-status.config";
|
import { BOOKING_STATUS_STYLES } from "@/features/bookings/booking-status.config";
|
||||||
|
|
||||||
|
const statusColorMap: Record<string, string> = {
|
||||||
|
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 }) {
|
export function BookingStatusBadge({ status }: { status: string }) {
|
||||||
const style = BOOKING_STATUS_STYLES[status] ?? {
|
const style = BOOKING_STATUS_STYLES[status] ?? {
|
||||||
label: status,
|
label: status,
|
||||||
color: "bg-muted text-muted-foreground border-border",
|
color: "gray",
|
||||||
};
|
};
|
||||||
|
const color = statusColorMap[status] ?? "gray";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
color={color}
|
||||||
className={cn(
|
variant="light"
|
||||||
"px-2 py-0.5 text-[9px] font-bold uppercase tracking-wider",
|
size="sm"
|
||||||
style.color,
|
radius="md"
|
||||||
)}
|
tt="uppercase"
|
||||||
|
fw={600}
|
||||||
|
style={{ fontSize: "0.7rem", letterSpacing: "0.05em" }}
|
||||||
>
|
>
|
||||||
{style.label}
|
{style.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -8,27 +8,24 @@ import {
|
|||||||
Wallet,
|
Wallet,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { Group, Badge, UnstyledButton, Stack, Text } from "@mantine/core";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BOOKING_LIST_TABS,
|
BOOKING_LIST_TABS,
|
||||||
type BookingStatusTabKey,
|
type BookingStatusTabKey,
|
||||||
} from "@/features/bookings/booking-status.config";
|
} from "@/features/bookings/booking-status.config";
|
||||||
import { bookingGlass } from "./booking-ui.styles";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const TAB_ICONS: Record<BookingStatusTabKey, React.ReactNode> = {
|
const TAB_ICONS: Record<BookingStatusTabKey, React.ReactNode> = {
|
||||||
all: <LayoutGrid className="size-3.5" strokeWidth={1.75} />,
|
all: <LayoutGrid size={18} strokeWidth={1.75} />,
|
||||||
intake: <Inbox className="size-3.5" strokeWidth={1.75} />,
|
intake: <Inbox size={18} strokeWidth={1.75} />,
|
||||||
in_approval: <ClipboardCheck className="size-3.5" strokeWidth={1.75} />,
|
in_approval: <ClipboardCheck size={18} strokeWidth={1.75} />,
|
||||||
approved_contract: <FileSignature className="size-3.5" strokeWidth={1.75} />,
|
approved_contract: <FileSignature size={18} strokeWidth={1.75} />,
|
||||||
payment: <Wallet className="size-3.5" strokeWidth={1.75} />,
|
payment: <Wallet size={18} strokeWidth={1.75} />,
|
||||||
operations: <Train className="size-3.5" strokeWidth={1.75} />,
|
operations: <Train size={18} strokeWidth={1.75} />,
|
||||||
completed: <CheckCircle className="size-3.5" strokeWidth={1.75} />,
|
completed: <CheckCircle size={18} strokeWidth={1.75} />,
|
||||||
closed: <XCircle className="size-3.5" strokeWidth={1.75} />,
|
closed: <XCircle size={18} strokeWidth={1.75} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeTabText = "text-black";
|
|
||||||
|
|
||||||
interface BookingStatusTabsProps {
|
interface BookingStatusTabsProps {
|
||||||
active: BookingStatusTabKey;
|
active: BookingStatusTabKey;
|
||||||
onChange: (tab: BookingStatusTabKey) => void;
|
onChange: (tab: BookingStatusTabKey) => void;
|
||||||
@@ -41,66 +38,68 @@ export function BookingStatusTabs({
|
|||||||
counts,
|
counts,
|
||||||
}: BookingStatusTabsProps) {
|
}: BookingStatusTabsProps) {
|
||||||
return (
|
return (
|
||||||
<div className={bookingGlass.tabRail}>
|
<Group
|
||||||
<div
|
gap="sm"
|
||||||
className="flex flex-nowrap gap-1.5 overflow-x-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
|
wrap="wrap"
|
||||||
role="tablist"
|
p="md"
|
||||||
aria-label="Booking status filters"
|
style={{
|
||||||
>
|
background: "var(--mantine-color-gray-0)",
|
||||||
{BOOKING_LIST_TABS.map((tab) => {
|
borderRadius: "12px",
|
||||||
const isActive = active === tab.key;
|
border: "1px solid var(--mantine-color-gray-2)",
|
||||||
const count = counts?.[tab.key];
|
}}
|
||||||
return (
|
>
|
||||||
<button
|
{BOOKING_LIST_TABS.map((tab) => {
|
||||||
key={tab.key}
|
const isActive = active === tab.key;
|
||||||
type="button"
|
const count = counts?.[tab.key];
|
||||||
role="tab"
|
return (
|
||||||
aria-selected={isActive}
|
<UnstyledButton
|
||||||
onClick={() => onChange(tab.key)}
|
key={tab.key}
|
||||||
className={cn(
|
onClick={() => onChange(tab.key)}
|
||||||
"flex min-w-[8rem] shrink-0 flex-col items-start gap-0.5 rounded-lg px-3 py-2.5 text-left transition-all duration-200",
|
style={{
|
||||||
isActive
|
background: isActive ? "white" : "transparent",
|
||||||
? bookingGlass.activeTab
|
border: isActive ? "1px solid var(--mantine-color-blue-3)" : "1px solid var(--mantine-color-gray-2)",
|
||||||
: "text-muted-foreground hover:bg-emerald-500/5 hover:text-foreground",
|
borderRadius: "10px",
|
||||||
)}
|
padding: "10px 16px",
|
||||||
>
|
transition: "all 0.2s ease",
|
||||||
<span className="flex w-full items-center justify-between gap-2">
|
cursor: "pointer",
|
||||||
<span
|
boxShadow: isActive ? "0 2px 8px rgba(59, 130, 246, 0.1)" : "none",
|
||||||
className={cn(
|
}}
|
||||||
"flex items-center gap-2 text-sm font-medium",
|
>
|
||||||
isActive ? activeTabText : "text-muted-foreground",
|
<Group gap="sm" justify="space-between" wrap="nowrap">
|
||||||
)}
|
<Group gap={8}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "32px",
|
||||||
|
height: "32px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
background: isActive ? "var(--mantine-color-blue-1)" : "var(--mantine-color-gray-1)",
|
||||||
|
color: isActive ? "var(--mantine-color-blue-7)" : "var(--mantine-color-gray-6)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span
|
{TAB_ICONS[tab.key]}
|
||||||
className={cn(
|
</div>
|
||||||
"flex size-7 shrink-0 items-center justify-center rounded-md",
|
<Text size="sm" fw={600}>
|
||||||
isActive
|
{tab.label}
|
||||||
? cn(bookingGlass.iconWellGreen, "text-black")
|
</Text>
|
||||||
: "border border-transparent bg-muted/30",
|
</Group>
|
||||||
)}
|
{count !== undefined && count > 0 && (
|
||||||
>
|
<Badge
|
||||||
{TAB_ICONS[tab.key]}
|
size="sm"
|
||||||
</span>
|
variant={isActive ? "filled" : "light"}
|
||||||
<span className="whitespace-nowrap">{tab.label}</span>
|
color={isActive ? "blue" : "gray"}
|
||||||
</span>
|
radius="lg"
|
||||||
{count !== undefined && count > 0 && (
|
>
|
||||||
<span
|
{count}
|
||||||
className={cn(
|
</Badge>
|
||||||
"rounded-full px-2 py-0.5 text-[10px] font-semibold tabular-nums",
|
)}
|
||||||
isActive
|
</Group>
|
||||||
? cn("bg-emerald-500/15", activeTabText)
|
</UnstyledButton>
|
||||||
: "bg-muted/50 text-muted-foreground",
|
);
|
||||||
)}
|
})}
|
||||||
>
|
</Group>
|
||||||
{count}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
|
import { Stack, Group, Text, Pagination, Card, SimpleGrid } from "@mantine/core";
|
||||||
|
|
||||||
import type { RuleEngineResourceConfig } from "@/pages/ruleEngine/config/resources";
|
import type { RuleEngineResourceConfig } from "@/pages/ruleEngine/config/resources";
|
||||||
import type { RuleEngineRecord } from "@/types/rule-engine";
|
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 RuleEngineRecordActions from "./RuleEngineRecordActions";
|
||||||
import { cardInitials, resolveCardPresentation } from "./ruleEngineCardMeta";
|
import { cardInitials, resolveCardPresentation } from "./ruleEngineCardMeta";
|
||||||
import { formatCell } from "./ruleEngineFormat";
|
import { formatCell } from "./ruleEngineFormat";
|
||||||
import { ruleEngineCard } from "./ruleEngineStyles";
|
|
||||||
|
|
||||||
export interface RuleEngineCardGridProps {
|
export interface RuleEngineCardGridProps {
|
||||||
config: RuleEngineResourceConfig;
|
config: RuleEngineResourceConfig;
|
||||||
@@ -14,7 +13,6 @@ export interface RuleEngineCardGridProps {
|
|||||||
status: "loading" | "error" | "success";
|
status: "loading" | "error" | "success";
|
||||||
emptyMessage: string;
|
emptyMessage: string;
|
||||||
itemLabel: string;
|
itemLabel: string;
|
||||||
table: Table<RuleEngineRecord>;
|
|
||||||
pagination: {
|
pagination: {
|
||||||
pageIndex: number;
|
pageIndex: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
@@ -29,13 +27,49 @@ export interface RuleEngineCardGridProps {
|
|||||||
onApproveRate?: (record: RuleEngineRecord) => void;
|
onApproveRate?: (record: RuleEngineRecord) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractLabel = (value: unknown): string => {
|
||||||
|
if (!value || typeof value !== "object") {
|
||||||
|
return String(value || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = value as Record<string, unknown>;
|
||||||
|
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 = ({
|
const RuleEngineCardGrid = ({
|
||||||
config,
|
config,
|
||||||
rows,
|
rows,
|
||||||
status,
|
status,
|
||||||
emptyMessage,
|
emptyMessage,
|
||||||
itemLabel,
|
itemLabel,
|
||||||
table,
|
|
||||||
pagination,
|
pagination,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
@@ -48,109 +82,156 @@ const RuleEngineCardGrid = ({
|
|||||||
|
|
||||||
if (status === "error") {
|
if (status === "error") {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center px-6 py-16 text-center">
|
<Stack align="center" justify="center" p="xl" style={{ minHeight: "400px" }}>
|
||||||
<p className="text-sm font-medium text-foreground">Failed to load data</p>
|
<Text size="lg" fw={600} c="red">Failed to load data</Text>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<Text size="sm" c="dimmed">
|
||||||
Please refresh the page or try again later.
|
Please refresh the page or try again later.
|
||||||
</p>
|
</Text>
|
||||||
</div>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === "loading") {
|
if (status === "loading") {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 gap-3 p-4 sm:grid-cols-2 lg:grid-cols-3 xl:gap-4">
|
<Stack gap="md" p="md">
|
||||||
{Array.from({ length: 6 }).map((_, index) => (
|
<SimpleGrid cols={{ base: 1, sm: 2, md: 2, lg: 3 }} spacing="md">
|
||||||
<div key={index} className={ruleEngineCard.skeleton}>
|
{Array.from({ length: 6 }).map((_, index) => (
|
||||||
<div className="flex gap-3">
|
<Card key={index} p="md" radius="lg" withBorder style={{ height: "280px", background: "var(--mantine-color-gray-0)" }}>
|
||||||
<div className="h-10 w-10 rounded-md bg-muted" />
|
<div style={{ animation: "pulse 2s infinite", opacity: 0.5 }}>
|
||||||
<div className="flex-1 space-y-2">
|
<div style={{ height: "20px", background: "var(--mantine-color-gray-3)", borderRadius: "4px", marginBottom: "12px" }} />
|
||||||
<div className="h-4 w-2/3 rounded-sm bg-muted" />
|
<div style={{ height: "16px", background: "var(--mantine-color-gray-3)", borderRadius: "4px", marginBottom: "20px", width: "80%" }} />
|
||||||
<div className="h-3 w-1/3 rounded-sm bg-muted" />
|
<div style={{ height: "16px", background: "var(--mantine-color-gray-3)", borderRadius: "4px", marginBottom: "8px" }} />
|
||||||
|
<div style={{ height: "16px", background: "var(--mantine-color-gray-3)", borderRadius: "4px" }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
<div className="mt-4 space-y-2">
|
))}
|
||||||
<div className="h-3 w-full rounded-sm bg-muted" />
|
</SimpleGrid>
|
||||||
<div className="h-3 w-4/5 rounded-sm bg-muted" />
|
</Stack>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === "success" && rows.length === 0) {
|
if (status === "success" && rows.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center px-6 py-16 text-center">
|
<Stack align="center" justify="center" p="xl" style={{ minHeight: "400px" }}>
|
||||||
<p className="text-sm font-medium text-foreground">{emptyMessage}</p>
|
<Text size="lg" fw={600}>{emptyMessage}</Text>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<Text size="sm" c="dimmed">
|
||||||
Try adjusting your search or add a new record.
|
Try adjusting your search or add a new record.
|
||||||
</p>
|
</Text>
|
||||||
</div>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const avatarColors = ["blue", "cyan", "grape", "green", "lime", "orange", "pink", "red", "teal", "violet", "yellow"];
|
||||||
|
const getAvatarColor = (title: string) => avatarColors[title.charCodeAt(0) % avatarColors.length];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Stack gap="md" p="md">
|
||||||
<div className="grid grid-cols-1 gap-3 p-4 sm:grid-cols-2 lg:grid-cols-3 xl:gap-4">
|
<SimpleGrid cols={{ base: 1, sm: 2, md: 2, lg: 3 }} spacing="md">
|
||||||
{rows.map((record) => {
|
{rows.map((record) => {
|
||||||
const title = String(record[presentation.titleKey] ?? "Untitled");
|
const titleValue = getSmartValue(record, presentation.titleKey);
|
||||||
const subtitle = presentation.subtitleKey
|
const title = extractLabel(titleValue);
|
||||||
? String(record[presentation.subtitleKey] ?? "")
|
|
||||||
: "";
|
const subtitleValue = presentation.subtitleKey
|
||||||
const code = presentation.codeKey
|
? getSmartValue(record, presentation.subtitleKey)
|
||||||
? String(record[presentation.codeKey] ?? "")
|
: null;
|
||||||
: "";
|
const subtitle = subtitleValue ? extractLabel(subtitleValue) : "";
|
||||||
|
|
||||||
|
const codeValue = presentation.codeKey
|
||||||
|
? getSmartValue(record, presentation.codeKey)
|
||||||
|
: null;
|
||||||
|
const code = codeValue ? extractLabel(codeValue) : "";
|
||||||
|
|
||||||
const statusValue = presentation.statusKey
|
const statusValue = presentation.statusKey
|
||||||
? record[presentation.statusKey]
|
? record[presentation.statusKey]
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article key={record.id} className={ruleEngineCard.article}>
|
<Card
|
||||||
<div className={ruleEngineCard.header}>
|
key={record.id}
|
||||||
<div className="flex items-start gap-3">
|
p="lg"
|
||||||
<div className={ruleEngineCard.avatar} aria-hidden>
|
radius="lg"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background: "white",
|
||||||
|
border: "1px solid var(--mantine-color-gray-2)",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
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)";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="flex-start" mb="md">
|
||||||
|
<Group gap="sm" style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "44px",
|
||||||
|
height: "44px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
background: `var(--mantine-color-${getAvatarColor(title)}-1)`,
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: 700,
|
||||||
|
color: `var(--mantine-color-${getAvatarColor(title)}-7)`,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{cardInitials(title)}
|
{cardInitials(title)}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div style={{ minWidth: 0, flex: 1 }}>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<Text size="sm" fw={700} truncate title={title}>
|
||||||
<h3 className={ruleEngineCard.title}>{title}</h3>
|
{title}
|
||||||
{presentation.statusKey
|
</Text>
|
||||||
? formatCell(statusValue, "activeBadge")
|
{code && (
|
||||||
: null}
|
<Text size="xs" c="dimmed" style={{ marginTop: "4px" }}>
|
||||||
</div>
|
{code}
|
||||||
{(code || subtitle) && (
|
</Text>
|
||||||
<div className="mt-1.5 flex flex-wrap items-center gap-2">
|
|
||||||
{code ? formatCell(code, "code") : null}
|
|
||||||
{subtitle ? (
|
|
||||||
<span className={ruleEngineCard.meta}>
|
|
||||||
{presentation.subtitleKey === "stepOrder"
|
|
||||||
? `Step ${subtitle}`
|
|
||||||
: subtitle}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Group>
|
||||||
</div>
|
{presentation.statusKey && (
|
||||||
|
<div>{formatCell(statusValue, "activeBadge")}</div>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
|
||||||
{presentation.detailColumns.length > 0 ? (
|
{(subtitle || presentation.detailColumns.length > 0) && (
|
||||||
<dl className="grid flex-1 gap-x-4 gap-y-3 px-4 py-3.5 sm:grid-cols-2">
|
<Stack gap="xs" style={{ flex: 1, marginBottom: "md" }}>
|
||||||
{presentation.detailColumns.map((col) => (
|
{subtitle && (
|
||||||
<div key={col.id} className="min-w-0">
|
<Group gap="xs">
|
||||||
<dt className={ruleEngineCard.detailLabel}>{col.header}</dt>
|
<Text size="xs" c="dimmed" fw={500}>
|
||||||
<dd className={ruleEngineCard.detailValue}>
|
{presentation.subtitleKey === "stepOrder" ? "Step" : "Type"}:
|
||||||
{formatCell(record[col.accessorKey], col.format)}
|
</Text>
|
||||||
</dd>
|
<Text size="xs" fw={500}>
|
||||||
</div>
|
{subtitle}
|
||||||
))}
|
</Text>
|
||||||
</dl>
|
</Group>
|
||||||
) : (
|
)}
|
||||||
<div className="flex-1 px-4 py-2" />
|
{presentation.detailColumns.map((col) => {
|
||||||
|
const displayValue = getSmartValue(record, col.accessorKey);
|
||||||
|
return (
|
||||||
|
<Group key={col.id} justify="space-between" gap="xs" align="flex-start">
|
||||||
|
<Text size="xs" c="dimmed" fw={500}>
|
||||||
|
{col.header}:
|
||||||
|
</Text>
|
||||||
|
<div style={{ textAlign: "right", flex: 1 }}>
|
||||||
|
{formatCell(displayValue, col.format)}
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={ruleEngineCard.footer}>
|
<Group justify="flex-end" gap="xs" style={{ borderTop: "1px solid var(--mantine-color-gray-1)", paddingTop: "md" }}>
|
||||||
<RuleEngineRecordActions
|
<RuleEngineRecordActions
|
||||||
record={record}
|
record={record}
|
||||||
config={config}
|
config={config}
|
||||||
@@ -162,26 +243,26 @@ const RuleEngineCardGrid = ({
|
|||||||
onSubmitRate={onSubmitRate}
|
onSubmitRate={onSubmitRate}
|
||||||
onApproveRate={onApproveRate}
|
onApproveRate={onApproveRate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Group>
|
||||||
</article>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</SimpleGrid>
|
||||||
|
|
||||||
<div className="border-t border-border bg-card">
|
{pagination.pageCount > 1 && (
|
||||||
<DataTableFooter
|
<Group justify="space-between" align="center" p="md" style={{ borderTop: "1px solid var(--mantine-color-gray-2)" }}>
|
||||||
table={table}
|
<Text size="sm" c="dimmed">
|
||||||
pagination={pagination}
|
Showing {Math.min(rows.length, pagination.pageSize)} of {pagination.totalCount} {itemLabel}
|
||||||
options={{
|
</Text>
|
||||||
labels: {
|
<Pagination
|
||||||
showing: "Showing",
|
value={pagination.pageIndex + 1}
|
||||||
ofLabel: "of",
|
total={pagination.pageCount}
|
||||||
items: itemLabel,
|
size="sm"
|
||||||
},
|
radius="md"
|
||||||
}}
|
/>
|
||||||
/>
|
</Group>
|
||||||
</div>
|
)}
|
||||||
</>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,18 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Modal,
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
TextInput,
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
Field,
|
|
||||||
FieldContent,
|
|
||||||
FieldLabel,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
Separator,
|
|
||||||
Switch,
|
|
||||||
Textarea,
|
Textarea,
|
||||||
} from "@edr/ui-common";
|
Select,
|
||||||
|
Switch,
|
||||||
|
Stack,
|
||||||
|
Group,
|
||||||
|
Divider,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
} from "@mantine/core";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RULE_ENGINE_SELECT_NONE,
|
RULE_ENGINE_SELECT_NONE,
|
||||||
@@ -29,8 +20,6 @@ import {
|
|||||||
} from "@/pages/ruleEngine/config/resources";
|
} from "@/pages/ruleEngine/config/resources";
|
||||||
import type { RuleEngineRecord } from "@/types/rule-engine";
|
import type { RuleEngineRecord } from "@/types/rule-engine";
|
||||||
|
|
||||||
import { ruleEngineField, ruleEngineSurface } from "./ruleEngineStyles";
|
|
||||||
|
|
||||||
export interface RuleEngineFormDialogProps {
|
export interface RuleEngineFormDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
@@ -142,133 +131,174 @@ const RuleEngineFormDialog = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Modal
|
||||||
<DialogContent className={ruleEngineSurface.dialog}>
|
opened={open}
|
||||||
<DialogHeader className="space-y-1">
|
onClose={() => onOpenChange(false)}
|
||||||
<DialogTitle className="text-lg font-semibold">{title}</DialogTitle>
|
title={title}
|
||||||
<DialogDescription>{description}</DialogDescription>
|
centered
|
||||||
</DialogHeader>
|
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)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-1">
|
<div style={{ maxHeight: "calc(60vh - 150px)", overflowY: "auto", paddingRight: "12px" }}>
|
||||||
<div className="max-h-[min(60vh,28rem)] space-y-4 overflow-y-auto pr-1">
|
<Stack gap="lg">
|
||||||
{fields.map((field) => (
|
{fields.map((field) => (
|
||||||
<Field key={field.name} orientation="vertical" className="gap-1.5">
|
<Box key={field.name}>
|
||||||
{field.type === "boolean" ? (
|
{field.type === "boolean" ? (
|
||||||
<div className={ruleEngineField.switchRow}>
|
<Group
|
||||||
<div className="min-w-0">
|
justify="space-between"
|
||||||
<FieldLabel htmlFor={field.name} className={ruleEngineField.label}>
|
align="center"
|
||||||
{field.label}
|
p="lg"
|
||||||
</FieldLabel>
|
style={{
|
||||||
<p className={ruleEngineField.switchHint}>
|
background: "linear-gradient(135deg, var(--mantine-color-blue-0) 0%, var(--mantine-color-cyan-0) 100%)",
|
||||||
{Boolean(values[field.name]) ? "Enabled" : "Disabled"}
|
borderRadius: "12px",
|
||||||
</p>
|
border: "1px solid var(--mantine-color-blue-2)",
|
||||||
</div>
|
}}
|
||||||
<Switch
|
>
|
||||||
id={field.name}
|
<div>
|
||||||
checked={Boolean(values[field.name])}
|
<Text size="sm" fw={600} mb="4px">
|
||||||
onCheckedChange={(checked) => setField(field.name, checked)}
|
{field.label}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{Boolean(values[field.name]) ? "✓ Enabled" : "○ Disabled"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={Boolean(values[field.name])}
|
||||||
|
onChange={(e) => setField(field.name, e.currentTarget.checked)}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
) : field.type === "select" ? (
|
||||||
|
<Select
|
||||||
|
label={
|
||||||
|
<Group gap="4px">
|
||||||
|
<span>{field.label}</span>
|
||||||
|
{field.required && <span style={{ color: "var(--mantine-color-red-6)" }}>*</span>}
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
selectOptionsLoading ? "Loading options..." : (field.placeholder ?? "Select an option")
|
||||||
|
}
|
||||||
|
value={resolveSelectValue(field, values)}
|
||||||
|
onChange={(v) =>
|
||||||
|
setField(field.name, v === RULE_ENGINE_SELECT_NONE ? "" : v)
|
||||||
|
}
|
||||||
|
disabled={selectOptionsLoading}
|
||||||
|
data={(field.options ?? []).filter((opt) => opt.value !== "").map((opt) => ({
|
||||||
|
label: opt.label,
|
||||||
|
value: opt.value,
|
||||||
|
}))}
|
||||||
|
searchable
|
||||||
|
clearable
|
||||||
|
size="md"
|
||||||
|
radius="lg"
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
borderColor: "var(--mantine-color-gray-3)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
) : field.type === "textarea" ? (
|
||||||
) : (
|
<Textarea
|
||||||
<>
|
label={
|
||||||
<FieldLabel htmlFor={field.name} className={ruleEngineField.label}>
|
<Group gap="4px">
|
||||||
{field.label}
|
<span>{field.label}</span>
|
||||||
{field.required ? (
|
{field.required && <span style={{ color: "var(--mantine-color-red-6)" }}>*</span>}
|
||||||
<span className={ruleEngineField.requiredMark}> *</span>
|
</Group>
|
||||||
) : null}
|
}
|
||||||
</FieldLabel>
|
value={String(values[field.name] ?? "")}
|
||||||
<FieldContent>
|
onChange={(e) => setField(field.name, e.currentTarget.value)}
|
||||||
{field.type === "select" ? (
|
placeholder={field.placeholder}
|
||||||
<Select
|
required={field.required}
|
||||||
value={resolveSelectValue(field, values)}
|
minRows={5}
|
||||||
onValueChange={(v) =>
|
size="md"
|
||||||
setField(
|
radius="lg"
|
||||||
field.name,
|
styles={{
|
||||||
v === RULE_ENGINE_SELECT_NONE ? "" : v,
|
input: {
|
||||||
)
|
borderColor: "var(--mantine-color-gray-3)",
|
||||||
}
|
},
|
||||||
disabled={selectOptionsLoading}
|
}}
|
||||||
>
|
/>
|
||||||
<SelectTrigger
|
) : (
|
||||||
id={field.name}
|
<TextInput
|
||||||
className={ruleEngineField.selectTrigger}
|
label={
|
||||||
>
|
<Group gap="4px">
|
||||||
<SelectValue
|
<span>{field.label}</span>
|
||||||
placeholder={
|
{field.required && <span style={{ color: "var(--mantine-color-red-6)" }}>*</span>}
|
||||||
selectOptionsLoading
|
</Group>
|
||||||
? "Loading options..."
|
}
|
||||||
: (field.placeholder ?? "Select an option")
|
type={
|
||||||
}
|
field.type === "number"
|
||||||
/>
|
? "number"
|
||||||
</SelectTrigger>
|
: field.type === "date"
|
||||||
<SelectContent className={ruleEngineField.selectContent}>
|
? "date"
|
||||||
{(field.options ?? [])
|
: "text"
|
||||||
.filter((opt) => opt.value !== "")
|
}
|
||||||
.map((opt) => (
|
value={String(values[field.name] ?? "")}
|
||||||
<SelectItem key={opt.value} value={opt.value}>
|
onChange={(e) => setField(field.name, e.currentTarget.value)}
|
||||||
{opt.label}
|
placeholder={field.placeholder}
|
||||||
</SelectItem>
|
required={field.required}
|
||||||
))}
|
size="md"
|
||||||
</SelectContent>
|
radius="lg"
|
||||||
</Select>
|
styles={{
|
||||||
) : field.type === "textarea" ? (
|
input: {
|
||||||
<Textarea
|
borderColor: "var(--mantine-color-gray-3)",
|
||||||
id={field.name}
|
},
|
||||||
value={String(values[field.name] ?? "")}
|
}}
|
||||||
onChange={(e) => setField(field.name, e.target.value)}
|
/>
|
||||||
placeholder={field.placeholder}
|
)}
|
||||||
className={ruleEngineField.textarea}
|
</Box>
|
||||||
/>
|
))}
|
||||||
) : (
|
</Stack>
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
type={
|
|
||||||
field.type === "number"
|
|
||||||
? "number"
|
|
||||||
: field.type === "date"
|
|
||||||
? "date"
|
|
||||||
: "text"
|
|
||||||
}
|
|
||||||
value={String(values[field.name] ?? "")}
|
|
||||||
onChange={(e) => setField(field.name, e.target.value)}
|
|
||||||
placeholder={field.placeholder}
|
|
||||||
className={ruleEngineField.input}
|
|
||||||
required={field.required}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</FieldContent>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator className="my-4" />
|
<Divider />
|
||||||
|
|
||||||
<DialogFooter className="gap-2 sm:gap-2">
|
<Group justify="flex-end" gap="sm">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
variant="light"
|
||||||
variant="outline"
|
|
||||||
className="rounded-md"
|
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
radius="lg"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" className="rounded-md" disabled={isSubmitting}>
|
<Button
|
||||||
{isSubmitting ? (
|
type="submit"
|
||||||
<>
|
disabled={isSubmitting}
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
leftSection={isSubmitting && <Loader2 size={18} style={{ animation: "spin 1s linear infinite" }} />}
|
||||||
Saving...
|
radius="lg"
|
||||||
</>
|
color="blue"
|
||||||
) : (
|
size="md"
|
||||||
"Save"
|
>
|
||||||
)}
|
{isSubmitting ? "Saving..." : "Save"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</Group>
|
||||||
</form>
|
</Stack>
|
||||||
</DialogContent>
|
</form>
|
||||||
</Dialog>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,10 @@ import {
|
|||||||
Send,
|
Send,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { Button, Menu, Group } from "@mantine/core";
|
||||||
|
|
||||||
import type { RuleEngineResourceConfig } from "@/pages/ruleEngine/config/resources";
|
import type { RuleEngineResourceConfig } from "@/pages/ruleEngine/config/resources";
|
||||||
import type { RuleEngineRecord } from "@/types/rule-engine";
|
import type { RuleEngineRecord } from "@/types/rule-engine";
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@edr/ui-common";
|
|
||||||
|
|
||||||
export interface RuleEngineRecordActionsProps {
|
export interface RuleEngineRecordActionsProps {
|
||||||
record: RuleEngineRecord;
|
record: RuleEngineRecord;
|
||||||
@@ -44,93 +38,83 @@ const RuleEngineRecordActions = ({
|
|||||||
const hasRateActions =
|
const hasRateActions =
|
||||||
config.slug === "rates" && (status === "DRAFT" || status === "PENDING_APPROVAL");
|
config.slug === "rates" && (status === "DRAFT" || status === "PENDING_APPROVAL");
|
||||||
|
|
||||||
const iconBtnClass =
|
|
||||||
layout === "compact"
|
|
||||||
? "h-8 w-8 rounded-md text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
||||||
: "h-8 w-8 rounded-md text-muted-foreground hover:bg-muted hover:text-foreground";
|
|
||||||
|
|
||||||
if (readOnly) {
|
if (readOnly) {
|
||||||
return onViewChain ? (
|
return onViewChain ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
variant="subtle"
|
||||||
variant="ghost"
|
size="xs"
|
||||||
size="icon"
|
|
||||||
className={iconBtnClass}
|
|
||||||
onClick={onViewChain}
|
onClick={onViewChain}
|
||||||
aria-label="View chain"
|
leftSection={<Eye size={16} />}
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
View
|
||||||
</Button>
|
</Button>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end gap-0.5">
|
<Group gap={2} justify="flex-end">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
variant="subtle"
|
||||||
variant="ghost"
|
size="xs"
|
||||||
size="icon"
|
|
||||||
className={iconBtnClass}
|
|
||||||
onClick={() => onEdit(record)}
|
onClick={() => onEdit(record)}
|
||||||
aria-label="Edit"
|
leftSection={<Pencil size={16} />}
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{config.slug === "approval-rules" && onViewChain ? (
|
{config.slug === "approval-rules" && onViewChain ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
variant="subtle"
|
||||||
variant="ghost"
|
size="xs"
|
||||||
size="icon"
|
|
||||||
className={iconBtnClass}
|
|
||||||
onClick={onViewChain}
|
onClick={onViewChain}
|
||||||
aria-label="View approval chain"
|
leftSection={<Eye size={16} />}
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
View Chain
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{hasRateActions ? (
|
{hasRateActions ? (
|
||||||
<DropdownMenu modal={false}>
|
<Menu position="bottom-end" shadow="md">
|
||||||
<DropdownMenuTrigger asChild>
|
<Menu.Target>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
variant="subtle"
|
||||||
variant="ghost"
|
size="xs"
|
||||||
size="icon"
|
leftSection={<MoreHorizontal size={16} />}
|
||||||
className={iconBtnClass}
|
|
||||||
aria-label="More actions"
|
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
More
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</Menu.Target>
|
||||||
<DropdownMenuContent align="end">
|
<Menu.Dropdown>
|
||||||
{status === "DRAFT" && onSubmitRate ? (
|
{status === "DRAFT" && onSubmitRate ? (
|
||||||
<DropdownMenuItem onSelect={() => onSubmitRate(record.id)}>
|
<Menu.Item
|
||||||
<Send />
|
leftSection={<Send size={14} />}
|
||||||
|
onClick={() => onSubmitRate(record.id)}
|
||||||
|
>
|
||||||
Submit for approval
|
Submit for approval
|
||||||
</DropdownMenuItem>
|
</Menu.Item>
|
||||||
) : null}
|
) : null}
|
||||||
{status === "PENDING_APPROVAL" && onApproveRate ? (
|
{status === "PENDING_APPROVAL" && onApproveRate ? (
|
||||||
<DropdownMenuItem onSelect={() => onApproveRate(record)}>
|
<Menu.Item
|
||||||
<CheckCircle2 />
|
leftSection={<CheckCircle2 size={14} />}
|
||||||
|
onClick={() => onApproveRate(record)}
|
||||||
|
>
|
||||||
Approve
|
Approve
|
||||||
</DropdownMenuItem>
|
</Menu.Item>
|
||||||
) : null}
|
) : null}
|
||||||
</DropdownMenuContent>
|
</Menu.Dropdown>
|
||||||
</DropdownMenu>
|
</Menu>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
variant="subtle"
|
||||||
variant="ghost"
|
size="xs"
|
||||||
size="icon"
|
|
||||||
className={`${iconBtnClass} hover:bg-red-50 hover:text-red-600`}
|
|
||||||
onClick={() => onDelete(record)}
|
onClick={() => onDelete(record)}
|
||||||
aria-label="Delete"
|
leftSection={<Trash2 size={16} />}
|
||||||
|
color="red"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { Filter, LayoutGrid, Plus, Search, Table2 } from "lucide-react";
|
import { LayoutGrid, Plus, Search, Table2 } from "lucide-react";
|
||||||
|
import { Button, TextInput, Group, SegmentedControl } from "@mantine/core";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Button, Input } from "@edr/ui-common";
|
|
||||||
|
|
||||||
import type { RuleEngineViewMode } from "./useRuleEngineViewMode";
|
import type { RuleEngineViewMode } from "./useRuleEngineViewMode";
|
||||||
import { ruleEngineToolbar } from "./ruleEngineStyles";
|
|
||||||
|
|
||||||
export interface RuleEngineToolbarProps {
|
export interface RuleEngineToolbarProps {
|
||||||
search: string;
|
search: string;
|
||||||
@@ -25,70 +22,69 @@ const RuleEngineToolbar = ({
|
|||||||
viewMode,
|
viewMode,
|
||||||
onViewModeChange,
|
onViewModeChange,
|
||||||
}: RuleEngineToolbarProps) => (
|
}: RuleEngineToolbarProps) => (
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
<Group gap="md" justify="space-between" align="center" wrap="nowrap">
|
||||||
<div className="relative min-w-0 flex-1 lg:max-w-md">
|
<TextInput
|
||||||
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
placeholder={searchPlaceholder}
|
||||||
<Input
|
value={search}
|
||||||
type="search"
|
onChange={(e) => onSearchChange(e.currentTarget.value)}
|
||||||
value={search}
|
leftSection={<Search size={18} />}
|
||||||
onChange={(e) => onSearchChange(e.target.value)}
|
size="md"
|
||||||
placeholder={searchPlaceholder}
|
radius="lg"
|
||||||
className={ruleEngineToolbar.search}
|
style={{ flex: 1, minWidth: 0 }}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
borderColor: "var(--mantine-color-gray-3)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group gap="md" align="center" justify="flex-end" wrap="nowrap">
|
||||||
|
<SegmentedControl
|
||||||
|
value={viewMode}
|
||||||
|
onChange={(value) => onViewModeChange(value as RuleEngineViewMode)}
|
||||||
|
size="sm"
|
||||||
|
radius="lg"
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
value: "table",
|
||||||
|
label: (
|
||||||
|
<Group gap={6} justify="center" wrap="nowrap">
|
||||||
|
<Table2 size={16} />
|
||||||
|
<span>Table</span>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "cards",
|
||||||
|
label: (
|
||||||
|
<Group gap={6} justify="center" wrap="nowrap">
|
||||||
|
<LayoutGrid size={16} />
|
||||||
|
<span>Cards</span>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
background: "var(--mantine-color-gray-1)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex shrink-0 flex-wrap items-center gap-2">
|
|
||||||
<div
|
|
||||||
className={ruleEngineToolbar.viewToggleGroup}
|
|
||||||
role="group"
|
|
||||||
aria-label="View mode"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onViewModeChange("table")}
|
|
||||||
className={cn(
|
|
||||||
ruleEngineToolbar.viewToggleBtn,
|
|
||||||
viewMode === "table"
|
|
||||||
? ruleEngineToolbar.viewToggleActive
|
|
||||||
: ruleEngineToolbar.viewToggleIdle,
|
|
||||||
)}
|
|
||||||
aria-pressed={viewMode === "table"}
|
|
||||||
>
|
|
||||||
<Table2 className="h-4 w-4" />
|
|
||||||
Table
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onViewModeChange("cards")}
|
|
||||||
className={cn(
|
|
||||||
ruleEngineToolbar.viewToggleBtn,
|
|
||||||
viewMode === "cards"
|
|
||||||
? ruleEngineToolbar.viewToggleActive
|
|
||||||
: ruleEngineToolbar.viewToggleIdle,
|
|
||||||
)}
|
|
||||||
aria-pressed={viewMode === "cards"}
|
|
||||||
>
|
|
||||||
<LayoutGrid className="h-4 w-4" />
|
|
||||||
Cards
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className={cn(ruleEngineToolbar.actionBtn, "gap-2 px-3")}
|
|
||||||
>
|
|
||||||
<Filter className="h-4 w-4" />
|
|
||||||
Filter
|
|
||||||
</Button>
|
|
||||||
{onAdd ? (
|
{onAdd ? (
|
||||||
<Button type="button" className={ruleEngineToolbar.primaryBtn} onClick={onAdd}>
|
<Button
|
||||||
<Plus className="h-4 w-4" />
|
onClick={onAdd}
|
||||||
|
leftSection={<Plus size={18} />}
|
||||||
|
size="sm"
|
||||||
|
radius="lg"
|
||||||
|
color="blue"
|
||||||
|
style={{ whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
{addLabel}
|
{addLabel}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</Group>
|
||||||
</div>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default RuleEngineToolbar;
|
export default RuleEngineToolbar;
|
||||||
|
|||||||
@@ -1,30 +1,49 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import { Badge, Text } from "@mantine/core";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import type { ColumnFormat } from "@/pages/ruleEngine/config/resources";
|
import type { ColumnFormat } from "@/pages/ruleEngine/config/resources";
|
||||||
import { Badge } from "@edr/ui-common";
|
|
||||||
|
|
||||||
const statusBadgeClass = (active: boolean) =>
|
const extractLabel = (value: unknown): string | null => {
|
||||||
cn(
|
if (!value || typeof value !== "object") return null;
|
||||||
"rounded-sm px-2 py-0.5 text-xs font-medium",
|
|
||||||
active
|
const obj = value as Record<string, unknown>;
|
||||||
? "border-emerald-200 bg-emerald-50 text-emerald-800"
|
return (
|
||||||
: "border-border bg-muted text-muted-foreground",
|
(typeof obj.label === "string" ? obj.label : null) ||
|
||||||
|
(typeof obj.cargoTypeName === "string" ? obj.cargoTypeName : null) ||
|
||||||
|
(typeof obj.code === "string" ? obj.code : null) ||
|
||||||
|
(typeof obj.name === "string" ? obj.name : null) ||
|
||||||
|
(typeof obj.actionLabel === "string" ? obj.actionLabel : null) ||
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const formatCell = (value: unknown, format?: ColumnFormat): ReactNode => {
|
export const formatCell = (value: unknown, format?: ColumnFormat): ReactNode => {
|
||||||
if (value === null || value === undefined || value === "") {
|
if (value === null || value === undefined || value === "") {
|
||||||
return <span className="text-muted-foreground">—</span>;
|
return <Text size="sm" c="dimmed">—</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle stringified objects (e.g., "[object Object]")
|
||||||
|
if (typeof value === "string" && value.trim() === "[object Object]") {
|
||||||
|
return <Text size="sm" c="dimmed">—</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format === "boolean") {
|
if (format === "boolean") {
|
||||||
return value ? "Yes" : "No";
|
if (typeof value === "string") {
|
||||||
|
const boolVal = value.toLowerCase() === "true" || value === "1";
|
||||||
|
return <Text size="sm">{boolVal ? "✓ Yes" : "✗ No"}</Text>;
|
||||||
|
}
|
||||||
|
return <Text size="sm">{value ? "✓ Yes" : "✗ No"}</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format === "activeBadge") {
|
if (format === "activeBadge") {
|
||||||
const active = Boolean(value);
|
const active = Boolean(value);
|
||||||
return (
|
return (
|
||||||
<Badge variant="outline" className={statusBadgeClass(active)}>
|
<Badge
|
||||||
|
color={active ? "green" : "gray"}
|
||||||
|
variant={active ? "filled" : "light"}
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
{active ? "Active" : "Inactive"}
|
{active ? "Active" : "Inactive"}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
@@ -32,14 +51,16 @@ export const formatCell = (value: unknown, format?: ColumnFormat): ReactNode =>
|
|||||||
|
|
||||||
if (format === "rateStatus") {
|
if (format === "rateStatus") {
|
||||||
const status = String(value);
|
const status = String(value);
|
||||||
const tone =
|
const color =
|
||||||
status === "LIVE"
|
status === "LIVE"
|
||||||
? "border-emerald-200 bg-emerald-50 text-emerald-800"
|
? "green"
|
||||||
: status === "DRAFT"
|
: status === "DRAFT"
|
||||||
? "border-amber-200 bg-amber-50 text-amber-800"
|
? "yellow"
|
||||||
: "border-sky-200 bg-sky-50 text-sky-800";
|
: status === "PENDING_APPROVAL"
|
||||||
|
? "orange"
|
||||||
|
: "blue";
|
||||||
return (
|
return (
|
||||||
<Badge variant="outline" className={cn("rounded-sm font-medium", tone)}>
|
<Badge color={color} variant="filled" size="sm" radius="md">
|
||||||
{status}
|
{status}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
@@ -48,38 +69,44 @@ export const formatCell = (value: unknown, format?: ColumnFormat): ReactNode =>
|
|||||||
if (format === "code") {
|
if (format === "code") {
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="light"
|
||||||
className="rounded-sm border border-border bg-muted/80 font-mono text-[11px] font-medium text-foreground"
|
color="blue"
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: "0.05em",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{String(value)}
|
{String(value).toUpperCase()}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format === "date") {
|
if (format === "date") {
|
||||||
const d = new Date(String(value));
|
const d = new Date(String(value));
|
||||||
return Number.isNaN(d.getTime()) ? String(value) : d.toLocaleDateString();
|
if (Number.isNaN(d.getTime())) return <Text size="sm">{String(value)}</Text>;
|
||||||
|
return <Text size="sm">{d.toLocaleDateString()}</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format === "entityLabel" && value && typeof value === "object") {
|
if (format === "entityLabel" && value && typeof value === "object") {
|
||||||
const entity = value as { label?: string; code?: string; cargoTypeName?: string };
|
const label = extractLabel(value);
|
||||||
const label =
|
if (label) {
|
||||||
entity.label?.trim() ||
|
return <Text size="sm">{label}</Text>;
|
||||||
entity.cargoTypeName?.trim() ||
|
}
|
||||||
entity.code?.trim();
|
return <Text size="sm" c="dimmed">—</Text>;
|
||||||
return label ? (
|
|
||||||
<span>{label}</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">—</span>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format === "rateLabel") {
|
if (format === "rateLabel") {
|
||||||
if (!value || typeof value !== "object") {
|
if (!value || typeof value !== "object") {
|
||||||
return value ? (
|
return value ? (
|
||||||
<span className="font-mono text-xs text-muted-foreground">{String(value)}</span>
|
<Text size="sm" c="dimmed" style={{ fontFamily: "monospace" }}>
|
||||||
|
{String(value)}
|
||||||
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground">—</span>
|
<Text size="sm" c="dimmed">—</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const rate = value as {
|
const rate = value as {
|
||||||
@@ -95,11 +122,17 @@ export const formatCell = (value: unknown, format?: ColumnFormat): ReactNode =>
|
|||||||
rate.rateUnit?.replace(/_/g, " "),
|
rate.rateUnit?.replace(/_/g, " "),
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
return parts.length > 0 ? (
|
return parts.length > 0 ? (
|
||||||
<span>{parts.join(" · ")}</span>
|
<Text size="sm">{parts.join(" · ")}</Text>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground">—</span>
|
<Text size="sm" c="dimmed">—</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return String(value);
|
if (typeof value === "object") {
|
||||||
|
const label = extractLabel(value);
|
||||||
|
if (label) return <Text size="sm">{label}</Text>;
|
||||||
|
return <Text size="sm" c="dimmed">—</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Text size="sm">{String(value)}</Text>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
export const ruleEngineSurface = {
|
export const ruleEngineSurface = {
|
||||||
pageCard:
|
pageCard:
|
||||||
"overflow-hidden rounded-lg border border-border bg-card shadow-sm",
|
"overflow-hidden rounded-lg border border-border bg-card shadow-sm",
|
||||||
pageCardToolbar: "border-b border-border bg-muted/30 px-4 py-3 sm:px-5 sm:py-3.5",
|
pageCardToolbar: "border-b border-border bg-muted/30 px-3 py-2.5 sm:px-4 sm:py-3",
|
||||||
dialog: "max-h-[90vh] overflow-y-auto rounded-lg border-border sm:max-w-lg",
|
dialog: "max-h-[90vh] overflow-y-auto rounded-lg border-border sm:max-w-lg",
|
||||||
dialogSm: "rounded-lg border-border sm:max-w-md",
|
dialogSm: "rounded-lg border-border sm:max-w-md",
|
||||||
} as const;
|
} as const;
|
||||||
@@ -24,30 +24,30 @@ export const ruleEngineField = {
|
|||||||
|
|
||||||
export const ruleEngineToolbar = {
|
export const ruleEngineToolbar = {
|
||||||
search:
|
search:
|
||||||
"h-10 rounded-md border border-input bg-background pl-10 text-sm shadow-xs placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30",
|
"h-9 rounded-md border border-input bg-background pl-9 text-sm shadow-xs placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30 sm:h-10 sm:pl-10",
|
||||||
viewToggleGroup:
|
viewToggleGroup:
|
||||||
"flex h-10 items-center rounded-md border border-border bg-muted/40 p-0.5",
|
"flex h-9 items-center rounded-md border border-border bg-muted/40 p-0.5 sm:h-10",
|
||||||
viewToggleBtn:
|
viewToggleBtn:
|
||||||
"inline-flex h-8 items-center gap-1.5 rounded-sm px-3 text-sm font-medium transition-colors",
|
"inline-flex h-7 items-center gap-1 rounded-sm px-2 text-xs font-medium transition-colors sm:h-8 sm:gap-1.5 sm:px-3 sm:text-sm",
|
||||||
viewToggleActive: "bg-background text-foreground shadow-sm",
|
viewToggleActive: "bg-background text-foreground shadow-sm",
|
||||||
viewToggleIdle: "text-muted-foreground hover:bg-background/60 hover:text-foreground",
|
viewToggleIdle: "text-muted-foreground hover:bg-background/60 hover:text-foreground",
|
||||||
actionBtn: "h-10 rounded-md shadow-xs",
|
actionBtn: "h-9 rounded-md shadow-xs sm:h-10",
|
||||||
primaryBtn: "h-10 gap-2 rounded-md px-4 text-sm font-medium shadow-xs",
|
primaryBtn: "h-9 gap-1.5 rounded-md px-3 text-xs font-medium shadow-xs sm:h-10 sm:gap-2 sm:px-4 sm:text-sm",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ruleEngineCard = {
|
export const ruleEngineCard = {
|
||||||
article:
|
article:
|
||||||
"group flex flex-col overflow-hidden rounded-lg border border-border bg-card shadow-sm transition-shadow duration-200 hover:shadow-md",
|
"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-4 py-3.5",
|
header: "border-b border-border bg-muted/25 px-3 py-2.5 sm:px-4 sm:py-3.5",
|
||||||
avatar:
|
avatar:
|
||||||
"flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-primary/12 text-sm font-semibold text-primary",
|
"flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary/12 text-xs font-semibold text-primary sm:h-10 sm:w-10 sm:text-sm",
|
||||||
title: "truncate text-[15px] font-semibold text-foreground",
|
title: "truncate text-sm font-semibold text-foreground sm:text-[15px]",
|
||||||
meta: "text-xs text-muted-foreground",
|
meta: "text-xs text-muted-foreground",
|
||||||
detailLabel:
|
detailLabel:
|
||||||
"text-[11px] font-medium uppercase tracking-wide text-muted-foreground",
|
"text-[10px] font-medium uppercase tracking-wide text-muted-foreground sm:text-[11px]",
|
||||||
detailValue: "mt-0.5 text-sm text-foreground",
|
detailValue: "mt-0.5 text-xs text-foreground sm:text-sm",
|
||||||
footer: "mt-auto border-t border-border bg-muted/20 px-3 py-2.5",
|
footer: "mt-auto border-t border-border bg-muted/20 px-2 py-2 sm:px-3 sm:py-2.5",
|
||||||
skeleton: "animate-pulse rounded-lg border border-border bg-muted/30 p-4",
|
skeleton: "animate-pulse rounded-lg border border-border bg-muted/30 p-3 sm:p-4",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ruleEngineTable = {
|
export const ruleEngineTable = {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { MantineProvider } from "@mantine/core";
|
||||||
|
import "@mantine/core/styles.css";
|
||||||
import "@edr/ui-common/styles.css";
|
import "@edr/ui-common/styles.css";
|
||||||
import "../index.css";
|
import "../index.css";
|
||||||
import "@edr/ui-common/theme.css";
|
import "@edr/ui-common/theme.css";
|
||||||
@@ -41,14 +43,15 @@ if (!rootElement) {
|
|||||||
|
|
||||||
createRoot(rootElement).render(
|
createRoot(rootElement).render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<MantineProvider>
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<App />
|
<App />
|
||||||
<Toaster position="top-right" />
|
<Toaster position="top-right" />
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</StrictMode>,
|
</StrictMode>
|
||||||
|
</MantineProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,16 +4,14 @@ import { useAuth } from "@/auth/useAuth";
|
|||||||
import { canAccessRuleEngineResource } from "@/lib/permissions";
|
import { canAccessRuleEngineResource } from "@/lib/permissions";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { Card, Button, Modal, Stack, Group, Text, List } from "@mantine/core";
|
||||||
|
|
||||||
import RuleEngineCardGrid from "@/components/ruleEngine/RuleEngineCardGrid";
|
import RuleEngineCardGrid from "@/components/ruleEngine/RuleEngineCardGrid";
|
||||||
import RuleEngineFormDialog from "@/components/ruleEngine/RuleEngineFormDialog";
|
import RuleEngineFormDialog from "@/components/ruleEngine/RuleEngineFormDialog";
|
||||||
import RuleEngineRecordActions from "@/components/ruleEngine/RuleEngineRecordActions";
|
import RuleEngineRecordActions from "@/components/ruleEngine/RuleEngineRecordActions";
|
||||||
import RuleEngineToolbar from "@/components/ruleEngine/RuleEngineToolbar";
|
import RuleEngineToolbar from "@/components/ruleEngine/RuleEngineToolbar";
|
||||||
import { formatCell } from "@/components/ruleEngine/ruleEngineFormat";
|
import { formatCell } from "@/components/ruleEngine/ruleEngineFormat";
|
||||||
import {
|
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
||||||
ruleEngineSurface,
|
|
||||||
ruleEngineTable,
|
|
||||||
} from "@/components/ruleEngine/ruleEngineStyles";
|
|
||||||
import { useRuleEngineViewMode } from "@/components/ruleEngine/useRuleEngineViewMode";
|
import { useRuleEngineViewMode } from "@/components/ruleEngine/useRuleEngineViewMode";
|
||||||
import {
|
import {
|
||||||
DEFAULT_CONFIGURATION_SLUG,
|
DEFAULT_CONFIGURATION_SLUG,
|
||||||
@@ -34,15 +32,8 @@ import {
|
|||||||
} from "@/hooks/rule-engine/useRuleEngine";
|
} from "@/hooks/rule-engine/useRuleEngine";
|
||||||
import type { RuleEngineRecord } from "@/types/rule-engine";
|
import type { RuleEngineRecord } from "@/types/rule-engine";
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
DataTable,
|
DataTable,
|
||||||
DataTableFooter,
|
DataTableFooter,
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
usePagination,
|
usePagination,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
@@ -177,15 +168,6 @@ const RuleEngineResourcePage = () => {
|
|||||||
[filteredRows.length, meta?.total, pageCount, pagination.pageIndex, pagination.pageSize],
|
[filteredRows.length, meta?.total, pageCount, pagination.pageIndex, pagination.pageSize],
|
||||||
);
|
);
|
||||||
|
|
||||||
const cardTable = useReactTable({
|
|
||||||
data: filteredRows,
|
|
||||||
columns: [] as ColumnDef<RuleEngineRecord>[],
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
manualPagination: true,
|
|
||||||
pageCount,
|
|
||||||
state: { pagination },
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleApproveRate = useCallback(
|
const handleApproveRate = useCallback(
|
||||||
(record: RuleEngineRecord) => {
|
(record: RuleEngineRecord) => {
|
||||||
@@ -284,9 +266,9 @@ const RuleEngineResourcePage = () => {
|
|||||||
const itemLabel = config.label.toLowerCase();
|
const itemLabel = config.label.toLowerCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Stack gap="lg">
|
||||||
<Card className={ruleEngineSurface.pageCard}>
|
<Card p="lg" radius="lg" withBorder style={{ background: "white", boxShadow: "0 1px 3px rgba(0, 0, 0, 0.05)" }}>
|
||||||
<div className={ruleEngineSurface.pageCardToolbar}>
|
<Stack gap="md">
|
||||||
<RuleEngineToolbar
|
<RuleEngineToolbar
|
||||||
search={search}
|
search={search}
|
||||||
onSearchChange={(v) => {
|
onSearchChange={(v) => {
|
||||||
@@ -299,65 +281,64 @@ const RuleEngineResourcePage = () => {
|
|||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
onViewModeChange={setViewMode}
|
onViewModeChange={setViewMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{viewMode === "table" ? (
|
{viewMode === "table" ? (
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={filteredRows}
|
data={filteredRows}
|
||||||
status={tableStatus}
|
status={tableStatus}
|
||||||
error={
|
error={
|
||||||
isError
|
isError
|
||||||
? {
|
? {
|
||||||
message: "Failed to load data",
|
message: "Failed to load data",
|
||||||
description:
|
description:
|
||||||
error instanceof Error ? error.message : "Unknown error",
|
error instanceof Error ? error.message : "Unknown error",
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
emptyMessage={`No ${itemLabel} found.`}
|
emptyMessage={`No ${itemLabel} found.`}
|
||||||
pagination={paginationState}
|
pagination={paginationState}
|
||||||
tableOptions={{
|
tableOptions={{
|
||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
pageCount,
|
pageCount,
|
||||||
state: { pagination },
|
state: { pagination },
|
||||||
onPaginationChange: setPagination,
|
onPaginationChange: setPagination,
|
||||||
}}
|
}}
|
||||||
containerClassName="border-0 shadow-none [&_[data-slot=table-row]]:border-border"
|
containerClassName="border-0 shadow-none [&_[data-slot=table-row]]:border-border"
|
||||||
footerClassName="border-t border-border bg-card"
|
footerClassName="border-t border-border bg-card"
|
||||||
footer={({ table, pagination: footerPagination }) => (
|
footer={({ table, pagination: footerPagination }) => (
|
||||||
<DataTableFooter
|
<DataTableFooter
|
||||||
table={table}
|
table={table}
|
||||||
pagination={footerPagination}
|
pagination={footerPagination}
|
||||||
options={{
|
options={{
|
||||||
labels: {
|
labels: {
|
||||||
showing: "Showing",
|
showing: "Showing",
|
||||||
ofLabel: "of",
|
ofLabel: "of",
|
||||||
items: itemLabel,
|
items: itemLabel,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RuleEngineCardGrid
|
<RuleEngineCardGrid
|
||||||
config={config}
|
config={config}
|
||||||
rows={filteredRows}
|
rows={filteredRows}
|
||||||
status={tableStatus}
|
status={tableStatus}
|
||||||
emptyMessage={`No ${itemLabel} found.`}
|
emptyMessage={`No ${itemLabel} found.`}
|
||||||
itemLabel={itemLabel}
|
itemLabel={itemLabel}
|
||||||
table={cardTable}
|
pagination={paginationState}
|
||||||
pagination={paginationState}
|
readOnly={!canManage}
|
||||||
readOnly={!canManage}
|
onEdit={canManage ? openEdit : undefined}
|
||||||
onEdit={canManage ? openEdit : undefined}
|
onDelete={canManage ? setDeleteTarget : undefined}
|
||||||
onDelete={canManage ? setDeleteTarget : undefined}
|
onViewChain={
|
||||||
onViewChain={
|
config.slug === "approval-rules" ? () => setChainOpen(true) : undefined
|
||||||
config.slug === "approval-rules" ? () => setChainOpen(true) : undefined
|
}
|
||||||
}
|
onSubmitRate={canManage ? (id) => submit.mutate(id) : undefined}
|
||||||
onSubmitRate={canManage ? (id) => submit.mutate(id) : undefined}
|
onApproveRate={canManage ? handleApproveRate : undefined}
|
||||||
onApproveRate={canManage ? handleApproveRate : undefined}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<RuleEngineFormDialog
|
<RuleEngineFormDialog
|
||||||
@@ -380,20 +361,23 @@ const RuleEngineResourcePage = () => {
|
|||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dialog open={Boolean(deleteTarget)} onOpenChange={(o) => !o && setDeleteTarget(null)}>
|
<Modal
|
||||||
<DialogContent className={ruleEngineSurface.dialogSm}>
|
opened={Boolean(deleteTarget)}
|
||||||
<DialogHeader>
|
onClose={() => setDeleteTarget(null)}
|
||||||
<DialogTitle>Delete record?</DialogTitle>
|
title="Delete record?"
|
||||||
<DialogDescription>
|
centered
|
||||||
This will soft-delete the selected {config.label.toLowerCase()} record.
|
size="sm"
|
||||||
</DialogDescription>
|
>
|
||||||
</DialogHeader>
|
<Stack gap="md">
|
||||||
<div className="flex justify-end gap-2">
|
<Text size="sm">
|
||||||
<Button variant="outline" onClick={() => setDeleteTarget(null)}>
|
This will soft-delete the selected {config.label.toLowerCase()} record.
|
||||||
|
</Text>
|
||||||
|
<Group justify="flex-end" gap="sm">
|
||||||
|
<Button variant="default" onClick={() => setDeleteTarget(null)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
color="red"
|
||||||
disabled={remove.isPending}
|
disabled={remove.isPending}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!deleteTarget) return;
|
if (!deleteTarget) return;
|
||||||
@@ -401,47 +385,51 @@ const RuleEngineResourcePage = () => {
|
|||||||
onSuccess: () => setDeleteTarget(null),
|
onSuccess: () => setDeleteTarget(null),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
leftSection={remove.isPending && <Loader2 size={16} />}
|
||||||
>
|
>
|
||||||
{remove.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : "Delete"}
|
{remove.isPending ? "Deleting..." : "Delete"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Group>
|
||||||
</DialogContent>
|
</Stack>
|
||||||
</Dialog>
|
</Modal>
|
||||||
|
|
||||||
<Dialog open={chainOpen} onOpenChange={setChainOpen}>
|
<Modal
|
||||||
<DialogContent className={ruleEngineSurface.dialog}>
|
opened={chainOpen}
|
||||||
<DialogHeader>
|
onClose={() => setChainOpen(false)}
|
||||||
<DialogTitle>Approval chain</DialogTitle>
|
title="Approval chain"
|
||||||
<DialogDescription>Configured approval steps from the API.</DialogDescription>
|
centered
|
||||||
</DialogHeader>
|
size="md"
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
{chainLoading ? (
|
{chainLoading ? (
|
||||||
<div className="flex justify-center py-8">
|
<Group justify="center" p="xl">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
<Loader2 size={32} style={{ animation: "spin 1s linear infinite" }} />
|
||||||
</div>
|
</Group>
|
||||||
) : (
|
) : (
|
||||||
<ol className="space-y-3">
|
<>
|
||||||
{(chainData ?? []).length === 0 ? (
|
{(chainData ?? []).length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">No approval rules configured.</p>
|
<Text size="sm" c="dimmed">No approval rules configured.</Text>
|
||||||
) : (
|
) : (
|
||||||
(chainData ?? []).map((step, index) => (
|
<List spacing="md">
|
||||||
<li
|
{(chainData ?? []).map((step, index) => (
|
||||||
key={String(step.id ?? index)}
|
<List.Item key={String(step.id ?? index)}>
|
||||||
className="rounded-md border border-border bg-muted/30 px-4 py-3 text-sm"
|
<Stack gap="xs">
|
||||||
>
|
<Text size="sm" fw={500}>
|
||||||
<p className="font-medium text-foreground">
|
Step {String(step.stepOrder ?? index + 1)}: {String(step.actionLabel ?? "")}
|
||||||
Step {String(step.stepOrder ?? index + 1)}: {String(step.actionLabel ?? "")}
|
</Text>
|
||||||
</p>
|
<Text size="sm" c="dimmed">
|
||||||
<p className="text-muted-foreground">
|
Role: {String(step.requiredRole ?? "—")}
|
||||||
Role: {String(step.requiredRole ?? "—")}
|
</Text>
|
||||||
</p>
|
</Stack>
|
||||||
</li>
|
</List.Item>
|
||||||
))
|
))}
|
||||||
|
</List>
|
||||||
)}
|
)}
|
||||||
</ol>
|
</>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</Stack>
|
||||||
</Dialog>
|
</Modal>
|
||||||
</div>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -270,6 +270,8 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
|||||||
category: "rules",
|
category: "rules",
|
||||||
subtitle: "VGM limits by container and trade direction",
|
subtitle: "VGM limits by container and trade direction",
|
||||||
searchPlaceholder: "Search weight limit rules...",
|
searchPlaceholder: "Search weight limit rules...",
|
||||||
|
cardTitleKey: "containerType",
|
||||||
|
cardSubtitleKey: "tradeDirection",
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
id: "containerType",
|
id: "containerType",
|
||||||
|
|||||||
95
pnpm-lock.yaml
generated
95
pnpm-lock.yaml
generated
@@ -35,7 +35,7 @@ importers:
|
|||||||
version: 2.9.14
|
version: 2.9.14
|
||||||
typeorm:
|
typeorm:
|
||||||
specifier: 0.3.30
|
specifier: 0.3.30
|
||||||
version: 0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))
|
version: 0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@24.13.0)(typescript@5.9.3))
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.5.4
|
specifier: ^5.5.4
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -184,6 +184,15 @@ importers:
|
|||||||
'@edr/ui-common':
|
'@edr/ui-common':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../packages/ui-common
|
version: link:../../../packages/ui-common
|
||||||
|
'@mantine/core':
|
||||||
|
specifier: ^9.3.0
|
||||||
|
version: 9.3.0(@mantine/hooks@9.3.0(react@19.2.6))(@types/react@18.3.29)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
|
'@mantine/hooks':
|
||||||
|
specifier: ^9.3.0
|
||||||
|
version: 9.3.0(react@19.2.6)
|
||||||
|
'@tabler/icons-react':
|
||||||
|
specifier: ^3.44.0
|
||||||
|
version: 3.44.0(react@19.2.6)
|
||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.100.11
|
specifier: ^5.100.11
|
||||||
version: 5.100.11(react@19.2.6)
|
version: 5.100.11(react@19.2.6)
|
||||||
@@ -1939,6 +1948,13 @@ packages:
|
|||||||
react: 19.2.6
|
react: 19.2.6
|
||||||
react-dom: 19.2.6
|
react-dom: 19.2.6
|
||||||
|
|
||||||
|
'@mantine/core@9.3.0':
|
||||||
|
resolution: {integrity: sha512-mHVCm61YVW9ipy9eHiKMqsRUm3TkOErbdw7zHs0HRw5g403nf7tSTqNGvaYE+aX1Py874qMkrUzeQfj4bjiiBA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@mantine/hooks': 9.3.0
|
||||||
|
react: 19.2.6
|
||||||
|
react-dom: 19.2.6
|
||||||
|
|
||||||
'@mantine/dates@8.3.18':
|
'@mantine/dates@8.3.18':
|
||||||
resolution: {integrity: sha512-FHx5teJOhupI0gO2o5evtVYQEdqOjayOkLRhEQfB5Nc5DvcysfPfmNILGkc1Nrp9ZQeQWKLT9qr+CkcCXwHOaw==}
|
resolution: {integrity: sha512-FHx5teJOhupI0gO2o5evtVYQEdqOjayOkLRhEQfB5Nc5DvcysfPfmNILGkc1Nrp9ZQeQWKLT9qr+CkcCXwHOaw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1953,6 +1969,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: 19.2.6
|
react: 19.2.6
|
||||||
|
|
||||||
|
'@mantine/hooks@9.3.0':
|
||||||
|
resolution: {integrity: sha512-QoSr9WI4WsKWrM3qFYYizHUn3+n+CVcFMYe4sdlnmFPStvs6BacPODKJSbFlYl73Z20t82JIy0eKqt4noHQI2g==}
|
||||||
|
peerDependencies:
|
||||||
|
react: 19.2.6
|
||||||
|
|
||||||
'@mapbox/node-pre-gyp@1.0.11':
|
'@mapbox/node-pre-gyp@1.0.11':
|
||||||
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
|
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -5167,6 +5188,8 @@ packages:
|
|||||||
axios@1.16.1:
|
axios@1.16.1:
|
||||||
resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==}
|
resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==}
|
||||||
|
|
||||||
|
axios@1.17.0: {}
|
||||||
|
|
||||||
b4a@1.8.1:
|
b4a@1.8.1:
|
||||||
resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==}
|
resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6285,15 +6308,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
escodegen@2.1.0:
|
||||||
|
resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==}
|
||||||
|
engines: {node: '>=6.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
eslint-config-prettier@10.1.8:
|
eslint-config-prettier@10.1.8:
|
||||||
resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
|
resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '>=7.0.0'
|
eslint: '>=7.0.0'
|
||||||
escodegen@2.1.0:
|
|
||||||
resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==}
|
|
||||||
engines: {node: '>=6.0'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
eslint-config-prettier@9.1.2:
|
eslint-config-prettier@9.1.2:
|
||||||
resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==}
|
resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==}
|
||||||
@@ -12558,7 +12582,7 @@ snapshots:
|
|||||||
|
|
||||||
'@jest/pattern@30.4.0':
|
'@jest/pattern@30.4.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.13.0
|
'@types/node': 20.19.41
|
||||||
jest-regex-util: 30.4.0
|
jest-regex-util: 30.4.0
|
||||||
|
|
||||||
'@jest/reporters@29.7.0':
|
'@jest/reporters@29.7.0':
|
||||||
@@ -12727,7 +12751,7 @@ snapshots:
|
|||||||
'@jest/schemas': 30.4.1
|
'@jest/schemas': 30.4.1
|
||||||
'@types/istanbul-lib-coverage': 2.0.6
|
'@types/istanbul-lib-coverage': 2.0.6
|
||||||
'@types/istanbul-reports': 3.0.4
|
'@types/istanbul-reports': 3.0.4
|
||||||
'@types/node': 24.13.0
|
'@types/node': 20.19.41
|
||||||
'@types/yargs': 17.0.35
|
'@types/yargs': 17.0.35
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
|
|
||||||
@@ -12781,6 +12805,19 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/react'
|
- '@types/react'
|
||||||
|
|
||||||
|
'@mantine/core@9.3.0(@mantine/hooks@9.3.0(react@19.2.6))(@types/react@18.3.29)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/react': 0.27.19(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
|
'@mantine/hooks': 9.3.0(react@19.2.6)
|
||||||
|
clsx: 2.1.1
|
||||||
|
react: 19.2.6
|
||||||
|
react-dom: 19.2.6(react@19.2.6)
|
||||||
|
react-number-format: 5.4.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
|
react-remove-scroll: 2.7.2(@types/react@18.3.29)(react@19.2.6)
|
||||||
|
type-fest: 5.6.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
|
||||||
'@mantine/dates@8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@19.2.6))(@types/react@18.3.29)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@8.3.18(react@19.2.6))(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
|
'@mantine/dates@8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@19.2.6))(@types/react@18.3.29)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@8.3.18(react@19.2.6))(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mantine/core': 8.3.18(@mantine/hooks@8.3.18(react@19.2.6))(@types/react@18.3.29)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
'@mantine/core': 8.3.18(@mantine/hooks@8.3.18(react@19.2.6))(@types/react@18.3.29)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
@@ -12794,6 +12831,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.6
|
react: 19.2.6
|
||||||
|
|
||||||
|
'@mantine/hooks@9.3.0(react@19.2.6)':
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.6
|
||||||
|
|
||||||
'@mapbox/node-pre-gyp@1.0.11':
|
'@mapbox/node-pre-gyp@1.0.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-libc: 2.1.2
|
detect-libc: 2.1.2
|
||||||
@@ -15222,7 +15263,7 @@ snapshots:
|
|||||||
'@types/jsonwebtoken@9.0.10':
|
'@types/jsonwebtoken@9.0.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
'@types/node': 24.13.0
|
'@types/node': 20.19.41
|
||||||
|
|
||||||
'@types/methods@1.1.4': {}
|
'@types/methods@1.1.4': {}
|
||||||
|
|
||||||
@@ -16756,6 +16797,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
b4a@1.8.1: {}
|
b4a@1.8.1: {}
|
||||||
|
|
||||||
babel-jest@29.7.0(@babel/core@7.29.0):
|
babel-jest@29.7.0(@babel/core@7.29.0):
|
||||||
@@ -17966,9 +18008,6 @@ snapshots:
|
|||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
escape-string-regexp@4.0.0: {}
|
||||||
|
|
||||||
eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.7.0)):
|
|
||||||
dependencies:
|
|
||||||
eslint: 9.39.4(jiti@2.7.0)
|
|
||||||
escodegen@2.1.0:
|
escodegen@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
esprima: 4.0.1
|
esprima: 4.0.1
|
||||||
@@ -17977,6 +18016,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
|
|
||||||
|
eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.7.0)):
|
||||||
|
dependencies:
|
||||||
|
eslint: 9.39.4(jiti@2.7.0)
|
||||||
|
|
||||||
eslint-config-prettier@9.1.2(eslint@8.57.1):
|
eslint-config-prettier@9.1.2(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
@@ -19708,7 +19751,7 @@ snapshots:
|
|||||||
jest-haste-map@30.4.1:
|
jest-haste-map@30.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/types': 30.4.1
|
'@jest/types': 30.4.1
|
||||||
'@types/node': 24.13.0
|
'@types/node': 20.19.41
|
||||||
anymatch: 3.1.3
|
anymatch: 3.1.3
|
||||||
fb-watchman: 2.0.2
|
fb-watchman: 2.0.2
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -20000,7 +20043,7 @@ snapshots:
|
|||||||
jest-util@30.4.1:
|
jest-util@30.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/types': 30.4.1
|
'@jest/types': 30.4.1
|
||||||
'@types/node': 24.13.0
|
'@types/node': 20.19.41
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
ci-info: 4.4.0
|
ci-info: 4.4.0
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -20061,7 +20104,7 @@ snapshots:
|
|||||||
|
|
||||||
jest-worker@30.4.1:
|
jest-worker@30.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.13.0
|
'@types/node': 20.19.41
|
||||||
'@ungap/structured-clone': 1.3.1
|
'@ungap/structured-clone': 1.3.1
|
||||||
jest-util: 30.4.1
|
jest-util: 30.4.1
|
||||||
merge-stream: 2.0.0
|
merge-stream: 2.0.0
|
||||||
@@ -23151,6 +23194,30 @@ snapshots:
|
|||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@24.13.0)(typescript@5.9.3)):
|
||||||
|
dependencies:
|
||||||
|
'@sqltools/formatter': 1.2.5
|
||||||
|
ansis: 4.3.0
|
||||||
|
app-root-path: 3.1.0
|
||||||
|
buffer: 6.0.3
|
||||||
|
dayjs: 1.11.20
|
||||||
|
debug: 4.4.3
|
||||||
|
dedent: 1.7.2(babel-plugin-macros@3.1.0)
|
||||||
|
dotenv: 16.6.1
|
||||||
|
glob: 10.5.0
|
||||||
|
reflect-metadata: 0.2.2
|
||||||
|
sha.js: 2.4.12
|
||||||
|
sql-highlight: 6.1.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
uuid: 11.1.1
|
||||||
|
yargs: 17.7.2
|
||||||
|
optionalDependencies:
|
||||||
|
pg: 8.21.0
|
||||||
|
ts-node: 10.9.2(@types/node@24.13.0)(typescript@5.9.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- babel-plugin-macros
|
||||||
|
- supports-color
|
||||||
|
|
||||||
typescript-eslint@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3):
|
typescript-eslint@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
|
'@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
|
||||||
|
|||||||
Reference in New Issue
Block a user