Files
edr-platform/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx
2026-06-23 15:02:25 +03:00

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;