mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
changes
This commit is contained in:
@@ -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<RuleEngineRecord>;
|
||||
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<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 = ({
|
||||
config,
|
||||
rows,
|
||||
status,
|
||||
emptyMessage,
|
||||
itemLabel,
|
||||
table,
|
||||
pagination,
|
||||
onEdit,
|
||||
onDelete,
|
||||
@@ -48,109 +82,156 @@ const RuleEngineCardGrid = ({
|
||||
|
||||
if (status === "error") {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center px-6 py-16 text-center">
|
||||
<p className="text-sm font-medium text-foreground">Failed to load data</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<Stack align="center" justify="center" p="xl" style={{ minHeight: "400px" }}>
|
||||
<Text size="lg" fw={600} c="red">Failed to load data</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Please refresh the page or try again later.
|
||||
</p>
|
||||
</div>
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "loading") {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-3 p-4 sm:grid-cols-2 lg:grid-cols-3 xl:gap-4">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<div key={index} className={ruleEngineCard.skeleton}>
|
||||
<div className="flex gap-3">
|
||||
<div className="h-10 w-10 rounded-md bg-muted" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-4 w-2/3 rounded-sm bg-muted" />
|
||||
<div className="h-3 w-1/3 rounded-sm bg-muted" />
|
||||
<Stack gap="md" p="md">
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 2, lg: 3 }} spacing="md">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<Card key={index} p="md" radius="lg" withBorder style={{ height: "280px", background: "var(--mantine-color-gray-0)" }}>
|
||||
<div style={{ animation: "pulse 2s infinite", opacity: 0.5 }}>
|
||||
<div style={{ height: "20px", background: "var(--mantine-color-gray-3)", borderRadius: "4px", marginBottom: "12px" }} />
|
||||
<div style={{ height: "16px", background: "var(--mantine-color-gray-3)", borderRadius: "4px", marginBottom: "20px", width: "80%" }} />
|
||||
<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 className="mt-4 space-y-2">
|
||||
<div className="h-3 w-full rounded-sm bg-muted" />
|
||||
<div className="h-3 w-4/5 rounded-sm bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "success" && rows.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center px-6 py-16 text-center">
|
||||
<p className="text-sm font-medium text-foreground">{emptyMessage}</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<Stack align="center" justify="center" p="xl" style={{ minHeight: "400px" }}>
|
||||
<Text size="lg" fw={600}>{emptyMessage}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Try adjusting your search or add a new record.
|
||||
</p>
|
||||
</div>
|
||||
</Text>
|
||||
</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 (
|
||||
<>
|
||||
<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">
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 2, lg: 3 }} spacing="md">
|
||||
{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 (
|
||||
<article key={record.id} className={ruleEngineCard.article}>
|
||||
<div className={ruleEngineCard.header}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={ruleEngineCard.avatar} aria-hidden>
|
||||
<Card
|
||||
key={record.id}
|
||||
p="lg"
|
||||
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)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className={ruleEngineCard.title}>{title}</h3>
|
||||
{presentation.statusKey
|
||||
? formatCell(statusValue, "activeBadge")
|
||||
: null}
|
||||
</div>
|
||||
{(code || subtitle) && (
|
||||
<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 style={{ minWidth: 0, flex: 1 }}>
|
||||
<Text size="sm" fw={700} truncate title={title}>
|
||||
{title}
|
||||
</Text>
|
||||
{code && (
|
||||
<Text size="xs" c="dimmed" style={{ marginTop: "4px" }}>
|
||||
{code}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Group>
|
||||
{presentation.statusKey && (
|
||||
<div>{formatCell(statusValue, "activeBadge")}</div>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{presentation.detailColumns.length > 0 ? (
|
||||
<dl className="grid flex-1 gap-x-4 gap-y-3 px-4 py-3.5 sm:grid-cols-2">
|
||||
{presentation.detailColumns.map((col) => (
|
||||
<div key={col.id} className="min-w-0">
|
||||
<dt className={ruleEngineCard.detailLabel}>{col.header}</dt>
|
||||
<dd className={ruleEngineCard.detailValue}>
|
||||
{formatCell(record[col.accessorKey], col.format)}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
) : (
|
||||
<div className="flex-1 px-4 py-2" />
|
||||
{(subtitle || presentation.detailColumns.length > 0) && (
|
||||
<Stack gap="xs" style={{ flex: 1, marginBottom: "md" }}>
|
||||
{subtitle && (
|
||||
<Group gap="xs">
|
||||
<Text size="xs" c="dimmed" fw={500}>
|
||||
{presentation.subtitleKey === "stepOrder" ? "Step" : "Type"}:
|
||||
</Text>
|
||||
<Text size="xs" fw={500}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
{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
|
||||
record={record}
|
||||
config={config}
|
||||
@@ -162,26 +243,26 @@ const RuleEngineCardGrid = ({
|
||||
onSubmitRate={onSubmitRate}
|
||||
onApproveRate={onApproveRate}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SimpleGrid>
|
||||
|
||||
<div className="border-t border-border bg-card">
|
||||
<DataTableFooter
|
||||
table={table}
|
||||
pagination={pagination}
|
||||
options={{
|
||||
labels: {
|
||||
showing: "Showing",
|
||||
ofLabel: "of",
|
||||
items: itemLabel,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
{pagination.pageCount > 1 && (
|
||||
<Group justify="space-between" align="center" p="md" style={{ borderTop: "1px solid var(--mantine-color-gray-2)" }}>
|
||||
<Text size="sm" c="dimmed">
|
||||
Showing {Math.min(rows.length, pagination.pageSize)} of {pagination.totalCount} {itemLabel}
|
||||
</Text>
|
||||
<Pagination
|
||||
value={pagination.pageIndex + 1}
|
||||
total={pagination.pageCount}
|
||||
size="sm"
|
||||
radius="md"
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user