import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list"; import { EMPTY_ADVANCED_FILTERS, type WorkOrderAdvancedFilters, } from "@/domain/work-orders/types/work-order-advanced-filters"; import type { WorkOrderBarFilters } from "@/domain/work-orders/types/work-order-list-filters"; import { BOARD_DISPATCHER_UNASSIGNED, DATE_RANGE_TO_PRESET, DOC_STATUS_API, LIFECYCLE_STATUS_API, NON_TERMINAL_LIFECYCLE_STATUSES, type WorkOrderBoardQueryParams, type WorkOrderBoardSearchQueryParams, } from "@/domain/work-orders/types/work-order-board"; import { mapWizardTypeToApi } from "@/domain/work-orders/mappers/work-order-board-mapper"; import { weekRange } from "@/domain/work-orders/utils/list-week-utils"; import type { CompletionDocFilter } from "@/domain/work-orders/types/work-order-advanced-filters"; import type { WizardWOStatus, WOType } from "@/domain/work-orders/types/work-order-wizard"; export const BOARD_SEARCH_MAX_PAGE_SIZE = 200; /** Wide Custom window so applied advanced search is not pinned to ThisWeek. */ export const ADVANCED_SEARCH_ALL_WEEKS_FROM = "2000-01-01"; export const ADVANCED_SEARCH_ALL_WEEKS_TO = "2099-12-31"; /** * Wide Custom window for SH-165. GET /board/search defaults omitted * `datePreset` to ThisWeek (`WorkOrderSearchDateRangeResolver`), which would * hide scheduled-unassigned WOs outside the current week. */ export const UNASSIGNED_QUEUE_DATE_FROM = "1970-01-01"; export const UNASSIGNED_QUEUE_DATE_TO = "2099-12-31"; function toBoardDispatcherId(id: string): string { if (id === ASSIGNEE_FILTER_UNASSIGNED || id === BOARD_DISPATCHER_UNASSIGNED) { return BOARD_DISPATCHER_UNASSIGNED; } return id; } function mapDispatchers(ids: Iterable): string[] | undefined { const list = [...ids].map(toBoardDispatcherId); return list.length > 0 ? list : undefined; } /** Maps UI type filters to API `types` + separate `overdue` flag. Overdue is never sent as types=99. */ function mapWoTypeFilters(types: Iterable): { types?: number[]; overdue?: boolean; } { const list: number[] = []; let overdue = false; for (const type of types) { if (type === "All") { continue; } if (type === "Overdue") { overdue = true; continue; } const api = mapWizardTypeToApi(type as WOType); if (api != null) { list.push(api); } } return { types: list.length > 0 ? list : undefined, overdue: overdue || undefined, }; } function mapStatuses(statuses: Iterable): number[] | undefined { const list: number[] = []; for (const status of statuses) { const value = LIFECYCLE_STATUS_API[status as WizardWOStatus]; if (typeof value === "number") { list.push(value); } else if (status === "Canceled" || status === "Cancelled") { list.push(LIFECYCLE_STATUS_API.Canceled); } } return list.length > 0 ? list : undefined; } function mapDocStatuses(docs: Iterable): number[] | undefined { const list: number[] = []; for (const doc of docs) { if (doc === "Yes") list.push(DOC_STATUS_API.Yes); else if (doc === "No") list.push(DOC_STATUS_API.No); else if (doc === "NN") list.push(DOC_STATUS_API.NotNeeded); } return list.length > 0 ? list : undefined; } function resolveVendorIds( vendorTechs: Iterable, vendorIdsByCompanyName?: Map, ): number[] | undefined { if (!vendorIdsByCompanyName) return undefined; const vendorIds: number[] = []; for (const name of vendorTechs) { const id = vendorIdsByCompanyName.get(name); if (typeof id === "number") { vendorIds.push(id); } } return vendorIds.length > 0 ? vendorIds : undefined; } function mapSearchFacetParams( filters: WorkOrderAdvancedFilters, ): Pick< WorkOrderBoardSearchQueryParams, | "severities" | "rescheduled" | "carriedOver" | "addOn" | "avetaOnly" | "flagColors" | "internalOnly" | "hasUplift" | "upliftStatuses" > { return { severities: filters.severities?.size ? [...filters.severities] : undefined, rescheduled: filters.rescheduled || undefined, carriedOver: filters.carriedOver || undefined, addOn: filters.addOn || undefined, avetaOnly: filters.avetaOnly || undefined, flagColors: filters.flagColors?.size ? [...filters.flagColors] : undefined, internalOnly: filters.internalOnly || undefined, hasUplift: filters.hasUplift || undefined, upliftStatuses: filters.hasUplift && filters.upliftStatuses?.size ? [...filters.upliftStatuses] : undefined, }; } /** Build GET /board query from weekly filter bar. */ export function toBoardQueryParams( bar: WorkOrderBarFilters, search: string, ): WorkOrderBoardQueryParams { const [, weekTo] = weekRange(bar.weekMonday); const typeFilters = bar.types.size > 0 ? mapWoTypeFilters(bar.types) : {}; return { weekStart: bar.weekMonday, weekEnd: weekTo, dispatchers: mapDispatchers(bar.dispatcherIds), types: typeFilters.types, overdue: typeFilters.overdue, search: search.trim().length >= 2 ? search.trim() : undefined, }; } function resolveAdvancedSearchDate(filters: WorkOrderAdvancedFilters): { datePreset?: WorkOrderBoardSearchQueryParams["datePreset"]; dateFrom?: string; dateTo?: string; } { // SH-338: server-side unscheduledOnly owns the date-range exemption; the // client must not fake a Custom window and sends no date params. if (filters.unscheduledOnly) { return {}; } if (filters.dateRange === EMPTY_ADVANCED_FILTERS.dateRange) { return { datePreset: "Custom", dateFrom: ADVANCED_SEARCH_ALL_WEEKS_FROM, dateTo: ADVANCED_SEARCH_ALL_WEEKS_TO, }; } const datePreset = DATE_RANGE_TO_PRESET[filters.dateRange]; if (datePreset === "Custom") { return { datePreset, dateFrom: filters.customFrom || undefined, dateTo: filters.customTo || undefined, }; } return { datePreset }; } /** Build GET /board/search query from advanced filters (+ optional bar/search). */ export function toBoardSearchQueryParams(input: { filters: WorkOrderAdvancedFilters; search?: string; page?: number; pageSize?: number; sortBy?: string; sortDir?: string; /** Optional company-name → vendorId resolution. */ vendorIdsByCompanyName?: Map; }): WorkOrderBoardSearchQueryParams { const { filters } = input; const date = resolveAdvancedSearchDate(filters); const pageSize = Math.min( input.pageSize ?? BOARD_SEARCH_MAX_PAGE_SIZE, BOARD_SEARCH_MAX_PAGE_SIZE, ); const typeFilters = mapWoTypeFilters(filters.types); const trimmedSearch = input.search?.trim(); const search = trimmedSearch && trimmedSearch.length >= 2 ? trimmedSearch : undefined; return { page: input.page ?? 0, pageSize, search, datePreset: date.datePreset, dateFrom: date.dateFrom, dateTo: date.dateTo, sites: filters.sites.size > 0 ? [...filters.sites] : undefined, types: typeFilters.types, overdue: typeFilters.overdue, dispatchers: mapDispatchers(filters.dispatchers), regions: filters.regions.size > 0 ? [...filters.regions] : undefined, statuses: mapStatuses(filters.statuses), pmTypes: filters.pmTypes.size > 0 ? [...filters.pmTypes] : undefined, vendorIds: resolveVendorIds(filters.vendorTechs, input.vendorIdsByCompanyName), docStatuses: mapDocStatuses(filters.docs), unscheduledOnly: filters.unscheduledOnly || undefined, ...mapSearchFacetParams(filters), sortBy: input.sortBy || "scheduledDate", sortDir: input.sortDir === "desc" ? "desc" : "asc", }; } /** ky/URLSearchParams-friendly object; arrays become repeated keys. */ export function buildBoardSearchParamsRecord( params: WorkOrderBoardQueryParams | WorkOrderBoardSearchQueryParams, ): Record { const out: Record = {}; for (const [key, value] of Object.entries(params)) { if (value === undefined || value === null || value === "") { continue; } if (Array.isArray(value)) { if (value.length === 0) continue; out[key] = value; continue; } out[key] = value as string | number | boolean; } return out; } export function woTypeFilterIsOverdue(type: string): boolean { return type === "Overdue"; } /** Week-independent GET /board/search params for the pinned Unassigned queue. */ export function toUnassignedQueueQueryParams( page = 0, pageSize = BOARD_SEARCH_MAX_PAGE_SIZE, ): WorkOrderBoardSearchQueryParams { return { page, pageSize: Math.min(pageSize || BOARD_SEARCH_MAX_PAGE_SIZE, BOARD_SEARCH_MAX_PAGE_SIZE), dispatchers: [BOARD_DISPATCHER_UNASSIGNED], statuses: [...NON_TERMINAL_LIFECYCLE_STATUSES], datePreset: "Custom", dateFrom: UNASSIGNED_QUEUE_DATE_FROM, dateTo: UNASSIGNED_QUEUE_DATE_TO, }; }