mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 03:38:17 +00:00
fix issue
This commit is contained in:
@@ -13,6 +13,7 @@ import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
FileUp,
|
||||
Flame,
|
||||
MapPin,
|
||||
MoveRight,
|
||||
Package,
|
||||
Receipt,
|
||||
Repeat,
|
||||
@@ -237,6 +239,19 @@ export default function NewShipmentPage() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. 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" {
|
||||
@@ -434,6 +449,7 @@ function NewShipmentBookingForm({
|
||||
contract.freightType === "CONTAINER" &&
|
||||
contract.equipmentReturn === "WITH_RETURN",
|
||||
unitOfMeasure: bulkUnitOfMeasure(contract),
|
||||
maxTonsPerWagon: bulkMaxTonsPerWagon(contract),
|
||||
// Intercity rides a passing train staff pick later — no date to choose.
|
||||
requiresDate: contract.tradeDirection !== "DOMESTIC",
|
||||
// Export completion locks onto a specific train — the pick is required
|
||||
@@ -688,20 +704,20 @@ function NewShipmentBookingForm({
|
||||
<Title
|
||||
order={1}
|
||||
fw={800}
|
||||
fz={26}
|
||||
fz={28}
|
||||
style={{ letterSpacing: "-0.01em" }}
|
||||
>
|
||||
{completeBookingId
|
||||
? isResubmit
|
||||
? "Change Your Booking"
|
||||
: "Complete Your Booking"
|
||||
? "Change shipment booking"
|
||||
: "Complete shipment booking"
|
||||
: "New Shipment Booking"}
|
||||
</Title>
|
||||
<Text size="sm" c="edr-muted" mt={4}>
|
||||
{completeBookingId
|
||||
? isResubmit
|
||||
? `Update the details below and pick a new shipment day, then resubmit your booking under contract ${contract.reference}.`
|
||||
: `Clearance is finalized — enter the cargo details and shipment day to complete your booking under contract ${contract.reference}.`
|
||||
: `Clearance is finalized. Enter cargo details and the binding shipment day to complete ${completeBooking?.reference ?? "this booking"} under contract ${contract.reference}.`
|
||||
: `Book a shipment against contract ${contract.reference}.`}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -791,17 +807,6 @@ function NewShipmentBookingForm({
|
||||
}}
|
||||
>
|
||||
<Box className="mx-auto max-w-4xl">
|
||||
{showValidationSummary ? (
|
||||
<Alert
|
||||
color="red"
|
||||
variant="light"
|
||||
radius="md"
|
||||
icon={<AlertCircle size={16} />}
|
||||
mb="sm"
|
||||
>
|
||||
Fix the highlighted fields before reviewing the price.
|
||||
</Alert>
|
||||
) : null}
|
||||
{blockOdd20ft ? (
|
||||
<Alert
|
||||
color="red"
|
||||
@@ -823,16 +828,42 @@ function NewShipmentBookingForm({
|
||||
{`${ft20Total} is an odd number of 20ft containers — this booking will be paired with another customer's odd booking to share a wagon, or held until one is available.`}
|
||||
</Alert>
|
||||
) : null}
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
type="button"
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
leftSection={<Receipt size={16} />}
|
||||
onClick={handleReview}
|
||||
>
|
||||
{isResubmit ? "Change booking" : "Review price & book"}
|
||||
</Button>
|
||||
<Group justify="space-between" wrap="nowrap" gap="md">
|
||||
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
|
||||
{showValidationSummary && (
|
||||
<>
|
||||
<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
|
||||
type="button"
|
||||
variant="default"
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
navigate(`/contracts/${contract.id}`)
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
leftSection={<Receipt size={16} />}
|
||||
onClick={handleReview}
|
||||
>
|
||||
{isResubmit ? "Change booking" : "Review price & book"}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -1212,15 +1243,45 @@ function RouteStep({
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Paper withBorder radius="md" p="md" style={{ borderColor: "#E6ECF2" }}>
|
||||
<Text fz={14} fw={600} c="#10202F">
|
||||
{routes[0]?.originYard?.label ?? "—"} →{" "}
|
||||
{routes[0]?.destinationYard?.label ?? "—"}
|
||||
</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">
|
||||
{routes[0]?.originYard?.label ?? "—"}
|
||||
</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">
|
||||
{routes[0]?.destinationYard?.label ?? "—"}
|
||||
</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>
|
||||
);
|
||||
@@ -1300,8 +1361,16 @@ function ScheduleStep({
|
||||
const selectedTrainId = form.watch("trainScheduleId");
|
||||
const isExportPick =
|
||||
contract.tradeDirection === "EXPORT" && Boolean(completeBookingId);
|
||||
const requestedWagonsValue = form.watch("requestedWagons");
|
||||
const wagonsEstimate = useMemo(() => {
|
||||
if (contract.freightType !== "CONTAINER") return undefined;
|
||||
// NUMBER_OF_WAGONS bulk states its wagon count outright — pass it through
|
||||
// so the train picker sizes fits/free against the real need instead of
|
||||
// falling back to the server's tonnage estimate.
|
||||
if (contract.freightType !== "CONTAINER") {
|
||||
if (bulkUnitOfMeasure(contract) !== "NUMBER_OF_WAGONS") return undefined;
|
||||
const wagons = Math.floor(Number(requestedWagonsValue || 0));
|
||||
return wagons >= 1 ? wagons : undefined;
|
||||
}
|
||||
const lines = containerLines ?? [];
|
||||
const ft20 = lines
|
||||
.filter((l) => l.containerSize === "20ft")
|
||||
@@ -1311,7 +1380,7 @@ function ScheduleStep({
|
||||
.reduce((s, l) => s + Number(l.quantity || 0), 0);
|
||||
const wagons = Math.ceil(ft20 / 2) + ft40;
|
||||
return wagons > 0 ? wagons : undefined;
|
||||
}, [contract.freightType, containerLines]);
|
||||
}, [contract, containerLines, requestedWagonsValue]);
|
||||
const exportTrainsQuery = useQuery({
|
||||
...api.bookings.getExportTrains.queryOptions({
|
||||
input: {
|
||||
|
||||
@@ -25,6 +25,13 @@ export interface ShipmentValidationContext {
|
||||
*/
|
||||
withReturnService?: boolean;
|
||||
unitOfMeasure?: "PER_TON" | "PER_ITEM" | "NUMBER_OF_WAGONS";
|
||||
/**
|
||||
* NUMBER_OF_WAGONS: the most tons one wagon of this cargo may carry. The
|
||||
* requested count must spread the tonnage no heavier than this, or the
|
||||
* server rejects the booking (assertWagonShareFits). Undefined when the
|
||||
* cargo type has no wagon type configured — the check then falls away.
|
||||
*/
|
||||
maxTonsPerWagon?: number | null;
|
||||
/**
|
||||
* Intercity (DOMESTIC) shipments ride a passing import/export train that
|
||||
* staff pick later, so no shipment day is chosen. Defaults to true.
|
||||
@@ -312,6 +319,23 @@ export function createShipmentFormSchema(ctx: ShipmentValidationContext) {
|
||||
path: ["requestedWagons"],
|
||||
message: "Enter the number of wagons needed (at least 1).",
|
||||
});
|
||||
} else {
|
||||
// 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.
|
||||
const tons = Number(data.cargoWeightTons || 0);
|
||||
const maxPerWagon = Number(ctx.maxTonsPerWagon || 0);
|
||||
if (tons > 0 && maxPerWagon > 0 && tons / wagons > maxPerWagon) {
|
||||
refineCtx.addIssue({
|
||||
code: "custom",
|
||||
path: ["requestedWagons"],
|
||||
message:
|
||||
`${tons} tons across ${wagons} wagon${wagons === 1 ? "" : "s"} loads ` +
|
||||
`${Math.round((tons / wagons) * 1000) / 1000}T per wagon, but a wagon of ` +
|
||||
`this cargo carries at most ${maxPerWagon}T — request at least ` +
|
||||
`${Math.ceil(tons / maxPerWagon)} wagons.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user