This commit is contained in:
Marshal
2026-07-17 13:57:59 +00:00
parent 3a697e12f2
commit 6467173c76
12 changed files with 642 additions and 46 deletions

View File

@@ -41,6 +41,8 @@ import {
useContainerTypeOptions,
useLiveRateOptions,
useWagonTypeOptions,
useYardOptions,
type YardOption,
usePriorityRuleWorkflow,
useRateChangeWorkflow,
useRateWorkflow,
@@ -72,6 +74,41 @@ const pathCategory = (pathname: string): RuleEngineNavCategory | undefined => {
return undefined;
};
/**
* Which yards may sit at one end of the leg a base-freight rate prices.
*
* The railway sells three shapes and each pins the countries: an import lands
* at a Djibouti port and rails inland, an export is the reverse, and intercity
* stays inside Ethiopia. Narrowing the dropdown is what stops an import rate
* from being configured Ethiopia → Ethiopia — the API rejects that too, but
* the admin should never be offered it. Non-base-freight rates carry no leg,
* so they get nothing.
*/
const yardOptionsForLegEnd = (
yards: YardOption[],
values: Record<string, unknown>,
end: "origin" | "destination",
): { label: string; value: string }[] => {
const appliesTo = String(values.appliesTo ?? "");
let country: string | undefined;
if (appliesTo === "INTERCITY") {
country = "Ethiopia";
} else if (appliesTo === "CONTAINER" || appliesTo === "BULK") {
const direction = String(values.tradeDirection ?? "");
// Direction is what decides the countries, so offer nothing until it is set
// rather than defaulting to one and letting it read as a real choice.
if (direction !== "IMPORT" && direction !== "EXPORT") return [];
const startsInEthiopia = direction === "EXPORT";
country = (end === "origin" ? startsInEthiopia : !startsInEthiopia)
? "Ethiopia"
: "Djibouti";
}
if (!country) return [];
return yards
.filter((yard) => yard.country === country)
.map(({ label, value }) => ({ label, value }));
};
const RuleEngineResourcePage = () => {
const { user } = useAuth();
const { resource: resourceSlug } = useParams<{ resource: string }>();
@@ -205,6 +242,11 @@ const RuleEngineResourcePage = () => {
useLiveRateOptions(usesLiveRateField);
const { data: wagonTypeOptions, isLoading: wagonTypeOptionsLoading } =
useWagonTypeOptions(usesWagonTypeField);
const usesYardField = Boolean(
config?.formFields.some((f) => f.name === "originYardId"),
);
const { data: yardOptions, isLoading: yardOptionsLoading } =
useYardOptions(usesYardField);
// 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
@@ -279,9 +321,22 @@ const RuleEngineResourcePage = () => {
options: wagonTypeOptions ?? [],
};
}
// Each end of the leg only offers yards in the country that end of the
// trade actually sits in, so an import can't be configured as if it
// started inland. Resolved per keystroke because the legal set changes
// with the direction the admin picks.
if (field.name === "originYardId" || field.name === "destinationYardId") {
const end = field.name === "originYardId" ? "origin" : "destination";
return {
...field,
type: "select" as const,
optionsFromValues: (values: Record<string, unknown>) =>
yardOptionsForLegEnd(yardOptions ?? [], values, end),
};
}
return field;
});
}, [config, cargoParentOptions, cargoLeafOptions, containerTypeOptions, liveRateOptions, wagonTypeOptions, isPriorityRules, allPriorityRules, editing, editingId]);
}, [config, cargoParentOptions, cargoLeafOptions, containerTypeOptions, liveRateOptions, wagonTypeOptions, yardOptions, isPriorityRules, allPriorityRules, editing, editingId]);
const rows = data?.items ?? [];
const meta = data?.meta;
@@ -709,7 +764,8 @@ const RuleEngineResourcePage = () => {
(usesContainerTypeField && containerTypeOptionsLoading) ||
(usesCargoTypeField && cargoLeafOptionsLoading) ||
(usesLiveRateField && liveRateOptionsLoading) ||
(usesWagonTypeField && wagonTypeOptionsLoading)
(usesWagonTypeField && wagonTypeOptionsLoading) ||
(usesYardField && yardOptionsLoading)
}
positionOptions={!editing ? createPositionOptions : undefined}
positionLoading={createPositionLoading}