mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-01 17:53:11 +00:00
The server honours only the id set when one is sent, so a facet applied during an ids drill-down showed as an active chip while doing nothing. Applying any facet now drops the selection; applying the panel unchanged keeps it. The ids param is also capped at the server limit whatever built the set.
283 lines
9.5 KiB
TypeScript
283 lines
9.5 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 { normalizeWorkOrderIds } from "@/domain/work-orders/utils/work-order-ids-filter";
|
|
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,
|
|
};
|
|
}
|
|
|
|
/** Sent as one comma-separated `ids` value; the server then ignores every other filter. */
|
|
function mapWorkOrderIds(ids: Set<number> | undefined): string | undefined {
|
|
// Normalized here too, so a caller that fills the set directly still stays within the server's limit.
|
|
const normalized = ids?.size ? normalizeWorkOrderIds(ids) : [];
|
|
return normalized.length > 0 ? normalized.join(",") : 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 {};
|
|
}
|
|
// 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),
|
|
ids: mapWorkOrderIds(filters.workOrderIds),
|
|
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,
|
|
};
|
|
}
|