mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
267 lines
9.4 KiB
TypeScript
267 lines
9.4 KiB
TypeScript
import type { OnChangeFn, PaginationState } from "@tanstack/react-table";
|
|
import { Stack, Group, Text, Card, SimpleGrid } from "@mantine/core";
|
|
|
|
import type { RuleEngineResourceConfig } from "@/pages/ruleEngine/config/resources";
|
|
import type { RuleEngineRecord } from "@/types/rule-engine";
|
|
|
|
import RuleEngineListFooter from "./RuleEngineListFooter";
|
|
import RuleEngineRecordActions from "./RuleEngineRecordActions";
|
|
import { cardInitials, resolveCardPresentation } from "./ruleEngineCardMeta";
|
|
import { formatCell } from "./ruleEngineFormat";
|
|
|
|
export interface RuleEngineCardGridProps {
|
|
config: RuleEngineResourceConfig;
|
|
rows: RuleEngineRecord[];
|
|
status: "loading" | "error" | "success";
|
|
emptyMessage: string;
|
|
itemLabel: string;
|
|
pagination: PaginationState;
|
|
pageCount: number;
|
|
totalCount: number;
|
|
onPaginationChange: OnChangeFn<PaginationState>;
|
|
onEdit?: (record: RuleEngineRecord) => void;
|
|
onDelete?: (record: RuleEngineRecord) => void;
|
|
readOnly?: boolean;
|
|
onViewChain?: () => void;
|
|
onSubmitRate?: (id: string) => 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 = ({
|
|
config,
|
|
rows,
|
|
status,
|
|
emptyMessage,
|
|
itemLabel,
|
|
pagination,
|
|
pageCount,
|
|
totalCount,
|
|
onPaginationChange,
|
|
onEdit,
|
|
onDelete,
|
|
onViewChain,
|
|
onSubmitRate,
|
|
onApproveRate,
|
|
readOnly = false,
|
|
}: RuleEngineCardGridProps) => {
|
|
const presentation = resolveCardPresentation(config);
|
|
|
|
if (status === "error") {
|
|
return (
|
|
<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.
|
|
</Text>
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
if (status === "loading") {
|
|
return (
|
|
<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>
|
|
</Card>
|
|
))}
|
|
</SimpleGrid>
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
if (status === "success" && rows.length === 0) {
|
|
return (
|
|
<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.
|
|
</Text>
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
const avatarBg = "#f1f5f9";
|
|
const avatarText = "#475569";
|
|
|
|
return (
|
|
<Stack gap="md" p="md">
|
|
<SimpleGrid cols={{ base: 1, sm: 2, md: 2, lg: 3 }} spacing="md">
|
|
{rows.map((record) => {
|
|
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 (
|
|
<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.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)";
|
|
}}
|
|
>
|
|
<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: avatarBg,
|
|
fontSize: "16px",
|
|
fontWeight: 700,
|
|
color: avatarText,
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{cardInitials(title)}
|
|
</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>
|
|
</Group>
|
|
{presentation.statusKey && (
|
|
<div>{formatCell(statusValue, "activeBadge")}</div>
|
|
)}
|
|
</Group>
|
|
|
|
{(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>
|
|
)}
|
|
|
|
<Group justify="flex-end" gap="xs" style={{ borderTop: "1px solid var(--mantine-color-gray-1)", paddingTop: "md" }}>
|
|
<RuleEngineRecordActions
|
|
record={record}
|
|
config={config}
|
|
layout="compact"
|
|
readOnly={readOnly}
|
|
onEdit={onEdit ?? (() => {})}
|
|
onDelete={onDelete ?? (() => {})}
|
|
onViewChain={onViewChain}
|
|
onSubmitRate={onSubmitRate}
|
|
onApproveRate={onApproveRate}
|
|
/>
|
|
</Group>
|
|
</Card>
|
|
);
|
|
})}
|
|
</SimpleGrid>
|
|
|
|
<RuleEngineListFooter
|
|
pagination={pagination}
|
|
pageCount={pageCount}
|
|
totalCount={totalCount}
|
|
itemLabel={itemLabel}
|
|
onPaginationChange={onPaginationChange}
|
|
/>
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
export default RuleEngineCardGrid;
|