mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
refactor pricing data seeder to fold surcharge types into rates update route meta subtitle to remove surcharge types enhance RuleEngineFormDialog to support conditional field visibility remove surcharge types from URL constants and related services add cargo leaf options query for bulk cargo type selection update RuleEngineResourcePage to utilize cargo leaf options modify resources configuration to remove surcharge types implement migration to fold surcharge types into rates create utility to derive legacy rate types from new rate structure
537 lines
19 KiB
TypeScript
537 lines
19 KiB
TypeScript
import type { SidebarItem } from "@/components/layout/types";
|
|
import type { RuleEngineResourceSlug } from "@/types/rule-engine";
|
|
|
|
export type RuleEngineNavCategory = "configuration" | "rules";
|
|
|
|
export type ColumnFormat =
|
|
| "text"
|
|
| "code"
|
|
| "boolean"
|
|
| "activeBadge"
|
|
| "rateStatus"
|
|
| "date"
|
|
| "number"
|
|
| "entityLabel"
|
|
| "rateLabel";
|
|
|
|
export type FormFieldType = "text" | "number" | "boolean" | "date" | "email" | "select" | "multiselect" | "textarea";
|
|
|
|
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;
|
|
/** 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[] };
|
|
}
|
|
|
|
export interface RuleEngineOrderConfig {
|
|
field: "displayOrder" | "stepOrder";
|
|
scopeField?: "requiresDirectorApproval";
|
|
label: string;
|
|
}
|
|
|
|
export interface RuleEngineResourceConfig {
|
|
slug: RuleEngineResourceSlug;
|
|
label: string;
|
|
subtitle: string;
|
|
category: RuleEngineNavCategory;
|
|
searchPlaceholder: string;
|
|
columns: ResourceColumn[];
|
|
formFields: FormFieldDef[];
|
|
supportsSearch?: boolean;
|
|
orderConfig?: RuleEngineOrderConfig;
|
|
/** 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" },
|
|
];
|
|
|
|
const APPROVAL_ROLES = [
|
|
{ label: "Line staff", value: "LINE_STAFF" },
|
|
{ label: "Director", value: "DIRECTOR" },
|
|
{ label: "CEO", 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 (per excess ton)", value: "OVERWEIGHT" },
|
|
{ label: "Reefer cargo", value: "REEFER" },
|
|
{ label: "Shipping line mapped", value: "SHIPPING_LINE" },
|
|
{ label: "Consolidation", value: "CONSOLIDATION" },
|
|
{ label: "Cancellation", value: "CANCELLATION" },
|
|
{ label: "Demurrage", value: "DEMURRAGE" },
|
|
{ label: "Shipping line extra fee (PIL)", value: "PIL_EXTRA_FEE" },
|
|
];
|
|
|
|
const RATE_UNITS =["PER_WAGON", "PER_TON", "PER_CONTAINER", "PER_KM", "PER_INVOICE", "FLAT"].map(
|
|
(v) => ({
|
|
label: v.replace(/_/g, " "),
|
|
value: v,
|
|
}),
|
|
);
|
|
|
|
const CURRENCIES = [
|
|
{ label: "USD", value: "USD" },
|
|
{ label: "ETB", value: "ETB" },
|
|
];
|
|
|
|
const PRIORITY_CONFIG_TYPES = [
|
|
{ label: "Wagon count", value: "WAGON" },
|
|
{ label: "Payment currency", value: "CURRENCY" },
|
|
];
|
|
|
|
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: "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: "parentGroupId",
|
|
label: "Parent group",
|
|
type: "select",
|
|
optional: true,
|
|
placeholder: "Select parent cargo type (optional)",
|
|
},
|
|
{ name: "showFreeTextBox", label: "Show free text box", type: "boolean" },
|
|
{ name: "requiresDirectorApproval", label: "Requires director approval", type: "boolean" },
|
|
{ name: "isActive", label: "Active", type: "boolean" },
|
|
],
|
|
},
|
|
{
|
|
slug: "container-types",
|
|
label: "Container Types",
|
|
category: "configuration",
|
|
subtitle: "Configure container sizes and wagon capacity",
|
|
searchPlaceholder: "Search container types...",
|
|
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" },
|
|
{ id: "wagonsPerUnit", header: "Wagons / unit", accessorKey: "wagonsPerUnit", format: "number" },
|
|
activeColumn,
|
|
],
|
|
formFields: [
|
|
{ name: "label", label: "Label", type: "text", required: true },
|
|
{ name: "sizeFt", label: "Size (ft)", type: "number", required: true },
|
|
{ name: "wagonsPerUnit", label: "Wagons per unit", type: "number", required: true },
|
|
{ name: "isReefer", label: "Reefer", type: "boolean" },
|
|
{ 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...",
|
|
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: "maxWagonsPerTrain", header: "Max / train", accessorKey: "maxWagonsPerTrain", format: "number" },
|
|
{
|
|
id: "supportedLoadTypes",
|
|
header: "Load types",
|
|
accessorKey: "supportedLoadTypes",
|
|
},
|
|
activeColumn,
|
|
],
|
|
formFields: [
|
|
{ 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 },
|
|
{
|
|
name: "maxWagonsPerTrain",
|
|
label: "Max wagons per train",
|
|
type: "number",
|
|
optional: true,
|
|
},
|
|
{
|
|
name: "supportedLoadTypes",
|
|
label: "Supported load types",
|
|
type: "textarea",
|
|
optional: true,
|
|
placeholder: "CONTAINER, BULK",
|
|
},
|
|
{ name: "isActive", label: "Active", type: "boolean" },
|
|
],
|
|
},
|
|
{
|
|
slug: "priority-configs",
|
|
label: "Priority Rules",
|
|
category: "rules",
|
|
subtitle: "Wagon-count and payment-currency scoring rules",
|
|
searchPlaceholder: "Search priority rules...",
|
|
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"] },
|
|
},
|
|
{ name: "minWagonCount", label: "Min wagon count", type: "number", required: true },
|
|
{ name: "maxWagonCount", label: "Max wagon count", type: "number", required: true },
|
|
{ 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" },
|
|
{ id: "priorityBonusPoints", header: "Bonus pts", accessorKey: "priorityBonusPoints", 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: "priorityBonusPoints", label: "Priority bonus points", type: "number" },
|
|
{ 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...",
|
|
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: "effectiveFrom", header: "From", accessorKey: "effectiveFrom", format: "date" },
|
|
{ id: "effectiveTo", header: "To", accessorKey: "effectiveTo", format: "date" },
|
|
],
|
|
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: "effectiveFrom", label: "Effective from", type: "date", required: true },
|
|
{ name: "effectiveTo", label: "Effective to", type: "date" },
|
|
],
|
|
},
|
|
{
|
|
slug: "yards",
|
|
label: "Yards",
|
|
category: "configuration",
|
|
subtitle: "Terminal and yard locations",
|
|
searchPlaceholder: "Search yards...",
|
|
orderConfig: { field: "displayOrder", label: "Display order" },
|
|
columns: [
|
|
codeColumn("code"),
|
|
{ id: "label", header: "Label", accessorKey: "label" },
|
|
{ id: "country", header: "Country", accessorKey: "country" },
|
|
{ id: "displayOrder", header: "Order", accessorKey: "displayOrder", format: "number" },
|
|
activeColumn,
|
|
],
|
|
formFields: [
|
|
{ name: "label", label: "Label", type: "text", required: true },
|
|
{ name: "country", label: "Country", type: "text", required: true },
|
|
{ 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...",
|
|
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...",
|
|
columns: [
|
|
{ id: "appliesTo", header: "Applies to", accessorKey: "appliesTo", format: "code" },
|
|
{ id: "trigger", header: "Trigger", accessorKey: "trigger" },
|
|
{ id: "rateValue", header: "Value", accessorKey: "rateValue", format: "number" },
|
|
{ id: "rateUnit", header: "Unit", accessorKey: "rateUnit" },
|
|
{ id: "status", header: "Status", accessorKey: "status", format: "rateStatus" },
|
|
{ id: "effectiveFrom", header: "From", accessorKey: "effectiveFrom", format: "date" },
|
|
],
|
|
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 only (intercity is domestic) ───
|
|
{
|
|
name: "tradeDirection",
|
|
label: "Trade direction",
|
|
type: "select",
|
|
required: true,
|
|
options: TRADE_DIRECTIONS.filter((d) => d.value !== "BOTH"),
|
|
showWhen: { field: "appliesTo", equals: ["BULK", "CONTAINER"] },
|
|
},
|
|
// ── Container type — Container & Intercity ────────────────────────────
|
|
{
|
|
name: "containerTypeId",
|
|
label: "Container type",
|
|
type: "select",
|
|
optional: true,
|
|
placeholder: "Select container type (optional)",
|
|
showWhen: { field: "appliesTo", equals: ["CONTAINER", "INTERCITY"] },
|
|
},
|
|
// ── Bulk cargo (leaf commodity) — Bulk & Intercity ───────────────────
|
|
{
|
|
name: "cargoTypeId",
|
|
label: "Bulk cargo type",
|
|
type: "select",
|
|
optional: true,
|
|
placeholder: "Select bulk commodity (optional)",
|
|
showWhen: { field: "appliesTo", equals: ["BULK", "INTERCITY"] },
|
|
},
|
|
{ name: "rateValue", label: "Rate value", type: "number", required: true },
|
|
{ name: "rateUnit", label: "Rate unit", type: "select", required: true, options: RATE_UNITS },
|
|
{ name: "effectiveFrom", label: "Effective from", type: "date", required: true },
|
|
{ name: "effectiveTo", label: "Effective to", type: "date" },
|
|
],
|
|
},
|
|
{
|
|
slug: "approval-rules",
|
|
label: "Approval Rules",
|
|
category: "rules",
|
|
cardTitleKey: "actionLabel",
|
|
cardSubtitleKey: "requiredRole",
|
|
subtitle: "Multi-step booking approval chain",
|
|
searchPlaceholder: "Search approval rules...",
|
|
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,
|
|
options: APPROVAL_ROLES,
|
|
},
|
|
{ name: "actionLabel", label: "Action label", type: "text", required: true },
|
|
{
|
|
name: "blocksRole",
|
|
label: "Blocks role",
|
|
type: "select",
|
|
optional: true,
|
|
options: [{ label: "None", value: RULE_ENGINE_SELECT_NONE }, ...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),
|
|
}));
|
|
|
|
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;
|