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;
}) => (
{index + 1}
{item.label}
{item.code ? (
{item.code}
) : null}
);
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([]);
const [directorItems, setDirectorItems] = useState([]);
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[]) => (
{(provided) => (
{listItems.length === 0 ? (
No items to reorder.
) : (
listItems.map((item, index) => (
{(dragProvided, snapshot) => (
)}
))
)}
{provided.placeholder}
)}
);
return (
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",
},
}}
>
Drag items anywhere in the list to set display order. Changes apply when you save.
{isLoading ? (
) : isScoped ? (
setTab((value as "standard" | "director") ?? "standard")}
>
Standard chain ({standardItems.length})
Director chain ({directorItems.length})
setFilter(e.currentTarget.value)}
/>
{renderList(filteredItems)}
setFilter(e.currentTarget.value)}
/>
{renderList(filteredItems)}
) : (
<>
setFilter(e.currentTarget.value)}
/>
{renderList(filteredItems)}
>
)}
{filter.trim() ? (
Clear the filter to drag and reorder items.
) : null}
) : undefined
}
>
{isSaving ? "Saving…" : "Save order"}
);
};
export default ManageRuleEngineOrderDialog;