diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/field-styles.css b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/field-styles.css new file mode 100644 index 000000000..b416d57b1 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/field-styles.css @@ -0,0 +1,34 @@ +/* + * Real CSS for the shared form fields. Mantine v7's `styles` prop only accepts + * flat properties — pseudo-classes and `&[data-...]` attribute selectors there + * are ignored (and `&[data-...]` logs an "Unsupported style property" warning). + * Those interactive states live here and are wired via `classNames` (see + * `fieldClassNames` in `shared.tsx`). + */ + +.edrFieldInput:hover { + border-color: #cbd8e4; +} + +.edrFieldInput:focus, +.edrFieldInput:focus-within { + border-color: #0ea371; + background: #ffffff; + box-shadow: + 0 0 0 3px rgba(14, 163, 113, 0.13), + 0 1px 2px rgba(16, 24, 40, 0.05); +} + +.edrFieldOption[data-combobox-selected] { + background: linear-gradient( + 135deg, + rgba(14, 163, 113, 0.1), + rgba(14, 163, 113, 0.05) + ); + color: #0a6f4d; + font-weight: 600; +} + +.edrFieldOption[data-combobox-active] { + background: #f1f6fa; +} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx index 5d8e362d7..8c461997f 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx @@ -26,6 +26,7 @@ import type { FieldError as RhfFieldError, } from "react-hook-form"; import type { BookingFormInputValues } from "./schema"; +import "./field-styles.css"; // Brand tokens (kept local so the form reads consistently with the booking // detail page and the scheduling step). @@ -267,7 +268,12 @@ export function StepHeader({ ); } -/** Shared Mantine input styling so every field in the form matches. */ +/** + * Shared Mantine input styling so every field in the form matches. Mantine v7's + * `styles` prop accepts only flat properties, so interactive states (`:hover`, + * `:focus`, `[data-combobox-selected]`…) live in `field-styles.css` and are + * applied through `fieldClassNames` below — never as `&`-nested keys here. + */ export const fieldStyles = { label: { fontWeight: 600, fontSize: 13, color: INK, marginBottom: 6 }, input: { @@ -284,12 +290,6 @@ export const fieldStyles = { boxShadow: "0 1px 2px rgba(16,24,40,0.04)", transition: "border-color 130ms ease, box-shadow 130ms ease, background 130ms ease", - "&:hover": { borderColor: "#CBD8E4" }, - "&:focus, &:focusWithin": { - borderColor: GREEN, - background: "#FFFFFF", - boxShadow: `0 0 0 3px ${GREEN}22, 0 1px 2px rgba(16,24,40,0.05)`, - }, }, section: { color: MUTED }, dropdown: { @@ -303,15 +303,19 @@ export const fieldStyles = { fontSize: 13.5, fontWeight: 500, padding: "9px 10px", - "&[data-combobox-selected]": { - background: `linear-gradient(135deg, ${GREEN}1A, ${GREEN}0D)`, - color: GREEN_DARK, - fontWeight: 600, - }, - "&[data-combobox-active]": { background: "#F1F6FA" }, }, } as const; +/** + * Class names carrying the field's interactive states (hover/focus ring and + * combobox selected/active option). Pair with `fieldStyles` on every Select / + * InputBase so the look matches and no unsupported-selector warning is logged. + */ +export const fieldClassNames = { + input: "edrFieldInput", + option: "edrFieldOption", +} as const; + export function SelectField({ field, error, @@ -349,6 +353,7 @@ export function SelectField({ leftSection={leftSection} comboboxProps={{ withinPortal: true, shadow: "md", radius: "md" }} styles={fieldStyles} + classNames={fieldClassNames} /> ); } @@ -401,6 +406,7 @@ export function AsyncComboboxField({ disabled={disabled} radius={10} styles={fieldStyles} + classNames={{ input: fieldClassNames.input }} value={searchQuery || selectedLabel} onChange={(e) => { onSearchChange(e.currentTarget.value); 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 775564c9f..ca2932d84 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx @@ -269,7 +269,15 @@ export default function NewContractPage() { const valid = await form.trigger(contractStepFields[step], { shouldFocus: true, }); - if (!valid) return; + if (!valid) { + // TEMP DEBUG — surface which step-1 fields block Continue. + // eslint-disable-next-line no-console + console.warn("[contract continue blocked] step", step, { + errors: JSON.parse(JSON.stringify(form.formState.errors)), + values: form.getValues(), + }); + return; + } // Step 2 — Documents: every required document must be on file or uploaded. if (step === 2 && docsValidatorRef.current && !docsValidatorRef.current()) { return; 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 47fb07304..33c501789 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 @@ -99,6 +99,16 @@ export type ContractKindOption = (typeof CONTRACT_KINDS)[number]; export const CONTAINER_SIZES = ["20ft", "40ft"] as const; export type ContainerSize = (typeof CONTAINER_SIZES)[number]; +// A GENERAL-contract quantity cap. The Mantine NumberInput backing these fields +// can briefly emit "" / undefined / NaN (cleared or never-touched field); those +// all mean "uncapped", so coerce them to 0 before the >= 0 check rather than +// letting them fail validation and silently block the Cargo & Route step. +const nonNegativeQuantityCap = z.preprocess( + (v) => + v === "" || v === null || v === undefined || Number.isNaN(v) ? 0 : v, + z.number().nonnegative(), +); + export const contractFormSchema = z .object({ // Operation drives trade direction (import/export → IMPORT/EXPORT; @@ -152,13 +162,18 @@ export const contractFormSchema = z // Optional commodity label for the contract PDF (container scope). cargoCommodityId: z.string().default(""), // GENERAL only: per-size container quantity cap (total bookable over the - // validity window). Keyed by size; 0/undefined = uncapped. - containerSizeCaps: z.record(z.string(), z.number().nonnegative()).default({}), + // validity window). Keyed by size; 0/undefined = uncapped. The NumberInput + // can momentarily hold "" / undefined (empty field) — coerce those to 0 so + // an untouched cap never blocks the step. + containerSizeCaps: z + .record(z.string(), nonNegativeQuantityCap) + .default({}), // Bulk scope: the cargo type path (group → commodity). cargoTypePath: z.array(z.string()).default([]), cargoFreeText: z.string().default(""), - // GENERAL only: total bulk tons/items bookable. 0 = uncapped. - bulkQuantityCap: z.number().nonnegative().default(0), + // GENERAL only: total bulk tons/items bookable. 0 = uncapped. Same empty- + // field coercion as the container caps above. + bulkQuantityCap: nonNegativeQuantityCap.default(0), // Contract-level billing flags. isHazardous: z.boolean().default(false), isRefrigerated: z.boolean().default(false), diff --git a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/shared.tsx b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/shared.tsx index 83322cae6..d633a6265 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/shared.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/shared.tsx @@ -27,7 +27,10 @@ export { StepLabel, } from "@/pages/bookings/new-booking-form/shared"; -import { fieldStyles } from "@/pages/bookings/new-booking-form/shared"; +import { + fieldClassNames, + fieldStyles, +} from "@/pages/bookings/new-booking-form/shared"; /** * Generic Select field — identical look to the booking wizard's SelectField but @@ -73,6 +76,7 @@ export function SelectField< leftSection={leftSection} comboboxProps={{ withinPortal: true, shadow: "md", radius: "md" }} styles={fieldStyles} + classNames={fieldClassNames} /> ); } @@ -120,7 +124,11 @@ export function AsyncComboboxField< }; return ( - + { onSearchChange(e.currentTarget.value);