From ab355d0e16023e3c954577abf01f9596a8c3864c Mon Sep 17 00:00:00 2001 From: Marshal Date: Mon, 20 Jul 2026 06:10:50 +0000 Subject: [PATCH] Enhance cargo description handling in contract forms --- .../src/pages/contracts/NewContractPage.tsx | 2 + .../new-contract-form/contractToForm.ts | 5 +- .../contracts/new-contract-form/schema.ts | 8 + .../new-contract-form/step3-cargo-scope.tsx | 201 +++++++++++++++--- .../new-contract-form/step8-review.tsx | 2 +- 5 files changed, 185 insertions(+), 33 deletions(-) diff --git a/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx b/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx index 8636726ca..d060b7651 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx @@ -520,6 +520,8 @@ export default function NewContractPage({ const cargoScope: Freight.CreateContractCargoScopeDto[] = isContainer ? data.enabledContainerSizes.map((size) => ({ containerSize: size, + // Required cargo description — what the containers carry. + cargoFreeText: data.cargoFreeText.trim() || undefined, })) : [ { diff --git a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/contractToForm.ts b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/contractToForm.ts index b2fc5168c..db9706e0c 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/contractToForm.ts +++ b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/contractToForm.ts @@ -119,7 +119,10 @@ export function contractToFormValues( enabledContainerSizes as ContractFormInputValues["enabledContainerSizes"], containerSizeCaps, cargoTypePath, - cargoFreeText: bulkRow?.cargoFreeText ?? "", + // Bulk: the commodity free-text; container: the required cargo + // description (stored on every size row — read the first). + cargoFreeText: + (isContainer ? scope[0]?.cargoFreeText : bulkRow?.cargoFreeText) ?? "", bulkQuantityCap: isGeneral && bulkRow?.quantityCap != null ? bulkRow.quantityCap : 0, isHazardous: contract.isHazardous, diff --git a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/schema.ts b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/schema.ts index 5a15970a4..c026b7b13 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/schema.ts +++ b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/schema.ts @@ -231,6 +231,14 @@ export const contractFormSchema = z message: "Enable at least one container size.", }); } + // Containerized cargo must say WHAT is inside — required description. + if (!data.cargoFreeText.trim()) { + ctx.addIssue({ + code: "custom", + path: ["cargoFreeText"], + message: "Describe the cargo carried in the containers.", + }); + } } if (data.cargoType === "bulk") { // Bulk scope: a commodity is required. diff --git a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step3-cargo-scope.tsx b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step3-cargo-scope.tsx index 3de8baad1..15b2ac4b7 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step3-cargo-scope.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step3-cargo-scope.tsx @@ -1,15 +1,16 @@ import { useEffect, useMemo, useRef } from "react"; import { Controller, type UseFormReturn } from "react-hook-form"; -import { Flame, RotateCcw, Snowflake } from "lucide-react"; +import { Check, Container, Flame, RotateCcw, Snowflake } from "lucide-react"; import { Box, Group, - MultiSelect, Select, Skeleton, Stack, Switch, Text, + Textarea, + UnstyledButton, } from "@mantine/core"; import type { Freight } from "@edr/types"; import { @@ -18,9 +19,21 @@ import { } from "./schema"; import { fieldStyles, SelectField, StepLabel } from "./shared"; -const CONTAINER_SIZE_OPTIONS = [ - { value: "20ft", label: "20ft Container (TEU)" }, - { value: "40ft", label: "40ft Container (FEU)" }, +const CONTAINER_SIZE_OPTIONS: Array<{ + value: "20ft" | "40ft"; + label: string; + description: string; +}> = [ + { + value: "20ft", + label: "20ft Container", + description: "Standard twenty-foot unit (TEU)", + }, + { + value: "40ft", + label: "40ft Container", + description: "Standard forty-foot unit (FEU)", + }, ]; const CARGO_TYPE_OPTIONS = [ @@ -115,6 +128,9 @@ export function Step3CargoScope({ onChange={(v) => { if (!v) return; field.onChange(v); + // cargoFreeText is shared (bulk commodity label / container + // description) — clear it so text never carries across types. + form.setValue("cargoFreeText", "", { shouldDirty: true }); if (v === "container") { form.setValue("cargoTypePath", [], { shouldDirty: true }); } else { @@ -134,34 +150,78 @@ export function Step3CargoScope({ )} /> - {/* Container scope: enabled sizes as a multi-select. */} - {cargoType === "container" && ( - ( - - field.onChange(v as ("20ft" | "40ft")[]) - } - onBlur={field.onBlur} - error={fieldState.error?.message} - radius={10} - checkIconPosition="right" - comboboxProps={{ withinPortal: true, shadow: "md", radius: "md" }} - styles={fieldStyles} - /> - )} - /> - )} + {/* Container scope: enabled sizes as tick-cards — tap to toggle, one or + both can be in scope. Clearer than a multi-select for two options. */} + {cargoType === "container" && ( + { + const selected = (field.value ?? []) as ("20ft" | "40ft")[]; + const toggle = (size: "20ft" | "40ft") => { + field.onChange( + selected.includes(size) + ? selected.filter((s) => s !== size) + : [...selected, size], + ); + field.onBlur(); + }; + return ( + + Container sizes in scope * + + Tick every size this contract should cover — you can select + both. + +
+ {CONTAINER_SIZE_OPTIONS.map((opt) => ( + toggle(opt.value)} + /> + ))} +
+ {fieldState.error?.message && ( + + {fieldState.error.message} + + )} +
+ ); + }} + /> + )} + + {/* Container scope: required description of what the containers carry. */} + {cargoType === "container" && ( + ( +