mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 12:18:11 +00:00
add per-container handling options for hazardous, reefer, and return services
- Introduced new boolean fields (isHazardous, isReefer, isReturn) in UnitDraft and related interfaces to allow individual container handling options. - Updated emptyUnit function to initialize these new fields. - Modified GlCreateBookingForm to handle and display these options for each container. - Adjusted calculations for hazardous, reefer, and return quantities based on the new handling options. - Updated the schema for container units and booking container lines to include handling options. - Added migration to support the new return flag in the database. - Enhanced various components to reflect gross weight calculations, ensuring consistency across the application.
This commit is contained in:
@@ -128,6 +128,10 @@ interface UnitDraft {
|
||||
containerNumber: string;
|
||||
sealNumber: string;
|
||||
vgmTons: string;
|
||||
/** Handling is per physical container; the line counts roll these up. */
|
||||
isHazardous: boolean;
|
||||
isReefer: boolean;
|
||||
isReturn: boolean;
|
||||
}
|
||||
|
||||
/** Mirrors the portal shipment form's container line: line-level quantity +
|
||||
@@ -150,7 +154,14 @@ interface BulkDraft {
|
||||
}
|
||||
|
||||
function emptyUnit(): UnitDraft {
|
||||
return { containerNumber: "", sealNumber: "", vgmTons: "" };
|
||||
return {
|
||||
containerNumber: "",
|
||||
sealNumber: "",
|
||||
vgmTons: "",
|
||||
isHazardous: false,
|
||||
isReefer: false,
|
||||
isReturn: false,
|
||||
};
|
||||
}
|
||||
|
||||
function emptyLine(size: string): ContainerLineDraft {
|
||||
@@ -285,6 +296,20 @@ export default function GlCreateBookingForm() {
|
||||
// Legacy contracts (no equipment return chosen at creation) keep the old
|
||||
// booking-level toggle.
|
||||
const legacyReturnToggle = isContainer && !contract?.equipmentReturn;
|
||||
/**
|
||||
* Handling switches offered on each container row — only the services this
|
||||
* contract was created with, since the server rejects the others.
|
||||
*/
|
||||
const handlingColumns = (
|
||||
[
|
||||
contract?.isHazardous && { key: "isHazardous", label: "Hazardous" },
|
||||
contract?.isReefer && { key: "isReefer", label: "Refrigerated" },
|
||||
contractWithReturn && { key: "isReturn", label: "With return" },
|
||||
] as Array<false | undefined | { key: keyof UnitDraft; label: string }>
|
||||
).filter(Boolean) as Array<{
|
||||
key: "isHazardous" | "isReefer" | "isReturn";
|
||||
label: string;
|
||||
}>;
|
||||
// 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";
|
||||
@@ -488,6 +513,18 @@ export default function GlCreateBookingForm() {
|
||||
enabled: cargoQuery !== null && !isIntercity,
|
||||
});
|
||||
|
||||
/**
|
||||
* Line handling totals are a roll-up of the per-container switches — the
|
||||
* count is however many containers ticked each service. Recomputed on every
|
||||
* unit change so the price estimate and payload follow the switches.
|
||||
*/
|
||||
const withDerivedCounts = (line: ContainerLineDraft): ContainerLineDraft => ({
|
||||
...line,
|
||||
hazardousQuantity: String(line.units.filter((u) => u.isHazardous).length),
|
||||
reeferQuantity: String(line.units.filter((u) => u.isReefer).length),
|
||||
returnQuantity: String(line.units.filter((u) => u.isReturn).length),
|
||||
});
|
||||
|
||||
// Keep the units array length in sync with the entered quantity.
|
||||
const syncUnits = (lineIdx: number, qty: number) => {
|
||||
setContainerLines((prev) =>
|
||||
@@ -496,7 +533,7 @@ export default function GlCreateBookingForm() {
|
||||
const next = [...line.units];
|
||||
while (next.length < qty) next.push(emptyUnit());
|
||||
next.length = Math.max(0, qty);
|
||||
return { ...line, units: next };
|
||||
return withDerivedCounts({ ...line, units: next });
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -511,11 +548,16 @@ export default function GlCreateBookingForm() {
|
||||
unitIdx: number,
|
||||
patch: Partial<UnitDraft>,
|
||||
) =>
|
||||
patchLine(lineIdx, {
|
||||
units: containerLines[lineIdx].units.map((u, i) =>
|
||||
i === unitIdx ? { ...u, ...patch } : u,
|
||||
setContainerLines((prev) =>
|
||||
prev.map((l, i) =>
|
||||
i === lineIdx
|
||||
? withDerivedCounts({
|
||||
...l,
|
||||
units: l.units.map((u, j) => (j === unitIdx ? { ...u, ...patch } : u)),
|
||||
})
|
||||
: l,
|
||||
),
|
||||
});
|
||||
);
|
||||
|
||||
// Same client-side validation as the customer portal shipment form
|
||||
// (new-shipment-form/schema.ts): ISO container numbers unique within the
|
||||
@@ -560,10 +602,15 @@ export default function GlCreateBookingForm() {
|
||||
hazardousQuantity: String(imported.filter((r) => r.hazardous).length),
|
||||
reeferQuantity: String(imported.filter((r) => r.reefer).length),
|
||||
returnQuantity: String(imported.filter((r) => r.withReturn).length),
|
||||
// The spreadsheet marks handling per row — carry it onto the
|
||||
// container it belongs to rather than collapsing it to a line count.
|
||||
units: imported.map((r) => ({
|
||||
containerNumber: r.containerNumber,
|
||||
sealNumber: r.sealNumber,
|
||||
vgmTons: String(r.vgmTons),
|
||||
isHazardous: Boolean(r.hazardous),
|
||||
isReefer: Boolean(r.reefer),
|
||||
isReturn: Boolean(r.withReturn),
|
||||
})),
|
||||
};
|
||||
}),
|
||||
@@ -742,6 +789,11 @@ export default function GlCreateBookingForm() {
|
||||
containerNumber: u.containerNumber.trim().toUpperCase(),
|
||||
...(u.sealNumber ? { sealNumber: u.sealNumber } : {}),
|
||||
vgmTons: Number(u.vgmTons) || 0,
|
||||
// Per-container handling — the server rolls these into the line
|
||||
// counts and bills each surcharge on the ticked containers only.
|
||||
isHazardous: Boolean(u.isHazardous),
|
||||
isReefer: Boolean(u.isReefer),
|
||||
...(contractWithReturn ? { isReturn: Boolean(u.isReturn) } : {}),
|
||||
})),
|
||||
}));
|
||||
} else {
|
||||
@@ -1175,73 +1227,24 @@ export default function GlCreateBookingForm() {
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
/>
|
||||
{contract.isHazardous && (
|
||||
<TextInput
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
label="Hazardous qty"
|
||||
min={0}
|
||||
value={line.hazardousQuantity}
|
||||
error={
|
||||
showErrors
|
||||
? lineErrors[lineIdx]?.hazardousQuantity
|
||||
: undefined
|
||||
}
|
||||
onChange={(e) =>
|
||||
patchLine(lineIdx, {
|
||||
hazardousQuantity: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
/>
|
||||
)}
|
||||
{contract.isReefer && (
|
||||
<TextInput
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
label="Reefer qty"
|
||||
min={0}
|
||||
value={line.reeferQuantity}
|
||||
error={
|
||||
showErrors
|
||||
? lineErrors[lineIdx]?.reeferQuantity
|
||||
: undefined
|
||||
}
|
||||
onChange={(e) =>
|
||||
patchLine(lineIdx, {
|
||||
reeferQuantity: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
radius={10}
|
||||
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>
|
||||
{handlingColumns.length > 0 ? (
|
||||
<Text fz={11} c="dimmed" mt={4}>
|
||||
Tick the services each individual container needs —
|
||||
charges apply only to the containers ticked
|
||||
{handlingColumns
|
||||
.map((col) => {
|
||||
const count = line.units.filter(
|
||||
(u) => u[col.key],
|
||||
).length;
|
||||
return count > 0 ? ` · ${count} ${col.label.toLowerCase()}` : "";
|
||||
})
|
||||
.join("")}
|
||||
.
|
||||
</Text>
|
||||
) : null}
|
||||
<Stack gap={10} mt={8}>
|
||||
{line.units.map((unit, unitIdx) => (
|
||||
<Group key={unitIdx} gap={10} grow align="flex-start">
|
||||
@@ -1296,6 +1299,22 @@ export default function GlCreateBookingForm() {
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
/>
|
||||
{handlingColumns.map((col) => (
|
||||
<Switch
|
||||
key={col.key}
|
||||
checked={Boolean(unit[col.key])}
|
||||
aria-label={`${col.label} — container ${unitIdx + 1}`}
|
||||
onChange={(e) =>
|
||||
patchUnit(lineIdx, unitIdx, {
|
||||
[col.key]: e.currentTarget.checked,
|
||||
})
|
||||
}
|
||||
label={unitIdx === 0 ? col.label : undefined}
|
||||
labelPosition="right"
|
||||
size="sm"
|
||||
mt={unitIdx === 0 ? 26 : 6}
|
||||
/>
|
||||
))}
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
Reference in New Issue
Block a user