feat(filters): route filter, select-styled trigger, tune default pin set

- New "route" FilterType: RouteBody popover (searchable origin +
  destination selects, apply once both are picked), wired into
  ContractRequestsPage and BookingRequestsPage. Bookings already had
  server-side originYardId/destinationYardId; contracts gets both new
  (contract_routes is one-to-many, so origin/destination are separate
  EXISTS subqueries, not a join).
- Inactive FilterPill trigger restyled to read like a closed Mantine
  Select (opaque solid border, trailing chevron) instead of a dashed
  "+" pill — trigger only, popover body/position unchanged.
- Search box text set to regular weight.
- A couple more filters (Direction, Freight) pinned by default per
  page on top of the existing always-pinned ones; the rest stay behind
  More filters.
This commit is contained in:
Nathnael
2026-08-15 07:23:43 +00:00
parent e5e75b4166
commit 839dfdbae3
10 changed files with 157 additions and 19 deletions

View File

@@ -48,6 +48,8 @@ export interface ContractListFilterOptions {
hasClearanceDocuments?: boolean;
createdFrom?: string;
createdTo?: string;
originYardId?: string;
destinationYardId?: string;
}
@Injectable()
@@ -489,6 +491,25 @@ export class ContractsRepository extends BaseRepository<Contract> {
if (options.createdTo) {
qb.andWhere('contract.created_at <= :createdTo', { createdTo: options.createdTo });
}
// Routes are one-to-many (a contract can list several lanes), so origin
// and destination each need their own EXISTS — a plain join would
// duplicate the contract row per matching route.
if (omit !== 'originYardId' && options.originYardId) {
qb.andWhere(
'EXISTS (SELECT 1 FROM freight.contract_routes cr_o ' +
'WHERE cr_o.contract_id = contract.id AND cr_o.deleted_at IS NULL ' +
'AND cr_o.origin_yard_id = :originYardId)',
{ originYardId: options.originYardId },
);
}
if (omit !== 'destinationYardId' && options.destinationYardId) {
qb.andWhere(
'EXISTS (SELECT 1 FROM freight.contract_routes cr_d ' +
'WHERE cr_d.contract_id = contract.id AND cr_d.deleted_at IS NULL ' +
'AND cr_d.destination_yard_id = :destinationYardId)',
{ destinationYardId: options.destinationYardId },
);
}
}
/**

View File

@@ -768,6 +768,8 @@ export class ContractsService {
paymentCurrency: filter.paymentCurrency,
createdFrom: filter.createdFrom,
createdTo: filter.createdTo,
originYardId: filter.originYardId,
destinationYardId: filter.destinationYardId,
search: filter.search,
sortBy: filter.sortBy,
sortOrder: filter.sortOrder,
@@ -789,6 +791,8 @@ export class ContractsService {
paymentCurrency: filter.paymentCurrency,
createdFrom: filter.createdFrom,
createdTo: filter.createdTo,
originYardId: filter.originYardId,
destinationYardId: filter.destinationYardId,
};
const [facets, metrics] = await Promise.all([

View File

@@ -61,6 +61,22 @@ export class FilterContractDto {
@IsIn([...PAYMENT_CURRENCIES])
paymentCurrency?: string;
@ApiPropertyOptional({
format: 'uuid',
description: 'Only contracts with a route starting at this yard.',
})
@IsOptional()
@IsUUID()
originYardId?: string;
@ApiPropertyOptional({
format: 'uuid',
description: 'Only contracts with a route ending at this yard.',
})
@IsOptional()
@IsUUID()
destinationYardId?: string;
@ApiPropertyOptional({ description: 'Filter contracts created on/after this date (ISO)' })
@IsOptional()
@IsDateString()