change price logic on the ,rule engine ui, auto generate the contrat

This commit is contained in:
marshal
2026-06-08 10:04:23 +03:00
parent 4993453993
commit 88df20be6b
76 changed files with 4907 additions and 225 deletions

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from "react";
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";
@@ -8,7 +8,10 @@ import { Card, Button, Modal, Stack, Group, Text, List } from "@mantine/core";
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 { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
@@ -29,6 +32,8 @@ import {
useRateWorkflow,
useRuleEngineList,
useRuleEngineMutations,
useRuleEngineOrderList,
useRuleEngineOrderMutations,
} from "@/hooks/rule-engine/useRuleEngine";
import type { RuleEngineRecord } from "@/types/rule-engine";
import {
@@ -65,6 +70,7 @@ const RuleEngineResourcePage = () => {
const [editing, setEditing] = 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(
config?.slug ?? DEFAULT_CONFIGURATION_SLUG,
);
@@ -81,10 +87,27 @@ const RuleEngineResourcePage = () => {
search: config?.supportsSearch ? search.trim() || undefined : undefined,
page: pagination.pageIndex + 1,
pageSize: pagination.pageSize,
...(config?.orderConfig
? {
sortBy: config.orderConfig.field,
sortOrder: "ASC" as const,
}
: {}),
}),
[config?.supportsSearch, search, pagination.pageIndex, pagination.pageSize],
[
config?.orderConfig,
config?.supportsSearch,
search,
pagination.pageIndex,
pagination.pageSize,
],
);
useEffect(() => {
setPagination((prev) => ({ pageIndex: 0, pageSize: prev.pageSize }));
setSearch("");
}, [config?.slug, setPagination]);
const { data, isLoading, isError, error } = useRuleEngineList(
config?.slug ?? DEFAULT_CONFIGURATION_SLUG,
listParams,
@@ -93,6 +116,14 @@ const RuleEngineResourcePage = () => {
const { create, update, remove } = useRuleEngineMutations(
config?.slug ?? DEFAULT_CONFIGURATION_SLUG,
);
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 { submit, approve } = useRateWorkflow();
const { data: chainData, isLoading: chainLoading } = useApprovalChain(
chainOpen && config?.slug === "approval-rules",
@@ -149,25 +180,24 @@ const RuleEngineResourcePage = () => {
const rows = data?.data ?? [];
const meta = data?.meta;
const pageCount = meta?.totalPages ?? 1;
const totalCount = meta?.total ?? rows.length;
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 { 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;
return createPositionList.data
.filter((row) => row.id)
.map((row) => ({
label: getOrderItemLabel(row, config.slug),
value: String(row.id),
}));
}, [config?.orderConfig, config?.slug, createPositionList?.data]);
const handleApproveRate = useCallback(
(record: RuleEngineRecord) => {
@@ -176,6 +206,13 @@ const RuleEngineResourcePage = () => {
[approve],
);
const handleMoveOrder = useCallback(
(id: string, direction: "up" | "down") => {
moveOrder.mutate({ id, direction });
},
[moveOrder],
);
const columns = useMemo((): ColumnDef<RuleEngineRecord>[] => {
if (!config) return [];
@@ -192,36 +229,47 @@ const RuleEngineResourcePage = () => {
base.push({
id: "actions",
header: "Actions",
size: 140,
minSize: 120,
size: config.orderConfig ? 200 : 140,
minSize: config.orderConfig ? 180 : 120,
meta: {
headerClassName,
cellClassName: `${cellClassName} whitespace-nowrap`,
},
cell: ({ row }) => (
<div onClick={(e) => e.stopPropagation()} data-stop-row-click>
<RuleEngineRecordActions
record={row.original}
config={config}
layout="row"
readOnly={!canManage}
onEdit={(record) => {
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}
/>
<Group gap="xs" wrap="nowrap" justify="flex-end">
{config.orderConfig && canManage ? (
<RuleEngineOrderControls
record={row.original}
orderConfig={config.orderConfig}
totalCount={totalCount}
disabled={moveOrder.isPending}
onMove={handleMoveOrder}
/>
) : null}
<RuleEngineRecordActions
record={row.original}
config={config}
layout="row"
readOnly={!canManage}
onEdit={(record) => {
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}
/>
</Group>
</div>
),
});
return base;
}, [canManage, config, submit, handleApproveRate]);
}, [canManage, config, submit, handleApproveRate, handleMoveOrder, moveOrder.isPending, totalCount]);
const tableStatus = isLoading ? "loading" : isError ? "error" : "success";
@@ -276,13 +324,21 @@ const RuleEngineResourcePage = () => {
<Stack gap="md">
<RuleEngineToolbar
search={search}
onSearchChange={(v) => {
setSearch(v);
setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
}}
onSearchChange={
config.supportsSearch
? (v) => {
setSearch(v);
setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
}
: undefined
}
showSearch={Boolean(config.supportsSearch)}
searchPlaceholder={config.searchPlaceholder}
onAdd={canManage ? openCreate : undefined}
addLabel={`Add ${config.label.replace(/s$/, "")}`}
onManageOrder={
canManage && config.orderConfig ? () => setOrderDialogOpen(true) : undefined
}
viewMode={viewMode}
onViewModeChange={setViewMode}
/>
@@ -290,7 +346,7 @@ const RuleEngineResourcePage = () => {
{viewMode === "table" ? (
<DataTable
columns={columns}
data={filteredRows}
data={rows}
status={tableStatus}
error={
isError
@@ -302,7 +358,12 @@ const RuleEngineResourcePage = () => {
: undefined
}
emptyMessage={`No ${itemLabel} found.`}
pagination={paginationState}
pagination={{
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
pageCount,
totalCount,
}}
tableOptions={{
manualPagination: true,
pageCount,
@@ -328,11 +389,14 @@ const RuleEngineResourcePage = () => {
) : (
<RuleEngineCardGrid
config={config}
rows={filteredRows}
rows={rows}
status={tableStatus}
emptyMessage={`No ${itemLabel} found.`}
itemLabel={itemLabel}
pagination={paginationState}
pagination={pagination}
pageCount={pageCount}
totalCount={totalCount}
onPaginationChange={setPagination}
readOnly={!canManage}
onEdit={canManage ? openEdit : undefined}
onDelete={canManage ? setDeleteTarget : undefined}
@@ -363,9 +427,27 @@ const RuleEngineResourcePage = () => {
(usesContainerTypeField && containerTypeOptionsLoading) ||
(usesLiveRateField && liveRateOptionsLoading)
}
positionOptions={!editing ? createPositionOptions : undefined}
positionLoading={createPositionLoading}
onSubmit={handleFormSubmit}
/>
{config.orderConfig ? (
<ManageRuleEngineOrderDialog
open={orderDialogOpen}
onOpenChange={setOrderDialogOpen}
config={config}
items={orderListData?.data ?? []}
isLoading={orderListLoading}
isSaving={reorder.isPending}
onSave={(payload) => {
reorder.mutate(payload, {
onSuccess: () => setOrderDialogOpen(false),
});
}}
/>
) : null}
<Modal
opened={Boolean(deleteTarget)}
onClose={() => setDeleteTarget(null)}

View File

@@ -36,6 +36,12 @@ export interface FormFieldDef {
placeholder?: string;
}
export interface RuleEngineOrderConfig {
field: "displayOrder" | "stepOrder";
scopeField?: "requiresDirectorApproval";
label: string;
}
export interface RuleEngineResourceConfig {
slug: RuleEngineResourceSlug;
label: string;
@@ -45,6 +51,7 @@ export interface RuleEngineResourceConfig {
columns: ResourceColumn[];
formFields: FormFieldDef[];
supportsSearch?: boolean;
orderConfig?: RuleEngineOrderConfig;
/** Primary line on card view (inferred from columns when omitted). */
cardTitleKey?: string;
/** Secondary line under title on card view (inferred when omitted). */
@@ -130,6 +137,7 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
subtitle: "Manage freight cargo classification and approval rules",
searchPlaceholder: "Search cargo types by name or code...",
supportsSearch: true,
orderConfig: { field: "displayOrder", label: "Display order" },
columns: [
codeColumn("code"),
{ id: "cargoTypeName", header: "Name", accessorKey: "cargoTypeName" },
@@ -154,7 +162,6 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
{ name: "showFreeTextBox", label: "Show free text box", type: "boolean" },
{ name: "requiresDirectorApproval", label: "Requires director approval", type: "boolean" },
{ name: "isActive", label: "Active", type: "boolean" },
{ name: "displayOrder", label: "Display order", type: "number" },
],
},
{
@@ -163,9 +170,11 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
category: "configuration",
subtitle: "Configure container sizes and wagon capacity",
searchPlaceholder: "Search container types...",
orderConfig: { field: "displayOrder", label: "Display order" },
columns: [
codeColumn("code"),
{ id: "label", header: "Label", accessorKey: "label" },
{ id: "displayOrder", header: "#", accessorKey: "displayOrder", format: "number" },
{ id: "sizeFt", header: "Size (ft)", accessorKey: "sizeFt", format: "number" },
{ id: "wagonsPerUnit", header: "Wagons / unit", accessorKey: "wagonsPerUnit", format: "number" },
activeColumn,
@@ -177,7 +186,6 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
{ name: "isReefer", label: "Reefer", type: "boolean" },
{ name: "isOpenTop", label: "Open top", type: "boolean" },
{ name: "isActive", label: "Active", type: "boolean" },
{ name: "displayOrder", label: "Display order", type: "number" },
],
},
{
@@ -254,9 +262,11 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
subtitle: "Freight service offerings and booking options",
searchPlaceholder: "Search service types...",
supportsSearch: true,
orderConfig: { field: "displayOrder", label: "Display order" },
columns: [
codeColumn("code"),
{ id: "serviceName", header: "Service name", accessorKey: "serviceName" },
{ id: "displayOrder", header: "#", accessorKey: "displayOrder", format: "number" },
{ id: "priorityBonusPoints", header: "Bonus pts", accessorKey: "priorityBonusPoints", format: "number" },
activeColumn,
],
@@ -269,7 +279,6 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
{ name: "includesCustoms", label: "Includes customs", type: "boolean" },
{ name: "priorityBonusPoints", label: "Priority bonus points", type: "number" },
{ name: "isActive", label: "Active", type: "boolean" },
{ name: "displayOrder", label: "Display order", type: "number" },
],
},
{
@@ -350,6 +359,7 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
category: "configuration",
subtitle: "Terminal and yard locations",
searchPlaceholder: "Search yards...",
orderConfig: { field: "displayOrder", label: "Display order" },
columns: [
codeColumn("code"),
{ id: "label", header: "Label", accessorKey: "label" },
@@ -361,7 +371,6 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
{ name: "label", label: "Label", type: "text", required: true },
{ name: "country", label: "Country", type: "text", required: true },
{ name: "isActive", label: "Active", type: "boolean" },
{ name: "displayOrder", label: "Display order", type: "number" },
],
},
{
@@ -436,6 +445,11 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
cardSubtitleKey: "requiredRole",
subtitle: "Multi-step booking approval chain",
searchPlaceholder: "Search approval rules...",
orderConfig: {
field: "stepOrder",
scopeField: "requiresDirectorApproval",
label: "Step order",
},
columns: [
{
id: "requiresDirectorApproval",
@@ -450,7 +464,6 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
],
formFields: [
{ name: "requiresDirectorApproval", label: "Requires director approval chain", type: "boolean" },
{ name: "stepOrder", label: "Step order", type: "number", required: true },
{
name: "requiredRole",
label: "Required role",