Merge branch 'freight_feature/contrat' of github.com:Tria-plc/edr-platform into freight_feature/contrat

This commit is contained in:
marshal
2026-06-28 22:39:49 +03:00
5 changed files with 92 additions and 20 deletions

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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),

View File

@@ -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 (
<Input.Wrapper label={label} error={error?.message} styles={fieldStyles}>
<Input.Wrapper
label={label}
error={error?.message}
styles={fieldStyles}
>
<Combobox
store={combobox}
disabled={disabled}
@@ -134,6 +142,7 @@ export function AsyncComboboxField<
disabled={disabled}
radius={10}
styles={fieldStyles}
classNames={{ input: fieldClassNames.input }}
value={searchQuery || selectedLabel}
onChange={(e) => {
onSearchChange(e.currentTarget.value);