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
This commit is contained in:
Nathnael
2026-07-21 09:08:53 +00:00
parent af171974f8
commit d76d198a18
4 changed files with 43 additions and 12 deletions

View File

@@ -41,6 +41,18 @@ export class CompaniesRepository extends BaseRepository<Company> {
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<Company>,
@@ -80,8 +92,8 @@ export class CompaniesRepository extends BaseRepository<Company> {
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<Company> {
}
// 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')

View File

@@ -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"])