From d76d198a180e37c8b6a0a680331be6f752e1376d Mon Sep 17 00:00:00 2001 From: Nathnael Date: Tue, 21 Jul 2026 09:08:53 +0000 Subject: [PATCH] fix(companies): default customer list to review-queue ordering Marketing asked for approval requests to surface in order instead of the alphabetical default. New sortBy=review tiers the list by what needs action - submitted applications awaiting first approval, then approved customers with a pending change request, then everyone else (drafts included) - newest first within each tier. Exposed as the backoffice "Needs review first" sort option and made the default on both ends. EDRFREIGHT-232 --- .../modules/companies/companies.repository.ts | 28 +++++++++++++++++-- .../companies/dto/list-companies-query.dto.ts | 16 +++++++---- .../src/pages/customers/CustomersPage.tsx | 8 ++++-- .../backoffice/src/types/customer.ts | 3 +- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/apps/edr-freight-api/src/modules/companies/companies.repository.ts b/apps/edr-freight-api/src/modules/companies/companies.repository.ts index 3ac12b11a..db8db0d2e 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.repository.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.repository.ts @@ -41,6 +41,18 @@ export class CompaniesRepository extends BaseRepository { AND ccr.deleted_at IS NULL )`; + /** + * The `sortBy = 'review'` queue ordering: whatever marketing must act on + * floats to the top. Tier 0 — submitted applications awaiting first approval + * (drafts excluded: nothing to review yet). Tier 1 — approved customers with + * a pending change request. Tier 2 — everyone else, drafts included. + */ + private static readonly REVIEW_TIER_SQL = `(CASE + WHEN company.status = 'pending' AND NOT ${CompaniesRepository.DRAFT_SQL} THEN 0 + WHEN ${CompaniesRepository.PENDING_CHANGE_REQUEST_SQL} THEN 1 + ELSE 2 + END)`; + constructor( @InjectRepository(Company) repo: Repository, @@ -80,8 +92,8 @@ export class CompaniesRepository extends BaseRepository { status, onboardingCompleted, hasPendingChangeRequest, - sortBy = 'name', - sortOrder = 'ASC', + sortBy = 'review', + sortOrder = 'DESC', } = query; const qb = this.repository @@ -137,8 +149,18 @@ export class CompaniesRepository extends BaseRepository { } // sortBy is whitelisted by @IsIn on the DTO, so it is safe to interpolate. + if (sortBy === 'review') { + // Queue ordering: actionable tiers first, newest first within each. The + // tier is selected under an alias because skip/take pagination with + // joins re-derives the ORDER BY in a subquery — a raw expression there + // breaks, a selected alias survives. + qb.addSelect(CompaniesRepository.REVIEW_TIER_SQL, 'review_tier') + .orderBy('review_tier', 'ASC') + .addOrderBy('company.createdAt', 'DESC'); + } else { + qb.orderBy(`company.${sortBy}`, sortOrder); + } const [items, total] = await qb - .orderBy(`company.${sortBy}`, sortOrder) // Names are not unique and createdAt can tie on bulk imports; the id // tiebreaker keeps paging stable instead of dropping/repeating rows. .addOrderBy('company.id', 'ASC') diff --git a/apps/edr-freight-api/src/modules/companies/dto/list-companies-query.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/list-companies-query.dto.ts index 8d4910ded..ffb600e36 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/list-companies-query.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/list-companies-query.dto.ts @@ -60,15 +60,19 @@ export class ListCompaniesQueryDto { hasPendingChangeRequest?: boolean; @ApiPropertyOptional({ - enum: ["name", "createdAt", "updatedAt"], - default: "name", - description: "Column to order by. Defaults to name for backwards compatibility.", + enum: ["review", "name", "createdAt", "updatedAt"], + default: "review", + description: + "Column to order by. The default `review` is a review-queue ordering: " + + "companies awaiting first approval, then those with a pending change " + + "request, then everyone else — newest first within each group. The " + + "other values are plain column sorts.", }) @IsOptional() - @IsIn(["name", "createdAt", "updatedAt"]) - sortBy?: "name" | "createdAt" | "updatedAt"; + @IsIn(["review", "name", "createdAt", "updatedAt"]) + sortBy?: "review" | "name" | "createdAt" | "updatedAt"; - @ApiPropertyOptional({ enum: ["ASC", "DESC"], default: "ASC" }) + @ApiPropertyOptional({ enum: ["ASC", "DESC"], default: "DESC" }) @IsOptional() @Transform(({ value }: { value: unknown }) => String(value).toUpperCase()) @IsIn(["ASC", "DESC"]) diff --git a/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx b/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx index d70e6d040..4c63dc09e 100644 --- a/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx @@ -81,6 +81,10 @@ const VIEW_FILTERS: Record< }; const SORT_OPTIONS = [ + // Queue ordering: awaiting first approval → pending profile changes → the + // rest, newest first within each group. The default, so whatever marketing + // must act on is always on top of the list. + { value: "review:DESC", label: "Needs review first" }, { value: "createdAt:DESC", label: "Newest first" }, { value: "createdAt:ASC", label: "Oldest first" }, { value: "name:ASC", label: "Name (A–Z)" }, @@ -93,11 +97,11 @@ export default function CustomersPage() { const [query, setQuery] = useState(""); const [debouncedQuery] = useDebouncedValue(query, 300); const [view, setView] = useState("all"); - const [sort, setSort] = useState("createdAt:DESC"); + const [sort, setSort] = useState("review:DESC"); const filter = useMemo(() => { const [sortBy, sortOrder] = sort.split(":") as [ - "name" | "createdAt" | "updatedAt", + "review" | "name" | "createdAt" | "updatedAt", "ASC" | "DESC", ]; return { diff --git a/apps/edr-freight-web/backoffice/src/types/customer.ts b/apps/edr-freight-web/backoffice/src/types/customer.ts index 419e93d25..53d3daed8 100644 --- a/apps/edr-freight-web/backoffice/src/types/customer.ts +++ b/apps/edr-freight-web/backoffice/src/types/customer.ts @@ -207,7 +207,8 @@ export interface CompanyListFilter { * already `active`, so `status` alone can never surface them. */ hasPendingChangeRequest?: boolean; - sortBy?: "name" | "createdAt" | "updatedAt"; + /** `review` = queue ordering: awaiting first approval → pending changes → rest, newest first within each. */ + sortBy?: "review" | "name" | "createdAt" | "updatedAt"; sortOrder?: "ASC" | "DESC"; }