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 17e3631e0..673e0fe64 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.repository.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.repository.ts @@ -84,6 +84,7 @@ export class CompaniesRepository extends BaseRepository { createdTo, onboardingCompleted, hasPendingChangeRequest, + profileType, sortBy = 'review', sortOrder = 'DESC', } = query; @@ -138,20 +139,46 @@ export class CompaniesRepository extends BaseRepository { if (search) { const term = `%${search.trim()}%`; + // Staff search by whatever is in front of them: the company name, the + // TIN/email, a profile reference off a document — and, since a TIN holds + // many licences, the trade name or licence number of the specific + // business a role operates as. All the per-profile terms share one EXISTS + // so a match on any of them qualifies the company once. qb.andWhere( `(company.name ILIKE :term OR company.tin ILIKE :term OR company.email ILIKE :term + OR company.licence_number ILIKE :term OR EXISTS ( SELECT 1 FROM freight.company_profiles cp WHERE cp.company_id = company.id - AND cp.reference ILIKE :term AND cp.deleted_at IS NULL + AND ( + cp.reference ILIKE :term + OR cp.etrade_business->>'tradeName' ILIKE :term + OR cp.etrade_business->>'licenceNumber' ILIKE :term + ) ))`, { term }, ); } + // Companies holding a given operational role. EXISTS rather than a filter + // on the joined `companyProfiles` alias: constraining the join would drop + // the company's OTHER profiles from the loaded entity, so the list would + // render an exporter-and-importer as importer-only. + if (profileType) { + qb.andWhere( + `EXISTS ( + SELECT 1 FROM freight.company_profiles cp_type + WHERE cp_type.company_id = company.id + AND cp_type.deleted_at IS NULL + AND cp_type.type = :profileType + )`, + { profileType }, + ); + } + // 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 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 18b816a2a..33a5f4131 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 @@ -15,6 +15,7 @@ import { CompanyStatus, CompanyType, } from "../entities/company.entity"; +import { ProfileType } from "../entities/company-profile.entity"; export class ListCompaniesQueryDto { @ApiPropertyOptional({ default: 1 }) @@ -56,6 +57,16 @@ export class ListCompaniesQueryDto { @IsIn(Object.values(CompanyNationality)) nationality?: CompanyNationality; + @ApiPropertyOptional({ + enum: ProfileType, + description: + "Only companies holding this operational role. A company may hold " + + "several; its other roles are still returned on the row.", + }) + @IsOptional() + @IsIn(Object.values(ProfileType)) + profileType?: ProfileType; + @ApiPropertyOptional({ description: "Registered on or after this instant (ISO)." }) @IsOptional() @IsDateString() 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 72eb38fe6..ef57313f6 100644 --- a/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx @@ -108,6 +108,24 @@ const CUSTOMER_FILTER_DEFS: FilterDef[] = [ ["customer", "freight_forwarder", "dj_freight_forwarder", "transporter"] as const ).map((value) => ({ value, label: humanize(value) })), }, + { + // The operational role, not `type` above: one `customer` company routinely + // holds importer AND exporter, so this asks "who does X?" rather than + // "what kind of company is this?". + key: "profileType", + label: "Role", + type: "enum", + multiple: false, + options: ( + [ + "importer", + "exporter", + "freight_forwarder", + "dj_freight_forwarder", + "transporter", + ] as const + ).map((value) => ({ value, label: humanize(value) })), + }, { key: "kind", label: "Sector", @@ -348,7 +366,7 @@ export default function CustomersPage() { ({ ...o }))} viewId="customers" > diff --git a/apps/edr-freight-web/backoffice/src/types/customer.ts b/apps/edr-freight-web/backoffice/src/types/customer.ts index e45112ecd..db5da275e 100644 --- a/apps/edr-freight-web/backoffice/src/types/customer.ts +++ b/apps/edr-freight-web/backoffice/src/types/customer.ts @@ -325,6 +325,13 @@ export interface CompanyListFilter { kind?: CompanyKind; status?: CompanyStatus; nationality?: CompanyNationality; + /** + * Only companies holding this operational role. Distinct from `type`, which + * is the company's own kind — a `customer` company can hold importer, + * exporter and forwarder roles at once, and its other roles still come back + * on the row. + */ + profileType?: ProfileType; /** ISO instants — inclusive bounds on the registration date. */ createdFrom?: string; createdTo?: string;