mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +00:00
Merge branch 'freight/develop' of github.com:Tria-plc/edr-platform into freight_feature/profile
This commit is contained in:
@@ -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 ?? "—")}
|
||||
|
||||
Reference in New Issue
Block a user