mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 12:18:11 +00:00
fix(clearance): flag bookings with documents still awaiting GL approval
This commit is contained in:
@@ -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(
|
findDocumentReview(
|
||||||
bookingId: string,
|
bookingId: string,
|
||||||
settingCode: string,
|
settingCode: string,
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ function makeService(overrides?: {
|
|||||||
findDocumentReviews: jest.fn().mockResolvedValue([]),
|
findDocumentReviews: jest.fn().mockResolvedValue([]),
|
||||||
update: jest.fn().mockResolvedValue(booking),
|
update: jest.fn().mockResolvedValue(booking),
|
||||||
findByStatuses: jest.fn().mockResolvedValue([]),
|
findByStatuses: jest.fn().mockResolvedValue([]),
|
||||||
|
findBookingsWithUnreviewedDocuments: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(new Set<string>()),
|
||||||
};
|
};
|
||||||
const bookingsService = {
|
const bookingsService = {
|
||||||
findById: jest.fn().mockResolvedValue(booking),
|
findById: jest.fn().mockResolvedValue(booking),
|
||||||
|
|||||||
@@ -1175,6 +1175,19 @@ export class BookingClearanceService {
|
|||||||
);
|
);
|
||||||
filtered.push(b);
|
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);
|
const rows = await this.attachContractSummary(filtered);
|
||||||
return this.narrowToYardScope(rows, user);
|
return this.narrowToYardScope(rows, user);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,8 +105,15 @@ export default function DocumentClearanceDetailPage() {
|
|||||||
const queried = docs.filter((d) => d.reviewStatus === "QUERIED").length;
|
const queried = docs.filter((d) => d.reviewStatus === "QUERIED").length;
|
||||||
const pending = total - approved - queried;
|
const pending = total - approved - queried;
|
||||||
const pct = total === 0 ? 0 : Math.round((approved / total) * 100);
|
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]);
|
}, [clearance]);
|
||||||
|
const awaitingReview = stats.awaitingReview;
|
||||||
|
|
||||||
const reference = booking?.reference ?? "Clearance";
|
const reference = booking?.reference ?? "Clearance";
|
||||||
// Phased customs clearance runs on every contract booking now — ONE_TIME and
|
// Phased customs clearance runs on every contract booking now — ONE_TIME and
|
||||||
@@ -219,7 +226,19 @@ export default function DocumentClearanceDetailPage() {
|
|||||||
Customs
|
Customs
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : 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
|
<Badge
|
||||||
variant="light"
|
variant="light"
|
||||||
color="edr-green"
|
color="edr-green"
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ export default function ContractClearanceListPage() {
|
|||||||
freightType: b.freightType ?? "—",
|
freightType: b.freightType ?? "—",
|
||||||
status: b.status,
|
status: b.status,
|
||||||
allDocsApproved: Boolean(b.allDocsApproved),
|
allDocsApproved: Boolean(b.allDocsApproved),
|
||||||
|
hasDocumentsAwaitingReview: Boolean(b.hasDocumentsAwaitingReview),
|
||||||
requested: requestedByBooking.get(b.id) ?? null,
|
requested: requestedByBooking.get(b.id) ?? null,
|
||||||
contractId: b.contractId ?? null,
|
contractId: b.contractId ?? null,
|
||||||
contractReference: b.contractReference ?? null,
|
contractReference: b.contractReference ?? null,
|
||||||
@@ -194,8 +195,13 @@ export default function ContractClearanceListPage() {
|
|||||||
const counts = useMemo(
|
const counts = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
all: allRows.length,
|
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(
|
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,
|
).length,
|
||||||
ready: allRows.filter((r) => r.status === "CLEARANCE_READY" || r.bookingCreated)
|
ready: allRows.filter((r) => r.status === "CLEARANCE_READY" || r.bookingCreated)
|
||||||
.length,
|
.length,
|
||||||
@@ -347,6 +353,8 @@ interface ShipmentBookingRow {
|
|||||||
status: string;
|
status: string;
|
||||||
/** Every required document approved, even before clearance is finalized. */
|
/** Every required document approved, even before clearance is finalized. */
|
||||||
allDocsApproved: boolean;
|
allDocsApproved: boolean;
|
||||||
|
/** A customer document is waiting on GL — including one added post-clearance. */
|
||||||
|
hasDocumentsAwaitingReview: boolean;
|
||||||
/** Requested quantities from the originating shipment request. */
|
/** Requested quantities from the originating shipment request. */
|
||||||
requested: Freight.RequestedShipmentLines | null;
|
requested: Freight.RequestedShipmentLines | null;
|
||||||
/** Contract this shipment booking was created under. */
|
/** Contract this shipment booking was created under. */
|
||||||
@@ -521,10 +529,17 @@ function ShipmentBookingsTable({
|
|||||||
header: () => <span className={bookingTable.headerCell}>Status</span>,
|
header: () => <span className={bookingTable.headerCell}>Status</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Group gap={6} wrap="nowrap">
|
<Group gap={6} wrap="nowrap">
|
||||||
{/* All docs approved but not yet finalized: the booking status is
|
{/* A document is waiting on GL. This outranks the booking status:
|
||||||
still DOCUMENTS_UNDER_REVIEW — show the real review state. */}
|
a file added after clearance was finalized leaves the status at
|
||||||
{row.original.status === "DOCUMENTS_UNDER_REVIEW" &&
|
CLEARANCE_READY, and the row must still call for the review. */}
|
||||||
row.original.allDocsApproved ? (
|
{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">
|
<Badge variant="light" color="edr-green" radius="sm">
|
||||||
Documents approved
|
Documents approved
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -236,6 +236,8 @@ export interface BookingDetail {
|
|||||||
customsClearingAgent?: string | null;
|
customsClearingAgent?: string | null;
|
||||||
/** ET clearance queue: every required document approved (pre-finalize). */
|
/** ET clearance queue: every required document approved (pre-finalize). */
|
||||||
allDocsApproved?: boolean;
|
allDocsApproved?: boolean;
|
||||||
|
/** ET clearance queue: a customer document is PENDING or QUERIED. */
|
||||||
|
hasDocumentsAwaitingReview?: boolean;
|
||||||
contractKind?: "ONE_TIME" | "GENERAL" | null;
|
contractKind?: "ONE_TIME" | "GENERAL" | null;
|
||||||
contractId?: string | null;
|
contractId?: string | null;
|
||||||
/** Reference of the contract this booking was created under (list column + search). */
|
/** Reference of the contract this booking was created under (list column + search). */
|
||||||
|
|||||||
Reference in New Issue
Block a user