mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
Enforce non-negative values for numeric inputs across various components
This commit is contained in:
@@ -307,13 +307,23 @@ const RuleEngineFormDialog = ({
|
||||
);
|
||||
}
|
||||
|
||||
const isNumber = field.type === "number";
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
key={field.name}
|
||||
label={label}
|
||||
type={field.type === "number" ? "number" : field.type === "date" ? "date" : "text"}
|
||||
type={isNumber ? "number" : field.type === "date" ? "date" : "text"}
|
||||
// Every rule-engine number (sizes, capacities, counts, points, rates,
|
||||
// display order) is a non-negative magnitude — reject negatives outright
|
||||
// rather than letting a typed "-" reach the API.
|
||||
min={isNumber ? 0 : undefined}
|
||||
value={String(values[field.name] ?? "")}
|
||||
onChange={(e) => setField(field.name, e.currentTarget.value)}
|
||||
onChange={(e) => {
|
||||
const next = e.currentTarget.value;
|
||||
if (isNumber && next.trim().startsWith("-")) return;
|
||||
setField(field.name, next);
|
||||
}}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
size="md"
|
||||
|
||||
@@ -392,6 +392,7 @@ export default function BookingWindowSettingsModal({
|
||||
}
|
||||
min={1}
|
||||
clampBehavior="none"
|
||||
allowNegative={false}
|
||||
allowDecimal={false}
|
||||
/>
|
||||
) : (
|
||||
@@ -410,6 +411,7 @@ export default function BookingWindowSettingsModal({
|
||||
}
|
||||
min={0}
|
||||
clampBehavior="none"
|
||||
allowNegative={false}
|
||||
allowDecimal={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -98,6 +98,7 @@ export default function DurationField({
|
||||
emitNative(v === "" ? "" : Number(v), unit)
|
||||
}
|
||||
clampBehavior="none"
|
||||
allowNegative={false}
|
||||
allowDecimal
|
||||
min={min != null ? convert(min, nativeUnit, unit) : 0}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -107,6 +107,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
setForm((current) => ({ ...current, maxTrainLengthMeters: value }))
|
||||
}
|
||||
clampBehavior="none"
|
||||
allowNegative={false}
|
||||
allowDecimal
|
||||
min={1}
|
||||
disabled={loading}
|
||||
@@ -119,6 +120,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
setForm((current) => ({ ...current, maxTrainWeightTons: value }))
|
||||
}
|
||||
clampBehavior="none"
|
||||
allowNegative={false}
|
||||
allowDecimal
|
||||
min={1}
|
||||
disabled={loading}
|
||||
@@ -130,6 +132,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
setForm((current) => ({ ...current, maxWagonsPerTrain: value }))
|
||||
}
|
||||
clampBehavior="none"
|
||||
allowNegative={false}
|
||||
allowDecimal
|
||||
min={1}
|
||||
disabled={loading}
|
||||
@@ -145,6 +148,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
}))
|
||||
}
|
||||
clampBehavior="none"
|
||||
allowNegative={false}
|
||||
allowDecimal
|
||||
min={0.001}
|
||||
disabled={loading}
|
||||
@@ -160,6 +164,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
}))
|
||||
}
|
||||
clampBehavior="none"
|
||||
allowNegative={false}
|
||||
allowDecimal
|
||||
min={0}
|
||||
disabled={loading}
|
||||
@@ -203,6 +208,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
setForm((current) => ({ ...current, windowOpenHour: value }))
|
||||
}
|
||||
clampBehavior="none"
|
||||
allowNegative={false}
|
||||
allowDecimal
|
||||
min={0}
|
||||
max={23}
|
||||
@@ -216,6 +222,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
setForm((current) => ({ ...current, windowCloseHour: value }))
|
||||
}
|
||||
clampBehavior="none"
|
||||
allowNegative={false}
|
||||
allowDecimal
|
||||
min={0}
|
||||
max={23}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useEffect, useMemo, useRef, type KeyboardEvent } from "react";
|
||||
import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form";
|
||||
import { Flame, Package, Plus, Snowflake, Trash2, Weight } from "lucide-react";
|
||||
import { ActionIcon, Button, Skeleton, Text, TextInput } from "@mantine/core";
|
||||
@@ -26,6 +26,15 @@ type BookingForm = UseFormReturn<
|
||||
BookingFormValues
|
||||
>;
|
||||
|
||||
/**
|
||||
* Every quantity on this step is a non-negative magnitude. A native number
|
||||
* input's `min` only constrains its stepper, so swallow the minus key before it
|
||||
* can put a negative into the field at all.
|
||||
*/
|
||||
const blockNegative = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "-") event.preventDefault();
|
||||
};
|
||||
|
||||
/**
|
||||
* One numbered toggle per container unit in the line — tap units to mark how
|
||||
* many are hazardous/refrigerated (2 hazardous → toggle 2 units on). Selection
|
||||
@@ -379,6 +388,7 @@ export function Step5CargoDetails({
|
||||
}}
|
||||
id="cargoWeight"
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
label={isPerItem ? "Quantity (Items) *" : "Quantity (Tons) *"}
|
||||
placeholder={isPerItem ? "e.g. 500" : "e.g. 1200"}
|
||||
leftSection={
|
||||
@@ -435,6 +445,7 @@ export function Step5CargoDetails({
|
||||
render={({ field: hq, fieldState }) => (
|
||||
<TextInput
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
size="sm"
|
||||
label={
|
||||
isPerItem
|
||||
@@ -483,6 +494,7 @@ export function Step5CargoDetails({
|
||||
render={({ field: rq, fieldState }) => (
|
||||
<TextInput
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
size="sm"
|
||||
label={
|
||||
isPerItem
|
||||
@@ -630,6 +642,7 @@ export function Step5CargoDetails({
|
||||
}}
|
||||
onBlur={qtyField.onBlur}
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
min={1}
|
||||
className="h-9 w-full rounded-lg border border-gray-200 bg-white px-3 text-center text-sm outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500"
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -58,6 +58,15 @@ type ShipmentForm = ReturnType<
|
||||
typeof useForm<ShipmentFormInputValues, any, ShipmentFormValues>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Every quantity on this form is a non-negative magnitude. A native number
|
||||
* input's `min` only constrains its stepper, so swallow the minus key before it
|
||||
* can put a negative into the field at all.
|
||||
*/
|
||||
const blockNegative = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "-") event.preventDefault();
|
||||
};
|
||||
|
||||
export default function NewShipmentPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -962,6 +971,7 @@ function CargoStep({
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
label="Quantity (tons)"
|
||||
placeholder="e.g. 1200"
|
||||
min={0}
|
||||
@@ -979,6 +989,7 @@ function CargoStep({
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
label="Item count (if applicable)"
|
||||
placeholder="e.g. 500"
|
||||
min={0}
|
||||
@@ -997,6 +1008,7 @@ function CargoStep({
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
label="Hazardous quantity"
|
||||
min={0}
|
||||
step={1}
|
||||
@@ -1015,6 +1027,7 @@ function CargoStep({
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
label="Refrigerated quantity"
|
||||
min={0}
|
||||
step={1}
|
||||
@@ -1093,6 +1106,7 @@ function ContainerLineEditor({
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
label="Quantity *"
|
||||
min={1}
|
||||
error={fieldState.error?.message}
|
||||
@@ -1113,6 +1127,7 @@ function ContainerLineEditor({
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
label="Hazardous qty"
|
||||
min={0}
|
||||
error={fieldState.error?.message}
|
||||
@@ -1130,6 +1145,7 @@ function ContainerLineEditor({
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
label="Reefer qty"
|
||||
min={0}
|
||||
error={fieldState.error?.message}
|
||||
@@ -1182,6 +1198,7 @@ function ContainerLineEditor({
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
label={u === 0 ? "VGM (tons) *" : undefined}
|
||||
placeholder="e.g. 24.5"
|
||||
min={0}
|
||||
|
||||
@@ -110,7 +110,13 @@ export function createShipmentFormSchema(ctx: ShipmentValidationContext) {
|
||||
});
|
||||
if (ctx.isHazardous) {
|
||||
const h = Number(line.hazardousQuantity || 0);
|
||||
if (h > qty) {
|
||||
if (h < 0) {
|
||||
refineCtx.addIssue({
|
||||
code: "custom",
|
||||
path: ["containers", i, "hazardousQuantity"],
|
||||
message: "Enter a valid hazardous quantity.",
|
||||
});
|
||||
} else if (h > qty) {
|
||||
refineCtx.addIssue({
|
||||
code: "custom",
|
||||
path: ["containers", i, "hazardousQuantity"],
|
||||
@@ -120,7 +126,13 @@ export function createShipmentFormSchema(ctx: ShipmentValidationContext) {
|
||||
}
|
||||
if (ctx.isReefer) {
|
||||
const r = Number(line.reeferQuantity || 0);
|
||||
if (r > qty) {
|
||||
if (r < 0) {
|
||||
refineCtx.addIssue({
|
||||
code: "custom",
|
||||
path: ["containers", i, "reeferQuantity"],
|
||||
message: "Enter a valid refrigerated quantity.",
|
||||
});
|
||||
} else if (r > qty) {
|
||||
refineCtx.addIssue({
|
||||
code: "custom",
|
||||
path: ["containers", i, "reeferQuantity"],
|
||||
@@ -130,10 +142,21 @@ export function createShipmentFormSchema(ctx: ShipmentValidationContext) {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const bulkCap =
|
||||
ctx.unitOfMeasure === "PER_ITEM"
|
||||
? Number(data.itemCount || 0)
|
||||
: Number(data.cargoWeightTons || 0);
|
||||
const isPerItem = ctx.unitOfMeasure === "PER_ITEM";
|
||||
const bulkCap = isPerItem
|
||||
? Number(data.itemCount || 0)
|
||||
: Number(data.cargoWeightTons || 0);
|
||||
|
||||
// The bulk cargo amount itself: a positive magnitude. Without this a
|
||||
// negative (typed past the input's `min`) reaches the API unchecked.
|
||||
const bulkPath = isPerItem ? "itemCount" : "cargoWeightTons";
|
||||
if (Number.isNaN(bulkCap) || bulkCap <= 0) {
|
||||
refineCtx.addIssue({
|
||||
code: "custom",
|
||||
path: [bulkPath],
|
||||
message: "Enter a quantity greater than 0.",
|
||||
});
|
||||
}
|
||||
|
||||
const boundBulkPortion = (
|
||||
on: boolean,
|
||||
|
||||
Reference in New Issue
Block a user