mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 23:35:42 +00:00
Merge pull request #452 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
This commit is contained in:
@@ -5,6 +5,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { DataSource, EntityManager, FindOptionsWhere, In, Repository, SelectQueryBuilder } from 'typeorm';
|
import { DataSource, EntityManager, FindOptionsWhere, In, Repository, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
import { ContainerType } from '../rule-engine/entities/container-type.entity';
|
import { ContainerType } from '../rule-engine/entities/container-type.entity';
|
||||||
|
import { Contract } from '../contracts/entities/contract.entity';
|
||||||
import { ContractRoute } from '../contracts/entities/contract-route.entity';
|
import { ContractRoute } from '../contracts/entities/contract-route.entity';
|
||||||
import { BookingApprovalStep } from './entities/booking-approval-step.entity';
|
import { BookingApprovalStep } from './entities/booking-approval-step.entity';
|
||||||
import { BookingCargoModifier } from './entities/booking-cargo-modifier.entity';
|
import { BookingCargoModifier } from './entities/booking-cargo-modifier.entity';
|
||||||
@@ -588,8 +589,10 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
|||||||
.leftJoinAndSelect('booking.approvalSteps', 'approvalSteps')
|
.leftJoinAndSelect('booking.approvalSteps', 'approvalSteps')
|
||||||
.leftJoinAndSelect('booking.consolidationPartner', 'consolidationPartner')
|
.leftJoinAndSelect('booking.consolidationPartner', 'consolidationPartner')
|
||||||
// Contract reference for the list column + search (no entity relation on
|
// Contract reference for the list column + search (no entity relation on
|
||||||
// Booking → contract, so join by id and select just the reference).
|
// Booking → contract, so join the entity by id and select just the
|
||||||
.leftJoin('freight.contracts', 'contract', 'contract.id = booking.contract_id')
|
// reference — a schema-qualified table string is parsed as alias.relation
|
||||||
|
// by TypeORM and crashes).
|
||||||
|
.leftJoin(Contract, 'contract', 'contract.id = booking.contract_id')
|
||||||
.addSelect('contract.reference', 'contract_reference')
|
.addSelect('contract.reference', 'contract_reference')
|
||||||
.where('booking.deleted_at IS NULL');
|
.where('booking.deleted_at IS NULL');
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,11 @@ export class ContractBookingService {
|
|||||||
// no override. Checked before any row is written.
|
// no override. Checked before any row is written.
|
||||||
if (freightType === 'CONTAINER') {
|
if (freightType === 'CONTAINER') {
|
||||||
await this.assertWithinMaxCapacity(contract, dto);
|
await this.assertWithinMaxCapacity(contract, dto);
|
||||||
|
// 20ft weight-pairing gate at CREATION: two 20ft on a wagon must differ
|
||||||
|
// ≤ the cap, and drawdown bookings never pass through submit — so this is
|
||||||
|
// their only chance to hard-block an unbalanceable set. Entry order is
|
||||||
|
// irrelevant (the check sorts by weight before pairing).
|
||||||
|
await this.assert20ftPairableAtCreate(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Denormalize route/direction/freight onto the booking for the scheduling engine.
|
// Denormalize route/direction/freight onto the booking for the scheduling engine.
|
||||||
@@ -761,6 +766,35 @@ export class ContractBookingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hard-block booking creation when the 20ft container weights cannot be
|
||||||
|
* balanced onto wagons (pair diff over the global cap). Same rule the
|
||||||
|
* shipment-form preview reports as `pairingErrors`, enforced server-side.
|
||||||
|
*/
|
||||||
|
private async assert20ftPairableAtCreate(
|
||||||
|
dto: CreateBookingUnderContractDto,
|
||||||
|
): Promise<void> {
|
||||||
|
const twentyFtUnits = (dto.containers ?? [])
|
||||||
|
.filter((line) => (line.containerSize ?? '').includes('20'))
|
||||||
|
.flatMap((line, lineIdx) =>
|
||||||
|
(line.units ?? []).map((u, idx) => ({
|
||||||
|
label: u.containerNumber || `20ft-${lineIdx + 1}.${idx + 1}`,
|
||||||
|
grossWeightTons: Number(u.vgmTons ?? 0),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
if (twentyFtUnits.length < 2) return;
|
||||||
|
|
||||||
|
const maxDiff = await this.max20ftPairDiffTons();
|
||||||
|
const violations = validate20ftWeightPairing(twentyFtUnits, maxDiff);
|
||||||
|
if (violations.length) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Cannot create booking — 20ft containers cannot be paired on wagons: ${violations
|
||||||
|
.map((v) => v.message)
|
||||||
|
.join(' ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async max20ftPairDiffTons(): Promise<number> {
|
private async max20ftPairDiffTons(): Promise<number> {
|
||||||
const row = await this.dataSource
|
const row = await this.dataSource
|
||||||
.getRepository(TrainSchedulingGlobalRules)
|
.getRepository(TrainSchedulingGlobalRules)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Center,
|
Center,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
Select,
|
Select,
|
||||||
Stack,
|
Stack,
|
||||||
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
Textarea,
|
Textarea,
|
||||||
TextInput,
|
TextInput,
|
||||||
@@ -82,12 +84,13 @@ interface UnitDraft {
|
|||||||
containerNumber: string;
|
containerNumber: string;
|
||||||
sealNumber: string;
|
sealNumber: string;
|
||||||
vgmTons: number | string;
|
vgmTons: number | string;
|
||||||
|
/** Per-unit flags — the line's hazardous/reefer counts are derived from these. */
|
||||||
|
hazardous: boolean;
|
||||||
|
reefer: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContainerLineDraft {
|
interface ContainerLineDraft {
|
||||||
containerSize: string;
|
containerSize: string;
|
||||||
hazardousQuantity: number | string;
|
|
||||||
reeferQuantity: number | string;
|
|
||||||
units: UnitDraft[];
|
units: UnitDraft[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +103,13 @@ interface BulkLineDraft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emptyUnit(): UnitDraft {
|
function emptyUnit(): UnitDraft {
|
||||||
return { containerNumber: "", sealNumber: "", vgmTons: "" };
|
return {
|
||||||
|
containerNumber: "",
|
||||||
|
sealNumber: "",
|
||||||
|
vgmTons: "",
|
||||||
|
hazardous: false,
|
||||||
|
reefer: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function bulkUnitOfMeasure(
|
function bulkUnitOfMeasure(
|
||||||
@@ -199,11 +208,13 @@ export default function GlCreateBookingForm() {
|
|||||||
setContainerLines(
|
setContainerLines(
|
||||||
lines.containers.map((c) => ({
|
lines.containers.map((c) => ({
|
||||||
containerSize: c.containerSize,
|
containerSize: c.containerSize,
|
||||||
hazardousQuantity: c.hazardousQuantity ?? "0",
|
// The request carries counts; pre-toggle the first N units so GL sees
|
||||||
reeferQuantity: c.reeferQuantity ?? "",
|
// the customer's declared hazardous/reefer split and can adjust it.
|
||||||
units: Array.from({ length: Math.max(1, c.quantity) }, () =>
|
units: Array.from({ length: Math.max(1, c.quantity) }, (_, i) => ({
|
||||||
emptyUnit(),
|
...emptyUnit(),
|
||||||
),
|
hazardous: i < Number(c.hazardousQuantity ?? 0),
|
||||||
|
reefer: i < Number(c.reeferQuantity ?? 0),
|
||||||
|
})),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
} else if (lines.bulk) {
|
} else if (lines.bulk) {
|
||||||
@@ -229,8 +240,6 @@ export default function GlCreateBookingForm() {
|
|||||||
setContainerLines(
|
setContainerLines(
|
||||||
containerSizes.map((size) => ({
|
containerSizes.map((size) => ({
|
||||||
containerSize: size,
|
containerSize: size,
|
||||||
hazardousQuantity: "0",
|
|
||||||
reeferQuantity: "0",
|
|
||||||
units: [emptyUnit()],
|
units: [emptyUnit()],
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
@@ -261,8 +270,8 @@ export default function GlCreateBookingForm() {
|
|||||||
containers: containerLines.map((l) => ({
|
containers: containerLines.map((l) => ({
|
||||||
containerSize: l.containerSize,
|
containerSize: l.containerSize,
|
||||||
quantity: l.units.length,
|
quantity: l.units.length,
|
||||||
hazardousQuantity: Number(l.hazardousQuantity || 0),
|
hazardousQuantity: l.units.filter((u) => u.hazardous).length,
|
||||||
reeferQuantity: Number(l.reeferQuantity || 0),
|
reeferQuantity: l.units.filter((u) => u.reefer).length,
|
||||||
})),
|
})),
|
||||||
bulkQuantity: bulkLines.reduce(
|
bulkQuantity: bulkLines.reduce(
|
||||||
(s, l) => s + Number(l.cargoWeightTons || l.itemCount || 0),
|
(s, l) => s + Number(l.cargoWeightTons || l.itemCount || 0),
|
||||||
@@ -384,12 +393,10 @@ export default function GlCreateBookingForm() {
|
|||||||
.map((l) => ({
|
.map((l) => ({
|
||||||
containerSize: l.containerSize,
|
containerSize: l.containerSize,
|
||||||
quantity: l.units.length,
|
quantity: l.units.length,
|
||||||
...(l.hazardousQuantity !== ""
|
// Counts are derived from the per-unit toggles — they can never
|
||||||
? { hazardousQuantity: Number(l.hazardousQuantity) }
|
// exceed the line quantity.
|
||||||
: {}),
|
hazardousQuantity: l.units.filter((u) => u.hazardous).length,
|
||||||
...(l.reeferQuantity !== ""
|
reeferQuantity: l.units.filter((u) => u.reefer).length,
|
||||||
? { reeferQuantity: Number(l.reeferQuantity) }
|
|
||||||
: {}),
|
|
||||||
units: l.units.map((u) => ({
|
units: l.units.map((u) => ({
|
||||||
containerNumber: u.containerNumber,
|
containerNumber: u.containerNumber,
|
||||||
...(u.sealNumber ? { sealNumber: u.sealNumber } : {}),
|
...(u.sealNumber ? { sealNumber: u.sealNumber } : {}),
|
||||||
@@ -652,7 +659,7 @@ export default function GlCreateBookingForm() {
|
|||||||
<Text fz={14} fw={700} mb={10}>
|
<Text fz={14} fw={700} mb={10}>
|
||||||
{line.containerSize} containers
|
{line.containerSize} containers
|
||||||
</Text>
|
</Text>
|
||||||
<Group gap={12} grow mb={12} align="flex-start">
|
<Group gap={12} mb={12} align="flex-start">
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Quantity *"
|
label="Quantity *"
|
||||||
min={1}
|
min={1}
|
||||||
@@ -660,37 +667,24 @@ export default function GlCreateBookingForm() {
|
|||||||
onChange={(v) => syncUnits(lineIdx, Number(v) || 0)}
|
onChange={(v) => syncUnits(lineIdx, Number(v) || 0)}
|
||||||
radius={10}
|
radius={10}
|
||||||
styles={fieldStyles}
|
styles={fieldStyles}
|
||||||
|
w={160}
|
||||||
/>
|
/>
|
||||||
{contract.isHazardous ? (
|
{contract.isHazardous ? (
|
||||||
<NumberInput
|
<Badge variant="light" color="red" radius="sm" mt={30}>
|
||||||
label="Hazardous qty"
|
{line.units.filter((u) => u.hazardous).length} hazardous
|
||||||
min={0}
|
</Badge>
|
||||||
value={line.hazardousQuantity}
|
|
||||||
onChange={(v) =>
|
|
||||||
patchLine(lineIdx, { hazardousQuantity: v })
|
|
||||||
}
|
|
||||||
radius={10}
|
|
||||||
styles={fieldStyles}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
{contract.isReefer ? (
|
{contract.isReefer ? (
|
||||||
<NumberInput
|
<Badge variant="light" color="blue" radius="sm" mt={30}>
|
||||||
label="Reefer qty"
|
{line.units.filter((u) => u.reefer).length} refrigerated
|
||||||
min={0}
|
</Badge>
|
||||||
value={line.reeferQuantity}
|
|
||||||
onChange={(v) =>
|
|
||||||
patchLine(lineIdx, { reeferQuantity: v })
|
|
||||||
}
|
|
||||||
radius={10}
|
|
||||||
styles={fieldStyles}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<StepLabel>Per-container details</StepLabel>
|
<StepLabel>Per-container details</StepLabel>
|
||||||
<Stack gap={10} mt={8}>
|
<Stack gap={10} mt={8}>
|
||||||
{line.units.map((unit, unitIdx) => (
|
{line.units.map((unit, unitIdx) => (
|
||||||
<Group key={unitIdx} gap={10} grow align="flex-start">
|
<Group key={unitIdx} gap={10} align="flex-start" wrap="nowrap">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={unitIdx === 0 ? "Container number *" : undefined}
|
label={unitIdx === 0 ? "Container number *" : undefined}
|
||||||
placeholder="e.g. MSCU1234567"
|
placeholder="e.g. MSCU1234567"
|
||||||
@@ -702,6 +696,7 @@ export default function GlCreateBookingForm() {
|
|||||||
}
|
}
|
||||||
radius={10}
|
radius={10}
|
||||||
styles={fieldStyles}
|
styles={fieldStyles}
|
||||||
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={unitIdx === 0 ? "Seal number" : undefined}
|
label={unitIdx === 0 ? "Seal number" : undefined}
|
||||||
@@ -714,6 +709,7 @@ export default function GlCreateBookingForm() {
|
|||||||
}
|
}
|
||||||
radius={10}
|
radius={10}
|
||||||
styles={fieldStyles}
|
styles={fieldStyles}
|
||||||
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label={unitIdx === 0 ? "VGM (tons) *" : undefined}
|
label={unitIdx === 0 ? "VGM (tons) *" : undefined}
|
||||||
@@ -726,7 +722,52 @@ export default function GlCreateBookingForm() {
|
|||||||
}
|
}
|
||||||
radius={10}
|
radius={10}
|
||||||
styles={fieldStyles}
|
styles={fieldStyles}
|
||||||
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
|
{/* Per-unit flags: toggle exactly the containers that are
|
||||||
|
hazardous / refrigerated; line counts derive from these. */}
|
||||||
|
{contract.isHazardous ? (
|
||||||
|
<Stack gap={6} align="center" style={{ flexShrink: 0 }}>
|
||||||
|
{unitIdx === 0 ? (
|
||||||
|
<Text fz={12} fw={600} c="#4A5A68">
|
||||||
|
Hazardous
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<Switch
|
||||||
|
color="red"
|
||||||
|
size="sm"
|
||||||
|
mt={unitIdx === 0 ? 0 : 8}
|
||||||
|
aria-label={`Container ${unitIdx + 1} hazardous`}
|
||||||
|
checked={unit.hazardous}
|
||||||
|
onChange={(e) =>
|
||||||
|
patchUnit(lineIdx, unitIdx, {
|
||||||
|
hazardous: e.currentTarget.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
) : null}
|
||||||
|
{contract.isReefer ? (
|
||||||
|
<Stack gap={6} align="center" style={{ flexShrink: 0 }}>
|
||||||
|
{unitIdx === 0 ? (
|
||||||
|
<Text fz={12} fw={600} c="#4A5A68">
|
||||||
|
Reefer
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<Switch
|
||||||
|
color="blue"
|
||||||
|
size="sm"
|
||||||
|
mt={unitIdx === 0 ? 0 : 8}
|
||||||
|
aria-label={`Container ${unitIdx + 1} refrigerated`}
|
||||||
|
checked={unit.reefer}
|
||||||
|
onChange={(e) =>
|
||||||
|
patchUnit(lineIdx, unitIdx, {
|
||||||
|
reefer: e.currentTarget.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
) : null}
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -26,6 +26,70 @@ type BookingForm = UseFormReturn<
|
|||||||
BookingFormValues
|
BookingFormValues
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* fills from unit 1: tapping unit N selects 1..N, tapping a selected unit N
|
||||||
|
* keeps 1..N-1 — the count is always derived, never free-typed, so it can't
|
||||||
|
* exceed the line quantity.
|
||||||
|
*/
|
||||||
|
function UnitCountToggles({
|
||||||
|
total,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
activeBg,
|
||||||
|
activeBorder,
|
||||||
|
activeColor,
|
||||||
|
}: {
|
||||||
|
total: number;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
label: string;
|
||||||
|
activeBg: string;
|
||||||
|
activeBorder: string;
|
||||||
|
activeColor: string;
|
||||||
|
}) {
|
||||||
|
const count = Math.min(total, Math.max(0, Math.floor(Number(value) || 0)));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text fz={12} fw={600} c="#4A5A68" mb={6}>
|
||||||
|
{label} · {count}/{total} selected
|
||||||
|
</Text>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{Array.from({ length: total }, (_, i) => {
|
||||||
|
const selected = i < count;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={selected}
|
||||||
|
aria-label={`Container ${i + 1}`}
|
||||||
|
onClick={() => onChange(String(selected ? i : i + 1))}
|
||||||
|
className="rounded-lg"
|
||||||
|
style={{
|
||||||
|
minWidth: 40,
|
||||||
|
padding: "6px 10px",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
cursor: "pointer",
|
||||||
|
border: `1.5px solid ${selected ? activeBorder : "#E6ECF2"}`,
|
||||||
|
background: selected ? activeBg : "#fff",
|
||||||
|
color: selected ? activeColor : "#6B7C8E",
|
||||||
|
transition:
|
||||||
|
"background 120ms ease, border-color 120ms ease, color 120ms ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
#{i + 1}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Step5CargoDetails({
|
export function Step5CargoDetails({
|
||||||
form,
|
form,
|
||||||
referenceData,
|
referenceData,
|
||||||
@@ -144,16 +208,6 @@ export function Step5CargoDetails({
|
|||||||
const lineQtyOf = (index: number) =>
|
const lineQtyOf = (index: number) =>
|
||||||
Math.max(1, Number(form.getValues(`containers.${index}.qty`) ?? 1) || 1);
|
Math.max(1, Number(form.getValues(`containers.${index}.qty`) ?? 1) || 1);
|
||||||
const lineMax = (index: number) => lineQtyOf(index);
|
const lineMax = (index: number) => lineQtyOf(index);
|
||||||
// When a flag is switched on, default its count to the whole line.
|
|
||||||
const defaultLineQty = (index: number) => lineQtyOf(index).toString();
|
|
||||||
// Clamp a typed value into 1..lineQty (empty stays empty so the field can be
|
|
||||||
// cleared; the schema flags an empty value as required while the switch is on).
|
|
||||||
const clampToLine = (raw: string, index: number) => {
|
|
||||||
if (raw === "") return "";
|
|
||||||
const n = Number(raw);
|
|
||||||
if (Number.isNaN(n)) return raw;
|
|
||||||
return Math.min(lineQtyOf(index), Math.max(1, Math.floor(n))).toString();
|
|
||||||
};
|
|
||||||
// After the line quantity changes, pull any active count back within bounds.
|
// After the line quantity changes, pull any active count back within bounds.
|
||||||
const clampDependentQty = (index: number, newLineQty: number) => {
|
const clampDependentQty = (index: number, newLineQty: number) => {
|
||||||
const max = Math.max(1, newLineQty);
|
const max = Math.max(1, newLineQty);
|
||||||
@@ -636,7 +690,7 @@ export function Step5CargoDetails({
|
|||||||
hazField.onChange(v);
|
hazField.onChange(v);
|
||||||
form.setValue(
|
form.setValue(
|
||||||
`containers.${index}.hazardousQty`,
|
`containers.${index}.hazardousQty`,
|
||||||
v ? defaultLineQty(index) : "0",
|
v ? "1" : "0",
|
||||||
{ shouldDirty: true, shouldValidate: true },
|
{ shouldDirty: true, shouldValidate: true },
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -645,22 +699,22 @@ export function Step5CargoDetails({
|
|||||||
name={`containers.${index}.hazardousQty`}
|
name={`containers.${index}.hazardousQty`}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
render={({ field: hq, fieldState }) => (
|
render={({ field: hq, fieldState }) => (
|
||||||
<TextInput
|
<div>
|
||||||
type="number"
|
<UnitCountToggles
|
||||||
size="sm"
|
total={lineMax(index)}
|
||||||
label="How many hazardous?"
|
value={hq.value ?? "0"}
|
||||||
min={1}
|
onChange={hq.onChange}
|
||||||
max={lineMax(index)}
|
label="Tap the hazardous containers"
|
||||||
value={hq.value ?? ""}
|
activeBg="#FBEAE7"
|
||||||
onChange={(e) =>
|
activeBorder="#E4A69B"
|
||||||
hq.onChange(
|
activeColor="#C0392B"
|
||||||
clampToLine(e.currentTarget.value, index),
|
/>
|
||||||
)
|
{fieldState.error?.message ? (
|
||||||
}
|
<Text fz={11} c="red.7" mt={4}>
|
||||||
onBlur={hq.onBlur}
|
{fieldState.error.message}
|
||||||
error={fieldState.error?.message}
|
</Text>
|
||||||
radius="md"
|
) : null}
|
||||||
/>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</ToggleRow>
|
</ToggleRow>
|
||||||
@@ -681,7 +735,7 @@ export function Step5CargoDetails({
|
|||||||
reeField.onChange(v);
|
reeField.onChange(v);
|
||||||
form.setValue(
|
form.setValue(
|
||||||
`containers.${index}.reeferQty`,
|
`containers.${index}.reeferQty`,
|
||||||
v ? defaultLineQty(index) : "0",
|
v ? "1" : "0",
|
||||||
{ shouldDirty: true, shouldValidate: true },
|
{ shouldDirty: true, shouldValidate: true },
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -690,22 +744,22 @@ export function Step5CargoDetails({
|
|||||||
name={`containers.${index}.reeferQty`}
|
name={`containers.${index}.reeferQty`}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
render={({ field: rq, fieldState }) => (
|
render={({ field: rq, fieldState }) => (
|
||||||
<TextInput
|
<div>
|
||||||
type="number"
|
<UnitCountToggles
|
||||||
size="sm"
|
total={lineMax(index)}
|
||||||
label="How many refrigerated?"
|
value={rq.value ?? "0"}
|
||||||
min={1}
|
onChange={rq.onChange}
|
||||||
max={lineMax(index)}
|
label="Tap the refrigerated containers"
|
||||||
value={rq.value ?? ""}
|
activeBg="#E9F0F8"
|
||||||
onChange={(e) =>
|
activeBorder="#A9C2E0"
|
||||||
rq.onChange(
|
activeColor="#2E5B96"
|
||||||
clampToLine(e.currentTarget.value, index),
|
/>
|
||||||
)
|
{fieldState.error?.message ? (
|
||||||
}
|
<Text fz={11} c="red.7" mt={4}>
|
||||||
onBlur={rq.onBlur}
|
{fieldState.error.message}
|
||||||
error={fieldState.error?.message}
|
</Text>
|
||||||
radius="md"
|
) : null}
|
||||||
/>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</ToggleRow>
|
</ToggleRow>
|
||||||
|
|||||||
Reference in New Issue
Block a user