fix issue

This commit is contained in:
Marshal
2026-08-27 20:58:44 +00:00
parent 1626903192
commit 8b8870e85e
8 changed files with 327 additions and 63 deletions

View File

@@ -43,6 +43,7 @@ import {
Flame,
Link2,
MapPin,
MoveRight,
Package,
Receipt,
Repeat,
@@ -204,6 +205,19 @@ function emptyLine(size: string): ContainerLineDraft {
};
}
/**
* Heaviest load one wagon of this contract's bulk cargo may take, as computed
* by the API from the cargo type's allowed wagon types. Null/undefined when no
* wagon type is configured — the wagon-count check then falls away.
*/
function bulkMaxTonsPerWagon(
contract: Freight.IContract,
): number | null | undefined {
return contract.cargoScope?.find(
(scope) => scope.cargoType?.maxTonsPerWagon != null,
)?.cargoType?.maxTonsPerWagon;
}
function bulkUnitOfMeasure(
contract: Freight.IContract,
): "PER_TON" | "PER_ITEM" | "NUMBER_OF_WAGONS" {
@@ -1000,8 +1014,20 @@ export default function GlCreateBookingForm() {
}
if (bulkUom === "NUMBER_OF_WAGONS") {
const wagons = Number(bulk.requestedWagons || 0);
const maxPerWagon = Number(
(contract && bulkMaxTonsPerWagon(contract)) || 0,
);
if (!Number.isInteger(wagons) || wagons < 1) {
errs.wagons = "Enter the number of wagons needed (at least 1).";
} else if (qty > 0 && maxPerWagon > 0 && qty / wagons > maxPerWagon) {
// Too few wagons for the tonnage can never ride: 200T across 3 wagons
// is 66.67T each on a 50T wagon. Mirrors the server's
// assertWagonShareFits so the button blocks before the API 400s.
errs.wagons =
`${qty} tons across ${wagons} wagon${wagons === 1 ? "" : "s"} loads ` +
`${Math.round((qty / wagons) * 1000) / 1000}T per wagon, but a wagon of ` +
`this cargo carries at most ${maxPerWagon}T — request at least ` +
`${Math.ceil(qty / maxPerWagon)} wagons.`;
}
}
const h = Number(bulk.hazardousQuantity || 0);
@@ -1017,7 +1043,7 @@ export default function GlCreateBookingForm() {
errs.reefer = `Can't exceed the cargo quantity (${qty}).`;
}
return errs;
}, [isContainer, bulk, bulkUom]);
}, [isContainer, bulk, bulkUom, contract]);
const dateError =
!isIntercity && !scheduledDate ? "Select a shipment date." : undefined;
@@ -1479,12 +1505,12 @@ export default function GlCreateBookingForm() {
const header = (
<Group justify="space-between" align="flex-end" wrap="wrap" gap="md" mb="lg">
<Box>
<Title order={1} fw={800} fz={26} style={{ letterSpacing: "-0.01em" }}>
{completeBookingId ? "Complete Shipment Booking" : "New Shipment Booking"}
<Title order={1} fw={800} fz={28} style={{ letterSpacing: "-0.01em" }}>
{completeBookingId ? "Complete shipment booking" : "New Shipment Booking"}
</Title>
<Text size="sm" c="dimmed" mt={4}>
{completeBookingId
? `Clearance is finalized — enter the cargo details and shipment day to complete the booking under contract ${contract.reference}.`
? `Clearance is finalized. Enter cargo details and the binding shipment day to complete this booking under contract ${contract.reference}.`
: `Book a shipment on behalf of the customer for contract ${contract.reference}.`}
</Text>
</Box>
@@ -1603,20 +1629,49 @@ export default function GlCreateBookingForm() {
styles={fieldStyles}
/>
) : (
<Paper withBorder radius="md" p="md" style={{ borderColor: "#E6ECF2" }}>
<Text fz={14} fw={600}>
{selectedRoute?.originYard?.label ??
selectedRoute?.originYard?.code ??
"—"}{" "}
{" "}
{selectedRoute?.destinationYard?.label ??
selectedRoute?.destinationYard?.code ??
"—"}
</Text>
<Text fz={12} c="dimmed" mt={2}>
<Group
wrap="nowrap"
gap={16}
align="center"
px={18}
py={18}
style={{
borderRadius: 14,
border: "1px solid #E6ECF2",
background: "#FBFCFD",
}}
>
<Box>
<Text fz={15} fw={700} c="#10202F">
{selectedRoute?.originYard?.label ??
selectedRoute?.originYard?.code ??
"—"}
</Text>
<Text fz={12} c="#6B7C8E" mt={2}>
Origin yard
</Text>
</Box>
<MoveRight size={20} color="#0A6F4D" style={{ flexShrink: 0 }} />
<Box>
<Text fz={15} fw={700} c="#10202F">
{selectedRoute?.destinationYard?.label ??
selectedRoute?.destinationYard?.code ??
"—"}
</Text>
<Text fz={12} c="#6B7C8E" mt={2}>
Destination yard
</Text>
</Box>
<Box style={{ flex: 1 }} />
<Badge
variant="light"
color="teal"
radius={8}
styles={{ label: { fontSize: 12, fontWeight: 600 } }}
>
{contract.tradeDirection}
</Text>
</Paper>
</Badge>
</Group>
)}
</StepCard>
@@ -2322,16 +2377,6 @@ export default function GlCreateBookingForm() {
even numbers. Add one more 20ft container or remove one — book{" "}
{ft20Total + 1} or {ft20Total - 1} instead of {ft20Total}.
</Alert>
) : showErrors && !formValid ? (
<Alert
color="red"
variant="light"
radius="md"
icon={<AlertCircle size={16} />}
mb="sm"
>
Fix the highlighted fields before reviewing the price.
</Alert>
) : partnerError ? (
// The review button is disabled while the parent booking is
// incomplete, so the click that would reveal the errors never
@@ -2346,7 +2391,35 @@ export default function GlCreateBookingForm() {
{partnerError}
</Alert>
) : null}
<Group justify="flex-end">
<Group justify="space-between" wrap="nowrap" gap="md">
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
{showErrors && !formValid && !oddBlocksSubmit && (
<>
<AlertCircle
size={15}
color="#C0392B"
style={{ flexShrink: 0 }}
/>
<Text fz={13} fw={500} c="#C0392B">
Fix the highlighted fields to review the price.
</Text>
</>
)}
</Group>
<Group gap="sm" wrap="nowrap">
<Button
variant="default"
radius="md"
onClick={() =>
navigate(
completeBookingId
? `/dashboard/clearance/${completeBookingId}`
: "/dashboard/contracts/clearance",
)
}
>
Cancel
</Button>
<Tooltip
label={
oddBlocksSubmit
@@ -2376,6 +2449,7 @@ export default function GlCreateBookingForm() {
</Button>
</Box>
</Tooltip>
</Group>
</Group>
</Box>
</Box>