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} ); }; export default ManageRuleEngineOrderDialog;