Merge branch 'freight/develop' of github.com:Tria-plc/edr-platform into freight_feature/profile

This commit is contained in:
marshal
2026-06-24 01:22:31 +03:00
53 changed files with 1797 additions and 2638 deletions

View File

@@ -1,48 +1,56 @@
import { useCallback, useEffect, 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 { Box, Card, Button, Modal, Stack, Group, Text, List, Loader } from "@mantine/core";
import type { ColumnDef } from "@edr/ui-common";
import {
Box,
Button,
Card,
Group,
List,
Loader,
Modal,
Stack,
Text,
} from "@mantine/core";
import { Plus } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Navigate, useLocation, useParams } from "react-router-dom";
import { PageContainer, PageHeader } from "@/components/page";
import ManageRuleEngineOrderDialog from "@/components/ruleEngine/ManageRuleEngineOrderDialog";
import RuleEngineCardGrid from "@/components/ruleEngine/RuleEngineCardGrid";
import RuleEngineFormDialog from "@/components/ruleEngine/RuleEngineFormDialog";
import ManageRuleEngineOrderDialog from "@/components/ruleEngine/ManageRuleEngineOrderDialog";
import RuleEngineOrderControls from "@/components/ruleEngine/RuleEngineOrderControls";
import RuleEngineRecordActions from "@/components/ruleEngine/RuleEngineRecordActions";
import { getOrderItemLabel } from "@/components/ruleEngine/ruleEngineOrder.utils";
import RuleEngineToolbar from "@/components/ruleEngine/RuleEngineToolbar";
import { formatCell } from "@/components/ruleEngine/ruleEngineFormat";
import { getOrderItemLabel } from "@/components/ruleEngine/ruleEngineOrder.utils";
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,
useRuleEngineOrderList,
useRuleEngineOrderMutations,
useApprovalChain,
useCargoTypeParentOptions,
useContainerTypeOptions,
useLiveRateOptions,
useRateWorkflow,
useRuleEngineList,
useRuleEngineMutations,
useRuleEngineOrderList,
useRuleEngineOrderMutations,
} from "@/hooks/rule-engine/useRuleEngine";
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 type { RuleEngineRecord } from "@/types/rule-engine";
import {
DataTable,
DataTableFooter,
getCoreRowModel,
usePagination,
useReactTable,
DataTable,
DataTableFooter,
usePagination,
} from "@edr/ui-common";
const pathCategory = (pathname: string): RuleEngineNavCategory | undefined => {
@@ -60,16 +68,17 @@ const RuleEngineResourcePage = () => {
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[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<RuleEngineRecord | null>(null);
const [deleteTarget, setDeleteTarget] = useState<RuleEngineRecord | null>(null);
const [deleteTarget, setDeleteTarget] = useState<RuleEngineRecord | null>(
null,
);
const [chainOpen, setChainOpen] = useState(false);
const [orderDialogOpen, setOrderDialogOpen] = useState(false);
const { viewMode, setViewMode } = useRuleEngineViewMode(
@@ -90,9 +99,9 @@ const RuleEngineResourcePage = () => {
pageSize: pagination.pageSize,
...(config?.orderConfig
? {
sortBy: config.orderConfig.field,
sortOrder: "ASC" as const,
}
sortBy: config.orderConfig.field,
sortOrder: "ASC" as const,
}
: {}),
}),
[
@@ -120,11 +129,12 @@ const RuleEngineResourcePage = () => {
const { reorder, moveOrder } = useRuleEngineOrderMutations(
config?.slug ?? DEFAULT_CONFIGURATION_SLUG,
);
const { data: orderListData, isLoading: orderListLoading } = useRuleEngineOrderList(
config?.slug ?? DEFAULT_CONFIGURATION_SLUG,
Boolean(orderDialogOpen && config?.orderConfig),
config?.orderConfig?.field,
);
const { data: orderListData, isLoading: orderListLoading } =
useRuleEngineOrderList(
config?.slug ?? DEFAULT_CONFIGURATION_SLUG,
Boolean(orderDialogOpen && config?.orderConfig),
config?.orderConfig?.field,
);
const { submit, approve } = useRateWorkflow();
const { data: chainData, isLoading: chainLoading } = useApprovalChain(
chainOpen && config?.slug === "approval-rules",
@@ -141,10 +151,7 @@ const RuleEngineResourcePage = () => {
const { data: cargoParentOptions, isLoading: cargoParentOptionsLoading } =
useCargoTypeParentOptions(editingId, config?.slug === "cargo-types");
const { data: containerTypeOptions, isLoading: containerTypeOptionsLoading } =
useContainerTypeOptions(
config?.slug === "rates",
usesContainerTypeField,
);
useContainerTypeOptions(config?.slug === "rates", usesContainerTypeField);
const { data: liveRateOptions, isLoading: liveRateOptionsLoading } =
useLiveRateOptions(usesLiveRateField);
@@ -154,10 +161,9 @@ const RuleEngineResourcePage = () => {
if (config.slug === "cargo-types" && field.name === "parentGroupId") {
return {
...field,
options:
cargoParentOptions ?? [
{ label: "None", value: RULE_ENGINE_SELECT_NONE },
],
options: cargoParentOptions ?? [
{ label: "None", value: RULE_ENGINE_SELECT_NONE },
],
};
}
if (field.name === "containerTypeId") {
@@ -183,14 +189,16 @@ const RuleEngineResourcePage = () => {
const pageCount = meta?.totalPages ?? 1;
const totalCount = meta?.total ?? rows.length;
const { data: createPositionList, isLoading: createPositionLoading } = useRuleEngineOrderList(
config?.slug ?? DEFAULT_CONFIGURATION_SLUG,
Boolean(formOpen && !editing && config?.orderConfig),
config?.orderConfig?.field,
);
const { data: createPositionList, isLoading: createPositionLoading } =
useRuleEngineOrderList(
config?.slug ?? DEFAULT_CONFIGURATION_SLUG,
Boolean(formOpen && !editing && config?.orderConfig),
config?.orderConfig?.field,
);
const createPositionOptions = useMemo(() => {
if (!config?.orderConfig || !createPositionList?.data?.length) return undefined;
if (!config?.orderConfig || !createPositionList?.data?.length)
return undefined;
return createPositionList.data
.filter((row) => row.id)
.map((row) => ({
@@ -199,7 +207,6 @@ const RuleEngineResourcePage = () => {
}));
}, [config?.orderConfig, config?.slug, createPositionList?.data]);
const handleApproveRate = useCallback(
(record: RuleEngineRecord) => {
approve.mutate(String(record.id));
@@ -259,7 +266,9 @@ const RuleEngineResourcePage = () => {
}}
onDelete={setDeleteTarget}
onViewChain={
config.slug === "approval-rules" ? () => setChainOpen(true) : undefined
config.slug === "approval-rules"
? () => setChainOpen(true)
: undefined
}
onSubmitRate={canManage ? (id) => submit.mutate(id) : undefined}
onApproveRate={canManage ? handleApproveRate : undefined}
@@ -270,7 +279,15 @@ const RuleEngineResourcePage = () => {
});
return base;
}, [canManage, config, submit, handleApproveRate, handleMoveOrder, moveOrder.isPending, totalCount]);
}, [
canManage,
config,
submit,
handleApproveRate,
handleMoveOrder,
moveOrder.isPending,
totalCount,
]);
const tableStatus = isLoading ? "loading" : isError ? "error" : "success";
@@ -350,15 +367,20 @@ const RuleEngineResourcePage = () => {
onSearchChange={
config.supportsSearch
? (v) => {
setSearch(v);
setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
}
setSearch(v);
setPagination({
pageIndex: 0,
pageSize: pagination.pageSize,
});
}
: undefined
}
showSearch={Boolean(config.supportsSearch)}
searchPlaceholder={config.searchPlaceholder}
onManageOrder={
canManage && config.orderConfig ? () => setOrderDialogOpen(true) : undefined
canManage && config.orderConfig
? () => setOrderDialogOpen(true)
: undefined
}
viewMode={viewMode}
onViewModeChange={setViewMode}
@@ -373,10 +395,12 @@ const RuleEngineResourcePage = () => {
error={
isError
? {
message: "Failed to load data",
description:
error instanceof Error ? error.message : "Unknown error",
}
message: "Failed to load data",
description:
error instanceof Error
? error.message
: "Unknown error",
}
: undefined
}
emptyMessage={`No ${itemLabel} found.`}
@@ -422,7 +446,9 @@ const RuleEngineResourcePage = () => {
onEdit={canManage ? openEdit : undefined}
onDelete={canManage ? setDeleteTarget : undefined}
onViewChain={
config.slug === "approval-rules" ? () => setChainOpen(true) : undefined
config.slug === "approval-rules"
? () => setChainOpen(true)
: undefined
}
onSubmitRate={canManage ? (id) => submit.mutate(id) : undefined}
onApproveRate={canManage ? handleApproveRate : undefined}
@@ -434,7 +460,11 @@ const RuleEngineResourcePage = () => {
<RuleEngineFormDialog
open={formOpen}
onOpenChange={setFormOpen}
title={editing ? `Edit ${config.label.replace(/s$/, "")}` : `Add ${config.label.replace(/s$/, "")}`}
title={
editing
? `Edit ${config.label.replace(/s$/, "")}`
: `Add ${config.label.replace(/s$/, "")}`
}
description={
editing
? `Update this ${config.label.toLowerCase()} record.`
@@ -478,7 +508,8 @@ const RuleEngineResourcePage = () => {
>
<Stack gap="md">
<Text size="sm">
This will soft-delete the selected {config.label.toLowerCase()} record.
This will soft-delete the selected {config.label.toLowerCase()}{" "}
record.
</Text>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={() => setDeleteTarget(null)}>
@@ -515,14 +546,17 @@ const RuleEngineResourcePage = () => {
) : (
<>
{(chainData ?? []).length === 0 ? (
<Text size="sm" c="dimmed">No approval rules configured.</Text>
<Text size="sm" c="dimmed">
No approval rules configured.
</Text>
) : (
<List spacing="md">
{(chainData ?? []).map((step, index) => (
<List.Item key={String(step.id ?? index)}>
<Stack gap="xs">
<Text size="sm" fw={500}>
Step {String(step.stepOrder ?? index + 1)}: {String(step.actionLabel ?? "")}
Step {String(step.stepOrder ?? index + 1)}:{" "}
{String(step.actionLabel ?? "")}
</Text>
<Text size="sm" c="dimmed">
Role: {String(step.requiredRole ?? "—")}