import { useCallback, useMemo, useState } from "react"; import { Navigate, useLocation, useParams } from "react-router-dom"; import { useAuth } from "@/auth/useAuth"; import { canAccessRuleEngineResource } from "@/lib/permissions"; import type { ColumnDef } from "@tanstack/react-table"; import { Loader2 } from "lucide-react"; import { Card, Button, Modal, Stack, Group, Text, List } from "@mantine/core"; import RuleEngineCardGrid from "@/components/ruleEngine/RuleEngineCardGrid"; import RuleEngineFormDialog from "@/components/ruleEngine/RuleEngineFormDialog"; import RuleEngineRecordActions from "@/components/ruleEngine/RuleEngineRecordActions"; import RuleEngineToolbar from "@/components/ruleEngine/RuleEngineToolbar"; import { formatCell } from "@/components/ruleEngine/ruleEngineFormat"; import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; import { useRuleEngineViewMode } from "@/components/ruleEngine/useRuleEngineViewMode"; import { DEFAULT_CONFIGURATION_SLUG, DEFAULT_RULES_SLUG, RULE_ENGINE_CATEGORY_BASE_PATH, RULE_ENGINE_SELECT_NONE, getRuleEngineResource, type RuleEngineNavCategory, } from "@/pages/ruleEngine/config/resources"; import { useApprovalChain, useCargoTypeParentOptions, useContainerTypeOptions, useLiveRateOptions, useRateWorkflow, useRuleEngineList, useRuleEngineMutations, } from "@/hooks/rule-engine/useRuleEngine"; import type { RuleEngineRecord } from "@/types/rule-engine"; import { DataTable, DataTableFooter, getCoreRowModel, usePagination, useReactTable, } from "@edr/ui-common"; const pathCategory = (pathname: string): RuleEngineNavCategory | undefined => { const normalized = pathname.toLowerCase(); if (normalized.startsWith("/dashboard/configuration")) return "configuration"; if (normalized.startsWith("/dashboard/rules")) return "rules"; return undefined; }; const RuleEngineResourcePage = () => { const { user } = useAuth(); const { resource: resourceSlug } = useParams<{ resource: string }>(); const location = useLocation(); const category = pathCategory(location.pathname); const config = resourceSlug ? getRuleEngineResource(resourceSlug) : undefined; const defaultPath = category ? `${RULE_ENGINE_CATEGORY_BASE_PATH[category]}/${ category === "rules" ? DEFAULT_RULES_SLUG : DEFAULT_CONFIGURATION_SLUG }` : `${RULE_ENGINE_CATEGORY_BASE_PATH.configuration}/${DEFAULT_CONFIGURATION_SLUG}`; const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [search, setSearch] = useState(""); const [formOpen, setFormOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [chainOpen, setChainOpen] = useState(false); const { viewMode, setViewMode } = useRuleEngineViewMode( config?.slug ?? DEFAULT_CONFIGURATION_SLUG, ); const canView = Boolean( config && canAccessRuleEngineResource(user, config.slug, "view"), ); const canManage = Boolean( config && canAccessRuleEngineResource(user, config.slug, "manage"), ); const listParams = useMemo( () => ({ search: config?.supportsSearch ? search.trim() || undefined : undefined, page: pagination.pageIndex + 1, pageSize: pagination.pageSize, }), [config?.supportsSearch, search, pagination.pageIndex, pagination.pageSize], ); const { data, isLoading, isError, error } = useRuleEngineList( config?.slug ?? DEFAULT_CONFIGURATION_SLUG, listParams, ); const { create, update, remove } = useRuleEngineMutations( config?.slug ?? DEFAULT_CONFIGURATION_SLUG, ); const { submit, approve } = useRateWorkflow(); const { data: chainData, isLoading: chainLoading } = useApprovalChain( chainOpen && config?.slug === "approval-rules", ); const editingId = editing?.id ? String(editing.id) : undefined; const usesContainerTypeField = Boolean( config?.formFields.some((f) => f.name === "containerTypeId"), ); const usesLiveRateField = Boolean( config?.formFields.some((f) => f.name === "rateId"), ); const { data: cargoParentOptions, isLoading: cargoParentOptionsLoading } = useCargoTypeParentOptions(editingId, config?.slug === "cargo-types"); const { data: containerTypeOptions, isLoading: containerTypeOptionsLoading } = useContainerTypeOptions( config?.slug === "rates", usesContainerTypeField, ); const { data: liveRateOptions, isLoading: liveRateOptionsLoading } = useLiveRateOptions(usesLiveRateField); const formFields = useMemo(() => { if (!config) return []; return config.formFields.map((field) => { if (config.slug === "cargo-types" && field.name === "parentGroupId") { return { ...field, options: cargoParentOptions ?? [ { label: "None", value: RULE_ENGINE_SELECT_NONE }, ], }; } if (field.name === "containerTypeId") { return { ...field, type: "select" as const, options: containerTypeOptions ?? [], }; } if (field.name === "rateId") { return { ...field, type: "select" as const, options: liveRateOptions ?? [], }; } return field; }); }, [config, cargoParentOptions, containerTypeOptions, liveRateOptions]); const rows = data?.data ?? []; const meta = data?.meta; const pageCount = meta?.totalPages ?? 1; const filteredRows = useMemo(() => { if (config?.supportsSearch || !search.trim()) return rows; const q = search.trim().toLowerCase(); return rows.filter((row) => JSON.stringify(row).toLowerCase().includes(q), ); }, [rows, search, config?.supportsSearch]); const paginationState = useMemo( () => ({ pageIndex: pagination.pageIndex, pageSize: pagination.pageSize, pageCount, totalCount: meta?.total ?? filteredRows.length, }), [filteredRows.length, meta?.total, pageCount, pagination.pageIndex, pagination.pageSize], ); const handleApproveRate = useCallback( (record: RuleEngineRecord) => { approve.mutate(String(record.id)); }, [approve], ); const columns = useMemo((): ColumnDef[] => { if (!config) return []; const headerClassName = ruleEngineTable.headerCell; const cellClassName = ruleEngineTable.bodyCell; const base: ColumnDef[] = config.columns.map((col) => ({ id: col.id, header: col.header, meta: { headerClassName, cellClassName }, cell: ({ row }) => formatCell(row.original[col.accessorKey], col.format), })); base.push({ id: "actions", header: "Actions", size: 140, minSize: 120, meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap`, }, cell: ({ row }) => (
e.stopPropagation()} data-stop-row-click> { setEditing(record); setFormOpen(true); }} onDelete={setDeleteTarget} onViewChain={ config.slug === "approval-rules" ? () => setChainOpen(true) : undefined } onSubmitRate={canManage ? (id) => submit.mutate(id) : undefined} onApproveRate={canManage ? handleApproveRate : undefined} />
), }); return base; }, [canManage, config, submit, handleApproveRate]); const tableStatus = isLoading ? "loading" : isError ? "error" : "success"; if (!resourceSlug || !category) { return ; } if (!config || config.category !== category) { return ; } if (!canView) { return ; } const openCreate = () => { setEditing(null); setFormOpen(true); }; const openEdit = (record: RuleEngineRecord) => { setEditing(record); setFormOpen(true); }; const handleFormSubmit = (values: Record) => { if (editing?.id) { update.mutate( { id: editing.id, payload: values }, { onSuccess: () => { setFormOpen(false); setEditing(null); }, }, ); } else { create.mutate(values, { onSuccess: () => { setFormOpen(false); setEditing(null); }, }); } }; const itemLabel = config.label.toLowerCase(); return ( { setSearch(v); setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); }} searchPlaceholder={config.searchPlaceholder} onAdd={canManage ? openCreate : undefined} addLabel={`Add ${config.label.replace(/s$/, "")}`} viewMode={viewMode} onViewModeChange={setViewMode} /> {viewMode === "table" ? ( ( )} /> ) : ( setChainOpen(true) : undefined } onSubmitRate={canManage ? (id) => submit.mutate(id) : undefined} onApproveRate={canManage ? handleApproveRate : undefined} /> )} setDeleteTarget(null)} title="Delete record?" centered size="sm" > This will soft-delete the selected {config.label.toLowerCase()} record. setChainOpen(false)} title="Approval chain" centered size="md" > {chainLoading ? ( ) : ( <> {(chainData ?? []).length === 0 ? ( No approval rules configured. ) : ( {(chainData ?? []).map((step, index) => ( Step {String(step.stepOrder ?? index + 1)}: {String(step.actionLabel ?? "")} Role: {String(step.requiredRole ?? "—")} ))} )} )} ); }; export default RuleEngineResourcePage;