mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 21:08:12 +00:00
implement Global Logistics booking process for customs contracts and enhance contract document handling
This commit is contained in:
@@ -11,19 +11,25 @@ import {
|
||||
Grid,
|
||||
Group,
|
||||
Loader,
|
||||
Modal,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
CheckCircle2,
|
||||
Container as ContainerIcon,
|
||||
FileText,
|
||||
Package,
|
||||
Plus,
|
||||
Receipt,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
@@ -36,6 +42,11 @@ import {
|
||||
useContractMutations,
|
||||
} from "@/hooks/contracts/useContracts";
|
||||
import { Boxes } from "lucide-react";
|
||||
import {
|
||||
computeGlShipmentTotal,
|
||||
formatRateUnit,
|
||||
type GlShipmentQuantities,
|
||||
} from "./gl-booking-form/total";
|
||||
|
||||
interface UnitDraft {
|
||||
containerNumber: string;
|
||||
@@ -73,6 +84,9 @@ export default function GlCreateBookingForm() {
|
||||
const [notes, setNotes] = useState("");
|
||||
const [containerLines, setContainerLines] = useState<ContainerLineDraft[]>([]);
|
||||
const [bulkLines, setBulkLines] = useState<BulkLineDraft[]>([]);
|
||||
// Price-confirm modal — GL reviews the estimate before booking on behalf of
|
||||
// the customer, mirroring the portal customer flow.
|
||||
const [priceOpen, setPriceOpen] = useState(false);
|
||||
|
||||
const isContainer = contract?.freightType === "CONTAINER";
|
||||
const routes = useMemo(
|
||||
@@ -104,6 +118,34 @@ export default function GlCreateBookingForm() {
|
||||
|
||||
const defaultBulkCargoTypeId = bulkCargoOptions[0]?.value ?? "";
|
||||
|
||||
// Normalized quantities for the client-side price estimate (same source the
|
||||
// portal customer sees: the contract's frozen unit rates × entered qty).
|
||||
const quantities: GlShipmentQuantities = useMemo(
|
||||
() => ({
|
||||
isContainer,
|
||||
containers: containerLines.map((l) => ({
|
||||
containerSize: l.containerSize,
|
||||
quantity: l.units.length,
|
||||
hazardousQuantity: Number(l.hazardousQuantity || 0),
|
||||
reeferQuantity: Number(l.reeferQuantity || 0),
|
||||
})),
|
||||
bulkQuantity: bulkLines.reduce(
|
||||
(s, l) => s + Number(l.cargoWeightTons || l.itemCount || 0),
|
||||
0,
|
||||
),
|
||||
bulkHazardousQuantity: bulkLines.reduce(
|
||||
(s, l) => s + Number(l.hazardousQuantity || 0),
|
||||
0,
|
||||
),
|
||||
}),
|
||||
[isContainer, containerLines, bulkLines],
|
||||
);
|
||||
|
||||
const priceTotal = useMemo(
|
||||
() => (contract ? computeGlShipmentTotal(contract, quantities) : null),
|
||||
[contract, quantities],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageContainer>
|
||||
@@ -119,7 +161,7 @@ export default function GlCreateBookingForm() {
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="Contract not found"
|
||||
backTo="/dashboard/clearance"
|
||||
backTo="/dashboard/contracts/clearance"
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
@@ -233,12 +275,15 @@ export default function GlCreateBookingForm() {
|
||||
<PageHeader
|
||||
title="Create booking (GL)"
|
||||
subtitle={`Enter the shipment details on behalf of the customer for contract ${contract.reference}.`}
|
||||
backTo={`/dashboard/clearance/${contract.id}`}
|
||||
backTo={`/dashboard/contracts/clearance/${contract.id}`}
|
||||
breadcrumbs={[
|
||||
{ label: "Document Clearance", href: "/dashboard/clearance" },
|
||||
{
|
||||
label: "Document Clearance",
|
||||
href: "/dashboard/contracts/clearance",
|
||||
},
|
||||
{
|
||||
label: contract.reference,
|
||||
href: `/dashboard/clearance/${contract.id}`,
|
||||
href: `/dashboard/contracts/clearance/${contract.id}`,
|
||||
},
|
||||
{ label: "Create booking" },
|
||||
]}
|
||||
@@ -578,20 +623,120 @@ export default function GlCreateBookingForm() {
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => navigate(`/dashboard/clearance/${contract.id}`)}
|
||||
onClick={() =>
|
||||
navigate(`/dashboard/contracts/clearance/${contract.id}`)
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
leftSection={<Receipt size={16} />}
|
||||
disabled={!canSubmit}
|
||||
loading={mutations.createBooking.isPending}
|
||||
onClick={handleSubmit}
|
||||
onClick={() => setPriceOpen(true)}
|
||||
>
|
||||
Create booking
|
||||
Review price & book
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
{/* Price-confirm — GL reviews the estimate, then books on behalf of the
|
||||
customer. The server recomputes the authoritative total on submit. */}
|
||||
<Modal
|
||||
opened={priceOpen}
|
||||
onClose={() => {
|
||||
if (!mutations.createBooking.isPending) setPriceOpen(false);
|
||||
}}
|
||||
centered
|
||||
radius="lg"
|
||||
size="lg"
|
||||
title={
|
||||
<Group gap={10}>
|
||||
<ThemeIcon variant="light" color="edr-green" radius="md" size={34}>
|
||||
<Receipt size={18} />
|
||||
</ThemeIcon>
|
||||
<Box>
|
||||
<Text fw={800} fz={16}>
|
||||
Confirm shipment price
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
Booking on behalf of the customer for {contract.reference}.
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
{priceTotal ? (
|
||||
<Stack gap="md">
|
||||
<Paper withBorder radius={16} p="lg">
|
||||
<Stack gap={10}>
|
||||
{priceTotal.lines.map((line, i) => (
|
||||
<Group key={i} justify="space-between" wrap="nowrap" gap="sm">
|
||||
<Box style={{ minWidth: 0 }}>
|
||||
<Text fz="sm" fw={500}>
|
||||
{line.label}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{line.quantity.toLocaleString()} ×{" "}
|
||||
{line.unitPrice.toLocaleString()} {priceTotal.currency} ·{" "}
|
||||
{formatRateUnit(line.unit)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text fz="sm" fw={600} style={{ whiteSpace: "nowrap" }}>
|
||||
{line.amount.toLocaleString()} {priceTotal.currency}
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
{priceTotal.lines.length === 0 && (
|
||||
<Text fz="sm" c="dimmed">
|
||||
No priced lines — check the cargo details.
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
<Divider my="md" />
|
||||
<Group justify="space-between" align="flex-end">
|
||||
<Text
|
||||
fz="xs"
|
||||
fw={700}
|
||||
tt="uppercase"
|
||||
c="edr-green"
|
||||
style={{ letterSpacing: "0.06em" }}
|
||||
>
|
||||
Total
|
||||
</Text>
|
||||
<Text fw={800} fz={28}>
|
||||
{priceTotal.total.toLocaleString()}{" "}
|
||||
<Text span fz={16} fw={700} c="dimmed">
|
||||
{priceTotal.currency}
|
||||
</Text>
|
||||
</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Group justify="space-between" mt="xs">
|
||||
<Button
|
||||
variant="default"
|
||||
radius="md"
|
||||
leftSection={<X size={16} />}
|
||||
onClick={() => setPriceOpen(false)}
|
||||
disabled={mutations.createBooking.isPending}
|
||||
>
|
||||
Back to edit
|
||||
</Button>
|
||||
<Button
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
leftSection={<CheckCircle2 size={16} />}
|
||||
loading={mutations.createBooking.isPending}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Confirm & book
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
) : null}
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user