feat: implement shipping line bookings management

- Add ShippingLineBookingsPage for listing and managing shipping line bookings.
- Create ShippingLineDocumentsModal for document uploads related to bookings.
- Introduce ShippingLineInitiateModal for initiating new shipping line bookings.
- Implement booking document state management with booking-doc-state utility.
- Add shipping line bookings service for API interactions.
- Update index to export new components and services.
- Enhance types for freight to include shipping line credits.
This commit is contained in:
marshalyordanos
2026-08-13 15:54:40 +03:00
parent 9aae132dd4
commit 9fff469ffa
50 changed files with 4485 additions and 77 deletions

View File

@@ -257,6 +257,34 @@ const RuleEngineFormDialog = ({
next.cargoTypeId = "";
next.rateUnit = "";
}
// Turning the shipping-line toggle on or off swaps the entire form, so
// nothing answered under the other shape may survive into the payload.
if (name === "isShippingLineRate") {
next.shippingLineCompanyId = "";
next.shippingLineRateKind = "";
next.shippingLineCargoKind = "";
next.appliesTo = "";
next.trigger = "";
next.containerTypeId = "";
next.cargoTypeId = "";
next.originYardId = "";
next.destinationYardId = "";
next.rateUnit = "";
}
// Base-vs-surcharge and container-vs-bulk each decide the scope field and
// the legal units for a shipping-line rate, exactly as appliesTo and
// cargoKind do on the customer form.
if (name === "shippingLineRateKind" || name === "shippingLineCargoKind") {
next.containerTypeId = "";
next.cargoTypeId = "";
next.rateUnit = "";
if (name === "shippingLineRateKind") {
next.shippingLineCargoKind = "";
next.trigger = "";
next.originYardId = "";
next.destinationYardId = "";
}
}
return next;
});
};
@@ -347,12 +375,23 @@ const RuleEngineFormDialog = ({
borderRadius: "var(--mantine-radius-md)",
}}
>
<Text size="sm" fw={600}>
{field.label}
</Text>
<Stack gap={2}>
<Text size="sm" fw={600}>
{field.label}
</Text>
{field.description ? (
<Text size="xs" c="dimmed">
{field.description}
</Text>
) : null}
</Stack>
<Switch
checked={Boolean(values[field.name])}
onChange={(e) => setField(field.name, e.currentTarget.checked)}
// A toggle that re-targets what an existing record means (e.g. who
// a rate is priced for) is create-only — flipping it on a saved row
// would silently change every booking that prices off it.
disabled={field.disabled || (field.disabledOnEdit && !!initialRecord)}
size="md"
color="edr-green"
/>
@@ -493,6 +532,12 @@ const RuleEngineFormDialog = ({
// Dynamic options (e.g. rate unit) resolve from the live form values so
// the choices track the other fields the admin has picked.
const options = field.optionsFromValues ? field.optionsFromValues(values) : (field.options ?? []);
// A derived select shows (and submits) its computed value and is locked,
// matching the text-input branch — used by fields the shape decides on the
// admin's behalf, e.g. a shipping-line rate's import-only direction.
const computedSelect = field.computeValue
? String(field.computeValue(values) ?? "")
: undefined;
return (
<Select
key={field.name}
@@ -501,9 +546,18 @@ const RuleEngineFormDialog = ({
placeholder={
selectOptionsLoading ? "Loading options..." : (field.placeholder ?? "Select an option")
}
value={resolveSelectValue(field, values)}
value={
computedSelect !== undefined
? computedSelect
: resolveSelectValue(field, values)
}
onChange={(v) => setField(field.name, v === RULE_ENGINE_SELECT_NONE ? "" : v)}
disabled={selectOptionsLoading}
disabled={
selectOptionsLoading ||
field.disabled ||
(field.disabledOnEdit && !!initialRecord) ||
computedSelect !== undefined
}
// Mantine's Select is not a native input, so `required` only marks it
// visually — handleSubmit is what actually blocks an empty one.
required={field.required}

View File

@@ -3,6 +3,8 @@ import toast from "react-hot-toast";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
import { api } from "@/services/api";
import { shippingLineCompaniesService } from "@/services/shippingLineCompanies.service";
import type { PaginatedShippingLineCompanies } from "@/types/shippingLineCompany";
import {
ruleEngineService,
type RuleEngineListParams,
@@ -165,6 +167,28 @@ export const useContainerTypeOptions = (
select: (rows) => buildContainerTypeSelectOptions(rows, includeNone),
});
/**
* Shipping lines a rate can be scoped to. Only ACTIVE lines are offered — the
* API refuses a rate filed against a suspended one, so listing them would only
* produce an error on submit. Sorted by name so the picker is scannable.
*/
export const useShippingLineCompanyOptions = (enabled = true) =>
useQuery({
queryKey: QUERY_KEYS.RULE_ENGINE.selectOptions("shipping-line-companies", {}),
// One page well past the number of carriers on the corridor; the picker
// needs the whole list, not a page of it.
queryFn: () => shippingLineCompaniesService.list(1, 200),
enabled,
select: (page: PaginatedShippingLineCompanies) =>
page.items
.filter((line) => line.status === "active")
.map((line) => ({
label: line.scacCode ? `${line.name} (${line.scacCode})` : line.name,
value: line.id,
}))
.sort((a, b) => a.label.localeCompare(b.label)),
});
/**
* Approval-step role options, sourced from the live IAM position types. The
* three pre-IAM role strings are appended (marked "(legacy)") so an approval

View File

@@ -136,9 +136,20 @@ export default function DocumentClearanceDetailPage() {
clearance?.milestones?.some(
(m) => m.milestoneCode === "DOCUMENTS_APPROVED" && m.status === "COMPLETED",
) ?? false;
const queriesLocked = Boolean(
(clearance as Freight.ContractClearanceView | undefined)?.preClearanceFinalized,
);
// Querying a document is only possible while the booking is actually in
// review — the server enforces exactly that (reviewDocument asserts
// DOCUMENTS_UNDER_REVIEW), so once clearance is finalized the button could
// only ever produce a 400.
//
// `preClearanceFinalized` alone was not enough: it is a phased-customs field,
// so a non-customs booking (self-clearance, and every shipping-line booking)
// never sets it and kept offering Query after Operations had finalized.
const queriesLocked =
Boolean(
(clearance as Freight.ContractClearanceView | undefined)
?.preClearanceFinalized,
) ||
(booking?.status != null && booking.status !== "DOCUMENTS_UNDER_REVIEW");
const workflowFiles =
(clearance as Freight.ContractClearanceView | undefined)?.workflowFiles ?? [];

View File

@@ -43,6 +43,7 @@ import {
useCargoTypeParentOptions,
useContainerTypeOptions,
useLiveRateOptions,
useShippingLineCompanyOptions,
useWagonTypeOptions,
useYardOptions,
type YardOption,
@@ -93,6 +94,20 @@ const yardOptionsForLegEnd = (
values: Record<string, unknown>,
end: "origin" | "destination",
): { label: string; value: string }[] => {
// A shipping-line rate names its shape in its own fields and is always
// import; map it onto the appliesTo/direction pair the rest of this function
// reads so the country narrowing is shared rather than duplicated.
if (values.isShippingLineRate === true) {
if (!values.shippingLineCompanyId) return [];
const isBase = values.shippingLineRateKind === "BASE";
values = {
...values,
appliesTo: isBase
? String(values.shippingLineCargoKind ?? "")
: "OTHER",
tradeDirection: "IMPORT",
};
}
const appliesTo = String(values.appliesTo ?? "");
let country: string | undefined;
if (appliesTo === "INTERCITY") {
@@ -280,6 +295,13 @@ const RuleEngineResourcePage = () => {
useContainerTypeOptions(false, usesContainerTypeField);
const { data: liveRateOptions, isLoading: liveRateOptionsLoading } =
useLiveRateOptions(usesLiveRateField);
const usesShippingLineField = Boolean(
config?.formFields.some((f) => f.name === "shippingLineCompanyId"),
);
const {
data: shippingLineOptions,
isLoading: shippingLineOptionsLoading,
} = useShippingLineCompanyOptions(usesShippingLineField);
const { data: wagonTypeOptions, isLoading: wagonTypeOptionsLoading } =
useWagonTypeOptions(usesWagonTypeField);
const usesYardField = Boolean(
@@ -390,6 +412,13 @@ const RuleEngineResourcePage = () => {
),
};
}
if (field.name === "shippingLineCompanyId") {
return {
...field,
type: "select" as const,
options: shippingLineOptions ?? [],
};
}
if (field.name === "rateId") {
return {
...field,
@@ -612,7 +641,39 @@ const RuleEngineResourcePage = () => {
const handleFormSubmit = (values: Record<string, unknown>) => {
let payload = values;
if (config.slug === "rates") {
if (config.slug === "rates" && values.isShippingLineRate === true) {
// A shipping-line rate asks its shape as "base freight vs surcharge" +
// "container vs bulk"; the API takes the same appliesTo/trigger pair as a
// customer rate, so translate here and drop the form-only fields. Always
// import (the only direction a line ships) and always USD.
const {
isShippingLineRate: _toggle,
shippingLineRateKind,
shippingLineCargoKind,
...rest
} = values;
void _toggle;
const isBase = shippingLineRateKind === "BASE";
payload = {
...rest,
appliesTo: isBase ? String(shippingLineCargoKind ?? "CONTAINER") : "OTHER",
trigger: isBase ? "ALWAYS" : values.trigger,
tradeDirection: "IMPORT",
currency: "USD",
};
if (editing?.id && editing.status === "LIVE") {
rateChangeWorkflow.submit.mutate(
{ rateId: String(editing.id), update: payload },
{
onSuccess: () => {
setFormOpen(false);
setEditing(null);
},
},
);
return;
}
} else if (config.slug === "rates") {
// Base-freight categories have no surcharge trigger field — the engine
// treats them as ALWAYS. Surcharges (Applies to = Other) keep their
// chosen trigger.
@@ -934,6 +995,7 @@ const RuleEngineResourcePage = () => {
(usesLiveRateField && liveRateOptionsLoading) ||
(usesWagonTypeField && wagonTypeOptionsLoading) ||
(usesYardField && yardOptionsLoading) ||
(usesShippingLineField && shippingLineOptionsLoading) ||
(usesApprovalRoleField && approvalRoleOptionsLoading)
}
positionOptions={!editing ? createPositionOptions : undefined}

View File

@@ -97,7 +97,16 @@ export interface RuleEngineOrderConfig {
export interface RuleEngineListTab {
key: string;
label: string;
filters: { appliesTo?: string; trigger?: 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 {
@@ -205,6 +214,36 @@ const INTERCITY_KINDS = [
{ 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 ?? ""));
@@ -214,7 +253,15 @@ const isBaseFreightRate = (values: Record<string, unknown>) =>
* the empty-container return surcharge (sold per route + container type).
*/
const isRouteScopedRate = (values: Record<string, unknown>) =>
isBaseFreightRate(values) ||
// 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" ||
["CUSTOMS_CLEARANCE", "WITH_RETURN", "FUEL"].includes(
String(values.trigger ?? ""),
))
: isBaseFreightRate(values)) ||
(String(values.appliesTo ?? "") === "OTHER" &&
["CUSTOMS_CLEARANCE", "WITH_RETURN", "FUEL"].includes(String(values.trigger ?? "")));
@@ -303,6 +350,31 @@ 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 [];
@@ -830,36 +902,51 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
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" } },
// "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" },
filters: { appliesTo: "FIRST_MILE,LAST_MILE", isShippingLineRate: "false" },
},
{
key: "customs",
label: "Customs clearance",
filters: { trigger: "CUSTOMS_CLEARANCE" },
filters: { trigger: "CUSTOMS_CLEARANCE", isShippingLineRate: "false" },
},
{
key: "return",
label: "Container return",
filters: { trigger: "WITH_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
@@ -879,6 +966,59 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
{ 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",
@@ -887,6 +1027,8 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
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 ──────────────────
{
@@ -897,6 +1039,20 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
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 route-
// scoped surcharges (customs clearance; empty-container return, which is
@@ -915,11 +1071,31 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
? FUEL_TRADE_DIRECTIONS
: TRADE_DIRECTIONS.filter((d) => d.value !== "BOTH"),
showIf: (v) =>
["BULK", "CONTAINER"].includes(String(v.appliesTo ?? "")) ||
(String(v.appliesTo ?? "") === "OTHER" &&
["CUSTOMS_CLEARANCE", "WITH_RETURN", "LASHING", "FUEL"].includes(
String(v.trigger ?? ""),
)),
!isShippingLineRate(v) &&
(["BULK", "CONTAINER"].includes(String(v.appliesTo ?? "")) ||
(String(v.appliesTo ?? "") === "OTHER" &&
["CUSTOMS_CLEARANCE", "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 is priced separately for containers
// (one rate per container type) and bulk ───────────────────────────────
@@ -1089,9 +1265,24 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
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"),
!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 ─
{
@@ -1101,8 +1292,23 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
optional: true,
placeholder: "Select bulk commodity (optional)",
showIf: (v) =>
v.appliesTo === "BULK" ||
(v.appliesTo === "INTERCITY" && v.intercityKind === "BULK"),
!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

View File

@@ -20,6 +20,11 @@ export interface RuleEngineListParams {
/** Rates category tabs — comma-separated appliesTo / trigger filters. */
appliesTo?: string;
trigger?: string;
/**
* Rates only: "true" lists shipping-line rates, "false" standard customer
* ones. Omitted lists both.
*/
isShippingLineRate?: string;
}
export interface RuleEngineReorderPayload {
@@ -214,6 +219,7 @@ export const ruleEngineService = {
requiresDirectorApproval: params?.requiresDirectorApproval,
appliesTo: params?.appliesTo,
trigger: params?.trigger,
isShippingLineRate: params?.isShippingLineRate,
},
});
return normalizeList<T>(response.data, page, pageSize);