mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 05:25:41 +00:00
changes
This commit is contained in:
@@ -19,6 +19,7 @@ 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 { nextPriorityRangeStart } from "@/pages/ruleEngine/priorityRuleRange";
|
||||
import RuleEngineCardGrid from "@/components/ruleEngine/RuleEngineCardGrid";
|
||||
import RuleEngineFormDialog from "@/components/ruleEngine/RuleEngineFormDialog";
|
||||
import RuleEngineOrderControls from "@/components/ruleEngine/RuleEngineOrderControls";
|
||||
@@ -145,10 +146,13 @@ const RuleEngineResourcePage = () => {
|
||||
);
|
||||
|
||||
// Priority rules never mutate directly: changes are filed for approval and a
|
||||
// pending queue renders above the table.
|
||||
// pending queue renders above the table. Validation errors (range collision,
|
||||
// gap, ceiling) surface in a modal so the text is impossible to miss.
|
||||
const isPriorityRules = config?.slug === "priority-configs";
|
||||
const [priorityError, setPriorityError] = useState<string | null>(null);
|
||||
const priorityWorkflow = usePriorityRuleWorkflow(
|
||||
Boolean(isPriorityRules && canView),
|
||||
setPriorityError,
|
||||
);
|
||||
|
||||
const editingId = editing?.id ? String(editing.id) : undefined;
|
||||
@@ -178,9 +182,36 @@ const RuleEngineResourcePage = () => {
|
||||
const { data: wagonTypeOptions, isLoading: wagonTypeOptionsLoading } =
|
||||
useWagonTypeOptions(usesWagonTypeField);
|
||||
|
||||
// Full rule list backing the auto-filled "min wagon count": the next range
|
||||
// always continues the chain for the selected type (per currency), so the
|
||||
// form needs every existing rule, not the current page.
|
||||
const { data: allPriorityRules } = useRuleEngineOrderList(
|
||||
config?.slug ?? DEFAULT_CONFIGURATION_SLUG,
|
||||
Boolean(isPriorityRules && formOpen),
|
||||
config?.orderConfig?.field,
|
||||
);
|
||||
|
||||
const formFields = useMemo(() => {
|
||||
if (!config) return [];
|
||||
return config.formFields.map((field) => {
|
||||
if (isPriorityRules && field.name === "minWagonCount") {
|
||||
return {
|
||||
...field,
|
||||
// Editing keeps the rule's own start (a lower gap never forces it to
|
||||
// move); creating always continues the chain / fills the lowest gap.
|
||||
computeValue: (values: Record<string, unknown>) =>
|
||||
editing?.minWagonCount != null
|
||||
? Number(editing.minWagonCount)
|
||||
: nextPriorityRangeStart(
|
||||
allPriorityRules ?? [],
|
||||
String(values.type ?? ""),
|
||||
!values.currency || values.currency === RULE_ENGINE_SELECT_NONE
|
||||
? null
|
||||
: String(values.currency),
|
||||
editingId,
|
||||
),
|
||||
};
|
||||
}
|
||||
if (config.slug === "cargo-types" && field.name === "parentGroupId") {
|
||||
return {
|
||||
...field,
|
||||
@@ -226,7 +257,7 @@ const RuleEngineResourcePage = () => {
|
||||
}
|
||||
return field;
|
||||
});
|
||||
}, [config, cargoParentOptions, cargoLeafOptions, containerTypeOptions, liveRateOptions, wagonTypeOptions]);
|
||||
}, [config, cargoParentOptions, cargoLeafOptions, containerTypeOptions, liveRateOptions, wagonTypeOptions, isPriorityRules, allPriorityRules, editing, editingId]);
|
||||
|
||||
const rows = data?.items ?? [];
|
||||
const meta = data?.meta;
|
||||
@@ -452,6 +483,25 @@ const RuleEngineResourcePage = () => {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Modal
|
||||
opened={priorityError != null}
|
||||
onClose={() => setPriorityError(null)}
|
||||
title="Cannot save priority rule"
|
||||
centered
|
||||
size="md"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="red.8">
|
||||
{priorityError}
|
||||
</Text>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={() => setPriorityError(null)}>
|
||||
OK
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
<Card p={0}>
|
||||
<Stack gap={0}>
|
||||
<Box px="md" pt="md" pb="sm" w="100%">
|
||||
|
||||
@@ -61,6 +61,13 @@ export interface FormFieldDef {
|
||||
* relation list (`wagonTypeIds` read from `record.wagonTypes`).
|
||||
*/
|
||||
getInitialValue?: (record: Record<string, unknown>) => unknown;
|
||||
/**
|
||||
* Fully derived field: its value is computed from the live form values on
|
||||
* every render and the input is locked. Used for the priority-rule min
|
||||
* wagon count, which always continues the previous range for the selected
|
||||
* type. Return null/undefined to leave the field empty (e.g. chain full).
|
||||
*/
|
||||
computeValue?: (values: Record<string, unknown>) => number | string | null;
|
||||
}
|
||||
|
||||
export interface RuleEngineOrderConfig {
|
||||
@@ -356,8 +363,21 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
placeholder: "Select a currency",
|
||||
hideWhen: { field: "type", equals: ["WAGON", "CUSTOMS"] },
|
||||
},
|
||||
{ name: "minWagonCount", label: "Min wagon count", type: "number", required: true },
|
||||
{ name: "maxWagonCount", label: "Max wagon count", type: "number", required: true },
|
||||
{
|
||||
name: "minWagonCount",
|
||||
label: "Min wagon count",
|
||||
type: "number",
|
||||
required: true,
|
||||
disabled: true,
|
||||
description: "Auto-filled — continues the previous range for the selected type",
|
||||
},
|
||||
{
|
||||
name: "maxWagonCount",
|
||||
label: "Max wagon count",
|
||||
type: "number",
|
||||
required: true,
|
||||
description: "Ceiling per type: WAGON 50 · CURRENCY 35 · CUSTOMS 15",
|
||||
},
|
||||
{ name: "scorePoints", label: "Score points", type: "number", required: true },
|
||||
{ name: "isActive", label: "Active", type: "boolean" },
|
||||
],
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Client mirror of the backend's contiguous-range rules for priority configs
|
||||
* (see PriorityConfigsService.assertNoRangeCollision): ranges per type — per
|
||||
* currency for CURRENCY — run 1..cap with no gaps and no overlaps, so the next
|
||||
* range always starts at the lowest uncovered wagon count. The backend
|
||||
* re-validates on submit AND on approval; this only drives the form prefill.
|
||||
*/
|
||||
|
||||
export type PriorityRuleType = "WAGON" | "CURRENCY" | "CUSTOMS";
|
||||
|
||||
/** Hard ceiling of each type's chain — keep in sync with the API's RANGE_CAPS. */
|
||||
export const PRIORITY_RANGE_CAPS: Record<PriorityRuleType, number> = {
|
||||
WAGON: 50,
|
||||
CURRENCY: 35,
|
||||
CUSTOMS: 15,
|
||||
};
|
||||
|
||||
export interface PriorityRangeRule {
|
||||
id?: unknown;
|
||||
type?: unknown;
|
||||
currency?: unknown;
|
||||
minWagonCount?: unknown;
|
||||
maxWagonCount?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the next range for `type` (+`currency`) must start, excluding
|
||||
* `excludeId` (the rule being edited). Null when the chain already covers
|
||||
* 1..cap — no further rule fits.
|
||||
*/
|
||||
export function nextPriorityRangeStart(
|
||||
rules: PriorityRangeRule[],
|
||||
type: string,
|
||||
currency: string | null | undefined,
|
||||
excludeId?: string,
|
||||
): number | null {
|
||||
const cap = PRIORITY_RANGE_CAPS[type as PriorityRuleType];
|
||||
if (!cap) return null;
|
||||
|
||||
const scoped = rules
|
||||
.filter(
|
||||
(r) =>
|
||||
String(r.type ?? "") === type &&
|
||||
(excludeId === undefined || String(r.id ?? "") !== excludeId) &&
|
||||
(type !== "CURRENCY" ||
|
||||
String(r.currency ?? "") === String(currency ?? "")),
|
||||
)
|
||||
.map((r) => ({
|
||||
min: Number(r.minWagonCount ?? 0),
|
||||
max: Number(r.maxWagonCount ?? 0),
|
||||
}))
|
||||
.sort((a, b) => a.min - b.min);
|
||||
|
||||
let next = 1;
|
||||
for (const r of scoped) {
|
||||
if (r.min > next) break; // gap before this rule — fill it first
|
||||
next = Math.max(next, r.max + 1);
|
||||
}
|
||||
return next > cap ? null : next;
|
||||
}
|
||||
Reference in New Issue
Block a user