mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
changes
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { isAxiosError } from "axios";
|
||||
import {
|
||||
AlertCircle,
|
||||
Check,
|
||||
@@ -218,6 +219,12 @@ export default function NewContractPage({
|
||||
},
|
||||
});
|
||||
|
||||
// 422 from generate-price = a required rate isn't configured for the chosen
|
||||
// route — the customer can't fix it, so it's surfaced as a blocking modal.
|
||||
const rateNotConfigured =
|
||||
isAxiosError(persistAndPriceMutation.error) &&
|
||||
persistAndPriceMutation.error.response?.status === 422;
|
||||
|
||||
const confirmMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!priceContractId) throw new Error("No contract to confirm");
|
||||
@@ -759,7 +766,7 @@ export default function NewContractPage({
|
||||
<StepIndicator step={step} steps={visibleSteps} />
|
||||
</Box>
|
||||
|
||||
{persistAndPriceMutation.isError && (
|
||||
{persistAndPriceMutation.isError && !rateNotConfigured && (
|
||||
<Alert
|
||||
color="red"
|
||||
icon={<AlertCircle size={16} />}
|
||||
@@ -777,6 +784,28 @@ export default function NewContractPage({
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Pricing not configured (422) — a rate is missing on the chosen
|
||||
route, nothing the customer can fix. Block with a modal. */}
|
||||
<Modal
|
||||
opened={rateNotConfigured}
|
||||
onClose={() => persistAndPriceMutation.reset()}
|
||||
title="Contract creation unavailable"
|
||||
centered
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Text size="sm">
|
||||
You can't create a contract right now — pricing hasn't
|
||||
been configured for the selected route yet. Please contact
|
||||
support for assistance.
|
||||
</Text>
|
||||
<Group justify="flex-end">
|
||||
<Button onClick={() => persistAndPriceMutation.reset()}>
|
||||
OK
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Step 0 — Setup: operation, contract, service, currency, miles. */}
|
||||
{step === 0 && (
|
||||
<StepCard>
|
||||
|
||||
@@ -111,7 +111,10 @@ export function computeShipmentTotal(
|
||||
const qty = Number(values.cargoWeightTons || values.itemCount || 0);
|
||||
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({
|
||||
@@ -150,6 +153,23 @@ export function computeShipmentTotal(
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = Number(values.cargoWeightTons || 0);
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user