mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Enforce non-negative values for numeric inputs across various components
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { InjectDataSource } from '@nestjs/typeorm';
|
import { InjectDataSource } from '@nestjs/typeorm';
|
||||||
import { DataSource, EntityManager, In, IsNull, Not, QueryFailedError } from 'typeorm';
|
import { DataSource, EntityManager, In, Not, QueryFailedError } from 'typeorm';
|
||||||
|
|
||||||
import { BookingsRepository } from '../bookings/bookings.repository';
|
import { BookingsRepository } from '../bookings/bookings.repository';
|
||||||
import { Booking } from '../bookings/entities/booking.entity';
|
import { Booking } from '../bookings/entities/booking.entity';
|
||||||
@@ -43,8 +43,6 @@ import { WagonAllocationBulkLoadsRepository } from '../train-schedules/wagon-all
|
|||||||
import { WagonAllocationContainerItemsRepository } from '../train-schedules/wagon-allocation-container-items.repository';
|
import { WagonAllocationContainerItemsRepository } from '../train-schedules/wagon-allocation-container-items.repository';
|
||||||
import { WagonBookingAllocationsRepository } from '../train-schedules/wagon-booking-allocations.repository';
|
import { WagonBookingAllocationsRepository } from '../train-schedules/wagon-booking-allocations.repository';
|
||||||
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
||||||
import { CargoType } from '../rule-engine/entities/cargo-type.entity';
|
|
||||||
import { ContainerType } from '../rule-engine/entities/container-type.entity';
|
|
||||||
import { WagonTypesRepository } from '../wagon-types/wagon-types.repository';
|
import { WagonTypesRepository } from '../wagon-types/wagon-types.repository';
|
||||||
import { Wagon } from '../wagons/entities/wagon.entity';
|
import { Wagon } from '../wagons/entities/wagon.entity';
|
||||||
import { AssignBookingsDto } from './dto/assign-bookings.dto';
|
import { AssignBookingsDto } from './dto/assign-bookings.dto';
|
||||||
@@ -3446,37 +3444,6 @@ export class TrainSchedulingService {
|
|||||||
return wagonType;
|
return wagonType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Soft wagon-type resolution for the customer-facing availability preview
|
|
||||||
* (getAvailableDaysForCargo). Reads the configured FK by cargo/container type;
|
|
||||||
* returns null (→ "no days") instead of throwing when nothing is configured,
|
|
||||||
* since this only estimates which days have wagons and creates no booking.
|
|
||||||
*/
|
|
||||||
private async resolveWagonTypeForPreview(
|
|
||||||
freightType: 'CONTAINER' | 'BULK',
|
|
||||||
cargoTypeCode: string | null,
|
|
||||||
): Promise<WagonType | null> {
|
|
||||||
if (freightType === 'BULK') {
|
|
||||||
if (!cargoTypeCode) return null;
|
|
||||||
const cargoType = await this.dataSource.getRepository(CargoType).findOne({
|
|
||||||
where: { code: cargoTypeCode },
|
|
||||||
relations: { wagonType: true },
|
|
||||||
});
|
|
||||||
return cargoType?.wagonType?.isActive ? cargoType.wagonType : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container preview: the input carries no specific container type, so use the
|
|
||||||
// wagon type of the first configured (active) container type.
|
|
||||||
const containerType = await this.dataSource
|
|
||||||
.getRepository(ContainerType)
|
|
||||||
.findOne({
|
|
||||||
where: { isActive: true, wagonTypeId: Not(IsNull()) },
|
|
||||||
relations: { wagonType: true },
|
|
||||||
order: { displayOrder: 'ASC' },
|
|
||||||
});
|
|
||||||
return containerType?.wagonType?.isActive ? containerType.wagonType : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stamp each plan slot with the leg it occupies (dynamic consist): the
|
* Stamp each plan slot with the leg it occupies (dynamic consist): the
|
||||||
* boarding/alighting yards of the bookings it carries. Null means the
|
* boarding/alighting yards of the bookings it carries. Null means the
|
||||||
@@ -4237,13 +4204,14 @@ export class TrainSchedulingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cargo-aware day pool: the EAT days that are actually FEASIBLE for the given
|
* Cargo-aware day pool: the EAT days a customer may pick for this cargo. A day
|
||||||
* cargo. A day is selectable only when ≥1 OPEN schedule on the route that day
|
* is selectable when ≥1 OPEN schedule on the route that day still has remaining
|
||||||
* has BOTH (a) enough AVAILABLE wagons of the cargo's matching type at that
|
* train capacity (not fully allocated). Wagon availability is deliberately NOT
|
||||||
* schedule's origin yard, and (b) remaining train capacity (not fully
|
* checked here: whether a matching wagon currently sits in the right yard is an
|
||||||
* allocated). Days with trains but not enough matching wagons are excluded.
|
* operational question staff resolve when they approve or reject the booking,
|
||||||
* Same `{ days: string[] }` shape as getAvailableDays — the customer still
|
* not something the customer can act on while choosing a date. Same
|
||||||
* picks a DAY, not a train.
|
* `{ days: string[] }` shape as getAvailableDays — the customer picks a DAY,
|
||||||
|
* not a train.
|
||||||
*/
|
*/
|
||||||
async getAvailableDaysForCargo(input: {
|
async getAvailableDaysForCargo(input: {
|
||||||
originYardId?: string;
|
originYardId?: string;
|
||||||
@@ -4259,85 +4227,17 @@ export class TrainSchedulingService {
|
|||||||
);
|
);
|
||||||
if (schedules.length === 0) return { days: [] };
|
if (schedules.length === 0) return { days: [] };
|
||||||
|
|
||||||
// Resolve the wagon type this cargo needs via the cargo/container-type FK.
|
|
||||||
// Soft (customer availability preview): no days if unresolved, never throws.
|
|
||||||
const requiredType = await this.resolveWagonTypeForPreview(
|
|
||||||
input.freightType,
|
|
||||||
input.cargoTypeCode ?? null,
|
|
||||||
);
|
|
||||||
if (!requiredType) return { days: [] };
|
|
||||||
|
|
||||||
// How many wagons of that type the cargo needs.
|
|
||||||
const slotsNeeded = this.wagonsNeededForCargo(input, requiredType);
|
|
||||||
void slotsNeeded; // TEMP: unused while the wagon-availability filter is off.
|
|
||||||
|
|
||||||
// TEMP (per request): wagon-availability filtering is DISABLED. A day is now
|
|
||||||
// offered whenever a bookable schedule that day has remaining train capacity
|
|
||||||
// — regardless of whether matching wagons are actually available at the
|
|
||||||
// origin / boarding yard. This surfaces days even when no wagon is on hand.
|
|
||||||
// Restore the block below to bring back the "enough matching wagons" gate.
|
|
||||||
//
|
|
||||||
// // AVAILABLE wagons of the required type, counted once per origin yard.
|
|
||||||
// const availableByYard = new Map<string, number>();
|
|
||||||
// const availableAt = async (yardId: string): Promise<number> => {
|
|
||||||
// const cached = availableByYard.get(yardId);
|
|
||||||
// if (cached !== undefined) return cached;
|
|
||||||
// const counts = await this.countFleetAvailability(yardId);
|
|
||||||
// const n =
|
|
||||||
// counts.find((c) => c.wagonTypeId === requiredType.id)?.available ?? 0;
|
|
||||||
// availableByYard.set(yardId, n);
|
|
||||||
// return n;
|
|
||||||
// };
|
|
||||||
|
|
||||||
const days = new Set<string>();
|
const days = new Set<string>();
|
||||||
for (const s of schedules) {
|
for (const s of schedules) {
|
||||||
const hasCapacity =
|
const hasCapacity =
|
||||||
Math.max(0, (s.maxWagons ?? 0) - (s.trainSet?.wagonCount ?? 0)) > 0;
|
Math.max(0, (s.maxWagons ?? 0) - (s.trainSet?.wagonCount ?? 0)) > 0;
|
||||||
if (!hasCapacity) continue;
|
if (!hasCapacity) continue;
|
||||||
// TEMP (per request): wagon-availability check commented out — see note
|
|
||||||
// above. Dynamic consist: wagons may ride from the train's origin OR
|
|
||||||
// already sit at the booking's own boarding yard and attach when the train
|
|
||||||
// arrives — either pool can serve a sub-corridor booking.
|
|
||||||
// let enoughWagons = (await availableAt(s.originStationId)) >= slotsNeeded;
|
|
||||||
// if (
|
|
||||||
// !enoughWagons &&
|
|
||||||
// input.originYardId &&
|
|
||||||
// input.originYardId !== s.originStationId
|
|
||||||
// ) {
|
|
||||||
// enoughWagons = (await availableAt(input.originYardId)) >= slotsNeeded;
|
|
||||||
// }
|
|
||||||
// if (!enoughWagons) continue;
|
|
||||||
if (s.scheduledDepartureDate)
|
if (s.scheduledDepartureDate)
|
||||||
days.add(eatDay(new Date(s.scheduledDepartureDate)));
|
days.add(eatDay(new Date(s.scheduledDepartureDate)));
|
||||||
}
|
}
|
||||||
return { days: [...days].sort() };
|
return { days: [...days].sort() };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wagons needed for a cargo (pre-booking estimate). BULK: ceil(weight /
|
|
||||||
* capacity). CONTAINER: TEU packing — 40ft = 2 TEU, 20ft = 1 TEU, 2 TEU per
|
|
||||||
* wagon. Mirrors wagon-plan.util without fabricating Booking entities.
|
|
||||||
*/
|
|
||||||
private wagonsNeededForCargo(
|
|
||||||
input: {
|
|
||||||
freightType: 'CONTAINER' | 'BULK';
|
|
||||||
totalWeightTons?: number;
|
|
||||||
containers?: Array<{ containerSize: string; quantity: number }>;
|
|
||||||
},
|
|
||||||
wagonType: WagonType,
|
|
||||||
): number {
|
|
||||||
if (input.freightType === 'BULK') {
|
|
||||||
const capacity = Number(wagonType.capacityTons) || 1;
|
|
||||||
const weight = Number(input.totalWeightTons ?? 0);
|
|
||||||
return Math.max(1, Math.ceil(weight / capacity));
|
|
||||||
}
|
|
||||||
const teu = (input.containers ?? []).reduce((sum, c) => {
|
|
||||||
const per = c.containerSize === '40ft' ? 2 : 1;
|
|
||||||
return sum + per * Math.max(0, Number(c.quantity ?? 0));
|
|
||||||
}, 0);
|
|
||||||
return Math.max(1, Math.ceil(teu / 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ordered stop yards of a schedule's route: origin → milestones → destination,
|
* Ordered stop yards of a schedule's route: origin → milestones → destination,
|
||||||
* de-duplicated. Falls back to the two-endpoint pseudo-route when the schedule
|
* de-duplicated. Falls back to the two-endpoint pseudo-route when the schedule
|
||||||
|
|||||||
@@ -307,13 +307,23 @@ const RuleEngineFormDialog = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNumber = field.type === "number";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
key={field.name}
|
key={field.name}
|
||||||
label={label}
|
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] ?? "")}
|
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}
|
placeholder={field.placeholder}
|
||||||
required={field.required}
|
required={field.required}
|
||||||
size="md"
|
size="md"
|
||||||
|
|||||||
@@ -392,6 +392,7 @@ export default function BookingWindowSettingsModal({
|
|||||||
}
|
}
|
||||||
min={1}
|
min={1}
|
||||||
clampBehavior="none"
|
clampBehavior="none"
|
||||||
|
allowNegative={false}
|
||||||
allowDecimal={false}
|
allowDecimal={false}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -410,6 +411,7 @@ export default function BookingWindowSettingsModal({
|
|||||||
}
|
}
|
||||||
min={0}
|
min={0}
|
||||||
clampBehavior="none"
|
clampBehavior="none"
|
||||||
|
allowNegative={false}
|
||||||
allowDecimal={false}
|
allowDecimal={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export default function DurationField({
|
|||||||
emitNative(v === "" ? "" : Number(v), unit)
|
emitNative(v === "" ? "" : Number(v), unit)
|
||||||
}
|
}
|
||||||
clampBehavior="none"
|
clampBehavior="none"
|
||||||
|
allowNegative={false}
|
||||||
allowDecimal
|
allowDecimal
|
||||||
min={min != null ? convert(min, nativeUnit, unit) : 0}
|
min={min != null ? convert(min, nativeUnit, unit) : 0}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
|||||||
setForm((current) => ({ ...current, maxTrainLengthMeters: value }))
|
setForm((current) => ({ ...current, maxTrainLengthMeters: value }))
|
||||||
}
|
}
|
||||||
clampBehavior="none"
|
clampBehavior="none"
|
||||||
|
allowNegative={false}
|
||||||
allowDecimal
|
allowDecimal
|
||||||
min={1}
|
min={1}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -119,6 +120,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
|||||||
setForm((current) => ({ ...current, maxTrainWeightTons: value }))
|
setForm((current) => ({ ...current, maxTrainWeightTons: value }))
|
||||||
}
|
}
|
||||||
clampBehavior="none"
|
clampBehavior="none"
|
||||||
|
allowNegative={false}
|
||||||
allowDecimal
|
allowDecimal
|
||||||
min={1}
|
min={1}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -130,6 +132,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
|||||||
setForm((current) => ({ ...current, maxWagonsPerTrain: value }))
|
setForm((current) => ({ ...current, maxWagonsPerTrain: value }))
|
||||||
}
|
}
|
||||||
clampBehavior="none"
|
clampBehavior="none"
|
||||||
|
allowNegative={false}
|
||||||
allowDecimal
|
allowDecimal
|
||||||
min={1}
|
min={1}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -145,6 +148,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
clampBehavior="none"
|
clampBehavior="none"
|
||||||
|
allowNegative={false}
|
||||||
allowDecimal
|
allowDecimal
|
||||||
min={0.001}
|
min={0.001}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -160,6 +164,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
clampBehavior="none"
|
clampBehavior="none"
|
||||||
|
allowNegative={false}
|
||||||
allowDecimal
|
allowDecimal
|
||||||
min={0}
|
min={0}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -203,6 +208,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
|||||||
setForm((current) => ({ ...current, windowOpenHour: value }))
|
setForm((current) => ({ ...current, windowOpenHour: value }))
|
||||||
}
|
}
|
||||||
clampBehavior="none"
|
clampBehavior="none"
|
||||||
|
allowNegative={false}
|
||||||
allowDecimal
|
allowDecimal
|
||||||
min={0}
|
min={0}
|
||||||
max={23}
|
max={23}
|
||||||
@@ -216,6 +222,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
|||||||
setForm((current) => ({ ...current, windowCloseHour: value }))
|
setForm((current) => ({ ...current, windowCloseHour: value }))
|
||||||
}
|
}
|
||||||
clampBehavior="none"
|
clampBehavior="none"
|
||||||
|
allowNegative={false}
|
||||||
allowDecimal
|
allowDecimal
|
||||||
min={0}
|
min={0}
|
||||||
max={23}
|
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 { Controller, useFieldArray, type UseFormReturn } from "react-hook-form";
|
||||||
import { Flame, Package, Plus, Snowflake, Trash2, Weight } from "lucide-react";
|
import { Flame, Package, Plus, Snowflake, Trash2, Weight } from "lucide-react";
|
||||||
import { ActionIcon, Button, Skeleton, Text, TextInput } from "@mantine/core";
|
import { ActionIcon, Button, Skeleton, Text, TextInput } from "@mantine/core";
|
||||||
@@ -26,6 +26,15 @@ type BookingForm = UseFormReturn<
|
|||||||
BookingFormValues
|
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
|
* 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
|
* many are hazardous/refrigerated (2 hazardous → toggle 2 units on). Selection
|
||||||
@@ -379,6 +388,7 @@ export function Step5CargoDetails({
|
|||||||
}}
|
}}
|
||||||
id="cargoWeight"
|
id="cargoWeight"
|
||||||
type="number"
|
type="number"
|
||||||
|
onKeyDown={blockNegative}
|
||||||
label={isPerItem ? "Quantity (Items) *" : "Quantity (Tons) *"}
|
label={isPerItem ? "Quantity (Items) *" : "Quantity (Tons) *"}
|
||||||
placeholder={isPerItem ? "e.g. 500" : "e.g. 1200"}
|
placeholder={isPerItem ? "e.g. 500" : "e.g. 1200"}
|
||||||
leftSection={
|
leftSection={
|
||||||
@@ -435,6 +445,7 @@ export function Step5CargoDetails({
|
|||||||
render={({ field: hq, fieldState }) => (
|
render={({ field: hq, fieldState }) => (
|
||||||
<TextInput
|
<TextInput
|
||||||
type="number"
|
type="number"
|
||||||
|
onKeyDown={blockNegative}
|
||||||
size="sm"
|
size="sm"
|
||||||
label={
|
label={
|
||||||
isPerItem
|
isPerItem
|
||||||
@@ -483,6 +494,7 @@ export function Step5CargoDetails({
|
|||||||
render={({ field: rq, fieldState }) => (
|
render={({ field: rq, fieldState }) => (
|
||||||
<TextInput
|
<TextInput
|
||||||
type="number"
|
type="number"
|
||||||
|
onKeyDown={blockNegative}
|
||||||
size="sm"
|
size="sm"
|
||||||
label={
|
label={
|
||||||
isPerItem
|
isPerItem
|
||||||
@@ -630,6 +642,7 @@ export function Step5CargoDetails({
|
|||||||
}}
|
}}
|
||||||
onBlur={qtyField.onBlur}
|
onBlur={qtyField.onBlur}
|
||||||
type="number"
|
type="number"
|
||||||
|
onKeyDown={blockNegative}
|
||||||
min={1}
|
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"
|
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 { useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
@@ -58,6 +58,15 @@ type ShipmentForm = ReturnType<
|
|||||||
typeof useForm<ShipmentFormInputValues, any, ShipmentFormValues>
|
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() {
|
export default function NewShipmentPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -962,6 +971,7 @@ function CargoStep({
|
|||||||
<TextInput
|
<TextInput
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
|
onKeyDown={blockNegative}
|
||||||
label="Quantity (tons)"
|
label="Quantity (tons)"
|
||||||
placeholder="e.g. 1200"
|
placeholder="e.g. 1200"
|
||||||
min={0}
|
min={0}
|
||||||
@@ -979,6 +989,7 @@ function CargoStep({
|
|||||||
<TextInput
|
<TextInput
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
|
onKeyDown={blockNegative}
|
||||||
label="Item count (if applicable)"
|
label="Item count (if applicable)"
|
||||||
placeholder="e.g. 500"
|
placeholder="e.g. 500"
|
||||||
min={0}
|
min={0}
|
||||||
@@ -997,6 +1008,7 @@ function CargoStep({
|
|||||||
<TextInput
|
<TextInput
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
|
onKeyDown={blockNegative}
|
||||||
label="Hazardous quantity"
|
label="Hazardous quantity"
|
||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
@@ -1015,6 +1027,7 @@ function CargoStep({
|
|||||||
<TextInput
|
<TextInput
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
|
onKeyDown={blockNegative}
|
||||||
label="Refrigerated quantity"
|
label="Refrigerated quantity"
|
||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
@@ -1093,6 +1106,7 @@ function ContainerLineEditor({
|
|||||||
<TextInput
|
<TextInput
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
|
onKeyDown={blockNegative}
|
||||||
label="Quantity *"
|
label="Quantity *"
|
||||||
min={1}
|
min={1}
|
||||||
error={fieldState.error?.message}
|
error={fieldState.error?.message}
|
||||||
@@ -1113,6 +1127,7 @@ function ContainerLineEditor({
|
|||||||
<TextInput
|
<TextInput
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
|
onKeyDown={blockNegative}
|
||||||
label="Hazardous qty"
|
label="Hazardous qty"
|
||||||
min={0}
|
min={0}
|
||||||
error={fieldState.error?.message}
|
error={fieldState.error?.message}
|
||||||
@@ -1130,6 +1145,7 @@ function ContainerLineEditor({
|
|||||||
<TextInput
|
<TextInput
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
|
onKeyDown={blockNegative}
|
||||||
label="Reefer qty"
|
label="Reefer qty"
|
||||||
min={0}
|
min={0}
|
||||||
error={fieldState.error?.message}
|
error={fieldState.error?.message}
|
||||||
@@ -1182,6 +1198,7 @@ function ContainerLineEditor({
|
|||||||
<TextInput
|
<TextInput
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
|
onKeyDown={blockNegative}
|
||||||
label={u === 0 ? "VGM (tons) *" : undefined}
|
label={u === 0 ? "VGM (tons) *" : undefined}
|
||||||
placeholder="e.g. 24.5"
|
placeholder="e.g. 24.5"
|
||||||
min={0}
|
min={0}
|
||||||
|
|||||||
@@ -110,7 +110,13 @@ export function createShipmentFormSchema(ctx: ShipmentValidationContext) {
|
|||||||
});
|
});
|
||||||
if (ctx.isHazardous) {
|
if (ctx.isHazardous) {
|
||||||
const h = Number(line.hazardousQuantity || 0);
|
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({
|
refineCtx.addIssue({
|
||||||
code: "custom",
|
code: "custom",
|
||||||
path: ["containers", i, "hazardousQuantity"],
|
path: ["containers", i, "hazardousQuantity"],
|
||||||
@@ -120,7 +126,13 @@ export function createShipmentFormSchema(ctx: ShipmentValidationContext) {
|
|||||||
}
|
}
|
||||||
if (ctx.isReefer) {
|
if (ctx.isReefer) {
|
||||||
const r = Number(line.reeferQuantity || 0);
|
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({
|
refineCtx.addIssue({
|
||||||
code: "custom",
|
code: "custom",
|
||||||
path: ["containers", i, "reeferQuantity"],
|
path: ["containers", i, "reeferQuantity"],
|
||||||
@@ -130,10 +142,21 @@ export function createShipmentFormSchema(ctx: ShipmentValidationContext) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const bulkCap =
|
const isPerItem = ctx.unitOfMeasure === "PER_ITEM";
|
||||||
ctx.unitOfMeasure === "PER_ITEM"
|
const bulkCap = isPerItem
|
||||||
? Number(data.itemCount || 0)
|
? Number(data.itemCount || 0)
|
||||||
: Number(data.cargoWeightTons || 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 = (
|
const boundBulkPortion = (
|
||||||
on: boolean,
|
on: boolean,
|
||||||
|
|||||||
Reference in New Issue
Block a user