This commit is contained in:
Marshal
2026-07-14 11:06:49 +00:00
parent 957a185a4d
commit 6d0cf50b4d
64 changed files with 4896 additions and 404 deletions

View File

@@ -54,8 +54,8 @@ interface CargoNode extends RuleEngineRecord {
requiresDirectorApproval?: boolean;
/** How this cargo is measured (PER_TON / PER_ITEM); null for groups/unset. */
unitOfMeasure?: string | null;
/** Wagon type FK used to carry this bulk cargo during scheduling; null if unset. */
wagonTypeId?: string | null;
/** Wagon types that can carry this bulk cargo during scheduling; empty if unset. */
wagonTypes?: { id: string; code?: string; name?: string }[];
isActive?: boolean;
displayOrder?: number;
}
@@ -82,16 +82,19 @@ const FORM_FIELDS: FormFieldDef[] = [
],
},
{
// Wagon type that carries this (bulk) commodity — drives train-scheduling
// wagon resolution. Optional: leave "None" for grouping categories and
// container/legacy cargo; set it on scheduled bulk commodities.
// Wagon types that can carry this (bulk) commodity — drive train-scheduling
// wagon resolution (the plan uses whichever type the train/yard has).
// Optional: leave empty for grouping categories and container/legacy cargo;
// set them on scheduled bulk commodities.
// Options injected at render from useWagonTypeOptions.
name: "wagonTypeId",
label: "Wagon type",
type: "select",
name: "wagonTypeIds",
label: "Wagon types",
type: "multiselect",
optional: true,
placeholder: "Select wagon type (bulk cargo)",
options: [{ label: "None", value: RULE_ENGINE_SELECT_NONE }],
placeholder: "Select wagon types (bulk cargo)",
options: [],
getInitialValue: (record) =>
((record.wagonTypes as { id: string }[] | undefined) ?? []).map((wt) => wt.id),
},
{ name: "requiresDirectorApproval", label: "Requires director approval", type: "boolean" },
{ name: "isActive", label: "Active", type: "boolean" },
@@ -119,19 +122,13 @@ const CargoTypesPage = () => {
const { create, update, remove } = useRuleEngineMutations(CARGO_SLUG);
// Wagon-type options for the "Wagon type" picker (bulk cargo → wagon FK).
// Wagon-type options for the "Wagon types" picker (bulk cargo → allowed list).
const { data: wagonTypeOptions } = useWagonTypeOptions(canManage);
const formFields = useMemo<FormFieldDef[]>(
() =>
FORM_FIELDS.map((field) =>
field.name === "wagonTypeId"
? {
...field,
options: [
{ label: "None", value: RULE_ENGINE_SELECT_NONE },
...(wagonTypeOptions ?? []),
],
}
field.name === "wagonTypeIds"
? { ...field, options: wagonTypeOptions ?? [] }
: field,
),
[wagonTypeOptions],

View File

@@ -153,7 +153,9 @@ const RuleEngineResourcePage = () => {
config?.formFields.some((f) => f.name === "rateId"),
);
const usesWagonTypeField = Boolean(
config?.formFields.some((f) => f.name === "wagonTypeId"),
config?.formFields.some(
(f) => f.name === "wagonTypeId" || f.name === "wagonTypeIds",
),
);
const { data: cargoParentOptions, isLoading: cargoParentOptionsLoading } =
@@ -206,6 +208,13 @@ const RuleEngineResourcePage = () => {
options: wagonTypeOptions ?? [],
};
}
if (field.name === "wagonTypeIds") {
return {
...field,
type: "multiselect" as const,
options: wagonTypeOptions ?? [],
};
}
return field;
});
}, [config, cargoParentOptions, cargoLeafOptions, containerTypeOptions, liveRateOptions, wagonTypeOptions]);

View File

@@ -55,6 +55,12 @@ export interface FormFieldDef {
* from the fleet config's string-based `dynamicOptions` to avoid a clash.)
*/
optionsFromValues?: (values: Record<string, unknown>) => { label: string; value: string }[];
/**
* Derive the field's initial form value from the record being edited when it
* doesn't live under `record[name]` — e.g. a multiselect of ids backed by a
* relation list (`wagonTypeIds` read from `record.wagonTypes`).
*/
getInitialValue?: (record: Record<string, unknown>) => unknown;
}
export interface RuleEngineOrderConfig {
@@ -258,11 +264,14 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
{ name: "sizeFt", label: "Size (ft)", type: "number", required: true },
// Options injected at render from useWagonTypeOptions (RuleEngineResourcePage).
{
name: "wagonTypeId",
label: "Wagon type",
type: "select",
name: "wagonTypeIds",
label: "Wagon types",
type: "multiselect",
required: true,
description: "Wagon type used to carry this container during train scheduling.",
description:
"Wagon types that can carry this container during train scheduling (one container size per wagon at a time).",
getInitialValue: (record) =>
((record.wagonTypes as { id: string }[] | undefined) ?? []).map((wt) => wt.id),
},
{ name: "isOpenTop", label: "Open top", type: "boolean" },
{ name: "isActive", label: "Active", type: "boolean" },