mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 14:38:12 +00:00
enhance booking windows section with pagination and improved UI
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
||||
useParams,
|
||||
useSearchParams,
|
||||
} from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
CalendarDays,
|
||||
CheckCircle2,
|
||||
ChevronLeft,
|
||||
@@ -365,8 +366,11 @@ export default function GlCreateBookingForm() {
|
||||
(!needsRouteSelect || Boolean(contractRouteId)) &&
|
||||
(isContainer ? containerLines.some((l) => l.units.length > 0) : bulkLines.length > 0);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!scheduledDate || !contract || !windowOpen) return;
|
||||
/** The create-booking DTO from the current form state — shared by the
|
||||
* authoritative price preview and the actual submit so what GL confirms is
|
||||
* exactly what gets booked. */
|
||||
const buildPayload = (): Freight.CreateBookingUnderContractDto | null => {
|
||||
if (!scheduledDate || !contract) return null;
|
||||
|
||||
const payload: Freight.CreateBookingUnderContractDto = {
|
||||
scheduledDate,
|
||||
@@ -408,6 +412,56 @@ export default function GlCreateBookingForm() {
|
||||
}));
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
// Authoritative price preview (same pricing pass the booking persists at
|
||||
// create): rail freight + first/last mile + overweight + every surcharge.
|
||||
// Fired when the price modal opens; the modal falls back to the contract
|
||||
// unit-rate estimate while it loads.
|
||||
const validateShipmentMutation = useMutation({
|
||||
mutationFn: (dto: Freight.CreateBookingUnderContractDto) =>
|
||||
contractsService.validateShipment(id ?? "", dto),
|
||||
});
|
||||
const validation = validateShipmentMutation.data ?? null;
|
||||
|
||||
const serverTotal = useMemo(() => {
|
||||
const items = validation?.lineItems;
|
||||
if (!items?.length) return null;
|
||||
return {
|
||||
currency: validation?.currency ?? priceTotal?.currency ?? "ETB",
|
||||
lines: items.map((li) => ({
|
||||
label: li.description,
|
||||
unitPrice: li.unitAmount,
|
||||
unit: li.unit.toLowerCase(),
|
||||
quantity: li.quantity,
|
||||
amount: li.amount,
|
||||
})),
|
||||
total:
|
||||
validation?.totalAmount ?? items.reduce((s, l) => s + l.amount, 0),
|
||||
};
|
||||
}, [validation, priceTotal]);
|
||||
|
||||
const displayTotal = serverTotal ?? priceTotal;
|
||||
const pairingErrors = validation?.pairingErrors ?? [];
|
||||
const overweightLines = validation?.overweightLines ?? [];
|
||||
|
||||
const openPriceModal = () => {
|
||||
setPriceOpen(true);
|
||||
const payload = buildPayload();
|
||||
if (payload) {
|
||||
validateShipmentMutation.reset();
|
||||
validateShipmentMutation.mutate(payload);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!contract || !windowOpen) return;
|
||||
// Never book past unresolved 20ft pairing hard-blocks.
|
||||
if (pairingErrors.length > 0) return;
|
||||
const payload = buildPayload();
|
||||
if (!payload) return;
|
||||
|
||||
mutations.createBooking.mutate(payload, {
|
||||
onSuccess: async (booking) => {
|
||||
if (requestId) {
|
||||
@@ -819,7 +873,7 @@ export default function GlCreateBookingForm() {
|
||||
radius="md"
|
||||
leftSection={<Receipt size={16} />}
|
||||
disabled={!canSubmit}
|
||||
onClick={() => setPriceOpen(true)}
|
||||
onClick={openPriceModal}
|
||||
>
|
||||
Review price & book
|
||||
</Button>
|
||||
@@ -850,11 +904,67 @@ export default function GlCreateBookingForm() {
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
{priceTotal ? (
|
||||
{displayTotal ? (
|
||||
<Stack gap="md">
|
||||
{validateShipmentMutation.isPending && (
|
||||
<Group gap={8} c="dimmed">
|
||||
<Loader size="xs" color="edr-green" />
|
||||
<Text fz="sm" c="dimmed">
|
||||
Computing the final price breakdown and checking container
|
||||
weights…
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{pairingErrors.length > 0 && (
|
||||
<Alert
|
||||
color="red"
|
||||
variant="light"
|
||||
radius="md"
|
||||
icon={<AlertCircle size={16} />}
|
||||
title="Cannot create booking — 20ft wagon pairing"
|
||||
>
|
||||
<Stack gap={6}>
|
||||
{pairingErrors.map((msg, i) => (
|
||||
<Text key={i} fz="sm" c="red.8">
|
||||
{msg}
|
||||
</Text>
|
||||
))}
|
||||
<Text fz="xs" c="red.7" mt={2}>
|
||||
Adjust the 20ft container weights or quantities so pairs
|
||||
differ by no more than 10 tons.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{overweightLines.length > 0 && (
|
||||
<Alert
|
||||
color="yellow"
|
||||
variant="light"
|
||||
radius="md"
|
||||
icon={<AlertTriangle size={16} />}
|
||||
title="Overweight containers"
|
||||
>
|
||||
<Stack gap={6}>
|
||||
{overweightLines.map((line, i) => (
|
||||
<Text key={i} fz="sm" c="#9A5B00">
|
||||
{line.containerTypeCode}: {line.totalVgmTons}t exceeds
|
||||
limit {line.maxAllowedTons}t (+{line.excessTons}t
|
||||
overweight)
|
||||
</Text>
|
||||
))}
|
||||
<Text fz="xs" c="#9A5B00" mt={2}>
|
||||
An overweight surcharge applies (included in the total
|
||||
below).
|
||||
</Text>
|
||||
</Stack>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Paper withBorder radius={16} p="lg" style={{ borderColor: "#E6ECF2" }}>
|
||||
<Stack gap={10}>
|
||||
{priceTotal.lines.map((line, i) => (
|
||||
{displayTotal.lines.map((line, i) => (
|
||||
<Group key={i} justify="space-between" wrap="nowrap" gap="sm">
|
||||
<Box style={{ minWidth: 0 }}>
|
||||
<Text fz="sm" fw={500}>
|
||||
@@ -862,16 +972,16 @@ export default function GlCreateBookingForm() {
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{line.quantity.toLocaleString()} ×{" "}
|
||||
{line.unitPrice.toLocaleString()} {priceTotal.currency} ·{" "}
|
||||
{line.unitPrice.toLocaleString()} {displayTotal.currency} ·{" "}
|
||||
{formatRateUnit(line.unit)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text fz="sm" fw={600} style={{ whiteSpace: "nowrap" }}>
|
||||
{line.amount.toLocaleString()} {priceTotal.currency}
|
||||
{line.amount.toLocaleString()} {displayTotal.currency}
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
{priceTotal.lines.length === 0 && (
|
||||
{displayTotal.lines.length === 0 && (
|
||||
<Text fz="sm" c="dimmed">
|
||||
No priced lines — check the cargo details.
|
||||
</Text>
|
||||
@@ -889,9 +999,9 @@ export default function GlCreateBookingForm() {
|
||||
Total
|
||||
</Text>
|
||||
<Text fw={800} fz={28}>
|
||||
{priceTotal.total.toLocaleString()}{" "}
|
||||
{displayTotal.total.toLocaleString()}{" "}
|
||||
<Text span fz={16} fw={700} c="dimmed">
|
||||
{priceTotal.currency}
|
||||
{displayTotal.currency}
|
||||
</Text>
|
||||
</Text>
|
||||
</Group>
|
||||
@@ -912,6 +1022,9 @@ export default function GlCreateBookingForm() {
|
||||
radius="md"
|
||||
leftSection={<CheckCircle2 size={16} />}
|
||||
loading={mutations.createBooking.isPending}
|
||||
disabled={
|
||||
validateShipmentMutation.isPending || pairingErrors.length > 0
|
||||
}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Confirm & book
|
||||
|
||||
Reference in New Issue
Block a user