fix issue

This commit is contained in:
Marshal
2026-08-20 18:10:29 +00:00
183 changed files with 14241 additions and 1265 deletions

View File

@@ -313,7 +313,9 @@ const RuleEngineResourcePage = () => {
(f) =>
f.name === "originYardId" ||
f.name === "fromYardId" ||
f.name === "toYardId",
f.name === "toYardId" ||
// Operational targets pick a station by yard code.
f.name === "dimensionKey",
),
);
const { data: yardOptions, isLoading: yardOptionsLoading } =
@@ -475,6 +477,20 @@ const RuleEngineResourcePage = () => {
.map(({ label, value }) => ({ label, value })),
};
}
// An operational target's key is a category, a container class, or a
// station's YARD CODE — never a yard id, because the reports match it
// against what their classification CASE emits.
if (field.name === "dimensionKey") {
const staticOptions = field.optionsFromValues;
return {
...field,
type: "select" as const,
optionsFromValues: (values: Record<string, unknown>) =>
String(values.dimension ?? "") === "station"
? (yardOptions ?? []).map(({ label, code }) => ({ label, value: code }))
: (staticOptions?.(values) ?? []),
};
}
if (field.name === "originYardId" || field.name === "destinationYardId") {
const end = field.name === "originYardId" ? "origin" : "destination";
return {

View File

@@ -142,6 +142,37 @@ const TRADE_DIRECTIONS = [
{ label: "Both", value: "BOTH" },
];
/**
* The cargo categories and container classes an operational target may be
* keyed on.
*
* Mirrors CARGO_CATEGORIES / CONTAINER_CLASSES in the API's
* `modules/reports/operations-classification.ts`, which is the source of truth:
* a report matches a target by this exact key, so a value here that the API
* does not emit is a plan the report will never find. The API spec
* `operations-classification.spec.ts` guards the API side of the pair.
*/
export const OPERATIONS_CARGO_CATEGORIES = [
{ label: "Multimodal container import", value: "CONTAINER_IMPORT_MULTIMODAL" },
{ label: "Unimodal container import", value: "CONTAINER_IMPORT_UNIMODAL" },
{ label: "Export container", value: "CONTAINER_EXPORT" },
{ label: "Empty container", value: "EMPTY_CONTAINER" },
{ label: "Fertilizer", value: "FERTILIZER" },
{ label: "RoRo", value: "RORO" },
{ label: "Break bulk", value: "BREAK_BULK" },
{ label: "Sand", value: "SAND" },
{ label: "Bulk", value: "BULK" },
{ label: "Other imports", value: "OTHER_IMPORT" },
{ label: "Other export cargo", value: "OTHER_EXPORT" },
];
export const OPERATIONS_CONTAINER_CLASSES = [
{ label: "Multimodal container import", value: "CONTAINER_IMPORT_MULTIMODAL" },
{ label: "Unimodal container import", value: "CONTAINER_IMPORT_UNIMODAL" },
{ label: "Full export container", value: "CONTAINER_EXPORT" },
{ label: "Empty container return", value: "EMPTY_CONTAINER_RETURN" },
];
// Mirrors the YardCountry enum in @edr/types — the only two countries on the line.
const YARD_COUNTRIES = [
{ label: "Ethiopia", value: "Ethiopia" },
@@ -478,6 +509,14 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
optional: true,
placeholder: "Select parent cargo type (optional)",
},
{
name: "fullTrainsetWagons",
label: "Wagons in a full trainset",
type: "number",
optional: true,
description:
"What the Trainset Performance report divides loaded wagons by — 37 for vehicles, 22 for sand. Leave blank to use the default in Operating standards.",
},
{ name: "requiresDirectorApproval", label: "Requires director approval", type: "boolean" },
{ name: "hasLashing", label: "Charge lashing fee", type: "boolean" },
{
@@ -639,6 +678,108 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
{ name: "isActive", label: "Active", type: "boolean", description: "Off suspends the officer regardless of the validity window" },
],
},
{
slug: "operations-targets",
label: "Operational Targets",
category: "configuration",
subtitle:
"Planned TEU, trainsets and tonnage per period — the Plan column in the operations reports",
searchPlaceholder: "Search by category, station or note...",
supportsSearch: true,
cardTitleKey: "appliesToLabel",
cardSubtitleKey: "periodStart",
columns: [
// The *Label columns are readable twins the API sends alongside the stored
// codes (see OperationsTargetsService.toRow) — the codes themselves are
// enums the reports join on and stay out of the grid.
{ id: "periodStart", header: "Period start", accessorKey: "periodStart", format: "date" },
{ id: "periodLabel", header: "Period", accessorKey: "periodLabel" },
{ id: "metricLabel", header: "Metric", accessorKey: "metricLabel" },
{ id: "dimensionLabel", header: "Plan by", accessorKey: "dimensionLabel" },
{ id: "appliesToLabel", header: "Applies to", accessorKey: "appliesToLabel" },
{ id: "cargoCategoryLabel", header: "Cargo category", accessorKey: "cargoCategoryLabel" },
{ id: "plannedValue", header: "Plan", accessorKey: "plannedValue", format: "number" },
],
formFields: [
{
name: "metric",
label: "Metric",
type: "select",
required: true,
options: [
{ label: "TEU", value: "TEU" },
{ label: "Trainsets", value: "TRAINSET" },
{ label: "Volume (tons)", value: "VOLUME_TONS" },
],
},
{
name: "periodType",
label: "Period",
type: "select",
required: true,
options: [
{ label: "Weekly", value: "week" },
{ label: "Monthly", value: "month" },
{ label: "Quarterly", value: "quarter" },
{ label: "Yearly", value: "year" },
],
},
{
name: "periodStart",
label: "Period start",
type: "date",
required: true,
description: "Any date inside the period — snapped to its start on save.",
},
{
name: "dimension",
label: "Plan by",
type: "select",
required: true,
options: [
{ label: "Cargo category", value: "cargo_category" },
{ label: "Station", value: "station" },
{ label: "Container class", value: "container_class" },
],
},
{
name: "dimensionKey",
label: "Applies to",
type: "select",
required: true,
placeholder: "Select",
// The valid keys depend on the chosen dimension, and must match what the
// reports emit exactly — a mismatch here is a target the report never
// finds. Station options are the live yard codes, injected by
// RuleEngineResourcePage.
optionsFromValues: (values) => {
const dimension = String(values.dimension ?? "");
if (dimension === "container_class") return OPERATIONS_CONTAINER_CLASSES;
if (dimension === "station") return [];
return OPERATIONS_CARGO_CATEGORIES;
},
},
{
name: "cargoCategory",
label: "Cargo category",
type: "select",
required: true,
// A station's plan is per station AND per cargo type — the OCC report
// plans Nagad-Mojo container and Nagad-Mojo fertilizer separately. The
// other two dimensions already carry the category in the key above.
showWhen: { field: "dimension", equals: ["station"] },
options: OPERATIONS_CARGO_CATEGORIES,
},
{
name: "plannedValue",
label: "Planned value",
type: "number",
required: true,
description: "TEU, trainsets or tonnes — whichever the metric above is.",
},
{ name: "note", label: "Note", type: "text", optional: true },
],
},
{
slug: "yard-distances",
label: "Yard Distances",
@@ -652,6 +793,7 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
{ id: "fromYardLabel", header: "From yard", accessorKey: "fromYardLabel" },
{ id: "toYardLabel", header: "To yard", accessorKey: "toYardLabel" },
{ id: "distanceKm", header: "Distance (km)", accessorKey: "distanceKm", format: "number" },
{ id: "standardHours", header: "Standard (hrs)", accessorKey: "standardHours", format: "number" },
],
formFields: [
// Options injected at render from useYardOptions (RuleEngineResourcePage).
@@ -665,6 +807,14 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
description:
"Symmetric — one entry covers both directions. Route segments between these yards use this value.",
},
{
name: "standardHours",
label: "Standard running time (hrs)",
type: "number",
optional: true,
description:
"What the Train Delays report judges this leg against — 21h Negad to GMP, 20h to Adama, 20.5h to Modjo, 22h to Sebeta. Leave blank to use the default in Operating standards.",
},
],
},
{