mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 17:23:12 +00:00
Some checks are pending
* fix(work-orders): add Unscheduled option to Advanced Filters Date range * fix(work-orders): page unscheduled results server-side --------- Co-authored-by: Codex Review Integration <codex-review@local.invalid> Co-authored-by: Alexandre Brandizzi <alex_brandizzi@hotmail.com>
270 lines
8.7 KiB
TypeScript
270 lines
8.7 KiB
TypeScript
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>): 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;
|
|
} {
|
|
// 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<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,
|
|
sites: filters.sites.size > 0 ? [...filters.sites] : undefined,
|
|
types: typeFilters.types,
|
|
overdue: typeFilters.overdue,
|
|
dispatchers: mapDispatchers(filters.dispatchers),
|
|
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,
|
|
};
|
|
}
|