shoc-frontend-new/src/domain/work-orders/utils/board-query-params.ts
Alexandre Brandizzi 0d6af0da59 fix(workorders): make the Advanced Filters date range narrow results (SH-391)
The Advanced Filters default was This week, and that value doubled as no
range, so an explicit This week searched every week. Other ranges passed
every undated WO through on the server. No range is now its own state
(nothing selected, like the prototype; clicking a selected range clears it).
It searches every week with undated rows, via includeDateless. Any selected
range narrows strictly. The pinned Unassigned queue and the WO# duplicate
lookup keep spanning every week.
2026-09-24 20:58:47 -03:00

274 lines
9 KiB
TypeScript

import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list";
import 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>): 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<WOType | string>): {
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<WizardWOStatus | string>): 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<CompletionDocFilter>): 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<string>,
vendorIdsByCompanyName?: Map<string, number>,
): 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;
includeDateless?: boolean;
} {
// 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 {};
}
// SH-391: no range selected searches every week, undated rows included. Any
// selected range (This week included) strictly narrows the results.
if (filters.dateRange === null) {
return {
datePreset: "Custom",
dateFrom: ADVANCED_SEARCH_ALL_WEEKS_FROM,
dateTo: ADVANCED_SEARCH_ALL_WEEKS_TO,
includeDateless: true,
};
}
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<string, number>;
}): 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,
includeDateless: date.includeDateless,
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<string, string | number | boolean | (string | number)[]> {
const out: Record<string, string | number | boolean | (string | number)[]> = {};
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,
includeDateless: true,
};
}