fix rate edit

This commit is contained in:
Marshal
2026-07-17 10:15:26 +00:00
parent 18311f22f7
commit 801872c106
20 changed files with 1227 additions and 112 deletions

View File

@@ -1,5 +1,8 @@
import { useAuth } from "@/auth/useAuth";
import { canAccessRuleEngineResource } from "@/lib/permissions";
import {
canAccessRuleEngineResource,
canApproveRuleEngineChange,
} from "@/lib/permissions";
import type { ColumnDef } from "@edr/ui-common";
import {
Box,
@@ -11,14 +14,16 @@ import {
Modal,
Stack,
Text,
Tooltip,
} from "@mantine/core";
import { Plus } from "lucide-react";
import { Clock, 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 PriorityRuleApprovalsSection from "@/pages/ruleEngine/PriorityRuleApprovalsSection";
import RateApprovalsSection from "@/pages/ruleEngine/RateApprovalsSection";
import { nextPriorityRangeStart } from "@/pages/ruleEngine/priorityRuleRange";
import RuleEngineCardGrid from "@/components/ruleEngine/RuleEngineCardGrid";
import RuleEngineFormDialog from "@/components/ruleEngine/RuleEngineFormDialog";
@@ -37,6 +42,7 @@ import {
useLiveRateOptions,
useWagonTypeOptions,
usePriorityRuleWorkflow,
useRateChangeWorkflow,
useRateWorkflow,
useRuleEngineList,
useRuleEngineMutations,
@@ -51,6 +57,7 @@ import {
getRuleEngineResource,
type RuleEngineNavCategory,
} from "@/pages/ruleEngine/config/resources";
import type { RateChangeRequest } from "@/services/ruleEngine/ruleEngine.service";
import type { RuleEngineRecord } from "@/types/rule-engine";
import {
DataTable,
@@ -145,6 +152,23 @@ const RuleEngineResourcePage = () => {
chainOpen && config?.slug === "approval-rules",
);
// A LIVE rate is what pricing charges, so editing one files a change request
// instead of mutating: the rate keeps its current value until an approver
// applies the change. DRAFT rates still edit directly.
const isRates = config?.slug === "rates";
const [rateError, setRateError] = useState<string | null>(null);
const rateChangeWorkflow = useRateChangeWorkflow(
Boolean(isRates && canView),
setRateError,
);
const canApproveRates = Boolean(isRates && canApproveRuleEngineChange(user, "rates"));
/** rateId → its pending change, for the row badge. */
const pendingByRateId = useMemo(() => {
const map = new Map<string, RateChangeRequest>();
for (const r of rateChangeWorkflow.pending.data ?? []) map.set(r.rateId, r);
return map;
}, [rateChangeWorkflow.pending.data]);
// Priority rules never mutate directly: changes are filed for approval and a
// pending queue renders above the table. Validation errors (range collision,
// gap, ceiling) surface in a modal so the text is impossible to miss.
@@ -306,7 +330,27 @@ const RuleEngineResourcePage = () => {
id: col.id,
header: col.header,
meta: { headerClassName, cellClassName },
cell: ({ row }) => formatCell(row.original[col.accessorKey], col.format),
cell: ({ row }) => {
const cell = formatCell(row.original[col.accessorKey], col.format);
// On the rate column, show the proposed value under the live one — the
// live value stays the headline because it is what still gets charged.
if (!isRates || col.accessorKey !== "rateValue") return cell;
const change = pendingByRateId.get(String(row.original.id));
if (!change || change.payload.rateValue === undefined) return cell;
return (
<Stack gap={0}>
{cell}
<Tooltip label="Awaiting approval — this rate still charges its current value">
<Group gap={4} wrap="nowrap">
<Clock size={11} color="var(--mantine-color-orange-6)" />
<Text size="xs" c="orange.7" fw={600}>
{Number(change.payload.rateValue).toLocaleString()} pending
</Text>
</Group>
</Tooltip>
</Stack>
);
},
}));
base.push({
@@ -357,6 +401,8 @@ const RuleEngineResourcePage = () => {
}, [
canManage,
config,
isRates,
pendingByRateId,
submit,
handleApproveRate,
handleMoveOrder,
@@ -400,6 +446,21 @@ const RuleEngineResourcePage = () => {
currency: "USD",
trigger: isSurcharge ? values.trigger : "ALWAYS",
};
// Editing a LIVE rate files a change request — the rate keeps charging
// its current value until an approver applies it. DRAFT rates fall
// through to the normal update below.
if (editing?.id && editing.status === "LIVE") {
rateChangeWorkflow.submit.mutate(
{ rateId: String(editing.id), update: payload },
{
onSuccess: () => {
setFormOpen(false);
setEditing(null);
},
},
);
return;
}
} else if (isPriorityRules) {
// Label is required by the backend but hidden in the UI for now.
payload = { ...values, label: String(Date.now()) };
@@ -483,6 +544,31 @@ const RuleEngineResourcePage = () => {
/>
) : null}
{isRates ? (
<RateApprovalsSection
requests={rateChangeWorkflow.pending.data ?? []}
canDecide={canApproveRates}
approve={rateChangeWorkflow.approve}
reject={rateChangeWorkflow.reject}
/>
) : null}
<Modal
opened={rateError != null}
onClose={() => setRateError(null)}
title="Cannot save rate change"
centered
>
<Text size="sm" c="red">
{rateError}
</Text>
<Group justify="flex-end" mt="md">
<Button variant="light" onClick={() => setRateError(null)}>
Close
</Button>
</Group>
</Modal>
<Modal
opened={priorityError != null}
onClose={() => setPriorityError(null)}