Merge pull request #719 from Tria-plc/freight_feature/usermanagement

implement empty-container return service: add return quantity handlin…
This commit is contained in:
marshal
2026-07-16 02:06:08 +03:00
committed by GitHub
20 changed files with 324 additions and 20 deletions

View File

@@ -97,6 +97,7 @@ interface LineErrors {
quantity?: string;
hazardousQuantity?: string;
reeferQuantity?: string;
returnQuantity?: string;
units?: string;
}
@@ -135,6 +136,8 @@ interface ContainerLineDraft {
quantity: string;
hazardousQuantity: string;
reeferQuantity: string;
/** Units of this line shipping with empty-container return (contract WITH_RETURN only). */
returnQuantity: string;
units: UnitDraft[];
}
@@ -155,6 +158,7 @@ function emptyLine(size: string): ContainerLineDraft {
quantity: "1",
hazardousQuantity: "0",
reeferQuantity: "0",
returnQuantity: "0",
units: [emptyUnit()],
};
}
@@ -272,6 +276,14 @@ export default function GlCreateBookingForm() {
}, [contract]);
const isContainer = contract?.freightType === "CONTAINER";
// The contract gates the empty-container return service — like hazardous.
// WITH_RETURN contracts capture a per-line return quantity instead of the
// legacy booking-level toggle; other contracts cannot switch it on.
const contractWithReturn =
isContainer && contract?.equipmentReturn === "WITH_RETURN";
// Legacy contracts (no equipment return chosen at creation) keep the old
// booking-level toggle.
const legacyReturnToggle = isContainer && !contract?.equipmentReturn;
// Intercity shipments ride a passing import/export train staff pick at
// finalize time — no shipment day is chosen and no window gate applies.
const isIntercity = contract?.tradeDirection === "DOMESTIC";
@@ -354,6 +366,7 @@ export default function GlCreateBookingForm() {
quantity: String(Math.max(1, c.quantity)),
hazardousQuantity: String(c.hazardousQuantity ?? 0),
reeferQuantity: String(c.reeferQuantity ?? 0),
returnQuantity: "0",
units: Array.from({ length: Math.max(1, c.quantity) }, emptyUnit),
})),
);
@@ -390,6 +403,7 @@ export default function GlCreateBookingForm() {
quantity: String(qty),
hazardousQuantity: "0",
reeferQuantity: "0",
returnQuantity: "0",
units: Array.from({ length: qty }, emptyUnit),
};
}),
@@ -542,6 +556,8 @@ export default function GlCreateBookingForm() {
quantity: String(imported.length),
hazardousQuantity: String(imported.filter((r) => r.hazardous).length),
reeferQuantity: String(imported.filter((r) => r.reefer).length),
returnQuantity:
prev.find((l) => l.containerSize === size)?.returnQuantity ?? "0",
units: imported.map((r) => ({
containerNumber: r.containerNumber,
sealNumber: r.sealNumber,
@@ -614,9 +630,17 @@ export default function GlCreateBookingForm() {
errs.reeferQuantity = `Can't exceed the ${qty} container(s) in this line.`;
}
}
if (contractWithReturn) {
const w = Number(line.returnQuantity || 0);
if (Number.isNaN(w) || w < 0) {
errs.returnQuantity = "Enter a valid return quantity.";
} else if (w > qty) {
errs.returnQuantity = `Can't exceed the ${qty} container(s) in this line.`;
}
}
return errs;
});
}, [isContainer, contract, containerLines]);
}, [isContainer, contract, containerLines, contractWithReturn]);
const bulkUom = contract ? bulkUnitOfMeasure(contract) : "PER_TON";
@@ -653,7 +677,11 @@ export default function GlCreateBookingForm() {
const cargoValid = isContainer
? lineErrors.every(
(e) =>
!e.quantity && !e.units && !e.hazardousQuantity && !e.reeferQuantity,
!e.quantity &&
!e.units &&
!e.hazardousQuantity &&
!e.reeferQuantity &&
!e.returnQuantity,
) &&
unitErrors.every((line) =>
line.every((e) => !e.containerNumber && !e.vgmTons),
@@ -675,8 +703,10 @@ export default function GlCreateBookingForm() {
? { scheduledDate: new Date(scheduledDate).toISOString() }
: {}),
...(notes.trim() ? { notes: notes.trim() } : {}),
// Equipment return is a container concern — bulk keeps the contract default.
...(isContainer
// Equipment return: WITH_RETURN contracts derive it server-side from the
// per-line return quantities; only legacy contracts (no value chosen at
// creation) still send the booking-level toggle. Bulk keeps the default.
...(legacyReturnToggle
? { equipmentReturn: withReturn ? "WITH_RETURN" : "WITHOUT_RETURN" }
: {}),
};
@@ -689,6 +719,9 @@ export default function GlCreateBookingForm() {
quantity: Number(l.quantity),
hazardousQuantity: Number(l.hazardousQuantity || 0) || undefined,
reeferQuantity: Number(l.reeferQuantity || 0) || undefined,
...(contractWithReturn
? { returnQuantity: Number(l.returnQuantity || 0) }
: {}),
units: l.units.map((u) => ({
containerNumber: u.containerNumber.trim().toUpperCase(),
...(u.sealNumber ? { sealNumber: u.sealNumber } : {}),
@@ -1168,6 +1201,28 @@ export default function GlCreateBookingForm() {
styles={fieldStyles}
/>
)}
{contractWithReturn && (
<TextInput
type="number"
onKeyDown={blockNegative}
label="With return qty"
description="Containers EDR returns empty"
min={0}
value={line.returnQuantity}
error={
showErrors
? lineErrors[lineIdx]?.returnQuantity
: undefined
}
onChange={(e) =>
patchLine(lineIdx, {
returnQuantity: e.currentTarget.value,
})
}
radius={10}
styles={fieldStyles}
/>
)}
</Group>
<StepLabel>Per-container details</StepLabel>
@@ -1323,7 +1378,9 @@ export default function GlCreateBookingForm() {
</StepCard>
)}
{isContainer ? (
{/* Legacy contracts only — WITH_RETURN contracts capture per-line
return quantities above, WITHOUT_RETURN contracts locked it off. */}
{legacyReturnToggle ? (
<StepCard>
<StepHeader
icon={<Repeat size={22} />}