From ca774c536f08f03e4484262d9fadfac7fb835c50 Mon Sep 17 00:00:00 2001 From: Marshal Date: Thu, 20 Aug 2026 07:37:35 +0000 Subject: [PATCH] fix(clearance): flag bookings with documents still awaiting GL approval --- .../modules/bookings/bookings.repository.ts | 23 +++++++++++++++++ .../booking-clearance.service.spec.ts | 3 +++ .../contracts/booking-clearance.service.ts | 13 ++++++++++ .../bookings/DocumentClearanceDetailPage.tsx | 23 +++++++++++++++-- .../contracts/ContractClearanceListPage.tsx | 25 +++++++++++++++---- .../backoffice/src/types/booking.ts | 2 ++ 6 files changed, 82 insertions(+), 7 deletions(-) diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts b/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts index b3ae07861..3ad75242d 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts @@ -620,6 +620,29 @@ export class BookingsRepository extends BaseRepository { }); } + /** + * Bookings (of those given) that have at least one customer document still + * waiting on GL — PENDING or QUERIED. Includes ad-hoc `custom_*` documents, + * which no milestone tracks, so a file added after clearance was finalized + * still surfaces as needing review. One query for a whole queue page. + */ + async findBookingsWithUnreviewedDocuments( + bookingIds: string[], + ): Promise> { + if (bookingIds.length === 0) return new Set(); + const rows = (await this.dataSource + .getRepository(BookingDocumentReview) + .createQueryBuilder('r') + .select('DISTINCT r.booking_id', 'bookingId') + .where('r.booking_id IN (:...bookingIds)', { bookingIds }) + .andWhere('r.status IN (:...statuses)', { + statuses: ['PENDING', 'QUERIED'], + }) + .andWhere('r.deleted_at IS NULL') + .getRawMany()) as Array<{ bookingId: string }>; + return new Set(rows.map((r) => r.bookingId)); + } + findDocumentReview( bookingId: string, settingCode: string, diff --git a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.spec.ts b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.spec.ts index 420854f8f..5cee12d3b 100644 --- a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.spec.ts +++ b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.spec.ts @@ -40,6 +40,9 @@ function makeService(overrides?: { findDocumentReviews: jest.fn().mockResolvedValue([]), update: jest.fn().mockResolvedValue(booking), findByStatuses: jest.fn().mockResolvedValue([]), + findBookingsWithUnreviewedDocuments: jest + .fn() + .mockResolvedValue(new Set()), }; const bookingsService = { findById: jest.fn().mockResolvedValue(booking), diff --git a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts index a128d194f..d9fe1825f 100644 --- a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts @@ -1175,6 +1175,19 @@ export class BookingClearanceService { ); filtered.push(b); } + + // A document added after clearance was finalized lands as PENDING without + // moving the booking's status — the row would otherwise still read + // "Clearance ready" while GL has something waiting. Ad-hoc documents are + // tracked by no milestone, so this reads the review rows directly. + const pending = await this.bookingsRepository.findBookingsWithUnreviewedDocuments( + filtered.map((b) => b.id), + ); + for (const b of filtered) { + (b as Booking & { hasDocumentsAwaitingReview?: boolean }) + .hasDocumentsAwaitingReview = pending.has(b.id); + } + const rows = await this.attachContractSummary(filtered); return this.narrowToYardScope(rows, user); } 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 555b0009e..d98039947 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx @@ -105,8 +105,15 @@ export default function DocumentClearanceDetailPage() { const queried = docs.filter((d) => d.reviewStatus === "QUERIED").length; const pending = total - approved - queried; const pct = total === 0 ? 0 : Math.round((approved / total) * 100); - return { total, approved, queried, pending, pct }; + // Documents actually sitting with GL: a file is present but not approved. + // Excludes required slots the customer never filled — those are on the + // customer, not on GL. + const awaitingReview = docs.filter( + (d) => d.file && d.reviewStatus !== "APPROVED", + ).length; + return { total, approved, queried, pending, pct, awaitingReview }; }, [clearance]); + const awaitingReview = stats.awaitingReview; const reference = booking?.reference ?? "Clearance"; // Phased customs clearance runs on every contract booking now — ONE_TIME and @@ -219,7 +226,19 @@ export default function DocumentClearanceDetailPage() { Customs ) : null} - {clearance.allApproved ? ( + {/* Waiting on GL: an uploaded document with no decision yet, or + one under query. `allApproved` only covers the REQUIRED set, + so an ad-hoc file added after clearance never moves it. */} + {awaitingReview > 0 ? ( + } + > + {awaitingReview} needs approval + + ) : clearance.allApproved ? ( ({ all: allRows.length, + // Counts anything actually waiting on GL, including a document added + // after clearance was finalized (the status stays CLEARANCE_READY). review: allRows.filter( - (r) => r.status === "AWAITING_DOCUMENTS" || r.status === "DOCUMENTS_UNDER_REVIEW", + (r) => + r.status === "AWAITING_DOCUMENTS" || + r.status === "DOCUMENTS_UNDER_REVIEW" || + r.hasDocumentsAwaitingReview, ).length, ready: allRows.filter((r) => r.status === "CLEARANCE_READY" || r.bookingCreated) .length, @@ -347,6 +353,8 @@ interface ShipmentBookingRow { status: string; /** Every required document approved, even before clearance is finalized. */ allDocsApproved: boolean; + /** A customer document is waiting on GL — including one added post-clearance. */ + hasDocumentsAwaitingReview: boolean; /** Requested quantities from the originating shipment request. */ requested: Freight.RequestedShipmentLines | null; /** Contract this shipment booking was created under. */ @@ -521,10 +529,17 @@ function ShipmentBookingsTable({ header: () => Status, cell: ({ row }) => ( - {/* 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 ? ( + {/* A document is waiting on GL. This outranks the booking status: + a file added after clearance was finalized leaves the status at + CLEARANCE_READY, and the row must still call for the review. */} + {row.original.hasDocumentsAwaitingReview ? ( + + Needs approval + + ) : /* 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 ? ( Documents approved diff --git a/apps/edr-freight-web/backoffice/src/types/booking.ts b/apps/edr-freight-web/backoffice/src/types/booking.ts index 493f734bf..258203002 100644 --- a/apps/edr-freight-web/backoffice/src/types/booking.ts +++ b/apps/edr-freight-web/backoffice/src/types/booking.ts @@ -236,6 +236,8 @@ export interface BookingDetail { customsClearingAgent?: string | null; /** ET clearance queue: every required document approved (pre-finalize). */ allDocsApproved?: boolean; + /** ET clearance queue: a customer document is PENDING or QUERIED. */ + hasDocumentsAwaitingReview?: boolean; contractKind?: "ONE_TIME" | "GENERAL" | null; contractId?: string | null; /** Reference of the contract this booking was created under (list column + search). */