Merge pull request #943 from Tria-plc/freight_feature/usermanagement

changes
This commit is contained in:
marshal
2026-07-23 17:20:24 +03:00
committed by GitHub
15 changed files with 362 additions and 160 deletions

View File

@@ -128,7 +128,10 @@ export function computeGlShipmentTotal(
const qty = q.bulkQuantity;
const rate =
rateFor(
(i) => (i.unit === "per_ton" || i.unit === "per_item") && !i.isClearance,
(i) =>
(i.unit === "per_ton" || i.unit === "per_item") &&
!i.isClearance &&
!i.conditionalOn,
) ?? items[0];
if (rate && qty > 0) {
lines.push({
@@ -165,6 +168,23 @@ export function computeGlShipmentTotal(
}
}
// Lashing / cargo securing — bulk-only, applies whenever the contract shows
// it (the commodity needs lashing). Per-ton scales by tonnage; per-wagon
// depends on the wagon capacity the train stocks — shown at real pricing.
const lashing = items.find((i) => i.conditionalOn === "has_lashing");
if (lashing && lashing.unit === "per_ton") {
const tons = q.bulkQuantity;
if (tons > 0) {
lines.push({
label: lashing.label,
unitPrice: lashing.unitPrice,
unit: lashing.unit,
quantity: tons,
amount: lashing.unitPrice * tons,
});
}
}
// Customs clearance service fee — billed on the booking invoice with the
// freight. Container fees estimate per size (per box, or per wagon: two 20ft
// share one); bulk per-ton scales by tonnage. Bulk per-wagon fees depend on

View File

@@ -213,6 +213,7 @@ const RuleEngineFormDialog = ({
// and the legal units (container → per box/wagon, bulk → per ton/wagon).
if (name === "cargoKind") {
next.containerTypeId = "";
next.cargoTypeId = "";
next.rateUnit = "";
}
return next;
@@ -339,6 +340,10 @@ const RuleEngineFormDialog = ({
value={resolveSelectValue(field, values)}
onChange={(v) => setField(field.name, v === RULE_ENGINE_SELECT_NONE ? "" : v)}
disabled={selectOptionsLoading}
// Native required blocks submit while a mandatory select is empty —
// without it the form posts and the API 400s (e.g. a container
// customs/lashing rate with no container type picked).
required={field.required}
data={options
.filter((opt) => opt.value !== "")
.map((opt) => ({

View File

@@ -172,7 +172,7 @@ const RATE_TRIGGERS = [
},
{ label: "Shipping line mapped", value: "SHIPPING_LINE" },
{ label: "Penalty", value: "CONSOLIDATION" },
{ label: "Lashing (per container type / bulk)", value: "LASHING" },
{ label: "Lashing (bulk, per cargo type)", value: "LASHING" },
{ label: "Cancellation", value: "CANCELLATION" },
{ label: "Shipping line extra fee (PIL)", value: "PIL_EXTRA_FEE" },
{ label: "Customs clearance service fee (billed with the booking)", value: "CUSTOMS_CLEARANCE" },
@@ -229,11 +229,13 @@ const allowedRateUnits = (
case "CANCELLATION":
return ["FLAT", "PER_INVOICE"];
case "CUSTOMS_CLEARANCE":
case "LASHING":
// Per cargo kind: container fees per box/wagon, bulk per ton/wagon.
return cargoKind === "BULK"
? ["PER_TON", "PER_WAGON"]
: ["PER_CONTAINER", "PER_WAGON"];
case "LASHING":
// Bulk-only cargo securing — per ton or per wagon.
return ["PER_TON", "PER_WAGON"];
case "CONSOLIDATION":
case "SHIPPING_LINE":
case "PIL_EXTRA_FEE":
@@ -722,10 +724,12 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
showIf: (v) =>
["BULK", "CONTAINER"].includes(String(v.appliesTo ?? "")) ||
(String(v.appliesTo ?? "") === "OTHER" &&
["CUSTOMS_CLEARANCE", "WITH_RETURN"].includes(String(v.trigger ?? ""))),
["CUSTOMS_CLEARANCE", "WITH_RETURN", "LASHING"].includes(
String(v.trigger ?? ""),
)),
},
// ── Cargo kind — customs clearance and lashing are priced separately
// for containers (one rate per container type) and bulk ────────────────
// ── Cargo kind — customs clearance is priced separately for containers
// (one rate per container type) and bulk ───────────────────────────────
{
name: "cargoKind",
label: "Cargo kind",
@@ -736,8 +740,7 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
description:
"Container fees bill per box or wagon (one rate per container type); bulk fees bill per ton or wagon.",
showIf: (v) =>
v.appliesTo === "OTHER" &&
["CUSTOMS_CLEARANCE", "LASHING"].includes(String(v.trigger ?? "")),
v.appliesTo === "OTHER" && v.trigger === "CUSTOMS_CLEARANCE",
// Not a stored column: a container fee carries its containerTypeId, a
// bulk fee carries none.
getInitialValue: (record) =>
@@ -752,9 +755,34 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
placeholder: "Which container type this fee covers",
showIf: (v) =>
v.appliesTo === "OTHER" &&
["CUSTOMS_CLEARANCE", "LASHING"].includes(String(v.trigger ?? "")) &&
v.trigger === "CUSTOMS_CLEARANCE" &&
v.cargoKind === "CONTAINER",
},
// ── Bulk cargo type — the bulk customs fee names its commodity ────────
{
name: "cargoTypeId",
label: "Bulk cargo type",
type: "select",
required: true,
placeholder: "Which bulk commodity this fee covers",
showIf: (v) =>
v.appliesTo === "OTHER" &&
v.trigger === "CUSTOMS_CLEARANCE" &&
v.cargoKind === "BULK",
},
// ── Bulk cargo type — lashing is bulk-only; may narrow to one leaf
// commodity (specific wins over the commodity-wide catch-all) ──────────
{
name: "cargoTypeId",
label: "Bulk cargo type",
type: "select",
optional: true,
placeholder: "All lashing commodities (optional)",
description:
"Leave empty to cover every lashing commodity; a commodity-specific rate wins over the catch-all.",
showIf: (v) =>
v.appliesTo === "OTHER" && v.trigger === "LASHING",
},
// ── Cargo kind — Intercity only (import/export get it from appliesTo) ─
{
name: "intercityKind",