Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts

1499 lines
58 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" | "tierList";
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;
/** Editable on create, locked when editing an existing record. */
disabledOnEdit?: 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;
/** Pre-selected value on create (no record yet) — e.g. last-mile currency = ETB. */
defaultValue?: string;
/**
* 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;
/**
* "true" = only shipping-line rates, "false" = only standard customer
* rates. Sent as a string because tab filters go on the query string
* verbatim.
*/
isShippingLineRate?: 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 (per wagon, per direction + cargo type)", value: "CANCELLATION" },
{ label: "Shipping line extra fee (PIL)", value: "PIL_EXTRA_FEE" },
{ label: "Customs clearance service fee (billed with the booking)", value: "CUSTOMS_CLEARANCE" },
{
label: "Ethiopian customs clearance service fee (Ethiopian-side-only services)",
value: "ETHIOPIAN_CUSTOMS_CLEARANCE",
},
{ label: "Fuel (per lane + cargo type)", value: "FUEL" },
];
/**
* Fuel lanes: import/export like base freight, plus a domestic intercity lane
* (stored as DOMESTIC — matches the booking's own trade direction).
*/
const FUEL_TRADE_DIRECTIONS = [
{ label: "Import", value: "IMPORT" },
{ label: "Export", value: "EXPORT" },
{ label: "Intercity", value: "DOMESTIC" },
];
/**
* 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" },
];
/**
* A shipping-line rate: priced for one carrier's own bookings instead of for
* every customer. The toggle drives the whole form — until a line is picked
* there is nothing to configure, and the shape questions (base freight vs
* surcharge, container vs bulk) are asked only after it is.
*/
const isShippingLineRate = (values: Record<string, unknown>) =>
values.isShippingLineRate === true;
/** A shipping-line rate whose owning line has been chosen — the rest unlocks. */
const hasShippingLine = (values: Record<string, unknown>) =>
isShippingLineRate(values) && Boolean(values.shippingLineCompanyId);
/**
* What a shipping-line rate prices. Deliberately narrower than the customer
* form's `appliesTo`: a line buys base rail freight (its own containers or
* bulk) or a surcharge, and nothing else — intercity and first/last mile are
* customer products.
*/
const SHIPPING_LINE_RATE_KINDS = [
{ label: "Base freight", value: "BASE" },
{ label: "Surcharge", value: "SURCHARGE" },
];
/** Container vs bulk, asked once a shipping-line base-freight rate is chosen. */
const SHIPPING_LINE_CARGO_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 ?? ""));
/** Surcharges sold per origin → destination leg (mirrors RatesService.isRouteScoped). */
export const ROUTE_SCOPED_TRIGGERS = [
"CUSTOMS_CLEARANCE",
"ETHIOPIAN_CUSTOMS_CLEARANCE",
"WITH_RETURN",
"FUEL",
];
/**
* Rates priced per leg: base rail freight, plus the customs clearance fees and
* the empty-container return surcharge (sold per route + container type).
*/
const isRouteScopedRate = (values: Record<string, unknown>) =>
// A shipping line's base freight is priced per leg exactly like a customer's;
// its surcharges are route-scoped on the same triggers.
(isShippingLineRate(values)
? hasShippingLine(values) &&
(values.shippingLineRateKind === "BASE" ||
ROUTE_SCOPED_TRIGGERS.includes(String(values.trigger ?? "")))
: isBaseFreightRate(values)) ||
(String(values.appliesTo ?? "") === "OTHER" &&
ROUTE_SCOPED_TRIGGERS.includes(String(values.trigger ?? "")));
/**
* Surcharges sold per cargo kind: the admin says container or bulk, then names
* the container type or bulk commodity the fee covers.
*/
const isCargoKindTrigger = (values: Record<string, unknown>) =>
["CUSTOMS_CLEARANCE", "ETHIOPIAN_CUSTOMS_CLEARANCE", "CANCELLATION"].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":
// Wagon cancellation fee — scales with the cancelled wagons only.
return ["PER_WAGON"];
case "CUSTOMS_CLEARANCE":
case "ETHIOPIAN_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 "FUEL":
// Per wagon (wagons × rate) or per liter (base liters × rate, once).
return ["PER_WAGON", "PER_LITER"];
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":
return ["PER_CONTAINER", "PER_TON", "PER_KM", "FLAT"];
case "LAST_MILE":
// PER_KM = container mode (distance-banded), PER_TON_KM = bulk mode.
return ["PER_KM", "PER_TON_KM", "PER_CONTAINER", "PER_TON", "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 = "",
) => {
// A shipping-line rate answers the same two questions under different names —
// map them onto the shape the unit table is keyed by. Base freight for a line
// is CONTAINER/BULK freight; a line surcharge is OTHER + its trigger.
if (isShippingLineRate(values)) {
const { shippingLineRateKind: kind, shippingLineCargoKind: cargoKind } = values;
if (kind === "BASE") {
if (cargoKind !== "CONTAINER" && cargoKind !== "BULK") return [];
return allowedRateUnits(
String(cargoKind),
"ALWAYS",
"",
cargoUnitOfMeasure,
).map(unitOption);
}
if (kind === "SURCHARGE" && values.trigger) {
return allowedRateUnits(
"OTHER",
String(values.trigger),
String(values.cargoKind ?? ""),
cargoUnitOfMeasure,
).map(unitOption);
}
return [];
}
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: "ETB (Birr)", value: "ETB" },
{ label: "USD", value: "USD" },
];
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: "hasFuel",
label: "Charge fuel fee",
type: "boolean",
description:
"Bookings of this cargo incur the fuel surcharge (configure the FUEL rate per lane under Rates).",
},
{ 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, disabledOnEdit: 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: "includesEthiopianCustomsOnly",
label: "Ethiopian customs only",
type: "boolean",
description:
"EDR clears customs on the Ethiopian side only. Same clearance flow; contracts and bookings price off the Ethiopian customs clearance rate instead of the standard one.",
showIf: (v) => v.includesCustoms === true,
},
{ 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: [
// "All" and every shape tab show customer rates only — a shipping line's
// negotiated price is its own list, not an extra row in the standard one.
{ key: "all", label: "All", filters: { isShippingLineRate: "false" } },
{
key: "shipping-line",
label: "Shipping line",
filters: { isShippingLineRate: "true" },
},
{ key: "container", label: "Container", filters: { appliesTo: "CONTAINER", isShippingLineRate: "false" } },
{ key: "bulk", label: "Bulk", filters: { appliesTo: "BULK", isShippingLineRate: "false" } },
{ key: "intercity", label: "Intercity", filters: { appliesTo: "INTERCITY", isShippingLineRate: "false" } },
{
key: "trucking",
label: "First / Last mile",
filters: { appliesTo: "FIRST_MILE,LAST_MILE", isShippingLineRate: "false" },
},
{
key: "customs",
label: "Customs clearance",
filters: { trigger: "CUSTOMS_CLEARANCE", isShippingLineRate: "false" },
},
{
key: "ethiopian-customs",
label: "Ethiopian customs",
filters: { trigger: "ETHIOPIAN_CUSTOMS_CLEARANCE", isShippingLineRate: "false" },
},
{
key: "return",
label: "Container return",
filters: { trigger: "WITH_RETURN", isShippingLineRate: "false" },
},
{
key: "surcharges",
label: "Surcharges",
filters: {
appliesTo: "OTHER",
isShippingLineRate: "false",
trigger:
"HAZARDOUS,OVERWEIGHT,REEFER,SHIPPING_LINE,CONSOLIDATION,LASHING,CANCELLATION,PIL_EXTRA_FEE,FUEL",
},
},
],
columns: [
// Blank on a standard customer rate; the owning carrier on a line rate.
{
id: "shippingLineCompany",
header: "Shipping line",
accessorKey: "shippingLineCompany",
format: "entityLabel",
},
{ 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" },
// Container last-mile distance bands; blank for every other rate shape.
{ id: "minKm", header: "From km", accessorKey: "minKm", format: "number" },
{ id: "maxKm", header: "To km", accessorKey: "maxKm", format: "number" },
{ id: "status", header: "Status", accessorKey: "status", format: "rateStatus" },
],
formFields: [
// ── Shipping line rate ────────────────────────────────────────────────
// Flipping this on replaces the whole customer form: the only question
// is which line, and the shape questions follow once it is answered.
{
name: "isShippingLineRate",
label: "Shipping line rate",
type: "boolean",
description:
"Price this rate for one shipping line's own bookings instead of for every customer. A line rate replaces the standard rate on that lane — it does not add to it.",
// The owner is part of a rate's identity, so switching an existing rate
// between customer and line pricing would silently re-target every
// booking that prices off it. Create a new rate instead.
disabledOnEdit: true,
getInitialValue: (record) => Boolean(record.shippingLineCompanyId),
},
{
name: "shippingLineCompanyId",
label: "Shipping line",
type: "select",
required: true,
placeholder: "Which shipping line this rate is for",
description:
"Only this line's bookings price off this rate. A lane the line has no rate for is blocked at booking rather than falling back to the customer price.",
disabledOnEdit: true,
showIf: isShippingLineRate,
},
// What the line is buying. Asked only after a line is picked, so the form
// stays a single question until then.
{
name: "shippingLineRateKind",
label: "Rate type",
type: "select",
required: true,
options: SHIPPING_LINE_RATE_KINDS,
placeholder: "Base freight or a surcharge?",
showIf: hasShippingLine,
// Not stored: base freight carries trigger ALWAYS, a surcharge anything else.
getInitialValue: (record) =>
!record.trigger || record.trigger === "ALWAYS" ? "BASE" : "SURCHARGE",
},
// Container vs bulk — the line form asks this directly instead of folding
// it into `appliesTo` the way the customer form does.
{
name: "shippingLineCargoKind",
label: "Cargo kind",
type: "select",
required: true,
options: SHIPPING_LINE_CARGO_KINDS,
placeholder: "Is this rate for containers or bulk?",
showIf: (v) => hasShippingLine(v) && v.shippingLineRateKind === "BASE",
getInitialValue: (record) =>
record.appliesTo === "BULK" ? "BULK" : "CONTAINER",
},
{
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.",
// Derived from the two questions above on a shipping-line rate.
showIf: (v) => !isShippingLineRate(v),
},
// ── 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"] },
showIf: (v) => !isShippingLineRate(v),
},
// The same trigger list for a shipping-line surcharge — a line incurs the
// same charges a customer does (hazard, reefer, demurrage …), just at its
// own negotiated price.
{
name: "trigger",
label: "Surcharge trigger",
type: "select",
required: true,
options: RATE_TRIGGERS,
placeholder: "What makes this surcharge apply?",
showIf: (v) =>
hasShippingLine(v) && v.shippingLineRateKind === "SURCHARGE",
},
// ── Trade direction — Bulk & Container base freight, plus the directed
// surcharges (customs clearance, cancellation, lashing, fuel; 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.appliesTo ?? "") === "OTHER" &&
String(v.trigger ?? "") === "WITH_RETURN"
? TRADE_DIRECTIONS.filter((d) => d.value === "IMPORT")
: String(v.appliesTo ?? "") === "OTHER" &&
String(v.trigger ?? "") === "FUEL"
? FUEL_TRADE_DIRECTIONS
: TRADE_DIRECTIONS.filter((d) => d.value !== "BOTH"),
showIf: (v) =>
!isShippingLineRate(v) &&
(["BULK", "CONTAINER"].includes(String(v.appliesTo ?? "")) ||
(String(v.appliesTo ?? "") === "OTHER" &&
[
"CUSTOMS_CLEARANCE",
"ETHIOPIAN_CUSTOMS_CLEARANCE",
"CANCELLATION",
"WITH_RETURN",
"LASHING",
"FUEL",
].includes(String(v.trigger ?? "")))),
},
// Shipping lines only ever ship import — the export leg is sold through
// the customer's contract — so the direction is stated, not asked. Shown
// as a locked field rather than hidden so the lane the yard pickers are
// filtered by is visible.
{
name: "tradeDirection",
label: "Trade direction",
type: "select",
required: true,
options: [{ label: "Import", value: "IMPORT" }],
description: "Shipping line rates are import-only.",
disabled: true,
// No defaultValue: field names repeat across form variants and the
// seeded initial value is shared, so defaulting here would pre-select
// Import on the customer form's own direction field too. computeValue
// pins IMPORT on submit and locks the input regardless.
computeValue: () => "IMPORT",
showIf: hasShippingLine,
},
// ── Cargo kind — customs clearance and the cancellation fee are priced
// separately for containers (one rate per container type) and bulk (one
// rate per commodity) ──────────────────────────────────────────────────
{
name: "cargoKind",
label: "Cargo kind",
type: "select",
required: true,
options: INTERCITY_KINDS,
placeholder: "Is this fee for containers or bulk?",
description:
"Container fees are set per container type; bulk fees per commodity. Customs: container per box or wagon, bulk per ton or wagon. Cancellation: per wagon.",
showIf: (v) => v.appliesTo === "OTHER" && isCargoKindTrigger(v),
// Not a stored column: a container fee carries its containerTypeId, a
// bulk fee its cargoTypeId.
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" &&
isCargoKindTrigger(v) &&
v.cargoKind === "CONTAINER",
},
// ── Bulk cargo type — the bulk 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" &&
isCargoKindTrigger(v) &&
v.cargoKind === "BULK",
},
// ── Cargo type — a fuel rate names the commodity it covers (different
// commodities price differently on the same lane) ──────────────────────
{
name: "cargoTypeId",
label: "Cargo type",
type: "select",
required: true,
placeholder: "Which cargo type this fuel rate covers",
description:
"Fuel is charged for bookings of this cargo type (needs “Charge fuel fee” enabled on the cargo type).",
showIf: (v) => v.appliesTo === "OTHER" && v.trigger === "FUEL",
},
// ── 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",
},
// ── Last mile — two calculation modes ─────────────────────────────────
// Bulk bills per ton per km (price = tons × km × rate); Container bills
// per km, banded by distance range with one rate row per container type
// per band (price = km × rate × quantity).
{
name: "lastMileMode",
label: "Calculation mode",
type: "select",
required: true,
options: [
{ label: "Bulk (per ton per km)", value: "BULK" },
{ label: "Container (per km, distance-banded)", value: "CONTAINER" },
],
description:
"Bulk: price = tons × km × rate. Container: price = km × band rate × quantity, one rate per container type per distance band.",
showWhen: { field: "appliesTo", equals: ["LAST_MILE"] },
// Not a stored column: the mode is recorded in the unit the API keeps.
getInitialValue: (record) =>
record.rateUnit === "PER_TON_KM" ? "BULK" : "CONTAINER",
},
{
name: "containerTypeId",
label: "Container type",
type: "select",
required: true,
placeholder: "Which container type this band prices",
description: "20ft and 40ft price differently — one rate per type per band.",
showIf: (v) =>
v.appliesTo === "LAST_MILE" && v.lastMileMode === "CONTAINER",
},
{
name: "minKm",
label: "From km",
type: "number",
required: true,
placeholder: "0",
description: "Band start (inclusive). Use 0 for the first band.",
showIf: (v) =>
v.appliesTo === "LAST_MILE" &&
(v.lastMileMode === "CONTAINER" || v.lastMileMode === "BULK"),
},
{
name: "maxKm",
label: "To km",
type: "number",
optional: true,
placeholder: "Leave empty for no upper limit",
description: "Band end (exclusive) — a 030 band covers up to but not including 30 km.",
showIf: (v) =>
v.appliesTo === "LAST_MILE" &&
(v.lastMileMode === "CONTAINER" || v.lastMileMode === "BULK"),
},
{
name: "currency",
label: "Currency",
type: "select",
required: true,
options: CURRENCIES,
showWhen: { field: "appliesTo", equals: ["LAST_MILE"] },
// Birr is the norm for domestic trucking; USD stays selectable.
defaultValue: "ETB",
getInitialValue: (record) => String(record.currency ?? "ETB"),
},
// ── Distance tiers (create only — the page swaps this for the single
// From/To/value fields when editing an existing band row). Each tier
// becomes its own rate row, so every band keeps edit/delete/approval. ──
{
name: "tiers",
label: "Distance tiers",
type: "tierList",
required: true,
description:
"One rate per distance range — the rate value is per km (container mode) or per ton per km (bulk mode). To km is exclusive (030 then 30+); leave the last tier's To km empty for no upper limit.",
showIf: (v) =>
v.appliesTo === "LAST_MILE" &&
(v.lastMileMode === "CONTAINER" || v.lastMileMode === "BULK"),
},
// ── 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) =>
!isShippingLineRate(v) &&
(v.appliesTo === "CONTAINER" ||
(v.appliesTo === "INTERCITY" && v.intercityKind === "CONTAINER") ||
(v.appliesTo === "OTHER" && v.trigger === "WITH_RETURN")),
},
// Container type for a shipping-line base-freight rate. Required here,
// unlike the customer form's optional catch-all: a line negotiates a
// price per box size, so an unscoped line rate has no meaning.
{
name: "containerTypeId",
label: "Container type",
type: "select",
required: true,
placeholder: "Which container type this rate covers",
showIf: (v) =>
hasShippingLine(v) &&
v.shippingLineRateKind === "BASE" &&
v.shippingLineCargoKind === "CONTAINER",
},
// ── 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) =>
!isShippingLineRate(v) &&
(v.appliesTo === "BULK" ||
(v.appliesTo === "INTERCITY" && v.intercityKind === "BULK")),
},
// Bulk commodity for a shipping-line base-freight rate. Its unit of
// measure decides the rate unit offered below — a counted commodity
// (PER_ITEM) prices per item where a weighed one prices per ton.
{
name: "cargoTypeId",
label: "Bulk cargo type",
type: "select",
required: true,
placeholder: "Which bulk commodity this rate covers",
showIf: (v) =>
hasShippingLine(v) &&
v.shippingLineRateKind === "BASE" &&
v.shippingLineCargoKind === "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",
showIf: (v) => v.appliesTo !== "LAST_MILE",
},
// Last-mile rates carry their own currency (birr or dollar) and the
// value is a per-km / per-ton·km price, so no hardcoded USD suffix.
{
name: "rateValue",
label: "Rate value",
type: "number",
required: true,
description:
"Container mode: price per km for this band. Bulk mode: price per ton per km.",
showWhen: { field: "appliesTo", equals: ["LAST_MILE"] },
},
// 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. Last mile derives its unit from the
// calculation mode instead.
{
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).",
showIf: (v) =>
String(v.trigger ?? "") !== "OVERWEIGHT" &&
String(v.appliesTo ?? "") !== "LAST_MILE",
},
// ── Base liters — per-liter fuel rates only ────────────────────────────
{
name: "baseLiters",
label: "Base (liters)",
type: "number",
required: true,
placeholder: "e.g. 100",
description:
"Liters the surcharge covers — price = base liters × rate value, charged once per booking.",
showIf: (v) =>
v.appliesTo === "OTHER" &&
v.trigger === "FUEL" &&
v.rateUnit === "PER_LITER",
},
],
},
{
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;