mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge pull request #1359 from Tria-plc/freight_feature/usermanagement
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(
|
||||
bookingId: string,
|
||||
settingCode: string,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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). */
|
||||
|
||||
Reference in New Issue
Block a user