implement exchange settings management and fallback rate handling

This commit is contained in:
Marshal
2026-08-04 11:22:47 +00:00
parent 7c78a815eb
commit 56697d8fc5
22 changed files with 805 additions and 85 deletions

View File

@@ -153,11 +153,13 @@ const RuleEngineFormDialog = ({
buildInitialValues(fields, initialRecord),
);
const [position, setPosition] = useState(RULE_ENGINE_POSITION_END);
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
useEffect(() => {
if (open) {
setValues(buildInitialValues(fields, initialRecord));
setPosition(RULE_ENGINE_POSITION_END);
setFieldErrors({});
}
}, [open, fields, initialRecord]);
@@ -185,6 +187,9 @@ const RuleEngineFormDialog = ({
const formRows = useMemo(() => buildFormRows(visibleFields), [visibleFields]);
const setField = (name: string, value: unknown) => {
setFieldErrors((current) =>
current[name] ? { ...current, [name]: "" } : current,
);
setValues((current) => {
const next = { ...current, [name]: value };
// Changing what a rate applies to (or its surcharge trigger) can invalidate
@@ -223,6 +228,10 @@ const RuleEngineFormDialog = ({
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
const payload: Record<string, unknown> = {};
// Required selects that are empty block the submit and mark themselves,
// rather than posting an incomplete payload for the API to reject.
setFieldErrors({});
let blocked = false;
for (const field of visibleFields) {
// Derived fields always submit their computed value — never stale state.
@@ -241,7 +250,16 @@ const RuleEngineFormDialog = ({
field.type === "select" &&
(raw === "" || raw === RULE_ENGINE_SELECT_NONE)
) {
// A required select left empty must not silently submit nothing — the
// API rejects the payload with a message that reads as if the admin
// skipped a field they never saw cleared (e.g. yards reset by a trade
// direction change). Surface it on the field instead.
if (!field.required) continue;
setFieldErrors((current) => ({
...current,
[field.name]: `${field.label} is required.`,
}));
blocked = true;
} else if (raw === "" || raw === undefined) {
if (!field.required) continue;
payload[field.name] = raw;
@@ -254,6 +272,8 @@ const RuleEngineFormDialog = ({
payload.code = String(payload.code).toUpperCase();
}
if (blocked) return;
if (!initialRecord && positionOptions && position !== RULE_ENGINE_POSITION_END) {
payload.insertAfterId = position;
}
@@ -340,10 +360,10 @@ const RuleEngineFormDialog = ({
value={resolveSelectValue(field, values)}
onChange={(v) => setField(field.name, v === RULE_ENGINE_SELECT_NONE ? "" : v)}
disabled={selectOptionsLoading}
// Native required blocks submit while a mandatory select is empty —
// without it the form posts and the API 400s (e.g. a container
// customs/lashing rate with no container type picked).
// Mantine's Select is not a native input, so `required` only marks it
// visually — handleSubmit is what actually blocks an empty one.
required={field.required}
error={fieldErrors[field.name] || undefined}
data={options
.filter((opt) => opt.value !== "")
.map((opt) => ({