mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 04:50:54 +00:00
change price logic on the ,rule engine ui, auto generate the contrat
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
DragDropContext,
|
||||
Draggable,
|
||||
Droppable,
|
||||
type DraggableProvided,
|
||||
type DraggableStateSnapshot,
|
||||
type DropResult,
|
||||
} from "@hello-pangea/dnd";
|
||||
import { GripVertical, Loader2 } from "lucide-react";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Modal,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
|
||||
import type { RuleEngineResourceConfig } from "@/pages/ruleEngine/config/resources";
|
||||
import type { RuleEngineRecord } from "@/types/rule-engine";
|
||||
|
||||
import { getOrderItemLabel, getOrderValue } from "./ruleEngineOrder.utils";
|
||||
|
||||
interface OrderDraftItem {
|
||||
id: string;
|
||||
label: string;
|
||||
code?: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface ManageRuleEngineOrderDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
config: RuleEngineResourceConfig;
|
||||
items: RuleEngineRecord[];
|
||||
isLoading: boolean;
|
||||
isSaving: boolean;
|
||||
onSave: (payload: { ids: string[]; requiresDirectorApproval?: boolean }) => void;
|
||||
}
|
||||
|
||||
const toDraftItems = (
|
||||
rows: RuleEngineRecord[],
|
||||
config: RuleEngineResourceConfig,
|
||||
): OrderDraftItem[] => {
|
||||
const field = config.orderConfig!.field;
|
||||
return [...rows]
|
||||
.sort((a, b) => getOrderValue(a, field) - getOrderValue(b, field))
|
||||
.map((row) => ({
|
||||
id: String(row.id),
|
||||
label: getOrderItemLabel(row, config.slug),
|
||||
code: row.code ? String(row.code) : undefined,
|
||||
order: getOrderValue(row, field),
|
||||
}));
|
||||
};
|
||||
|
||||
/** Reparent dragged row to body — fixes position:fixed inside Modal transforms. */
|
||||
const PortalAwareRow = ({
|
||||
snapshot,
|
||||
children,
|
||||
}: {
|
||||
snapshot: DraggableStateSnapshot;
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
if (snapshot.isDragging) {
|
||||
return createPortal(children, document.body);
|
||||
}
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const OrderRow = ({
|
||||
item,
|
||||
index,
|
||||
dragProvided,
|
||||
snapshot,
|
||||
}: {
|
||||
item: OrderDraftItem;
|
||||
index: number;
|
||||
dragProvided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
}) => (
|
||||
<PortalAwareRow snapshot={snapshot}>
|
||||
<Group
|
||||
ref={dragProvided.innerRef}
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
gap="sm"
|
||||
wrap="nowrap"
|
||||
p="sm"
|
||||
style={{
|
||||
...dragProvided.draggableProps.style,
|
||||
border: "1px solid var(--mantine-color-gray-3)",
|
||||
borderRadius: "var(--mantine-radius-md)",
|
||||
background: snapshot.isDragging ? "var(--mantine-color-gray-0)" : "white",
|
||||
boxShadow: snapshot.isDragging ? "0 8px 24px rgba(0, 0, 0, 0.12)" : undefined,
|
||||
cursor: snapshot.isDragging ? "grabbing" : "grab",
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
<Box c="dimmed" style={{ display: "flex", alignItems: "center" }}>
|
||||
<GripVertical size={18} />
|
||||
</Box>
|
||||
<Badge variant="light" color="gray" size="sm">
|
||||
{index + 1}
|
||||
</Badge>
|
||||
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text size="sm" fw={500} truncate>
|
||||
{item.label}
|
||||
</Text>
|
||||
{item.code ? (
|
||||
<Text size="xs" c="dimmed" truncate>
|
||||
{item.code}
|
||||
</Text>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Group>
|
||||
</PortalAwareRow>
|
||||
);
|
||||
|
||||
const ManageRuleEngineOrderDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
config,
|
||||
items,
|
||||
isLoading,
|
||||
isSaving,
|
||||
onSave,
|
||||
}: ManageRuleEngineOrderDialogProps) => {
|
||||
const isScoped = config.orderConfig?.scopeField === "requiresDirectorApproval";
|
||||
const [tab, setTab] = useState<"standard" | "director">("standard");
|
||||
const [filter, setFilter] = useState("");
|
||||
const [standardItems, setStandardItems] = useState<OrderDraftItem[]>([]);
|
||||
const [directorItems, setDirectorItems] = useState<OrderDraftItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (isScoped) {
|
||||
setStandardItems(
|
||||
toDraftItems(
|
||||
items.filter((row) => !row.requiresDirectorApproval),
|
||||
config,
|
||||
),
|
||||
);
|
||||
setDirectorItems(
|
||||
toDraftItems(
|
||||
items.filter((row) => row.requiresDirectorApproval),
|
||||
config,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setStandardItems(toDraftItems(items, config));
|
||||
}
|
||||
setFilter("");
|
||||
}, [open, items, config, isScoped]);
|
||||
|
||||
const activeItems = isScoped
|
||||
? tab === "director"
|
||||
? directorItems
|
||||
: standardItems
|
||||
: standardItems;
|
||||
|
||||
const setActiveItems = isScoped
|
||||
? tab === "director"
|
||||
? setDirectorItems
|
||||
: setStandardItems
|
||||
: setStandardItems;
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
const q = filter.trim().toLowerCase();
|
||||
if (!q) return activeItems;
|
||||
return activeItems.filter(
|
||||
(item) =>
|
||||
item.label.toLowerCase().includes(q) ||
|
||||
(item.code?.toLowerCase().includes(q) ?? false),
|
||||
);
|
||||
}, [activeItems, filter]);
|
||||
|
||||
const droppableId = isScoped
|
||||
? `rule-engine-order-${tab}`
|
||||
: "rule-engine-order-list";
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result.destination || filter.trim()) return;
|
||||
const sourceIndex = result.source.index;
|
||||
const destIndex = result.destination.index;
|
||||
if (sourceIndex === destIndex) return;
|
||||
|
||||
setActiveItems((prev) => {
|
||||
const next = [...prev];
|
||||
const [removed] = next.splice(sourceIndex, 1);
|
||||
next.splice(destIndex, 0, removed!);
|
||||
return next.map((item, index) => ({ ...item, order: index + 1 }));
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (isScoped) {
|
||||
onSave({
|
||||
ids: (tab === "director" ? directorItems : standardItems).map((item) => item.id),
|
||||
requiresDirectorApproval: tab === "director",
|
||||
});
|
||||
return;
|
||||
}
|
||||
onSave({ ids: standardItems.map((item) => item.id) });
|
||||
};
|
||||
|
||||
const renderList = (listItems: OrderDraftItem[]) => (
|
||||
<Droppable droppableId={droppableId}>
|
||||
{(provided) => (
|
||||
<Stack
|
||||
gap="xs"
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
style={{ minHeight: 120 }}
|
||||
>
|
||||
{listItems.length === 0 ? (
|
||||
<Text size="sm" c="dimmed" ta="center" py="xl">
|
||||
No items to reorder.
|
||||
</Text>
|
||||
) : (
|
||||
listItems.map((item, index) => (
|
||||
<Draggable
|
||||
key={item.id}
|
||||
draggableId={item.id}
|
||||
index={index}
|
||||
isDragDisabled={Boolean(filter.trim())}
|
||||
>
|
||||
{(dragProvided, snapshot) => (
|
||||
<OrderRow
|
||||
item={item}
|
||||
index={index}
|
||||
dragProvided={dragProvided}
|
||||
snapshot={snapshot}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</Stack>
|
||||
)}
|
||||
</Droppable>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => onOpenChange(false)}
|
||||
title={`Manage order · ${config.label}`}
|
||||
centered
|
||||
size="lg"
|
||||
radius="lg"
|
||||
transitionProps={{ duration: 0, transition: "fade" }}
|
||||
styles={{
|
||||
content: {
|
||||
transform: "none",
|
||||
overflow: "visible",
|
||||
},
|
||||
body: {
|
||||
overflow: "visible",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
Drag items anywhere in the list to set display order. Changes apply when you save.
|
||||
</Text>
|
||||
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="xl">
|
||||
<Loader2 size={28} style={{ animation: "spin 1s linear infinite" }} />
|
||||
</Group>
|
||||
) : isScoped ? (
|
||||
<Tabs
|
||||
value={tab}
|
||||
onChange={(value) => setTab((value as "standard" | "director") ?? "standard")}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="standard">Standard chain ({standardItems.length})</Tabs.Tab>
|
||||
<Tabs.Tab value="director">Director chain ({directorItems.length})</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="standard" pt="md">
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
placeholder="Filter items…"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.currentTarget.value)}
|
||||
/>
|
||||
<Box style={{ maxHeight: "50vh", overflowY: "auto", paddingRight: 4 }}>
|
||||
{renderList(filteredItems)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="director" pt="md">
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
placeholder="Filter items…"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.currentTarget.value)}
|
||||
/>
|
||||
<Box style={{ maxHeight: "50vh", overflowY: "auto", paddingRight: 4 }}>
|
||||
{renderList(filteredItems)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
) : (
|
||||
<>
|
||||
<TextInput
|
||||
placeholder="Filter items…"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.currentTarget.value)}
|
||||
/>
|
||||
<Box style={{ maxHeight: "50vh", overflowY: "auto", paddingRight: 4 }}>
|
||||
{renderList(filteredItems)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{filter.trim() ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
Clear the filter to drag and reorder items.
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="default" onClick={() => onOpenChange(false)} disabled={isSaving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="green"
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || isSaving}
|
||||
leftSection={
|
||||
isSaving ? (
|
||||
<Loader2 size={16} style={{ animation: "spin 1s linear infinite" }} />
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{isSaving ? "Saving…" : "Save order"}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</DragDropContext>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageRuleEngineOrderDialog;
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Stack, Group, Text, Pagination, Card, SimpleGrid } from "@mantine/core";
|
||||
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";
|
||||
@@ -13,12 +15,10 @@ export interface RuleEngineCardGridProps {
|
||||
status: "loading" | "error" | "success";
|
||||
emptyMessage: string;
|
||||
itemLabel: string;
|
||||
pagination: {
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
pageCount: number;
|
||||
totalCount: number;
|
||||
};
|
||||
pagination: PaginationState;
|
||||
pageCount: number;
|
||||
totalCount: number;
|
||||
onPaginationChange: OnChangeFn<PaginationState>;
|
||||
onEdit?: (record: RuleEngineRecord) => void;
|
||||
onDelete?: (record: RuleEngineRecord) => void;
|
||||
readOnly?: boolean;
|
||||
@@ -71,6 +71,9 @@ const RuleEngineCardGrid = ({
|
||||
emptyMessage,
|
||||
itemLabel,
|
||||
pagination,
|
||||
pageCount,
|
||||
totalCount,
|
||||
onPaginationChange,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onViewChain,
|
||||
@@ -249,19 +252,13 @@ const RuleEngineCardGrid = ({
|
||||
})}
|
||||
</SimpleGrid>
|
||||
|
||||
{pagination.pageCount > 1 && (
|
||||
<Group justify="space-between" align="center" p="md" style={{ borderTop: "1px solid var(--mantine-color-gray-2)" }}>
|
||||
<Text size="sm" c="dimmed">
|
||||
Showing {Math.min(rows.length, pagination.pageSize)} of {pagination.totalCount} {itemLabel}
|
||||
</Text>
|
||||
<Pagination
|
||||
value={pagination.pageIndex + 1}
|
||||
total={pagination.pageCount}
|
||||
size="sm"
|
||||
radius="md"
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
<RuleEngineListFooter
|
||||
pagination={pagination}
|
||||
pageCount={pageCount}
|
||||
totalCount={totalCount}
|
||||
itemLabel={itemLabel}
|
||||
onPaginationChange={onPaginationChange}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type FormFieldDef,
|
||||
} from "@/pages/ruleEngine/config/resources";
|
||||
import type { RuleEngineRecord } from "@/types/rule-engine";
|
||||
import { RULE_ENGINE_POSITION_END } from "./ruleEngineOrder.utils";
|
||||
|
||||
export interface RuleEngineFormDialogProps {
|
||||
open: boolean;
|
||||
@@ -30,6 +31,8 @@ export interface RuleEngineFormDialogProps {
|
||||
initialRecord?: RuleEngineRecord | null;
|
||||
isSubmitting: boolean;
|
||||
selectOptionsLoading?: boolean;
|
||||
positionOptions?: { label: string; value: string }[];
|
||||
positionLoading?: boolean;
|
||||
onSubmit: (values: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
@@ -146,15 +149,19 @@ const RuleEngineFormDialog = ({
|
||||
initialRecord,
|
||||
isSubmitting,
|
||||
selectOptionsLoading = false,
|
||||
positionOptions,
|
||||
positionLoading = false,
|
||||
onSubmit,
|
||||
}: RuleEngineFormDialogProps) => {
|
||||
const [values, setValues] = useState<Record<string, unknown>>(() =>
|
||||
buildInitialValues(fields, initialRecord),
|
||||
);
|
||||
const [position, setPosition] = useState(RULE_ENGINE_POSITION_END);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setValues(buildInitialValues(fields, initialRecord));
|
||||
setPosition(RULE_ENGINE_POSITION_END);
|
||||
}
|
||||
}, [open, fields, initialRecord]);
|
||||
|
||||
@@ -192,6 +199,10 @@ const RuleEngineFormDialog = ({
|
||||
payload.code = String(payload.code).toUpperCase();
|
||||
}
|
||||
|
||||
if (!initialRecord && positionOptions && position !== RULE_ENGINE_POSITION_END) {
|
||||
payload.insertAfterId = position;
|
||||
}
|
||||
|
||||
onSubmit(payload);
|
||||
};
|
||||
|
||||
@@ -315,6 +326,23 @@ const RuleEngineFormDialog = ({
|
||||
<Stack gap="lg">
|
||||
<Box style={{ maxHeight: "calc(65vh - 120px)", overflowY: "auto", paddingRight: 4 }}>
|
||||
<Stack gap="md">
|
||||
{!initialRecord && positionOptions ? (
|
||||
<Select
|
||||
label="Position"
|
||||
description="New items are appended to the end by default."
|
||||
value={position}
|
||||
onChange={(value) => setPosition(value ?? RULE_ENGINE_POSITION_END)}
|
||||
data={[
|
||||
{ label: "At end (default)", value: RULE_ENGINE_POSITION_END },
|
||||
...positionOptions,
|
||||
]}
|
||||
searchable
|
||||
disabled={positionLoading}
|
||||
size="md"
|
||||
radius="md"
|
||||
styles={inputStyles}
|
||||
/>
|
||||
) : null}
|
||||
{formRows.map((row) =>
|
||||
row.kind === "pair" ? (
|
||||
<SimpleGrid key={`${row.fields[0].name}-${row.fields[1].name}`} cols={2} spacing="md">
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { OnChangeFn, PaginationState } from "@tanstack/react-table";
|
||||
import { Group, Pagination, Select, Text } from "@mantine/core";
|
||||
|
||||
export interface RuleEngineListFooterProps {
|
||||
pagination: PaginationState;
|
||||
pageCount: number;
|
||||
totalCount: number;
|
||||
itemLabel: string;
|
||||
onPaginationChange: OnChangeFn<PaginationState>;
|
||||
}
|
||||
|
||||
const PAGE_SIZE_OPTIONS = ["5", "10", "25", "50"];
|
||||
|
||||
const RuleEngineListFooter = ({
|
||||
pagination,
|
||||
pageCount,
|
||||
totalCount,
|
||||
itemLabel,
|
||||
onPaginationChange,
|
||||
}: RuleEngineListFooterProps) => {
|
||||
const { pageIndex, pageSize } = pagination;
|
||||
const start = totalCount === 0 ? 0 : pageIndex * pageSize + 1;
|
||||
const end = Math.min((pageIndex + 1) * pageSize, totalCount);
|
||||
|
||||
const setPageIndex = (nextIndex: number) => {
|
||||
onPaginationChange({ pageIndex: nextIndex, pageSize });
|
||||
};
|
||||
|
||||
const setPageSize = (nextSize: number) => {
|
||||
onPaginationChange({ pageIndex: 0, pageSize: nextSize });
|
||||
};
|
||||
|
||||
return (
|
||||
<Group
|
||||
justify="space-between"
|
||||
align="center"
|
||||
wrap="wrap"
|
||||
p="md"
|
||||
style={{ borderTop: "1px solid var(--mantine-color-gray-2)" }}
|
||||
>
|
||||
<Group gap="md" align="center">
|
||||
<Group gap="xs" align="center">
|
||||
<Text size="sm" c="dimmed">
|
||||
Rows per page
|
||||
</Text>
|
||||
<Select
|
||||
value={String(pageSize)}
|
||||
onChange={(value) => value && setPageSize(Number(value))}
|
||||
data={PAGE_SIZE_OPTIONS}
|
||||
size="xs"
|
||||
w={70}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
Showing {start}–{end} of {totalCount} {itemLabel}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{pageCount > 1 && (
|
||||
<Pagination
|
||||
value={pageIndex + 1}
|
||||
total={pageCount}
|
||||
size="sm"
|
||||
radius="md"
|
||||
onChange={(page) => setPageIndex(page - 1)}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default RuleEngineListFooter;
|
||||
@@ -0,0 +1,60 @@
|
||||
import { ActionIcon, Group, Tooltip } from "@mantine/core";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
|
||||
import type { RuleEngineOrderConfig } from "@/pages/ruleEngine/config/resources";
|
||||
import type { RuleEngineRecord } from "@/types/rule-engine";
|
||||
|
||||
import { getOrderValue } from "./ruleEngineOrder.utils";
|
||||
|
||||
export interface RuleEngineOrderControlsProps {
|
||||
record: RuleEngineRecord;
|
||||
orderConfig: RuleEngineOrderConfig;
|
||||
totalCount: number;
|
||||
disabled?: boolean;
|
||||
onMove: (id: string, direction: "up" | "down") => void;
|
||||
}
|
||||
|
||||
const RuleEngineOrderControls = ({
|
||||
record,
|
||||
orderConfig,
|
||||
totalCount,
|
||||
disabled = false,
|
||||
onMove,
|
||||
}: RuleEngineOrderControlsProps) => {
|
||||
const id = String(record.id);
|
||||
const order = getOrderValue(record, orderConfig.field);
|
||||
|
||||
const canMoveUp = order > 1;
|
||||
const canMoveDown = orderConfig.scopeField ? true : order < totalCount;
|
||||
|
||||
return (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Tooltip label="Move up">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
disabled={disabled || !canMoveUp}
|
||||
onClick={() => onMove(id, "up")}
|
||||
aria-label="Move up"
|
||||
>
|
||||
<ChevronUp size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Move down">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
disabled={disabled || !canMoveDown}
|
||||
onClick={() => onMove(id, "down")}
|
||||
aria-label="Move down"
|
||||
>
|
||||
<ChevronDown size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default RuleEngineOrderControls;
|
||||
@@ -1,42 +1,50 @@
|
||||
import { LayoutGrid, Plus, Search, Table2 } from "lucide-react";
|
||||
import { LayoutGrid, ListOrdered, Plus, Search, Table2 } from "lucide-react";
|
||||
import { Button, TextInput, Group, SegmentedControl } from "@mantine/core";
|
||||
|
||||
import type { RuleEngineViewMode } from "./useRuleEngineViewMode";
|
||||
|
||||
export interface RuleEngineToolbarProps {
|
||||
search: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
searchPlaceholder: string;
|
||||
search?: string;
|
||||
onSearchChange?: (value: string) => void;
|
||||
searchPlaceholder?: string;
|
||||
showSearch?: boolean;
|
||||
onAdd?: () => void;
|
||||
addLabel?: string;
|
||||
onManageOrder?: () => void;
|
||||
viewMode: RuleEngineViewMode;
|
||||
onViewModeChange: (mode: RuleEngineViewMode) => void;
|
||||
}
|
||||
|
||||
const RuleEngineToolbar = ({
|
||||
search,
|
||||
search = "",
|
||||
onSearchChange,
|
||||
searchPlaceholder,
|
||||
searchPlaceholder = "Search…",
|
||||
showSearch = true,
|
||||
onAdd,
|
||||
addLabel = "Add",
|
||||
onManageOrder,
|
||||
viewMode,
|
||||
onViewModeChange,
|
||||
}: RuleEngineToolbarProps) => (
|
||||
<Group gap="md" justify="space-between" align="center" wrap="nowrap">
|
||||
<TextInput
|
||||
placeholder={searchPlaceholder}
|
||||
value={search}
|
||||
onChange={(e) => onSearchChange(e.currentTarget.value)}
|
||||
leftSection={<Search size={18} />}
|
||||
size="md"
|
||||
radius="lg"
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
styles={{
|
||||
input: {
|
||||
borderColor: "var(--mantine-color-gray-3)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{showSearch && onSearchChange ? (
|
||||
<TextInput
|
||||
placeholder={searchPlaceholder}
|
||||
value={search}
|
||||
onChange={(e) => onSearchChange(e.currentTarget.value)}
|
||||
leftSection={<Search size={18} />}
|
||||
size="md"
|
||||
radius="lg"
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
styles={{
|
||||
input: {
|
||||
borderColor: "var(--mantine-color-gray-3)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ flex: 1 }} />
|
||||
)}
|
||||
|
||||
<Group gap="md" align="center" justify="flex-end" wrap="nowrap">
|
||||
<SegmentedControl
|
||||
@@ -72,6 +80,21 @@ const RuleEngineToolbar = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
{onManageOrder ? (
|
||||
<Button
|
||||
onClick={onManageOrder}
|
||||
leftSection={<ListOrdered size={18} />}
|
||||
size="sm"
|
||||
radius="lg"
|
||||
variant="light"
|
||||
color="gray"
|
||||
fw={600}
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
Manage order
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{onAdd ? (
|
||||
<Button
|
||||
onClick={onAdd}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { RuleEngineRecord, RuleEngineResourceSlug } from "@/types/rule-engine";
|
||||
|
||||
export const RULE_ENGINE_POSITION_END = "__end__";
|
||||
|
||||
export function getOrderItemLabel(
|
||||
record: RuleEngineRecord,
|
||||
slug: RuleEngineResourceSlug,
|
||||
): string {
|
||||
const code = String(record.code ?? "").trim();
|
||||
switch (slug) {
|
||||
case "cargo-types":
|
||||
return String(record.cargoTypeName ?? (code || record.id));
|
||||
case "container-types":
|
||||
case "yards":
|
||||
case "shipping-lines":
|
||||
return String(record.label ?? (code || record.id));
|
||||
case "service-types":
|
||||
return String(record.serviceName ?? (code || record.id));
|
||||
case "approval-rules":
|
||||
return String(record.actionLabel ?? record.requiredRole ?? record.id);
|
||||
default:
|
||||
return String(record.label ?? record.code ?? record.id);
|
||||
}
|
||||
}
|
||||
|
||||
export function getOrderValue(
|
||||
record: RuleEngineRecord,
|
||||
field: "displayOrder" | "stepOrder",
|
||||
): number {
|
||||
const raw = record[field];
|
||||
return typeof raw === "number" ? raw : Number(raw ?? 0);
|
||||
}
|
||||
Reference in New Issue
Block a user