Files
edr-platform/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx

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;