fix(clearance): flag bookings with documents still awaiting GL approval

This commit is contained in:
Marshal
2026-08-20 07:37:35 +00:00
committed by Hagernesh
parent 21a611f1b7
commit bcf684814d
6 changed files with 82 additions and 7 deletions

View File

@@ -620,6 +620,29 @@ export class BookingsRepository extends BaseRepository<Booking> {
});
}
/**
* 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<Set<string>> {
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,

View File

@@ -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<string>()),
};
const bookingsService = {
findById: jest.fn().mockResolvedValue(booking),

View File

@@ -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);
}

View File

@@ -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
</Badge>
) : 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 ? (
<Badge
variant="filled"
color="orange"
radius="sm"
leftSection={<Clock size={13} />}
>
{awaitingReview} needs approval
</Badge>
) : clearance.allApproved ? (
<Badge
variant="light"
color="edr-green"

View File

@@ -165,6 +165,7 @@ export default function ContractClearanceListPage() {
freightType: b.freightType ?? "—",
status: b.status,
allDocsApproved: Boolean(b.allDocsApproved),
hasDocumentsAwaitingReview: Boolean(b.hasDocumentsAwaitingReview),
requested: requestedByBooking.get(b.id) ?? null,
contractId: b.contractId ?? null,
contractReference: b.contractReference ?? null,
@@ -194,8 +195,13 @@ export default function ContractClearanceListPage() {
const counts = useMemo(
() => ({
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: () => <span className={bookingTable.headerCell}>Status</span>,
cell: ({ row }) => (
<Group gap={6} wrap="nowrap">
{/* 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 ? (
<Badge variant="filled" color="orange" radius="sm">
Needs approval
</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>

View File

@@ -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). */