mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 14:38:12 +00:00
Merge branch 'freight_feature/contrat' of github.com:Tria-plc/edr-platform into freight_feature/contrat
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import type {
|
|||||||
FieldError as RhfFieldError,
|
FieldError as RhfFieldError,
|
||||||
} from "react-hook-form";
|
} from "react-hook-form";
|
||||||
import type { BookingFormInputValues } from "./schema";
|
import type { BookingFormInputValues } from "./schema";
|
||||||
|
import "./field-styles.css";
|
||||||
|
|
||||||
// Brand tokens (kept local so the form reads consistently with the booking
|
// Brand tokens (kept local so the form reads consistently with the booking
|
||||||
// detail page and the scheduling step).
|
// 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 = {
|
export const fieldStyles = {
|
||||||
label: { fontWeight: 600, fontSize: 13, color: INK, marginBottom: 6 },
|
label: { fontWeight: 600, fontSize: 13, color: INK, marginBottom: 6 },
|
||||||
input: {
|
input: {
|
||||||
@@ -284,12 +290,6 @@ export const fieldStyles = {
|
|||||||
boxShadow: "0 1px 2px rgba(16,24,40,0.04)",
|
boxShadow: "0 1px 2px rgba(16,24,40,0.04)",
|
||||||
transition:
|
transition:
|
||||||
"border-color 130ms ease, box-shadow 130ms ease, background 130ms ease",
|
"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 },
|
section: { color: MUTED },
|
||||||
dropdown: {
|
dropdown: {
|
||||||
@@ -303,15 +303,19 @@ export const fieldStyles = {
|
|||||||
fontSize: 13.5,
|
fontSize: 13.5,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
padding: "9px 10px",
|
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;
|
} 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({
|
export function SelectField({
|
||||||
field,
|
field,
|
||||||
error,
|
error,
|
||||||
@@ -349,6 +353,7 @@ export function SelectField({
|
|||||||
leftSection={leftSection}
|
leftSection={leftSection}
|
||||||
comboboxProps={{ withinPortal: true, shadow: "md", radius: "md" }}
|
comboboxProps={{ withinPortal: true, shadow: "md", radius: "md" }}
|
||||||
styles={fieldStyles}
|
styles={fieldStyles}
|
||||||
|
classNames={fieldClassNames}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -401,6 +406,7 @@ export function AsyncComboboxField({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
radius={10}
|
radius={10}
|
||||||
styles={fieldStyles}
|
styles={fieldStyles}
|
||||||
|
classNames={{ input: fieldClassNames.input }}
|
||||||
value={searchQuery || selectedLabel}
|
value={searchQuery || selectedLabel}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
onSearchChange(e.currentTarget.value);
|
onSearchChange(e.currentTarget.value);
|
||||||
|
|||||||
@@ -269,7 +269,15 @@ export default function NewContractPage() {
|
|||||||
const valid = await form.trigger(contractStepFields[step], {
|
const valid = await form.trigger(contractStepFields[step], {
|
||||||
shouldFocus: true,
|
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.
|
// Step 2 — Documents: every required document must be on file or uploaded.
|
||||||
if (step === 2 && docsValidatorRef.current && !docsValidatorRef.current()) {
|
if (step === 2 && docsValidatorRef.current && !docsValidatorRef.current()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -99,6 +99,16 @@ export type ContractKindOption = (typeof CONTRACT_KINDS)[number];
|
|||||||
export const CONTAINER_SIZES = ["20ft", "40ft"] as const;
|
export const CONTAINER_SIZES = ["20ft", "40ft"] as const;
|
||||||
export type ContainerSize = (typeof CONTAINER_SIZES)[number];
|
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
|
export const contractFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
// Operation drives trade direction (import/export → IMPORT/EXPORT;
|
// 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).
|
// Optional commodity label for the contract PDF (container scope).
|
||||||
cargoCommodityId: z.string().default(""),
|
cargoCommodityId: z.string().default(""),
|
||||||
// GENERAL only: per-size container quantity cap (total bookable over the
|
// GENERAL only: per-size container quantity cap (total bookable over the
|
||||||
// validity window). Keyed by size; 0/undefined = uncapped.
|
// validity window). Keyed by size; 0/undefined = uncapped. The NumberInput
|
||||||
containerSizeCaps: z.record(z.string(), z.number().nonnegative()).default({}),
|
// 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).
|
// Bulk scope: the cargo type path (group → commodity).
|
||||||
cargoTypePath: z.array(z.string()).default([]),
|
cargoTypePath: z.array(z.string()).default([]),
|
||||||
cargoFreeText: z.string().default(""),
|
cargoFreeText: z.string().default(""),
|
||||||
// GENERAL only: total bulk tons/items bookable. 0 = uncapped.
|
// GENERAL only: total bulk tons/items bookable. 0 = uncapped. Same empty-
|
||||||
bulkQuantityCap: z.number().nonnegative().default(0),
|
// field coercion as the container caps above.
|
||||||
|
bulkQuantityCap: nonNegativeQuantityCap.default(0),
|
||||||
// Contract-level billing flags.
|
// Contract-level billing flags.
|
||||||
isHazardous: z.boolean().default(false),
|
isHazardous: z.boolean().default(false),
|
||||||
isRefrigerated: z.boolean().default(false),
|
isRefrigerated: z.boolean().default(false),
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ export {
|
|||||||
StepLabel,
|
StepLabel,
|
||||||
} from "@/pages/bookings/new-booking-form/shared";
|
} 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
|
* Generic Select field — identical look to the booking wizard's SelectField but
|
||||||
@@ -73,6 +76,7 @@ export function SelectField<
|
|||||||
leftSection={leftSection}
|
leftSection={leftSection}
|
||||||
comboboxProps={{ withinPortal: true, shadow: "md", radius: "md" }}
|
comboboxProps={{ withinPortal: true, shadow: "md", radius: "md" }}
|
||||||
styles={fieldStyles}
|
styles={fieldStyles}
|
||||||
|
classNames={fieldClassNames}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -120,7 +124,11 @@ export function AsyncComboboxField<
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input.Wrapper label={label} error={error?.message} styles={fieldStyles}>
|
<Input.Wrapper
|
||||||
|
label={label}
|
||||||
|
error={error?.message}
|
||||||
|
styles={fieldStyles}
|
||||||
|
>
|
||||||
<Combobox
|
<Combobox
|
||||||
store={combobox}
|
store={combobox}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -134,6 +142,7 @@ export function AsyncComboboxField<
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
radius={10}
|
radius={10}
|
||||||
styles={fieldStyles}
|
styles={fieldStyles}
|
||||||
|
classNames={{ input: fieldClassNames.input }}
|
||||||
value={searchQuery || selectedLabel}
|
value={searchQuery || selectedLabel}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
onSearchChange(e.currentTarget.value);
|
onSearchChange(e.currentTarget.value);
|
||||||
|
|||||||
Reference in New Issue
Block a user