increase JSON body size limit, update awaiting shipment endpoint handling, and enhance contract document editor UI

This commit is contained in:
Marshal
2026-07-28 06:16:56 +00:00
parent f40a079420
commit 8319aad5c3
7 changed files with 101 additions and 127 deletions

View File

@@ -1,6 +1,6 @@
import { useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { useNavigate, useParams } from "react-router-dom";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import {
Alert,
Badge,
@@ -50,9 +50,20 @@ export default function DocumentClearanceDetailPage() {
const params = useParams<{ id?: string; bookingId?: string }>();
const id = params.id ?? params.bookingId;
const navigate = useNavigate();
const location = useLocation();
const { user } = useAuth();
const { view, viewer } = useFileViewer();
// The same shipment is opened from several worklists (GL Ethiopia clearance,
// the Operations clearance-documents hub, shipment requests…), so "back" is
// whichever list sent us here. Deep links have no sender: fall back to the
// hub this user actually works in.
const backTo =
(location.state as { from?: string } | null)?.from ??
(hasPermission(user, FREIGHT_PERMS.contracts.clearanceEtActions)
? "/dashboard/contracts/clearance"
: "/dashboard/contracts/clearance-documents");
const { data: booking } = useBookingDetail(id);
const {
data: clearance,
@@ -147,9 +158,9 @@ export default function DocumentClearanceDetailPage() {
<PageContainer>
<PageHeader
title="Clearance not found"
backTo="/dashboard/contracts/clearance"
backTo={backTo}
breadcrumbs={[
{ label: "Document Clearance", href: "/dashboard/contracts/clearance" },
{ label: "Document Clearance", href: backTo },
{ label: "Not found" },
]}
/>
@@ -165,9 +176,9 @@ export default function DocumentClearanceDetailPage() {
<Stack gap="lg">
<PageHeader
title={reference}
backTo="/dashboard/contracts/clearance"
backTo={backTo}
breadcrumbs={[
{ label: "Document Clearance", href: "/dashboard/contracts/clearance" },
{ label: "Document Clearance", href: backTo },
{ label: reference },
]}
meta={

View File

@@ -1,6 +1,6 @@
import { useCallback, useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import {
ActionIcon,
Badge,
@@ -139,6 +139,7 @@ export default function DocumentClearanceListPage({
opsMode?: boolean;
}) {
const navigate = useNavigate();
const location = useLocation();
const [pageTab, setPageTab] = useState<PageTab>("queue");
const [activeTab, setActiveTab] = useState<ClearanceTabKey>("all");
const [query, setQuery] = useState("");
@@ -205,8 +206,13 @@ export default function DocumentClearanceListPage({
}, [rows, pagination.pageIndex, pagination.pageSize]);
const openDetail = useCallback(
(id: string) => navigate(`/dashboard/clearance/${id}`),
[navigate],
// `from` so the detail page's Back returns to this list, whichever route
// it is mounted at (ops self-clearance review, history, …).
(id: string) =>
navigate(`/dashboard/clearance/${id}`, {
state: { from: location.pathname },
}),
[navigate, location.pathname],
);
const statusBadge = isHistory ? (

View File

@@ -253,7 +253,13 @@ export default function ClearanceDocumentsPage() {
columns={bookingColumns}
data={bookingRows}
status={tableStatus}
onRowClick={(row) => navigate(`/dashboard/clearance/${row.id}`)}
// `from` so the detail page's Back returns to THIS hub, not
// to whichever worklist the fallback would guess.
onRowClick={(row) =>
navigate(`/dashboard/clearance/${row.id}`, {
state: { from: "/dashboard/contracts/clearance-documents" },
})
}
pagination={{
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,

View File

@@ -8,7 +8,6 @@ import {
Card,
Group,
Menu,
Modal,
Stack,
Text,
TextInput,
@@ -37,15 +36,13 @@ import {
type ColumnDef,
} from "@edr/ui-common";
import type { Freight } from "@edr/types";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { useQuery } from "@tanstack/react-query";
import { PageContainer } from "@/components/page/PageContainer";
import { PageHeader } from "@/components/page/PageHeader";
import { KpiStrip } from "@/components/page/KpiStrip";
import { bookingTable } from "@/components/bookings/booking-ui.styles";
import { useAuth } from "@/auth/useAuth";
import { useAwaitingShipmentContracts } from "@/hooks/contracts/useContracts";
import { useBookingEtClearanceQueue } from "@/hooks/bookings/useBookings";
import type { BookingDetail } from "@/types/booking";
import { FREIGHT_PERMS, hasPermission, isDjiboutiGl } from "@/lib/permissions";
@@ -77,12 +74,12 @@ function CustomsBadge({ customs }: { customs: boolean }) {
/**
* Document Clearance hub (GL Ethiopia). Clearance always runs on the SHIPMENT:
* every row here is a booking instance in phased customs clearance, whatever
* kind of contract it draws on. Contracts appear only in the "Start shipment"
* dialog — a one-time customs contract has no instance until GL opens one.
* kind of contract it draws on. The "Start shipment" dialog (and its
* awaiting-shipment contract list) was removed — shipments are opened from the
* contract itself, not from this hub.
*/
export default function ContractClearanceListPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { user } = useAuth();
// Opening/creating a booking under a contract is a GL Ethiopia action — never
// available to Djibouti GL.
@@ -91,7 +88,6 @@ export default function ContractClearanceListPage() {
!isDjiboutiGl(user);
const [query, setQuery] = useState("");
const [startOpen, setStartOpen] = useState(false);
const { pagination, setPagination } = usePagination({ pageSize: 10 });
const {
@@ -102,9 +98,6 @@ export default function ContractClearanceListPage() {
refetch,
} = useBookingEtClearanceQueue(true);
const { data: awaitingShipment = [], refetch: refetchAwaiting } =
useAwaitingShipmentContracts(canCreateBooking);
// Shipment requests carry the requested quantities (per container type, or
// bulk weight/items). Map them onto the booking rows by createdBookingId so
// the queue shows what each shipment was requested for.
@@ -168,27 +161,13 @@ export default function ContractClearanceListPage() {
[allRows],
);
const initiate = useMutation({
mutationFn: (contract: Freight.IContract) =>
contractsService.initiateBookingUnderContract(
contract.id,
(contract.routes?.length ?? 0) > 1 ? contract.routes![0].id : undefined,
),
onSuccess: (booking) => {
toast.success(
`Shipment ${booking.reference} opened — the customer can now upload the clearance documents.`,
);
setStartOpen(false);
void refetchAwaiting();
void queryClient.invalidateQueries({ queryKey: ["bookings"] });
void refetch();
navigate(`/dashboard/clearance/${booking.id}`);
},
onError: (e: Error) => toast.error(e.message || "Could not open the shipment"),
});
const openBooking = useCallback(
(id: string) => navigate(`/dashboard/clearance/${id}`),
// `from` so the detail page's Back returns to this hub.
(id: string) =>
navigate(`/dashboard/clearance/${id}`, {
state: { from: "/dashboard/contracts/clearance" },
}),
[navigate],
);
@@ -210,17 +189,6 @@ export default function ContractClearanceListPage() {
}
action={
<Group gap="sm" wrap="nowrap">
{canCreateBooking ? (
<Button
radius="md"
color="edr-green"
leftSection={<PackagePlus size={16} />}
onClick={() => setStartOpen(true)}
>
Start shipment
{awaitingShipment.length > 0 ? ` (${awaitingShipment.length})` : ""}
</Button>
) : null}
<ActionIcon
variant="default"
size="lg"
@@ -322,63 +290,6 @@ export default function ContractClearanceListPage() {
</Card>
</Stack>
<Modal
opened={startOpen}
onClose={() => setStartOpen(false)}
title={<Text fw={700}>Start a customs shipment</Text>}
size="lg"
radius="md"
centered
>
<Stack gap="sm">
<Text size="sm" c="dimmed">
Executed one-time customs contracts with no open shipment. Opening one
creates the shipment instance the customer uploads his clearance
documents on you approve them and complete the booking here.
</Text>
{awaitingShipment.length === 0 ? (
<Stack align="center" gap={8} py={32}>
<ThemeIcon variant="light" color="gray" radius="xl" size={44}>
<Inbox size={20} />
</ThemeIcon>
<Text c="dimmed" size="sm">
Every executed customs contract already has a shipment.
</Text>
</Stack>
) : (
awaitingShipment.map((c) => (
<Card key={c.id} withBorder radius="md" p="sm">
<Group justify="space-between" wrap="nowrap" gap="sm">
<Box style={{ minWidth: 0 }}>
<Group gap={6} wrap="nowrap">
<FileText size={14} className="shrink-0 opacity-70" />
<Text fw={600} size="sm" truncate>
{c.reference}
</Text>
</Group>
<Text size="xs" c="dimmed" truncate>
{c.isGovernment
? (c.governmentInstitution ?? "Government")
: (c.company?.name ?? "—")}{" "}
· {c.tradeDirection ?? "—"} · {c.freightType ?? "—"}
</Text>
</Box>
<Button
size="compact-sm"
radius="md"
color="edr-green"
leftSection={<PackagePlus size={14} />}
loading={initiate.isPending && initiate.variables?.id === c.id}
onClick={() => initiate.mutate(c)}
>
Open shipment
</Button>
</Group>
</Card>
))
)}
</Stack>
</Modal>
</PageContainer>
);
}