Fix searching on GET APIs

This commit is contained in:
SepehrYahyaee
2026-06-23 10:22:46 +03:30
parent 16d3c54613
commit c6b417ced7
9 changed files with 223 additions and 45 deletions

View File

@@ -9,9 +9,10 @@ import {
MaxLength, MaxLength,
Min, Min,
} from "class-validator"; } from "class-validator";
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { UNIFIED_FILE_STATUS_KEYS } from "src/helpers/unified-file-status"; import { UNIFIED_FILE_STATUS_KEYS } from "src/helpers/unified-file-status";
/** Allowed sort keys for V2 list endpoints (claims / blame). */ /** Allowed sort keys for V2 list endpoints (claims / blame / insurer files). */
export const LIST_SORT_BY_V2 = [ export const LIST_SORT_BY_V2 = [
"publicId", "publicId",
"createdAt", "createdAt",
@@ -21,9 +22,16 @@ export const LIST_SORT_BY_V2 = [
export type ListSortByV2 = (typeof LIST_SORT_BY_V2)[number]; export type ListSortByV2 = (typeof LIST_SORT_BY_V2)[number];
export const LIST_FILE_TYPE_V2 = [
BlameRequestType.THIRD_PARTY,
BlameRequestType.CAR_BODY,
] as const;
export type ListFileTypeV2 = (typeof LIST_FILE_TYPE_V2)[number];
/** /**
* Optional query params for V2 lists. If neither `page` nor `limit` is sent, * Optional query params for V2 expert/insurer file lists.
* the full filtered+sorted list is returned (backward compatible). * If neither `page` nor `limit` is sent, the full filtered+sorted list is returned.
*/ */
export class ListQueryV2Dto { export class ListQueryV2Dto {
@ApiPropertyOptional({ @ApiPropertyOptional({
@@ -68,7 +76,7 @@ export class ListQueryV2Dto {
@ApiPropertyOptional({ @ApiPropertyOptional({
description: description:
"Case-insensitive substring match on `publicId` and `requestNo` (and other endpoint-specific fields).", "Case-insensitive substring match on `publicId`, `requestNo`, status, and other endpoint-specific fields.",
example: "A142", example: "A142",
maxLength: 64, maxLength: 64,
}) })
@@ -85,4 +93,13 @@ export class ListQueryV2Dto {
@IsOptional() @IsOptional()
@IsIn([...UNIFIED_FILE_STATUS_KEYS]) @IsIn([...UNIFIED_FILE_STATUS_KEYS])
unifiedStatus?: (typeof UNIFIED_FILE_STATUS_KEYS)[number]; unifiedStatus?: (typeof UNIFIED_FILE_STATUS_KEYS)[number];
@ApiPropertyOptional({
enum: LIST_FILE_TYPE_V2,
description:
"Filter by blame file type: third-party liability (`THIRD_PARTY`) or car body (`CAR_BODY`).",
})
@IsOptional()
@IsIn([...LIST_FILE_TYPE_V2])
fileType?: ListFileTypeV2;
} }

View File

@@ -1,10 +1,42 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsIn, IsISO8601, IsOptional } from "class-validator";
import {
LIST_FILE_TYPE_V2,
ListFileTypeV2,
} from "src/common/dto/list-query-v2.dto";
import { import {
UNIFIED_FILE_STATUS_KEYS, UNIFIED_FILE_STATUS_KEYS,
UnifiedFileStatusDefinition, UnifiedFileStatusDefinition,
UnifiedFileStatusKey, UnifiedFileStatusKey,
} from "src/helpers/unified-file-status"; } from "src/helpers/unified-file-status";
export class UnifiedFileStatusReportQueryDto {
@ApiPropertyOptional({
description: "Optional start datetime (ISO 8601) for portfolio date filter.",
example: "2026-01-01T00:00:00.000Z",
})
@IsOptional()
@IsISO8601({ strict: false })
from?: string;
@ApiPropertyOptional({
description: "Optional end datetime (ISO 8601) for portfolio date filter.",
example: "2026-12-31T23:59:59.999Z",
})
@IsOptional()
@IsISO8601({ strict: false })
to?: string;
@ApiPropertyOptional({
enum: LIST_FILE_TYPE_V2,
description:
"Count only files of this blame type (`THIRD_PARTY` or `CAR_BODY`).",
})
@IsOptional()
@IsIn([...LIST_FILE_TYPE_V2])
fileType?: ListFileTypeV2;
}
export class UnifiedFileStatusDefinitionDto implements UnifiedFileStatusDefinition { export class UnifiedFileStatusDefinitionDto implements UnifiedFileStatusDefinition {
@ApiProperty({ enum: UNIFIED_FILE_STATUS_KEYS }) @ApiProperty({ enum: UNIFIED_FILE_STATUS_KEYS })
key: UnifiedFileStatusKey; key: UnifiedFileStatusKey;

View File

@@ -28,9 +28,13 @@ import {
getUnifiedFileStatusCatalog, getUnifiedFileStatusCatalog,
resolveUnifiedFileStatus, resolveUnifiedFileStatus,
} from "src/helpers/unified-file-status"; } from "src/helpers/unified-file-status";
import { applyListQueryV2 } from "src/helpers/list-query-v2"; import { applyListQueryV2, isInListDateRange, parseListDateRange } from "src/helpers/list-query-v2";
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service"; import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto"; import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import {
UnifiedFileStatusReportDto,
UnifiedFileStatusReportQueryDto,
} from "src/common/dto/unified-file-status-report.dto";
import { import {
ExpertFileAssignResultDto, ExpertFileAssignResultDto,
ExpertFileAssignStatus, ExpertFileAssignStatus,
@@ -156,8 +160,25 @@ export class ExpertBlameService {
}); });
} }
async getUnifiedFileStatusReportV2(actor: any) { async getUnifiedFileStatusReportV2(
const rows = await this.collectBlamePortfolioDocs(actor); actor: any,
query: UnifiedFileStatusReportQueryDto = {},
): Promise<UnifiedFileStatusReportDto> {
let rows = await this.collectBlamePortfolioDocs(actor);
const { fromDate, toDate } = parseListDateRange(query.from, query.to);
rows = rows.filter((doc) =>
isInListDateRange(
(doc as { createdAt?: Date }).createdAt,
fromDate,
toDate,
),
);
if (query.fileType) {
rows = rows.filter(
(doc) =>
String((doc as { type?: string }).type ?? "") === query.fileType,
);
}
const claimByPublicId = await this.loadClaimsByPublicIds( const claimByPublicId = await this.loadClaimsByPublicIds(
rows.map((d) => String((d as { publicId?: string }).publicId ?? "")), rows.map((d) => String((d as { publicId?: string }).publicId ?? "")),
); );
@@ -590,6 +611,8 @@ export class ExpertBlameService {
claimStatus: claim?.status, claimStatus: claim?.status,
}); });
}, },
fileType: (doc) =>
String((doc as { type?: string }).type ?? "") || undefined,
searchExtras: (doc) => { searchExtras: (doc) => {
const d = doc as { const d = doc as {
_id?: unknown; _id?: unknown;

View File

@@ -26,6 +26,10 @@ import { CurrentUser } from "src/decorators/user.decorator";
import { RoleEnum } from "src/Types&Enums/role.enum"; import { RoleEnum } from "src/Types&Enums/role.enum";
import { ExpertBlameService } from "./expert-blame.service"; import { ExpertBlameService } from "./expert-blame.service";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto"; import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import {
UnifiedFileStatusReportDto,
UnifiedFileStatusReportQueryDto,
} from "src/common/dto/unified-file-status-report.dto";
import { AllRequestDtoRsV2 } from "./dto/all-request.dto"; import { AllRequestDtoRsV2 } from "./dto/all-request.dto";
import { SubmitReplyDto } from "./dto/reply.dto"; import { SubmitReplyDto } from "./dto/reply.dto";
import { ResendRequestDto } from "./dto/resend.dto"; import { ResendRequestDto } from "./dto/resend.dto";
@@ -44,8 +48,9 @@ export class ExpertBlameV2Controller {
description: description:
"Damage experts (`expert`): tenant-scoped **DISAGREEMENT** queue (available, locked, or decided by you). " + "Damage experts (`expert`): tenant-scoped **DISAGREEMENT** queue (available, locked, or decided by you). " +
"Field experts (`field_expert`): all expert-initiated blame files they created (LINK + IN_PERSON). Use expert-claim for claims after blame completion. " + "Field experts (`field_expert`): all expert-initiated blame files they created (LINK + IN_PERSON). Use expert-claim for claims after blame completion. " +
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus` (calculated blame+claim status).", "Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus`, `fileType` (THIRD_PARTY | CAR_BODY).",
}) })
@ApiResponse({ status: 200, type: AllRequestDtoRsV2 })
async findAll( async findAll(
@CurrentUser() actor: any, @CurrentUser() actor: any,
@Query() query: ListQueryV2Dto, @Query() query: ListQueryV2Dto,
@@ -64,11 +69,18 @@ export class ExpertBlameV2Controller {
@ApiOperation({ @ApiOperation({
summary: "Unified file status catalog + counts (blame + linked claim)", summary: "Unified file status catalog + counts (blame + linked claim)",
description: description:
"Calculatable statuses for this expert's blame portfolio. Filter lists with `unifiedStatus` query param.", "Calculatable statuses for this expert's blame portfolio. Filter lists with `unifiedStatus` and `fileType` query params. Optional `from` / `to` ISO dates narrow the counted portfolio.",
}) })
async getUnifiedFileStatusReportV2(@CurrentUser() actor: any) { @ApiResponse({ status: 200, type: UnifiedFileStatusReportDto })
async getUnifiedFileStatusReportV2(
@CurrentUser() actor: any,
@Query() query: UnifiedFileStatusReportQueryDto,
): Promise<UnifiedFileStatusReportDto> {
try { try {
return await this.expertBlameService.getUnifiedFileStatusReportV2(actor); return await this.expertBlameService.getUnifiedFileStatusReportV2(
actor,
query,
);
} catch (error) { } catch (error) {
if (error instanceof HttpException) throw error; if (error instanceof HttpException) throw error;
throw new InternalServerErrorException( throw new InternalServerErrorException(

View File

@@ -141,7 +141,15 @@ import {
/** Maximum sum of line `totalPayment` across the claim (Toman; priced parts + factor lines after validation). */ /** Maximum sum of line `totalPayment` across the claim (Toman; priced parts + factor lines after validation). */
import { getClaimV2TotalPaymentCapToman } from "src/constants/repair-amount-limits"; import { getClaimV2TotalPaymentCapToman } from "src/constants/repair-amount-limits";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto"; import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { applyListQueryV2 } from "src/helpers/list-query-v2"; import {
UnifiedFileStatusReportDto,
UnifiedFileStatusReportQueryDto,
} from "src/common/dto/unified-file-status-report.dto";
import {
applyListQueryV2,
isInListDateRange,
parseListDateRange,
} from "src/helpers/list-query-v2";
import { import {
countUnifiedFileStatuses, countUnifiedFileStatuses,
getUnifiedFileStatusCatalog, getUnifiedFileStatusCatalog,
@@ -3416,13 +3424,37 @@ export class ExpertClaimService {
return buckets; return buckets;
} }
async getUnifiedFileStatusReportV2(actor: any) { async getUnifiedFileStatusReportV2(
actor: any,
query: UnifiedFileStatusReportQueryDto = {},
): Promise<UnifiedFileStatusReportDto> {
const { claims, blameById } = const { claims, blameById } =
await this.collectClaimPortfolioWithBlame(actor); await this.collectClaimPortfolioWithBlame(actor);
const { fromDate, toDate } = parseListDateRange(query.from, query.to);
const filteredClaims = claims.filter((c) => {
if (
!isInListDateRange(
c.createdAt as Date | string | undefined,
fromDate,
toDate,
)
) {
return false;
}
if (!query.fileType) return true;
const blame = c.blameRequestId
? blameById.get(String(c.blameRequestId))
: undefined;
const type =
blame?.type ??
(c as { snapshot?: { accident?: { type?: string } } })?.snapshot
?.accident?.type;
return type === query.fileType;
});
return { return {
statuses: getUnifiedFileStatusCatalog(), statuses: getUnifiedFileStatusCatalog(),
counts: countUnifiedFileStatuses( counts: countUnifiedFileStatuses(
claims.map((c) => { filteredClaims.map((c) => {
const blame = c.blameRequestId const blame = c.blameRequestId
? blameById.get(String(c.blameRequestId)) ? blameById.get(String(c.blameRequestId))
: undefined; : undefined;
@@ -3532,6 +3564,7 @@ export class ExpertClaimService {
createdAt: (r) => r.createdAt, createdAt: (r) => r.createdAt,
requestNo: (r) => r.publicId, requestNo: (r) => r.publicId,
status: (r) => r.unifiedFileStatus ?? r.status, status: (r) => r.unifiedFileStatus ?? r.status,
fileType: (r) => r.blameRequestType,
searchExtras: (r) => searchExtras: (r) =>
[ [
r.claimRequestId, r.claimRequestId,

View File

@@ -31,6 +31,10 @@ import { RoleEnum } from "src/Types&Enums/role.enum";
import { ExpertFileAssignResultDto } from "src/common/dto/expert-file-assign-result.dto"; import { ExpertFileAssignResultDto } from "src/common/dto/expert-file-assign-result.dto";
import { ExpertClaimService } from "./expert-claim.service"; import { ExpertClaimService } from "./expert-claim.service";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto"; import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import {
UnifiedFileStatusReportDto,
UnifiedFileStatusReportQueryDto,
} from "src/common/dto/unified-file-status-report.dto";
import { GetClaimListV2ResponseDto } from "./dto/claim-list-v2.dto"; import { GetClaimListV2ResponseDto } from "./dto/claim-list-v2.dto";
import { import {
ClaimSubmitResendV2Dto, ClaimSubmitResendV2Dto,
@@ -69,10 +73,17 @@ export class ExpertClaimV2Controller {
@ApiOperation({ @ApiOperation({
summary: "Unified file status catalog + counts (claim + linked blame)", summary: "Unified file status catalog + counts (claim + linked blame)",
description: description:
"Calculatable statuses for this expert's claim portfolio. Filter lists with `unifiedStatus` query param.", "Calculatable statuses for this expert's claim portfolio. Filter lists with `unifiedStatus` and `fileType` query params. Optional `from` / `to` ISO dates narrow the counted portfolio.",
}) })
async getUnifiedFileStatusReportV2(@CurrentUser() actor: any) { @ApiResponse({ status: 200, type: UnifiedFileStatusReportDto })
return await this.expertClaimService.getUnifiedFileStatusReportV2(actor); async getUnifiedFileStatusReportV2(
@CurrentUser() actor: any,
@Query() query: UnifiedFileStatusReportQueryDto,
): Promise<UnifiedFileStatusReportDto> {
return await this.expertClaimService.getUnifiedFileStatusReportV2(
actor,
query,
);
} }
@Get("report/status-counts") @Get("report/status-counts")
@@ -127,7 +138,8 @@ export class ExpertClaimV2Controller {
summary: "List available claim requests for damage expert", summary: "List available claim requests for damage expert",
description: description:
"Damage experts: tenant queue (`WAITING_FOR_DAMAGE_EXPERT`), locked/in-progress, factor validation. " + "Damage experts: tenant queue (`WAITING_FOR_DAMAGE_EXPERT`), locked/in-progress, factor validation. " +
"Field experts: all claims linked to blame files they initiated (`initiatedByFieldExpertId` or matching `blameRequestId`). Optional query: `search`, `sortBy`, `sortOrder`, `page`, `limit`, `unifiedStatus`.", "Field experts: all claims linked to blame files they initiated (`initiatedByFieldExpertId` or matching `blameRequestId`). " +
"Optional query: `search`, `sortBy`, `sortOrder`, `page`, `limit`, `unifiedStatus`, `fileType` (THIRD_PARTY | CAR_BODY).",
}) })
@ApiResponse({ status: 200, type: GetClaimListV2ResponseDto }) @ApiResponse({ status: 200, type: GetClaimListV2ResponseDto })
async getClaimListV2( async getClaimListV2(

View File

@@ -16,6 +16,7 @@ import {
ApiOperation, ApiOperation,
ApiParam, ApiParam,
ApiQuery, ApiQuery,
ApiResponse,
ApiTags, ApiTags,
} from "@nestjs/swagger"; } from "@nestjs/swagger";
import { Types } from "mongoose"; import { Types } from "mongoose";
@@ -27,6 +28,10 @@ import { FileRating } from "src/request-management/entities/schema/request-manag
import { RoleEnum } from "src/Types&Enums/role.enum"; import { RoleEnum } from "src/Types&Enums/role.enum";
import { ExpertInsurerService } from "./expert-insurer.service"; import { ExpertInsurerService } from "./expert-insurer.service";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto"; import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import {
UnifiedFileStatusReportDto,
UnifiedFileStatusReportQueryDto,
} from "src/common/dto/unified-file-status-report.dto";
import { CreateBranchDto } from "src/client/dto/create-branch.dto"; import { CreateBranchDto } from "src/client/dto/create-branch.dto";
import { import {
CreateBlameExpertByInsurerDto, CreateBlameExpertByInsurerDto,
@@ -166,7 +171,7 @@ export class ExpertInsurerController {
@ApiOperation({ @ApiOperation({
summary: "List insurer files (blame + claim merged by publicId)", summary: "List insurer files (blame + claim merged by publicId)",
description: description:
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus`.", "Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus`, `fileType` (THIRD_PARTY | CAR_BODY).",
}) })
async getAllFiles( async getAllFiles(
@CurrentUser() insurer, @CurrentUser() insurer,
@@ -182,27 +187,16 @@ export class ExpertInsurerController {
@ApiOperation({ @ApiOperation({
summary: "Unified file status catalog + counts (blame + claim)", summary: "Unified file status catalog + counts (blame + claim)",
description: description:
"Returns calculatable unified statuses and per-status counts for this insurer's files. Use `unifiedStatus` on GET files to filter.", "Calculatable unified statuses and per-status counts for this insurer's files. Filter lists with `unifiedStatus` and `fileType`. Optional `from` / `to` ISO dates narrow the counted portfolio.",
})
@ApiQuery({
name: "from",
required: false,
description: "Optional start datetime (ISO string)",
})
@ApiQuery({
name: "to",
required: false,
description: "Optional end datetime (ISO string)",
}) })
@ApiResponse({ status: 200, type: UnifiedFileStatusReportDto })
async getUnifiedFileStatusReport( async getUnifiedFileStatusReport(
@CurrentUser() actor, @CurrentUser() actor,
@Query("from") from?: string, @Query() query: UnifiedFileStatusReportQueryDto,
@Query("to") to?: string, ): Promise<UnifiedFileStatusReportDto> {
) {
return await this.expertInsurerService.getInsurerUnifiedFileStatusReport( return await this.expertInsurerService.getInsurerUnifiedFileStatusReport(
actor, actor,
from, query,
to,
); );
} }

View File

@@ -40,7 +40,15 @@ import { enrichBlamePartiesForAgreementView } from "src/helpers/blame-party-agre
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum"; import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum"; import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto"; import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { applyListQueryV2 } from "src/helpers/list-query-v2"; import {
UnifiedFileStatusReportDto,
UnifiedFileStatusReportQueryDto,
} from "src/common/dto/unified-file-status-report.dto";
import {
applyListQueryV2,
isInListDateRange,
parseListDateRange,
} from "src/helpers/list-query-v2";
import { import {
countUnifiedFileStatuses, countUnifiedFileStatuses,
getUnifiedFileStatusCatalog, getUnifiedFileStatusCatalog,
@@ -1328,6 +1336,7 @@ export class ExpertInsurerService {
createdAt: (f) => f.updatedAt ?? f.createdAt, createdAt: (f) => f.updatedAt ?? f.createdAt,
requestNo: (f) => f.publicId, requestNo: (f) => f.publicId,
status: (f) => f.unifiedFileStatus, status: (f) => f.unifiedFileStatus,
fileType: (f) => f.fileType,
searchExtras: (f) => searchExtras: (f) =>
[ [
f.fileType, f.fileType,
@@ -1352,16 +1361,18 @@ export class ExpertInsurerService {
async getInsurerUnifiedFileStatusReport( async getInsurerUnifiedFileStatusReport(
actor: any, actor: any,
from?: string, query: UnifiedFileStatusReportQueryDto = {},
to?: string, ): Promise<UnifiedFileStatusReportDto> {
) {
const clientObjectId = this.getClientId(actor); const clientObjectId = this.getClientId(actor);
const { fromDate, toDate } = this.parseDateRange(from, to); const { fromDate, toDate } = parseListDateRange(query.from, query.to);
const files = await this.buildMergedInsurerFiles(clientObjectId); const files = await this.buildMergedInsurerFiles(clientObjectId);
const inRange = files.filter((f) => let inRange = files.filter((f) =>
this.isInDateRange(f.createdAt, fromDate, toDate), isInListDateRange(f.createdAt, fromDate, toDate),
); );
if (query.fileType) {
inRange = inRange.filter((f) => f.fileType === query.fileType);
}
return { return {
statuses: getUnifiedFileStatusCatalog(), statuses: getUnifiedFileStatusCatalog(),
@@ -1946,7 +1957,10 @@ export class ExpertInsurerService {
under_review: number; under_review: number;
waiting_for_documents_resend: number; waiting_for_documents_resend: number;
}> { }> {
const report = await this.getInsurerUnifiedFileStatusReport(actor, from, to); const report = await this.getInsurerUnifiedFileStatusReport(actor, {
from,
to,
});
const counts = report.counts; const counts = report.counts;
return { return {
all: counts.all ?? 0, all: counts.all ?? 0,

View File

@@ -1,4 +1,5 @@
import type { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto"; import type { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { BadRequestException } from "@nestjs/common";
const MAX_LIMIT = 100; const MAX_LIMIT = 100;
const DEFAULT_LIMIT_WHEN_PAGE_SET = 20; const DEFAULT_LIMIT_WHEN_PAGE_SET = 20;
@@ -8,10 +9,45 @@ export type ListItemGetters<T> = {
createdAt: (row: T) => Date | string | number | undefined; createdAt: (row: T) => Date | string | number | undefined;
requestNo?: (row: T) => string | undefined; requestNo?: (row: T) => string | undefined;
status: (row: T) => string | undefined; status: (row: T) => string | undefined;
/** Blame file type (`THIRD_PARTY` | `CAR_BODY`) when available. */
fileType?: (row: T) => string | undefined;
/** Extra strings to match against `search` (e.g. claimRequestId, vehicle). */ /** Extra strings to match against `search` (e.g. claimRequestId, vehicle). */
searchExtras?: (row: T) => string[]; searchExtras?: (row: T) => string[];
}; };
export function parseListDateRange(
from?: string,
to?: string,
): { fromDate?: Date; toDate?: Date } {
const fromDate = from ? new Date(from) : undefined;
const toDate = to ? new Date(to) : undefined;
if (fromDate && Number.isNaN(fromDate.getTime())) {
throw new BadRequestException("Invalid 'from' date");
}
if (toDate && Number.isNaN(toDate.getTime())) {
throw new BadRequestException("Invalid 'to' date");
}
if (fromDate && toDate && fromDate > toDate) {
throw new BadRequestException("'from' must be before or equal to 'to'");
}
return { fromDate, toDate };
}
export function isInListDateRange(
value: Date | string | number | undefined,
fromDate?: Date,
toDate?: Date,
): boolean {
if (!fromDate && !toDate) return true;
if (value == null) return false;
const d = value instanceof Date ? value : new Date(value);
if (Number.isNaN(d.getTime())) return false;
if (fromDate && d < fromDate) return false;
if (toDate && d > toDate) return false;
return true;
}
function normalizeSearch(raw?: string): string | undefined { function normalizeSearch(raw?: string): string | undefined {
const t = raw?.trim().toLowerCase(); const t = raw?.trim().toLowerCase();
return t || undefined; return t || undefined;
@@ -40,8 +76,13 @@ export function applyListQueryV2<T>(
} { } {
const search = normalizeSearch(query.search); const search = normalizeSearch(query.search);
let filtered = rows; let filtered = rows;
if (query.fileType && getters.fileType) {
filtered = filtered.filter(
(row) => getters.fileType!(row) === query.fileType,
);
}
if (search) { if (search) {
filtered = rows.filter((row) => { filtered = filtered.filter((row) => {
const chunks: string[] = []; const chunks: string[] = [];
const pid = getters.publicId(row); const pid = getters.publicId(row);
if (pid) chunks.push(pid); if (pid) chunks.push(pid);