mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 08:32:54 +00:00
280 lines
8.9 KiB
TypeScript
280 lines
8.9 KiB
TypeScript
import type { OnChangeFn, PaginationState } from "@edr/ui-common";
|
|
import { Stack, Group, Text, Card, SimpleGrid, Skeleton } from "@mantine/core";
|
|
|
|
import type { RuleEngineResourceConfig } from "@/pages/ruleEngine/config/resources";
|
|
import type { RuleEngineRecord } from "@/types/rule-engine";
|
|
|
|
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="lg">
|
|
<Group gap="sm" mb="md">
|
|
<Skeleton height={44} width={44} radius="md" />
|
|
<Stack gap={6} style={{ flex: 1 }}>
|
|
<Skeleton height={14} width="70%" radius="sm" />
|
|
<Skeleton height={10} width="40%" radius="sm" />
|
|
</Stack>
|
|
</Group>
|
|
<Skeleton height={12} radius="sm" mb={8} />
|
|
<Skeleton height={12} width="80%" radius="sm" />
|
|
</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 = "var(--mantine-color-gray-1)";
|
|
const avatarText = "var(--mantine-color-gray-7)";
|
|
|
|
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"
|
|
style={{ display: "flex", flexDirection: "column" }}
|
|
>
|
|
<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" mb="md" style={{ flex: 1 }}>
|
|
{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"
|
|
pt="md"
|
|
style={{ borderTop: "1px solid var(--mantine-color-gray-2)" }}
|
|
>
|
|
<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;
|