mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 17:08:18 +00:00
generate contract by the system, add waggon type,fix ui
This commit is contained in:
@@ -121,8 +121,8 @@ const RuleEngineCardGrid = ({
|
||||
);
|
||||
}
|
||||
|
||||
const avatarColors = ["blue", "cyan", "grape", "green", "lime", "orange", "pink", "red", "teal", "violet", "yellow"];
|
||||
const getAvatarColor = (title: string) => avatarColors[title.charCodeAt(0) % avatarColors.length];
|
||||
const avatarBg = "#f1f5f9";
|
||||
const avatarText = "#475569";
|
||||
|
||||
return (
|
||||
<Stack gap="md" p="md">
|
||||
@@ -160,8 +160,8 @@ const RuleEngineCardGrid = ({
|
||||
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)";
|
||||
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";
|
||||
@@ -178,10 +178,10 @@ const RuleEngineCardGrid = ({
|
||||
width: "44px",
|
||||
height: "44px",
|
||||
borderRadius: "8px",
|
||||
background: `var(--mantine-color-${getAvatarColor(title)}-1)`,
|
||||
background: avatarBg,
|
||||
fontSize: "16px",
|
||||
fontWeight: 700,
|
||||
color: `var(--mantine-color-${getAvatarColor(title)}-7)`,
|
||||
color: avatarText,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import {
|
||||
Modal,
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
Switch,
|
||||
Stack,
|
||||
Group,
|
||||
Divider,
|
||||
Text,
|
||||
Box,
|
||||
SimpleGrid,
|
||||
Divider,
|
||||
} from "@mantine/core";
|
||||
|
||||
import {
|
||||
@@ -32,6 +33,43 @@ export interface RuleEngineFormDialogProps {
|
||||
onSubmit: (values: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
type FormRow =
|
||||
| { kind: "pair"; fields: [FormFieldDef, FormFieldDef] }
|
||||
| { kind: "single"; field: FormFieldDef };
|
||||
|
||||
const isShortField = (field: FormFieldDef) =>
|
||||
field.type === "text" ||
|
||||
field.type === "number" ||
|
||||
field.type === "select" ||
|
||||
field.type === "date";
|
||||
|
||||
const buildFormRows = (fields: FormFieldDef[]): FormRow[] => {
|
||||
const rows: FormRow[] = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < fields.length) {
|
||||
const field = fields[index];
|
||||
|
||||
if (field.type === "textarea" || field.type === "boolean") {
|
||||
rows.push({ kind: "single", field });
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const next = fields[index + 1];
|
||||
if (next && isShortField(next)) {
|
||||
rows.push({ kind: "pair", fields: [field, next] });
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.push({ kind: "single", field });
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
const buildInitialValues = (
|
||||
fields: FormFieldDef[],
|
||||
record?: RuleEngineRecord | null,
|
||||
@@ -42,6 +80,8 @@ const buildInitialValues = (
|
||||
if (raw !== undefined && raw !== null) {
|
||||
if (field.type === "date" && typeof raw === "string") {
|
||||
values[field.name] = raw.slice(0, 10);
|
||||
} else if (Array.isArray(raw)) {
|
||||
values[field.name] = raw.join(", ");
|
||||
} else {
|
||||
values[field.name] = raw;
|
||||
}
|
||||
@@ -74,11 +114,34 @@ const resolveSelectValue = (
|
||||
return String(raw);
|
||||
};
|
||||
|
||||
const inputStyles = {
|
||||
label: { fontWeight: 600, marginBottom: 6, color: "var(--mantine-color-gray-8)" },
|
||||
input: {
|
||||
borderColor: "#e2e8f0",
|
||||
background: "white",
|
||||
transition: "border-color 0.15s ease, box-shadow 0.15s ease",
|
||||
"&:focus": {
|
||||
borderColor: "var(--freight-brand)",
|
||||
boxShadow: "0 0 0 3px var(--freight-brand-ring)",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const FieldLabel = ({ label, required }: { label: string; required?: boolean }) => (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<span>{label}</span>
|
||||
{required ? (
|
||||
<Text component="span" c="red" size="sm">
|
||||
*
|
||||
</Text>
|
||||
) : null}
|
||||
</Group>
|
||||
);
|
||||
|
||||
const RuleEngineFormDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
description,
|
||||
fields,
|
||||
initialRecord,
|
||||
isSubmitting,
|
||||
@@ -95,6 +158,8 @@ const RuleEngineFormDialog = ({
|
||||
}
|
||||
}, [open, fields, initialRecord]);
|
||||
|
||||
const formRows = useMemo(() => buildFormRows(fields), [fields]);
|
||||
|
||||
const setField = (name: string, value: unknown) => {
|
||||
setValues((current) => ({ ...current, [name]: value }));
|
||||
};
|
||||
@@ -130,157 +195,147 @@ const RuleEngineFormDialog = ({
|
||||
onSubmit(payload);
|
||||
};
|
||||
|
||||
const renderField = (field: FormFieldDef) => {
|
||||
if (field.type === "boolean") {
|
||||
return (
|
||||
<Group
|
||||
key={field.name}
|
||||
justify="space-between"
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
gap="md"
|
||||
px="md"
|
||||
style={{
|
||||
minHeight: 42,
|
||||
background: "#f8fafc",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "var(--mantine-radius-md)",
|
||||
}}
|
||||
>
|
||||
<Text size="sm" fw={600}>
|
||||
{field.label}
|
||||
</Text>
|
||||
<Switch
|
||||
checked={Boolean(values[field.name])}
|
||||
onChange={(e) => setField(field.name, e.currentTarget.checked)}
|
||||
size="md"
|
||||
color="green"
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
const label = <FieldLabel label={field.label} required={field.required} />;
|
||||
|
||||
if (field.type === "select") {
|
||||
return (
|
||||
<Select
|
||||
key={field.name}
|
||||
label={label}
|
||||
placeholder={
|
||||
selectOptionsLoading ? "Loading options..." : (field.placeholder ?? "Select an option")
|
||||
}
|
||||
value={resolveSelectValue(field, values)}
|
||||
onChange={(v) => setField(field.name, v === RULE_ENGINE_SELECT_NONE ? "" : v)}
|
||||
disabled={selectOptionsLoading}
|
||||
data={(field.options ?? [])
|
||||
.filter((opt) => opt.value !== "")
|
||||
.map((opt) => ({
|
||||
label: opt.label,
|
||||
value: opt.value,
|
||||
}))}
|
||||
searchable
|
||||
clearable
|
||||
size="md"
|
||||
radius="md"
|
||||
styles={inputStyles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === "textarea") {
|
||||
return (
|
||||
<Textarea
|
||||
key={field.name}
|
||||
label={label}
|
||||
value={String(values[field.name] ?? "")}
|
||||
onChange={(e) => setField(field.name, e.currentTarget.value)}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
minRows={4}
|
||||
autosize
|
||||
maxRows={8}
|
||||
size="md"
|
||||
radius="md"
|
||||
styles={inputStyles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
key={field.name}
|
||||
label={label}
|
||||
type={field.type === "number" ? "number" : field.type === "date" ? "date" : "text"}
|
||||
value={String(values[field.name] ?? "")}
|
||||
onChange={(e) => setField(field.name, e.currentTarget.value)}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
size="md"
|
||||
radius="md"
|
||||
styles={inputStyles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => onOpenChange(false)}
|
||||
title={title}
|
||||
title={
|
||||
<Text size="lg" fw={700} lh={1.2}>
|
||||
{title}
|
||||
</Text>
|
||||
}
|
||||
centered
|
||||
size="md"
|
||||
size={720}
|
||||
radius="lg"
|
||||
padding="xl"
|
||||
overlayProps={{ backgroundOpacity: 0.45, blur: 3 }}
|
||||
styles={{
|
||||
header: {
|
||||
paddingBottom: "20px",
|
||||
borderBottom: "1px solid var(--mantine-color-gray-2)",
|
||||
content: {
|
||||
maxWidth: "min(720px, 95vw)",
|
||||
},
|
||||
body: {
|
||||
paddingTop: "24px",
|
||||
paddingBottom: "24px",
|
||||
},
|
||||
title: {
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: 700,
|
||||
color: "var(--mantine-color-gray-9)",
|
||||
paddingTop: 20,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="lg">
|
||||
<Text size="sm" c="dimmed">
|
||||
{description}
|
||||
</Text>
|
||||
|
||||
<div style={{ maxHeight: "calc(60vh - 150px)", overflowY: "auto", paddingRight: "12px" }}>
|
||||
<Stack gap="lg">
|
||||
{fields.map((field) => (
|
||||
<Box key={field.name}>
|
||||
{field.type === "boolean" ? (
|
||||
<Group
|
||||
justify="space-between"
|
||||
align="center"
|
||||
p="lg"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, var(--mantine-color-blue-0) 0%, var(--mantine-color-cyan-0) 100%)",
|
||||
borderRadius: "12px",
|
||||
border: "1px solid var(--mantine-color-blue-2)",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Text size="sm" fw={600} mb="4px">
|
||||
{field.label}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{Boolean(values[field.name]) ? "✓ Enabled" : "○ Disabled"}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={Boolean(values[field.name])}
|
||||
onChange={(e) => setField(field.name, e.currentTarget.checked)}
|
||||
size="lg"
|
||||
/>
|
||||
</Group>
|
||||
) : field.type === "select" ? (
|
||||
<Select
|
||||
label={
|
||||
<Group gap="4px">
|
||||
<span>{field.label}</span>
|
||||
{field.required && <span style={{ color: "var(--mantine-color-red-6)" }}>*</span>}
|
||||
</Group>
|
||||
}
|
||||
placeholder={
|
||||
selectOptionsLoading ? "Loading options..." : (field.placeholder ?? "Select an option")
|
||||
}
|
||||
value={resolveSelectValue(field, values)}
|
||||
onChange={(v) =>
|
||||
setField(field.name, v === RULE_ENGINE_SELECT_NONE ? "" : v)
|
||||
}
|
||||
disabled={selectOptionsLoading}
|
||||
data={(field.options ?? []).filter((opt) => opt.value !== "").map((opt) => ({
|
||||
label: opt.label,
|
||||
value: opt.value,
|
||||
}))}
|
||||
searchable
|
||||
clearable
|
||||
size="md"
|
||||
radius="lg"
|
||||
styles={{
|
||||
input: {
|
||||
borderColor: "var(--mantine-color-gray-3)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : field.type === "textarea" ? (
|
||||
<Textarea
|
||||
label={
|
||||
<Group gap="4px">
|
||||
<span>{field.label}</span>
|
||||
{field.required && <span style={{ color: "var(--mantine-color-red-6)" }}>*</span>}
|
||||
</Group>
|
||||
}
|
||||
value={String(values[field.name] ?? "")}
|
||||
onChange={(e) => setField(field.name, e.currentTarget.value)}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
minRows={5}
|
||||
size="md"
|
||||
radius="lg"
|
||||
styles={{
|
||||
input: {
|
||||
borderColor: "var(--mantine-color-gray-3)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="4px">
|
||||
<span>{field.label}</span>
|
||||
{field.required && <span style={{ color: "var(--mantine-color-red-6)" }}>*</span>}
|
||||
</Group>
|
||||
}
|
||||
type={
|
||||
field.type === "number"
|
||||
? "number"
|
||||
: field.type === "date"
|
||||
? "date"
|
||||
: "text"
|
||||
}
|
||||
value={String(values[field.name] ?? "")}
|
||||
onChange={(e) => setField(field.name, e.currentTarget.value)}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
size="md"
|
||||
radius="lg"
|
||||
styles={{
|
||||
input: {
|
||||
borderColor: "var(--mantine-color-gray-3)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
<Box style={{ maxHeight: "calc(65vh - 120px)", overflowY: "auto", paddingRight: 4 }}>
|
||||
<Stack gap="md">
|
||||
{formRows.map((row) =>
|
||||
row.kind === "pair" ? (
|
||||
<SimpleGrid key={`${row.fields[0].name}-${row.fields[1].name}`} cols={2} spacing="md">
|
||||
<Box style={{ minWidth: 0 }}>{renderField(row.fields[0])}</Box>
|
||||
<Box style={{ minWidth: 0 }}>{renderField(row.fields[1])}</Box>
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
<Box key={row.field.name}>{renderField(row.field)}</Box>
|
||||
),
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="light"
|
||||
variant="default"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
radius="lg"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
Cancel
|
||||
@@ -288,9 +343,15 @@ const RuleEngineFormDialog = ({
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
leftSection={isSubmitting && <Loader2 size={18} style={{ animation: "spin 1s linear infinite" }} />}
|
||||
radius="lg"
|
||||
color="blue"
|
||||
leftSection={
|
||||
isSubmitting ? (
|
||||
<Loader2 size={18} style={{ animation: "spin 1s linear infinite" }} />
|
||||
) : undefined
|
||||
}
|
||||
radius="md"
|
||||
color="green"
|
||||
variant="filled"
|
||||
fw={600}
|
||||
size="md"
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save"}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Send,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { Button, Menu, Group } from "@mantine/core";
|
||||
import { ActionIcon, Button, Group, Menu, Tooltip } from "@mantine/core";
|
||||
|
||||
import type { RuleEngineResourceConfig } from "@/pages/ruleEngine/config/resources";
|
||||
import type { RuleEngineRecord } from "@/types/rule-engine";
|
||||
@@ -23,6 +23,16 @@ export interface RuleEngineRecordActionsProps {
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
const actionGroupStyle = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
borderRadius: 10,
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
background: "var(--mantine-color-gray-0)",
|
||||
padding: 3,
|
||||
} as const;
|
||||
|
||||
const RuleEngineRecordActions = ({
|
||||
record,
|
||||
config,
|
||||
@@ -37,85 +47,209 @@ const RuleEngineRecordActions = ({
|
||||
const status = String(record.status ?? "");
|
||||
const hasRateActions =
|
||||
config.slug === "rates" && (status === "DRAFT" || status === "PENDING_APPROVAL");
|
||||
const showViewChain = config.slug === "approval-rules" && onViewChain;
|
||||
|
||||
if (readOnly) {
|
||||
return onViewChain ? (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
onClick={onViewChain}
|
||||
leftSection={<Eye size={16} />}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
return showViewChain ? (
|
||||
<Tooltip label="View approval chain">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="md"
|
||||
radius="md"
|
||||
onClick={onViewChain}
|
||||
aria-label="View approval chain"
|
||||
>
|
||||
<Eye size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Group gap={2} justify="flex-end">
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
onClick={() => onEdit(record)}
|
||||
leftSection={<Pencil size={16} />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
if (layout === "compact") {
|
||||
return (
|
||||
<Group gap={6} wrap="nowrap" justify="flex-end">
|
||||
{showViewChain ? (
|
||||
<Button
|
||||
variant="light"
|
||||
color="gray"
|
||||
size="compact-sm"
|
||||
radius="md"
|
||||
onClick={onViewChain}
|
||||
leftSection={<Eye size={14} />}
|
||||
>
|
||||
Chain
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{config.slug === "approval-rules" && onViewChain ? (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
onClick={onViewChain}
|
||||
leftSection={<Eye size={16} />}
|
||||
>
|
||||
View Chain
|
||||
</Button>
|
||||
{hasRateActions ? (
|
||||
<RateActionsMenu
|
||||
status={status}
|
||||
onSubmitRate={onSubmitRate}
|
||||
onApproveRate={onApproveRate}
|
||||
record={record}
|
||||
compact
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div style={actionGroupStyle}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="compact-sm"
|
||||
radius="md"
|
||||
onClick={() => onEdit(record)}
|
||||
leftSection={<Pencil size={14} />}
|
||||
styles={{ root: { fontWeight: 600 } }}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="red"
|
||||
size="compact-sm"
|
||||
radius="md"
|
||||
onClick={() => onDelete(record)}
|
||||
leftSection={<Trash2 size={14} />}
|
||||
styles={{ root: { fontWeight: 600 } }}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Group gap={6} wrap="nowrap" justify="flex-end">
|
||||
{showViewChain ? (
|
||||
<Tooltip label="View approval chain">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="gray"
|
||||
size="md"
|
||||
radius="md"
|
||||
onClick={onViewChain}
|
||||
aria-label="View approval chain"
|
||||
style={{
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
background: "white",
|
||||
}}
|
||||
>
|
||||
<Eye size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
||||
{hasRateActions ? (
|
||||
<Menu position="bottom-end" shadow="md">
|
||||
<Menu.Target>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
leftSection={<MoreHorizontal size={16} />}
|
||||
>
|
||||
More
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{status === "DRAFT" && onSubmitRate ? (
|
||||
<Menu.Item
|
||||
leftSection={<Send size={14} />}
|
||||
onClick={() => onSubmitRate(record.id)}
|
||||
>
|
||||
Submit for approval
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
{status === "PENDING_APPROVAL" && onApproveRate ? (
|
||||
<Menu.Item
|
||||
leftSection={<CheckCircle2 size={14} />}
|
||||
onClick={() => onApproveRate(record)}
|
||||
>
|
||||
Approve
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
<RateActionsMenu
|
||||
status={status}
|
||||
onSubmitRate={onSubmitRate}
|
||||
onApproveRate={onApproveRate}
|
||||
record={record}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
onClick={() => onDelete(record)}
|
||||
leftSection={<Trash2 size={16} />}
|
||||
color="red"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<div style={actionGroupStyle}>
|
||||
<Tooltip label="Edit">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="md"
|
||||
radius="md"
|
||||
onClick={() => onEdit(record)}
|
||||
aria-label="Edit record"
|
||||
style={{
|
||||
background: "white",
|
||||
}}
|
||||
>
|
||||
<Pencil size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
size="md"
|
||||
radius="md"
|
||||
onClick={() => onDelete(record)}
|
||||
aria-label="Delete record"
|
||||
style={{
|
||||
background: "var(--mantine-color-red-0)",
|
||||
}}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
function RateActionsMenu({
|
||||
status,
|
||||
onSubmitRate,
|
||||
onApproveRate,
|
||||
record,
|
||||
compact = false,
|
||||
}: {
|
||||
status: string;
|
||||
onSubmitRate?: (id: string) => void;
|
||||
onApproveRate?: (record: RuleEngineRecord) => void;
|
||||
record: RuleEngineRecord;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Menu position="bottom-end" shadow="md" withinPortal>
|
||||
<Menu.Target>
|
||||
{compact ? (
|
||||
<Button
|
||||
variant="light"
|
||||
color="gray"
|
||||
size="compact-sm"
|
||||
radius="md"
|
||||
leftSection={<MoreHorizontal size={14} />}
|
||||
>
|
||||
More
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip label="More actions">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="gray"
|
||||
size="md"
|
||||
radius="md"
|
||||
aria-label="More actions"
|
||||
style={{
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
background: "white",
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{status === "DRAFT" && onSubmitRate ? (
|
||||
<Menu.Item
|
||||
leftSection={<Send size={14} />}
|
||||
onClick={() => onSubmitRate(record.id)}
|
||||
>
|
||||
Submit for approval
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
{status === "PENDING_APPROVAL" && onApproveRate ? (
|
||||
<Menu.Item
|
||||
leftSection={<CheckCircle2 size={14} />}
|
||||
onClick={() => onApproveRate(record)}
|
||||
>
|
||||
Approve
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
export default RuleEngineRecordActions;
|
||||
|
||||
@@ -44,6 +44,7 @@ const RuleEngineToolbar = ({
|
||||
onChange={(value) => onViewModeChange(value as RuleEngineViewMode)}
|
||||
size="sm"
|
||||
radius="lg"
|
||||
color="green"
|
||||
data={[
|
||||
{
|
||||
value: "table",
|
||||
@@ -77,7 +78,9 @@ const RuleEngineToolbar = ({
|
||||
leftSection={<Plus size={18} />}
|
||||
size="sm"
|
||||
radius="lg"
|
||||
color="blue"
|
||||
color="green"
|
||||
variant="filled"
|
||||
fw={600}
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
{addLabel}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
const TITLE_KEY_PRIORITY = [
|
||||
"cargoTypeName",
|
||||
"serviceName",
|
||||
"name",
|
||||
"label",
|
||||
"actionLabel",
|
||||
"rateType",
|
||||
|
||||
@@ -91,6 +91,12 @@ export const formatCell = (value: unknown, format?: ColumnFormat): ReactNode =>
|
||||
return <Text size="sm">{d.toLocaleDateString()}</Text>;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
<Text size="sm">{value.length > 0 ? value.join(", ") : "—"}</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (format === "entityLabel" && value && typeof value === "object") {
|
||||
const label = extractLabel(value);
|
||||
if (label) {
|
||||
|
||||
@@ -40,7 +40,7 @@ export const ruleEngineCard = {
|
||||
"group flex flex-col overflow-hidden rounded-lg border border-border bg-card shadow-sm transition-shadow duration-200 hover:shadow-md",
|
||||
header: "border-b border-border bg-muted/25 px-3 py-2.5 sm:px-4 sm:py-3.5",
|
||||
avatar:
|
||||
"flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary/12 text-xs font-semibold text-primary sm:h-10 sm:w-10 sm:text-sm",
|
||||
"flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-[#f1f5f9] text-xs font-semibold text-slate-600 sm:h-10 sm:w-10 sm:text-sm",
|
||||
title: "truncate text-sm font-semibold text-foreground sm:text-[15px]",
|
||||
meta: "text-xs text-muted-foreground",
|
||||
detailLabel:
|
||||
|
||||
Reference in New Issue
Block a user