import type { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto"; import { BadRequestException } from "@nestjs/common"; const MAX_LIMIT = 100; const DEFAULT_LIMIT_WHEN_PAGE_SET = 20; export type ListItemGetters = { publicId: (row: T) => string | undefined; createdAt: (row: T) => Date | string | number | undefined; requestNo?: (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). */ 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 { const t = raw?.trim().toLowerCase(); return t || undefined; } function toTime(v: Date | string | number | undefined): number { if (v == null) return 0; const n = v instanceof Date ? v.getTime() : new Date(v as string | number).getTime(); return Number.isFinite(n) ? n : 0; } /** * Filter, sort, and optionally paginate in-memory list results. * Pagination applies only when `query.page` or `query.limit` is set. */ export function applyListQueryV2( rows: T[], getters: ListItemGetters, query: ListQueryV2Dto, ): { list: T[]; total: number; page?: number; limit?: number; totalPages?: number; } { const search = normalizeSearch(query.search); let filtered = rows; if (query.fileType && getters.fileType) { filtered = filtered.filter( (row) => getters.fileType!(row) === query.fileType, ); } if (search) { filtered = filtered.filter((row) => { const chunks: string[] = []; const pid = getters.publicId(row); if (pid) chunks.push(pid); const rno = getters.requestNo?.(row); if (rno) chunks.push(rno); const st = getters.status(row); if (st) chunks.push(st); const extras = getters.searchExtras?.(row) ?? []; for (const e of extras) { if (e) chunks.push(e); } const hay = chunks.join(" ").toLowerCase(); return hay.includes(search); }); } const sortBy = query.sortBy ?? "createdAt"; let sortOrder = query.sortOrder; if (!sortOrder) { sortOrder = sortBy === "publicId" || sortBy === "requestNo" ? "asc" : "desc"; } const dir = sortOrder === "asc" ? 1 : -1; const sorted = [...filtered].sort((a, b) => { switch (sortBy) { case "publicId": { const va = String(getters.publicId(a) ?? ""); const vb = String(getters.publicId(b) ?? ""); return dir * va.localeCompare(vb, "en", { numeric: true, sensitivity: "base" }); } case "requestNo": { const va = String( getters.requestNo?.(a) ?? getters.publicId(a) ?? "", ); const vb = String( getters.requestNo?.(b) ?? getters.publicId(b) ?? "", ); return dir * va.localeCompare(vb, "en", { numeric: true, sensitivity: "base" }); } case "status": { const va = String(getters.status(a) ?? ""); const vb = String(getters.status(b) ?? ""); return dir * va.localeCompare(vb, "en", { sensitivity: "base" }); } case "createdAt": default: { const ta = toTime(getters.createdAt(a)); const tb = toTime(getters.createdAt(b)); return dir * (ta - tb); } } }); const total = sorted.length; const wantsPage = query.page != null; const wantsLimit = query.limit != null; if (!wantsPage && !wantsLimit) { return { list: sorted, total }; } const limit = Math.min( MAX_LIMIT, Math.max(1, query.limit ?? (wantsPage ? DEFAULT_LIMIT_WHEN_PAGE_SET : MAX_LIMIT)), ); const page = Math.max(1, query.page ?? 1); const skip = (page - 1) * limit; const list = sorted.slice(skip, skip + limit); const totalPages = Math.max(1, Math.ceil(total / limit)); return { list, total, page, limit, totalPages }; }