From 8319aad5c3d06b65a8ffb441ecbcf65058b16910 Mon Sep 17 00:00:00 2001 From: Marshal Date: Tue, 28 Jul 2026 06:16:56 +0000 Subject: [PATCH] increase JSON body size limit, update awaiting shipment endpoint handling, and enhance contract document editor UI --- .../contracts/ContractDocumentEditorModal.tsx | 64 +++++++++-- .../src/components/contracts/StampUpload.tsx | 2 +- .../src/hooks/contracts/useContracts.ts | 14 +-- .../bookings/DocumentClearanceDetailPage.tsx | 21 +++- .../bookings/DocumentClearanceListPage.tsx | 12 +- .../contracts/ClearanceDocumentsPage.tsx | 8 +- .../contracts/ContractClearanceListPage.tsx | 107 ++---------------- 7 files changed, 101 insertions(+), 127 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/ContractDocumentEditorModal.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/ContractDocumentEditorModal.tsx index b944b5d12..59300b458 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/ContractDocumentEditorModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/ContractDocumentEditorModal.tsx @@ -9,6 +9,7 @@ import { Group, Loader, Modal, + ScrollArea, // Select, // ponytail: unused now the validity dropdown below is commented out Stack, Text, @@ -274,17 +275,63 @@ export function ContractDocumentEditorModal({ ? draft?.nextApproverRole ? `Only the current approver (${draft.nextApproverRole}) can edit this document right now.` : "This document can no longer be edited — the contract has advanced beyond approval." - : "Articles come from this contract's template. Set the validity dates, then accept."} + : "This document is read-only — it is accepted exactly as the template produced it. Set the validity dates, then accept."} - setDocumentTitle(e.currentTarget.value)} - disabled={locked} - /> + {/* Accept is a REVIEW step: the document is shown exactly as the + template produced it, with nothing editable. Any wording change + belongs to the template or to the separate edit action. */} + {mode === "accept" ? ( + + + + {documentTitle || "Contract document"} + + {whereasClauses.length > 0 && ( + + {whereasClauses.map((clause, i) => ( + + WHEREAS {clause} + + ))} + + )} + + {articles.length === 0 ? ( + + This template carries no articles. + + ) : ( + articles.map((article, index) => ( + + + Article {index + 1} + {article.title ? ` — ${article.title}` : ""} + + + {article.body} + + + )) + )} + + + ) : ( + setDocumentTitle(e.currentTarget.value)} + disabled={locked} + /> + )} + + {mode !== "accept" && ( @@ -340,6 +387,7 @@ export function ContractDocumentEditorModal({ )} + )} {/* Article editing is hidden for now (frontend only) — staff accept the contract on the template's articles as-is. The articles themselves diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/StampUpload.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/StampUpload.tsx index 1c1e4d87c..bdf47c5fd 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/StampUpload.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/StampUpload.tsx @@ -2,7 +2,7 @@ import { useRef, useState } from "react"; import { Box, Button, Group, Image, Paper, Stack, Text } from "@mantine/core"; import { RefreshCw, Stamp, X } from "lucide-react"; -const MAX_STAMP_MB = 5; +const MAX_STAMP_MB = 10; export interface StampUploadProps { /** Stamp image as a data URL, or null when none is attached yet. */ diff --git a/apps/edr-freight-web/backoffice/src/hooks/contracts/useContracts.ts b/apps/edr-freight-web/backoffice/src/hooks/contracts/useContracts.ts index 4c50b79b2..bc0697996 100644 --- a/apps/edr-freight-web/backoffice/src/hooks/contracts/useContracts.ts +++ b/apps/edr-freight-web/backoffice/src/hooks/contracts/useContracts.ts @@ -53,17 +53,9 @@ export function useContractClearanceQueue(enabled = true) { }); } -/** - * GL worklist: executed customs contracts still waiting for their shipment - * instance to be opened (clearance itself lives on the booking). - */ -export function useAwaitingShipmentContracts(enabled = true) { - return useQuery({ - queryKey: QUERY_KEYS.CONTRACTS.clearanceQueue("AWAITING_SHIPMENT"), - queryFn: () => contractsService.getAwaitingShipmentContracts(), - enabled, - }); -} +// The awaiting-shipment worklist hook was removed with the clearance hub's +// "Start shipment" dialog — nothing calls GET /contracts/awaiting-shipment any +// more. The endpoint still exists server-side if the worklist comes back. export function useContractClearanceHistory(enabled = true) { return useQuery({ diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx index 4cb242189..88af3cad1 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx @@ -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() { @@ -165,9 +176,9 @@ export default function DocumentClearanceDetailPage() { ("queue"); const [activeTab, setActiveTab] = useState("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 ? ( diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx index 246954fe3..d31c60dd4 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx @@ -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, diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx index 1b0ffa3de..3d1e20bb6 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx @@ -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={ - {canCreateBooking ? ( - - ) : null} - setStartOpen(false)} - title={Start a customs shipment} - size="lg" - radius="md" - centered - > - - - 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. - - {awaitingShipment.length === 0 ? ( - - - - - - Every executed customs contract already has a shipment. - - - ) : ( - awaitingShipment.map((c) => ( - - - - - - - {c.reference} - - - - {c.isGovernment - ? (c.governmentInstitution ?? "Government") - : (c.company?.name ?? "—")}{" "} - · {c.tradeDirection ?? "—"} · {c.freightType ?? "—"} - - - - - - )) - )} - - ); }