View manual (offline) payment channel settings

Confirm offline (bank transfer) invoice payment
This commit is contained in:
Marshal
2026-08-20 06:57:57 +00:00
parent c38fcff00d
commit 4b07ff328d
10 changed files with 196 additions and 56 deletions

View File

@@ -149,24 +149,12 @@ export default function DocumentClearanceDetailPage() {
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)[0]?.note ?? null;
const docsPhaseComplete =
clearance?.milestones?.some(
(m) => m.milestoneCode === "DOCUMENTS_APPROVED" && m.status === "COMPLETED",
) ?? false;
// Querying a document is only possible while the booking is actually in
// review — the server enforces exactly that (reviewDocument asserts
// DOCUMENTS_UNDER_REVIEW), so once clearance is finalized the button could
// only ever produce a 400.
//
// `preClearanceFinalized` alone was not enough: it is a phased-customs field,
// so a non-customs booking (self-clearance, and every shipping-line booking)
// never sets it and kept offering Query after Operations had finalized.
const queriesLocked =
Boolean(
(clearance as Freight.ContractClearanceView | undefined)
?.preClearanceFinalized,
) ||
(booking?.status != null && booking.status !== "DOCUMENTS_UNDER_REVIEW");
// Documents stay reviewable for as long as the customer can still submit
// them — until the shipment is paid, not merely until clearance is
// finalized. `documentsOpen` is the server's own predicate (the same one
// both the upload and review endpoints gate on), so the buttons are shown
// exactly when the API would accept them.
const documentsClosed = clearance?.documentsOpen === false;
const workflowFiles =
(clearance as Freight.ContractClearanceView | undefined)?.workflowFiles ?? [];
@@ -346,8 +334,8 @@ export default function DocumentClearanceDetailPage() {
<ClearanceReviewSection
bookingId={id!}
hideSummary
approvalsLocked={isPhasedGeneral && docsPhaseComplete}
queriesLocked={queriesLocked}
approvalsLocked={documentsClosed}
queriesLocked={documentsClosed}
phasedCustoms={isPhasedGeneral}
onChanged={() => void refetch()}
/>

View File

@@ -164,6 +164,7 @@ export default function ContractClearanceListPage() {
tradeDirection: b.tradeDirection ?? "—",
freightType: b.freightType ?? "—",
status: b.status,
allDocsApproved: Boolean(b.allDocsApproved),
requested: requestedByBooking.get(b.id) ?? null,
contractId: b.contractId ?? null,
contractReference: b.contractReference ?? null,
@@ -344,6 +345,8 @@ interface ShipmentBookingRow {
tradeDirection: string;
freightType: string;
status: string;
/** Every required document approved, even before clearance is finalized. */
allDocsApproved: boolean;
/** Requested quantities from the originating shipment request. */
requested: Freight.RequestedShipmentLines | null;
/** Contract this shipment booking was created under. */
@@ -518,13 +521,22 @@ function ShipmentBookingsTable({
header: () => <span className={bookingTable.headerCell}>Status</span>,
cell: ({ row }) => (
<Group gap={6} wrap="nowrap">
<Badge
variant="light"
color={shipmentStatusColor(row.original.status)}
radius="sm"
>
{prettyStatus(row.original.status)}
</Badge>
{/* All docs approved but not yet finalized: the booking status is
still DOCUMENTS_UNDER_REVIEW — show the real review state. */}
{row.original.status === "DOCUMENTS_UNDER_REVIEW" &&
row.original.allDocsApproved ? (
<Badge variant="light" color="edr-green" radius="sm">
Documents approved
</Badge>
) : (
<Badge
variant="light"
color={shipmentStatusColor(row.original.status)}
radius="sm"
>
{prettyStatus(row.original.status)}
</Badge>
)}
{row.original.bookingCreated ? (
<Tooltip label="Booking created by GL Ethiopia" withArrow>
<Badge

View File

@@ -234,6 +234,8 @@ export interface BookingDetail {
equipmentReturn?: string;
customsClearingEnabled?: boolean;
customsClearingAgent?: string | null;
/** ET clearance queue: every required document approved (pre-finalize). */
allDocsApproved?: boolean;
contractKind?: "ONE_TIME" | "GENERAL" | null;
contractId?: string | null;
/** Reference of the contract this booking was created under (list column + search). */

View File

@@ -209,6 +209,17 @@ export function ClearanceFlow({ booking, flow, footer }: ClearanceFlowProps) {
</>
)}
{/* Clearance is done but the shipment is not paid yet: documents stay
open, so say so — otherwise the upload box below reads as leftover UI. */}
{canUpload && !isInitialUpload && status !== "DOCUMENTS_UNDER_REVIEW" ? (
<Alert color="blue" variant="light" radius="md" mt="md" p="xs">
<Text fz="12px">
Need to send something else? You can still add documents to this
shipment until it is paid.
</Text>
</Alert>
) : null}
{canUpload ? (
<ClearanceAdHocUploadSection
rows={adHoc}

View File

@@ -116,8 +116,14 @@ export function useClearanceFlow(booking: Freight.IBooking) {
// just sees that clearance is done and GL is preparing the booking.
const awaitingGlCompletion =
isReady && isBareInstance && Boolean(clearance?.includesCustoms);
// Documents stay open until the shipment is paid: a customs shipment keeps
// collecting paperwork (amended invoices, revised packing lists) well past
// clearance finalization. `documentsOpen` is the server's own predicate, so
// the upload control appears exactly when the API would accept a file.
// Older API builds omit the field — fall back to the previous rule there.
const canUpload =
status === "AWAITING_DOCUMENTS" || status === "DOCUMENTS_UNDER_REVIEW";
clearance?.documentsOpen ??
(status === "AWAITING_DOCUMENTS" || status === "DOCUMENTS_UNDER_REVIEW");
// The very first upload (nothing in review yet). Here every required document
// must be provided. Once GL has started reviewing (DOCUMENTS_UNDER_REVIEW) the
// customer is only re-uploading queried/pending docs, so we don't re-gate on