Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts
2026-08-03 08:20:18 +00:00

1096 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { SidebarItem } from "@/components/layout/types";
import { ruleEngineViewKey } from "@/lib/permissions";
import type { RuleEngineResourceSlug } from "@/types/rule-engine";
export type RuleEngineNavCategory = "configuration" | "rules";
export type ColumnFormat =
| "text"
| "code"
| "boolean"
| "activeBadge"
| "rateStatus"
| "validityBadge"
| "date"
| "number"
| "currency"
| "entityLabel"
| "rateLabel";
export type FormFieldType = "text" | "number" | "boolean" | "date" | "email" | "select" | "multiselect" | "textarea" | "radio";
export interface ResourceColumn {
id: string;
header: string;
accessorKey: string;
format?: ColumnFormat;
}
/** Radix Select cannot use empty string as an item value; use this for optional "none" choices. */
export const RULE_ENGINE_SELECT_NONE = "__none__";
export interface FormFieldDef {
name: string;
label: string;
type: FormFieldType;
required?: boolean;
optional?: boolean;
options?: { label: string; value: string }[];
placeholder?: string;
description?: string;
disabled?: boolean;
/** Trailing unit label shown inside the input (e.g. "USD" on a rate value). */
suffix?: string;
/** Hide this field when another field currently equals one of these values. */
hideWhen?: { field: string; equals: string[] };
/**
* Show this field ONLY when another field currently equals one of these
* values (inverse of hideWhen). When both are set, the field must satisfy
* showWhen and not match hideWhen.
*/
showWhen?: { field: string; equals: string[] };
/**
* Show this field only when the predicate accepts the live form values — for
* visibility that depends on more than one field, which `showWhen` cannot
* express (the intercity container/bulk pickers hang off both `appliesTo`
* and `intercityKind`). Combines with showWhen/hideWhen: all must pass.
*/
showIf?: (values: Record<string, unknown>) => boolean;
/**
* Select options computed from other fields' current values. When set, the
* form resolves the option list at render time from the live form state
* instead of the static `options` list. Used for the rate unit selector,
* whose valid choices depend on `appliesTo` + `trigger`. (Named distinctly
* 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;
/**
* 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 {
field: "displayOrder" | "stepOrder";
scopeField?: "requiresDirectorApproval";
label: string;
}
/**
* A category tab above a resource list. The active tab's `filters` are sent to
* the list endpoint verbatim, so filtering happens server-side (values may be
* comma-separated lists, e.g. appliesTo: "FIRST_MILE,LAST_MILE").
*/
export interface RuleEngineListTab {
key: string;
label: string;
filters: { appliesTo?: string; trigger?: string };
}
export interface RuleEngineResourceConfig {
slug: RuleEngineResourceSlug;
label: string;
subtitle: string;
category: RuleEngineNavCategory;
searchPlaceholder: string;
columns: ResourceColumn[];
formFields: FormFieldDef[];
supportsSearch?: boolean;
orderConfig?: RuleEngineOrderConfig;
/** Server-filtered category tabs rendered above the list (rates page). */
listTabs?: RuleEngineListTab[];
/** Primary line on card view (inferred from columns when omitted). */
cardTitleKey?: string;
/** Secondary line under title on card view (inferred when omitted). */
cardSubtitleKey?: string;
/** Code badge on card header (inferred from code column when omitted). */
cardCodeKey?: string;
}
export const RULE_ENGINE_CATEGORY_BASE_PATH: Record<RuleEngineNavCategory, string> = {
configuration: "/dashboard/configuration",
rules: "/dashboard/rules",
};
const TRADE_DIRECTIONS = [
{ label: "Import", value: "IMPORT" },
{ label: "Export", value: "EXPORT" },
{ label: "Both", value: "BOTH" },
];
// Mirrors the YardCountry enum in @edr/types — the only two countries on the line.
const YARD_COUNTRIES = [
{ label: "Ethiopia", value: "Ethiopia" },
{ label: "Djibouti", value: "Djibouti" },
];
/**
* The three role strings the approval chain was hardcoded to before it was
* driven by IAM position types. Kept only so rows still stored against them
* render a readable label instead of a blank select — the live options come
* from GET /approval-rules/position-types (see `useApprovalRoleOptions`).
*/
export const LEGACY_APPROVAL_ROLES = [
{ label: "Line staff (legacy)", value: "LINE_STAFF" },
{ label: "Director (legacy)", value: "DIRECTOR" },
{ label: "CEO (legacy)", value: "CEO" },
];
/**
* Friendly, admin-facing rate categories. Choosing one drives which fields the
* Rate form shows (see the `rates` resource below). Base-freight categories
* carry a trade direction + container/bulk scope; OTHER is for surcharges.
*/
const RATE_APPLIES_TO = [
{ label: "Bulk (base freight)", value: "BULK" },
{ label: "Container (base freight)", value: "CONTAINER" },
{ label: "Intercity (base freight)", value: "INTERCITY" },
{ label: "First mile", value: "FIRST_MILE" },
{ label: "Last mile", value: "LAST_MILE" },
{ label: "Other (surcharge)", value: "OTHER" },
];
/** Surcharge triggers — only relevant when Applies to = Other. */
const RATE_TRIGGERS = [
{ label: "Hazardous cargo", value: "HAZARDOUS" },
{
label: "Overweight (export only — import derives from container price)",
value: "OVERWEIGHT",
},
{ label: "Reefer cargo", value: "REEFER" },
{
label: "Empty container return (import, per route + container type)",
value: "WITH_RETURN",
},
{ label: "Shipping line mapped", value: "SHIPPING_LINE" },
{ label: "Penalty", value: "CONSOLIDATION" },
{ label: "Lashing (bulk, per cargo type)", value: "LASHING" },
{ label: "Cancellation", value: "CANCELLATION" },
{ label: "Shipping line extra fee (PIL)", value: "PIL_EXTRA_FEE" },
{ label: "Customs clearance service fee (billed with the booking)", value: "CUSTOMS_CLEARANCE" },
];
/**
* Intercity runs inside Ethiopia and can carry either boxes or bulk, but the
* two price differently. The admin says which up front and the form then asks
* for the matching scope field — this choice is not stored on the rate itself;
* the API reads container-vs-bulk back off whichever scope field was filled.
*/
const INTERCITY_KINDS = [
{ label: "Container", value: "CONTAINER" },
{ label: "Bulk", value: "BULK" },
];
/** True when the rate being edited is base rail freight, which is priced per leg. */
const isBaseFreightRate = (values: Record<string, unknown>) =>
["BULK", "CONTAINER", "INTERCITY"].includes(String(values.appliesTo ?? ""));
/**
* Rates priced per leg: base rail freight, plus the customs clearance fee and
* the empty-container return surcharge (sold per route + container type).
*/
const isRouteScopedRate = (values: Record<string, unknown>) =>
isBaseFreightRate(values) ||
(String(values.appliesTo ?? "") === "OTHER" &&
["CUSTOMS_CLEARANCE", "WITH_RETURN"].includes(String(values.trigger ?? "")));
const unitOption = (value: string) => ({ label: value.replace(/_/g, " "), value });
/**
* Valid weighting units for a rate shape — mirrors the API's
* `allowedRateUnits`. The unit is driven by the *type* being billed: containers
* bill per container, bulk per ton, overweight always per excess ton, etc. A
* rate scoped to a break-bulk commodity (unit of measure = PER_ITEM) offers
* PER_ITEM wherever a weighed one offers PER_TON. Kept in sync with
* apps/edr-freight-api/.../entities/rate-unit.util.ts.
*/
const allowedRateUnits = (
appliesTo: string,
trigger: string,
cargoKind = "",
cargoUnitOfMeasure = "",
): string[] => {
const units = unitsForShape(appliesTo, trigger, cargoKind);
return cargoUnitOfMeasure === "PER_ITEM"
? units.map((u) => (u === "PER_TON" ? "PER_ITEM" : u))
: units;
};
const unitsForShape = (
appliesTo: string,
trigger: string,
cargoKind = "",
): string[] => {
if (appliesTo === "OTHER") {
switch (trigger) {
case "OVERWEIGHT":
return ["PER_TON"];
case "REEFER":
case "HAZARDOUS":
case "DEMURRAGE":
return ["PER_CONTAINER", "PER_TON"];
case "WITH_RETURN":
// Container-only service — per returned container, per wagon, or flat.
return ["PER_CONTAINER", "PER_WAGON", "FLAT"];
case "CANCELLATION":
return ["FLAT", "PER_INVOICE"];
case "CUSTOMS_CLEARANCE":
// Per cargo kind: container fees per box/wagon, bulk per ton/wagon.
return cargoKind === "BULK"
? ["PER_TON", "PER_WAGON"]
: ["PER_CONTAINER", "PER_WAGON"];
case "LASHING":
// Bulk-only cargo securing — per ton or per wagon.
return ["PER_TON", "PER_WAGON"];
case "CONSOLIDATION":
case "SHIPPING_LINE":
case "PIL_EXTRA_FEE":
return ["PER_CONTAINER", "FLAT"];
default:
return ["FLAT", "PER_TON", "PER_CONTAINER"];
}
}
switch (appliesTo) {
case "CONTAINER":
return ["PER_CONTAINER", "PER_WAGON"];
case "BULK":
return ["PER_TON", "PER_WAGON"];
case "INTERCITY":
return ["PER_CONTAINER", "PER_TON", "PER_WAGON", "PER_KM"];
case "FIRST_MILE":
case "LAST_MILE":
return ["PER_CONTAINER", "PER_TON", "PER_KM", "FLAT"];
default:
return ["FLAT"];
}
};
/**
* Unit choices for the rate form. `cargoUnitOfMeasure` is how the bulk
* commodity picked in the form is counted (PER_TON / PER_ITEM) — injected by
* RuleEngineResourcePage, which is the layer that has the cargo type list.
*/
export const rateUnitOptions = (
values: Record<string, unknown>,
cargoUnitOfMeasure = "",
) => {
const appliesTo = String(values.appliesTo ?? "");
const trigger = appliesTo === "OTHER" ? String(values.trigger ?? "") : "ALWAYS";
if (!appliesTo) return [];
return allowedRateUnits(
appliesTo,
trigger,
String(values.cargoKind ?? ""),
cargoUnitOfMeasure,
).map(unitOption);
};
const CURRENCIES = [
{ label: "USD", value: "USD" },
{ label: "ETB", value: "ETB" },
];
const PRIORITY_CONFIG_TYPES = [
{ label: "Wagon count", value: "WAGON" },
{ label: "Payment currency", value: "CURRENCY" },
{ label: "Customs clearance", value: "CUSTOMS" },
];
const codeColumn = (key: string, header = "Code"): ResourceColumn => ({
id: key,
header,
accessorKey: key,
format: "code",
});
const activeColumn: ResourceColumn = {
id: "isActive",
header: "Status",
accessorKey: "isActive",
format: "activeBadge",
};
export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
{
slug: "cargo-types",
label: "Cargo Types",
category: "configuration",
subtitle: "Manage freight cargo classification and approval rules",
searchPlaceholder: "Search cargo types by name or code...",
supportsSearch: true,
orderConfig: { field: "displayOrder", label: "Display order" },
columns: [
codeColumn("code"),
{ id: "cargoTypeName", header: "Name", accessorKey: "cargoTypeName" },
{ id: "unitOfMeasure", header: "Billed by", accessorKey: "unitOfMeasure" },
{
id: "requiresDirectorApproval",
header: "Director approval",
accessorKey: "requiresDirectorApproval",
format: "boolean",
},
{ id: "displayOrder", header: "Order", accessorKey: "displayOrder", format: "number" },
activeColumn,
],
formFields: [
{ name: "cargoTypeName", label: "Cargo type name", type: "text", required: true },
{
name: "unitOfMeasure",
label: "Billed by",
type: "select",
optional: true,
placeholder: "Not set (container/legacy cargo)",
description:
"Bulk storage/demurrage bills per ton for Tonnage cargo, per unit for Countable cargo (e.g. Machinery, Truck, Automobile, Livestock).",
options: [
{ label: "Tonnage (per ton)", value: "PER_TON" },
{ label: "Countable (per item)", value: "PER_ITEM" },
],
},
{
name: "parentGroupId",
label: "Parent group",
type: "select",
optional: true,
placeholder: "Select parent cargo type (optional)",
},
{ name: "requiresDirectorApproval", label: "Requires director approval", type: "boolean" },
{ name: "hasLashing", label: "Charge lashing fee", type: "boolean" },
{ name: "isActive", label: "Active", type: "boolean" },
],
},
{
slug: "container-types",
label: "Container Types",
category: "configuration",
subtitle: "Configure container sizes",
searchPlaceholder: "Search container types...",
supportsSearch: true,
orderConfig: { field: "displayOrder", label: "Display order" },
columns: [
codeColumn("code"),
{ id: "label", header: "Label", accessorKey: "label" },
{ id: "displayOrder", header: "#", accessorKey: "displayOrder", format: "number" },
{ id: "sizeFt", header: "Size (ft)", accessorKey: "sizeFt", format: "number" },
activeColumn,
],
formFields: [
{ name: "label", label: "Label", type: "text", required: true },
{ name: "sizeFt", label: "Size (ft)", type: "number", required: true },
// Options injected at render from useWagonTypeOptions (RuleEngineResourcePage).
{
name: "wagonTypeIds",
label: "Wagon types",
type: "multiselect",
required: true,
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" },
],
},
{
slug: "wagon-types",
label: "Wagon Types",
category: "configuration",
subtitle: "Configure wagon classes used for capacity and train planning",
searchPlaceholder: "Search wagon types by name or code...",
// No supportsSearch: wagon-types is served by its own module, which does
// not implement server-side search (unlike the 9 rule-engine resources).
cardTitleKey: "name",
columns: [
codeColumn("code"),
{ id: "name", header: "Name", accessorKey: "name" },
{ id: "capacityTons", header: "Capacity (t)", accessorKey: "capacityTons", format: "number" },
{ id: "lengthMeters", header: "Length (m)", accessorKey: "lengthMeters", format: "number" },
{ id: "tareWeightTons", header: "Tare (t)", accessorKey: "tareWeightTons", format: "number" },
{
id: "supportedLoadTypes",
header: "Load types",
accessorKey: "supportedLoadTypes",
},
activeColumn,
],
formFields: [
{ name: "code", label: "Code", type: "text", required: true },
{ name: "name", label: "Name", type: "text", required: true },
{ name: "capacityTons", label: "Capacity (tons)", type: "number", required: true },
{ name: "lengthMeters", label: "Length (meters)", type: "number", required: true },
// The locomotive's pull limit is a GROSS limit, so capacity planning charges
// `cargo + wagons × tare` against it. The API rejects a create without this.
{
name: "tareWeightTons",
label: "Tare weight (tons)",
type: "number",
required: true,
description: "Empty wagon weight — counts against the locomotive's pull limit alongside the cargo",
},
{
name: "supportedLoadTypes",
label: "Supported load types",
type: "textarea",
optional: true,
placeholder: "CONTAINER, BULK",
},
{ name: "isActive", label: "Active", type: "boolean" },
],
},
{
slug: "truck-types",
label: "Truck Types",
category: "configuration",
subtitle:
"Configure the truck configurations vehicles are registered against — capacity and whether a trailer applies",
searchPlaceholder: "Search truck types by name or code...",
cardTitleKey: "name",
columns: [
codeColumn("code"),
{ id: "name", header: "Name", accessorKey: "name" },
{ id: "capacityTons", header: "Capacity (t)", accessorKey: "capacityTons", format: "number" },
{ id: "hasTrailer", header: "Has trailer", accessorKey: "hasTrailer" },
activeColumn,
],
formFields: [
{ name: "code", label: "Code", type: "text", required: true },
{ name: "name", label: "Name", type: "text", required: true },
{
name: "capacityTons",
label: "Capacity (tons)",
type: "number",
optional: true,
description: "Pre-fills the capacity of every vehicle registered on this type",
},
// Drives the trailer plate on vehicle registration: a rigid truck (Casoni)
// has none, so registering one with a trailer plate is rejected.
{
name: "hasTrailer",
label: "Pulls a trailer",
type: "boolean",
description: "Off for a rigid truck (e.g. Casoni) — its registration has no trailer plate",
},
{ name: "description", label: "Description", type: "textarea", optional: true },
{ name: "isActive", label: "Active", type: "boolean" },
],
},
{
slug: "transit-agents",
label: "Transit Agents",
category: "configuration",
subtitle:
"Djibouti transit officers GL Djibouti may assign to a shipment — each carries a validity window",
searchPlaceholder: "Search transit agents by name...",
cardTitleKey: "name",
columns: [
{ id: "name", header: "Name", accessorKey: "name" },
{ id: "validFrom", header: "Valid from", accessorKey: "validFrom", format: "date" },
{ id: "validTo", header: "Valid to", accessorKey: "validTo", format: "date" },
{
id: "validityStatus",
header: "Validity",
accessorKey: "validityStatus",
format: "validityBadge",
},
activeColumn,
],
formFields: [
{ name: "name", label: "Name", type: "text", required: true },
{ name: "validFrom", label: "Valid from", type: "date", required: true },
{
name: "validTo",
label: "Valid to",
type: "date",
required: true,
description: "Expired or not-yet-started agents can't be assigned — extend the dates or add a new one",
},
{ name: "isActive", label: "Active", type: "boolean", description: "Off suspends the officer regardless of the validity window" },
],
},
{
slug: "yard-distances",
label: "Yard Distances",
category: "configuration",
subtitle: "Rail distance between yard pairs — routes read their segment km from here",
searchPlaceholder: "Search by yard name or code...",
supportsSearch: true,
cardTitleKey: "fromYardLabel",
cardSubtitleKey: "toYardLabel",
columns: [
{ id: "fromYardLabel", header: "From yard", accessorKey: "fromYardLabel" },
{ id: "toYardLabel", header: "To yard", accessorKey: "toYardLabel" },
{ id: "distanceKm", header: "Distance (km)", accessorKey: "distanceKm", format: "number" },
],
formFields: [
// Options injected at render from useYardOptions (RuleEngineResourcePage).
{ name: "fromYardId", label: "From yard", type: "select", required: true, placeholder: "Select yard" },
{ name: "toYardId", label: "To yard", type: "select", required: true, placeholder: "Select yard" },
{
name: "distanceKm",
label: "Distance (km)",
type: "number",
required: true,
description:
"Symmetric — one entry covers both directions. Route segments between these yards use this value.",
},
],
},
{
slug: "priority-configs",
label: "Priority Rules",
category: "rules",
subtitle: "Wagon-count, payment-currency, and customs scoring rules",
searchPlaceholder: "Search priority rules...",
supportsSearch: true,
orderConfig: { field: "displayOrder", label: "Display order" },
columns: [
{ id: "type", header: "Type", accessorKey: "type" },
{ id: "currency", header: "Currency", accessorKey: "currency" },
{ id: "minWagonCount", header: "Min wagons", accessorKey: "minWagonCount", format: "number" },
{ id: "maxWagonCount", header: "Max wagons", accessorKey: "maxWagonCount", format: "number" },
{ id: "scorePoints", header: "Points", accessorKey: "scorePoints", format: "number" },
activeColumn,
],
formFields: [
{
name: "type",
label: "Type",
type: "select",
required: true,
options: PRIORITY_CONFIG_TYPES,
placeholder: "Wagon count or payment currency",
},
{
name: "currency",
label: "Currency",
type: "select",
optional: true,
options: [{ label: "None", value: RULE_ENGINE_SELECT_NONE }, ...CURRENCIES],
placeholder: "Select a currency",
hideWhen: { field: "type", equals: ["WAGON", "CUSTOMS"] },
},
{
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: "No upper limit — must be at least the min wagon count",
},
{ name: "scorePoints", label: "Score points", type: "number", required: true },
{ name: "isActive", label: "Active", type: "boolean" },
],
},
{
slug: "service-types",
label: "Service Types",
category: "configuration",
subtitle: "Freight service offerings and booking options",
searchPlaceholder: "Search service types...",
supportsSearch: true,
orderConfig: { field: "displayOrder", label: "Display order" },
columns: [
codeColumn("code"),
{ id: "serviceName", header: "Service name", accessorKey: "serviceName" },
{ id: "displayOrder", header: "#", accessorKey: "displayOrder", format: "number" },
activeColumn,
],
formFields: [
{ name: "serviceName", label: "Service name", type: "text", required: true },
{ name: "description", label: "Description", type: "textarea" },
{ name: "canBeBookedAlone", label: "Can be booked alone", type: "boolean" },
{ name: "includesFirstMile", label: "Includes first mile", type: "boolean" },
{ name: "includesLastMile", label: "Includes last mile", type: "boolean" },
{ name: "includesCustoms", label: "Includes customs", type: "boolean" },
{ name: "isActive", label: "Active", type: "boolean" },
],
},
{
slug: "weight-limit-rules",
label: "Weight Limit Rules",
category: "rules",
subtitle: "VGM limits by container and trade direction",
searchPlaceholder: "Search weight limit rules...",
supportsSearch: true,
cardTitleKey: "containerType",
cardSubtitleKey: "tradeDirection",
columns: [
{
id: "containerType",
header: "Container",
accessorKey: "containerType",
format: "entityLabel",
},
{ id: "tradeDirection", header: "Direction", accessorKey: "tradeDirection" },
{ id: "maxVgmTons", header: "Max VGM (t)", accessorKey: "maxVgmTons", format: "number" },
{
id: "maxCapacityTons",
header: "Max capacity (t)",
accessorKey: "maxCapacityTons",
format: "number",
},
],
formFields: [
{
name: "containerTypeId",
label: "Container type",
type: "select",
required: true,
placeholder: "Select container type",
},
{
name: "tradeDirection",
label: "Trade direction",
type: "select",
required: true,
options: TRADE_DIRECTIONS,
},
{ name: "maxVgmTons", label: "Max VGM (tons)", type: "number", required: true },
{
name: "maxCapacityTons",
label: "Max capacity (tons)",
type: "number",
optional: true,
description:
"Hard ceiling — a booking whose line weight exceeds this cannot be created at all. Leave empty for no ceiling (overweight surcharge only).",
},
],
},
{
slug: "yards",
label: "Yards",
category: "configuration",
subtitle: "Terminal and yard locations",
searchPlaceholder: "Search yards...",
supportsSearch: true,
orderConfig: { field: "displayOrder", label: "Display order" },
columns: [
codeColumn("code"),
{ id: "label", header: "Label", accessorKey: "label" },
{ id: "country", header: "Country", accessorKey: "country" },
{
id: "hasFacility",
header: "Facility",
accessorKey: "hasFacility",
format: "boolean",
},
{
id: "hasContainerFacilityOrigin",
header: "Container origin",
accessorKey: "hasContainerFacilityOrigin",
format: "boolean",
},
{
id: "hasContainerFacilityDestination",
header: "Container dest.",
accessorKey: "hasContainerFacilityDestination",
format: "boolean",
},
{
id: "hasBulkFacilityOrigin",
header: "Bulk origin",
accessorKey: "hasBulkFacilityOrigin",
format: "boolean",
},
{
id: "hasBulkFacilityDestination",
header: "Bulk dest.",
accessorKey: "hasBulkFacilityDestination",
format: "boolean",
},
{ id: "displayOrder", header: "Order", accessorKey: "displayOrder", format: "number" },
activeColumn,
],
formFields: [
{ name: "label", label: "Label", type: "text", required: true },
{
name: "country",
label: "Country",
type: "select",
required: true,
options: YARD_COUNTRIES,
},
{
name: "hasFacility",
label: "Has load/unload facility",
type: "boolean",
description:
"This yard can load and unload cargo. Intercity bookings can only be loaded at their origin and unloaded at their destination when it is a facility.",
},
{
name: "hasContainerFacilityOrigin",
label: "Container facility — origin",
type: "boolean",
description: "Can load containers onto a train. Offered as a contract origin for container freight.",
showIf: (values) => Boolean(values.hasFacility),
},
{
name: "hasContainerFacilityDestination",
label: "Container facility — destination",
type: "boolean",
description: "Can receive containers off a train. Offered as a contract destination for container freight.",
showIf: (values) => Boolean(values.hasFacility),
},
{
name: "hasBulkFacilityOrigin",
label: "Bulk facility — origin",
type: "boolean",
description: "Can load bulk cargo onto a train. Offered as a contract origin for bulk freight.",
showIf: (values) => Boolean(values.hasFacility),
},
{
name: "hasBulkFacilityDestination",
label: "Bulk facility — destination",
type: "boolean",
description: "Can receive bulk cargo off a train. Offered as a contract destination for bulk freight.",
showIf: (values) => Boolean(values.hasFacility),
},
{ name: "isActive", label: "Active", type: "boolean" },
],
},
{
slug: "shipping-lines",
label: "Shipping Lines",
category: "configuration",
subtitle: "Shipping line codes and pricing mappings",
searchPlaceholder: "Search shipping lines...",
supportsSearch: true,
columns: [
codeColumn("code"),
{ id: "label", header: "Label", accessorKey: "label" },
{ id: "mappedToCode", header: "Mapped to", accessorKey: "mappedToCode" },
{
id: "showExtraFeeNotice",
header: "Extra fee notice",
accessorKey: "showExtraFeeNotice",
format: "boolean",
},
activeColumn,
],
formFields: [
{ name: "code", label: "Code", type: "text", required: true },
{ name: "label", label: "Label", type: "text", required: true },
{ name: "mappedToCode", label: "Mapped to code", type: "text" },
{ name: "showExtraFeeNotice", label: "Show extra fee notice", type: "boolean" },
{ name: "isActive", label: "Active", type: "boolean" },
],
},
{
slug: "rates",
label: "Rates",
category: "rules",
cardTitleKey: "appliesTo",
cardSubtitleKey: "currency",
subtitle: "Freight rates and approval workflow",
searchPlaceholder: "Search rates by type or status...",
supportsSearch: true,
// Category tabs — each filters server-side by appliesTo / trigger.
listTabs: [
{ key: "all", label: "All", filters: {} },
{ key: "container", label: "Container", filters: { appliesTo: "CONTAINER" } },
{ key: "bulk", label: "Bulk", filters: { appliesTo: "BULK" } },
{ key: "intercity", label: "Intercity", filters: { appliesTo: "INTERCITY" } },
{
key: "trucking",
label: "First / Last mile",
filters: { appliesTo: "FIRST_MILE,LAST_MILE" },
},
{
key: "customs",
label: "Customs clearance",
filters: { trigger: "CUSTOMS_CLEARANCE" },
},
{
key: "return",
label: "Container return",
filters: { trigger: "WITH_RETURN" },
},
{
key: "surcharges",
label: "Surcharges",
filters: {
appliesTo: "OTHER",
trigger:
"HAZARDOUS,OVERWEIGHT,REEFER,SHIPPING_LINE,CONSOLIDATION,LASHING,CANCELLATION,PIL_EXTRA_FEE",
},
},
],
columns: [
{ id: "appliesTo", header: "Applies to", accessorKey: "appliesTo", format: "code" },
{ id: "trigger", header: "Trigger", accessorKey: "trigger" },
// Base freight is priced per leg, so the route is what tells two otherwise
// identical rates apart. Surcharges have no leg and render as "—".
{ id: "originYard", header: "From", accessorKey: "originYard", format: "entityLabel" },
{
id: "destinationYard",
header: "To",
accessorKey: "destinationYard",
format: "entityLabel",
},
{ id: "rateValue", header: "Value", accessorKey: "rateValue", format: "currency" },
{ id: "rateUnit", header: "Unit", accessorKey: "rateUnit" },
{ id: "status", header: "Status", accessorKey: "status", format: "rateStatus" },
],
formFields: [
{
name: "appliesTo",
label: "Applies to",
type: "select",
required: true,
options: RATE_APPLIES_TO,
description:
"Pick what this rate is for. Bulk/Container/Intercity are base freight; Other is an auto-applied surcharge.",
},
// ── Surcharge trigger — only when Applies to = Other ──────────────────
{
name: "trigger",
label: "Surcharge trigger",
type: "select",
required: true,
options: RATE_TRIGGERS,
placeholder: "What makes this surcharge apply?",
showWhen: { field: "appliesTo", equals: ["OTHER"] },
},
// ── Trade direction — Bulk & Container base freight, plus the route-
// scoped surcharges (customs clearance; empty-container return, which is
// import-only for now so export is not offered) ────────────────────────
{
name: "tradeDirection",
label: "Trade direction",
type: "select",
required: true,
optionsFromValues: (v: Record<string, unknown>) =>
String(v.trigger ?? "") === "WITH_RETURN" &&
String(v.appliesTo ?? "") === "OTHER"
? TRADE_DIRECTIONS.filter((d) => d.value === "IMPORT")
: TRADE_DIRECTIONS.filter((d) => d.value !== "BOTH"),
showIf: (v) =>
["BULK", "CONTAINER"].includes(String(v.appliesTo ?? "")) ||
(String(v.appliesTo ?? "") === "OTHER" &&
["CUSTOMS_CLEARANCE", "WITH_RETURN", "LASHING"].includes(
String(v.trigger ?? ""),
)),
},
// ── Cargo kind — customs clearance is priced separately for containers
// (one rate per container type) and bulk ───────────────────────────────
{
name: "cargoKind",
label: "Cargo kind",
type: "select",
required: true,
options: INTERCITY_KINDS,
placeholder: "Is this fee for containers or bulk?",
description:
"Container fees bill per box or wagon (one rate per container type); bulk fees bill per ton or wagon.",
showIf: (v) =>
v.appliesTo === "OTHER" && v.trigger === "CUSTOMS_CLEARANCE",
// Not a stored column: a container fee carries its containerTypeId, a
// bulk fee carries none.
getInitialValue: (record) =>
record.containerTypeId ? "CONTAINER" : "BULK",
},
// ── Container type — a container fee names the type it covers ─────────
{
name: "containerTypeId",
label: "Container type",
type: "select",
required: true,
placeholder: "Which container type this fee covers",
showIf: (v) =>
v.appliesTo === "OTHER" &&
v.trigger === "CUSTOMS_CLEARANCE" &&
v.cargoKind === "CONTAINER",
},
// ── Bulk cargo type — the bulk customs fee names its commodity ────────
{
name: "cargoTypeId",
label: "Bulk cargo type",
type: "select",
required: true,
placeholder: "Which bulk commodity this fee covers",
showIf: (v) =>
v.appliesTo === "OTHER" &&
v.trigger === "CUSTOMS_CLEARANCE" &&
v.cargoKind === "BULK",
},
// ── Bulk cargo type — lashing is bulk-only; may narrow to one leaf
// commodity (specific wins over the commodity-wide catch-all) ──────────
{
name: "cargoTypeId",
label: "Bulk cargo type",
type: "select",
optional: true,
placeholder: "All lashing commodities (optional)",
description:
"Leave empty to cover every lashing commodity; a commodity-specific rate wins over the catch-all.",
showIf: (v) =>
v.appliesTo === "OTHER" && v.trigger === "LASHING",
},
// ── Cargo kind — Intercity only (import/export get it from appliesTo) ─
{
name: "intercityKind",
label: "Cargo type",
type: "select",
required: true,
options: INTERCITY_KINDS,
placeholder: "Is this rate for containers or bulk?",
description: "Intercity prices containers and bulk differently — pick which this covers.",
showWhen: { field: "appliesTo", equals: ["INTERCITY"] },
// Not a stored column: an existing rate records its kind in the rateType
// the API derived (INTERCITY_BULK / INTERCITY_CONTAINER).
getInitialValue: (record) =>
record.rateType === "INTERCITY_BULK" ? "BULK" : "CONTAINER",
},
// ── Container type — Container freight, container-kind intercity, and
// the empty-container return surcharge (20ft vs 40ft price differently) ─
{
name: "containerTypeId",
label: "Container type",
type: "select",
optional: true,
placeholder: "Select container type (optional)",
showIf: (v) =>
v.appliesTo === "CONTAINER" ||
(v.appliesTo === "INTERCITY" && v.intercityKind === "CONTAINER") ||
(v.appliesTo === "OTHER" && v.trigger === "WITH_RETURN"),
},
// ── Bulk cargo (leaf commodity) — Bulk freight, and bulk-kind intercity ─
{
name: "cargoTypeId",
label: "Bulk cargo type",
type: "select",
optional: true,
placeholder: "Select bulk commodity (optional)",
showIf: (v) =>
v.appliesTo === "BULK" ||
(v.appliesTo === "INTERCITY" && v.intercityKind === "BULK"),
},
// ── The leg this rate prices — base freight only ──────────────────────
// Options are narrowed to the countries the direction allows (import
// starts in Djibouti, export in Ethiopia, intercity stays in Ethiopia);
// see RuleEngineResourcePage, which injects the yard lists.
{
name: "originYardId",
label: "Origin yard",
type: "select",
required: true,
placeholder: "Where the leg starts",
showIf: isRouteScopedRate,
},
{
name: "destinationYardId",
label: "Destination yard",
type: "select",
required: true,
placeholder: "Where the leg ends",
showIf: isRouteScopedRate,
},
{ name: "rateValue", label: "Rate value", type: "number", required: true, suffix: "USD" },
// Unit choices are driven by the rate shape (appliesTo + trigger). Overweight
// is always per excess ton, so the unit field is hidden for it — the API
// forces PER_TON regardless.
{
name: "rateUnit",
label: "Rate unit",
type: "select",
required: true,
optionsFromValues: rateUnitOptions,
description:
"Weighting basis — options depend on what the rate applies to, and for bulk on how the picked commodity is counted (per ton or per item).",
hideWhen: { field: "trigger", equals: ["OVERWEIGHT"] },
},
],
},
{
slug: "approval-rules",
label: "Approval Rules",
category: "rules",
cardTitleKey: "actionLabel",
cardSubtitleKey: "requiredRole",
subtitle: "Multi-step booking approval chain",
searchPlaceholder: "Search approval rules...",
supportsSearch: true,
orderConfig: {
field: "stepOrder",
scopeField: "requiresDirectorApproval",
label: "Step order",
},
columns: [
{
id: "requiresDirectorApproval",
header: "Director chain",
accessorKey: "requiresDirectorApproval",
format: "boolean",
},
{ id: "stepOrder", header: "Step", accessorKey: "stepOrder", format: "number" },
{ id: "requiredRole", header: "Role", accessorKey: "requiredRole" },
{ id: "actionLabel", header: "Action", accessorKey: "actionLabel" },
{ id: "blocksRole", header: "Blocks", accessorKey: "blocksRole" },
],
formFields: [
{ name: "requiresDirectorApproval", label: "Requires director approval chain", type: "boolean" },
{
name: "requiredRole",
label: "Required role",
type: "select",
required: true,
// Replaced at render time with live IAM position types (+ legacy values).
options: LEGACY_APPROVAL_ROLES,
},
{ name: "actionLabel", label: "Action label", type: "text", required: true },
{
name: "blocksRole",
label: "Blocks role",
type: "select",
optional: true,
// Replaced at render time with live IAM position types (+ legacy values).
options: [{ label: "None", value: RULE_ENGINE_SELECT_NONE }, ...LEGACY_APPROVAL_ROLES],
},
],
},
];
export const RULE_ENGINE_RESOURCE_MAP = Object.fromEntries(
RULE_ENGINE_RESOURCES.map((r) => [r.slug, r]),
) as Record<RuleEngineResourceSlug, RuleEngineResourceConfig>;
export const getRuleEngineResource = (slug: string): RuleEngineResourceConfig | undefined =>
RULE_ENGINE_RESOURCE_MAP[slug as RuleEngineResourceSlug];
export const ruleEngineResourcePath = (slug: RuleEngineResourceSlug): string => {
const resource = RULE_ENGINE_RESOURCE_MAP[slug];
return `${RULE_ENGINE_CATEGORY_BASE_PATH[resource.category]}/${slug}`;
};
export const getCategorySidebarChildren = (
category: RuleEngineNavCategory,
): SidebarItem[] =>
RULE_ENGINE_RESOURCES.filter((r) => r.category === category).map((r) => ({
label: r.label,
href: ruleEngineResourcePath(r.slug),
permission: ruleEngineViewKey(r.slug),
}));
export const DEFAULT_CONFIGURATION_SLUG: RuleEngineResourceSlug = "cargo-types";
export const DEFAULT_RULES_SLUG: RuleEngineResourceSlug = "priority-configs";
/** @deprecated Use DEFAULT_CONFIGURATION_SLUG */
export const DEFAULT_RULE_ENGINE_SLUG = DEFAULT_CONFIGURATION_SLUG;