import { API_PATHS } from "@/api/api-paths"; import { ApiError, mapHttpStatusToMessage } from "@/api/api-error"; import { api, apiDelete, apiGet, apiPost, apiPostForm, apiPut } from "@/api/api"; import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response"; import { BoardPatchConflictError, BoardPatchValidationError, } from "@/domain/work-orders/errors/board-patch-errors"; import { workOrderBoardDocumentsApi } from "@/domain/work-orders/api/work-order-board-documents-api"; import { mapDispatchComment, mapDispatchDetail, mapDispatchSignoff, mapDispatchToVendorPayload, mapEmployeeOptions, mapLocationOptions, mapTaskListTemplates, mapUpdateDispatchPayload, mapVendorDropdownItems, mapWorkOrderDetail, mapWorkOrderFormToCreatePayload, mapWorkOrderFormToUpdateFormData, mapWorkOrderTableRow, mapWorkOrderTableRows, } from "@/domain/work-orders/mappers/work-order-mapper"; import { mapWizardDraftToBoardCreatePayload } from "@/domain/work-orders/mappers/work-order-board-mapper"; import { mapWorkOrderBoardDetail } from "@/domain/work-orders/mappers/work-order-board-detail-mapper"; import type { WorkOrderFormSchemaValues } from "@/domain/work-orders/schemas/work-order-schema"; import type { DispatchDetail, DispatchToVendorPayload, EmployeeOption, LocationOption, TaskListTemplate, UpdateDispatchPayload, WorkOrderVendorUpdatePayload, VendorDropdownItem, WorkOrderDetail, WorkOrderFormValues, WorkOrderUser, } from "@/domain/work-orders/types/work-order"; import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard"; import type { WorkOrderBoardListResult, WorkOrderBoardPatchPayload, WorkOrderBoardQueryParams, WorkOrderBoardResponseApi, WorkOrderBoardSearchQueryParams, } from "@/domain/work-orders/types/work-order-board"; import type { WorkOrderBoardDetail } from "@/domain/work-orders/types/work-order-board-detail"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import { DISPATCH_LOCATION_WO_PAGE_SIZE, type WorkOrdersListParams, type WorkOrdersListResult, } from "@/domain/work-orders/types/work-order-list"; import { buildBoardSearchParamsRecord } from "@/domain/work-orders/utils/board-query-params"; function buildSearchParams(params: WorkOrdersListParams): Record { const searchParams: Record = { search: params.search ?? "", page: params.page ?? 1, pageSize: params.pageSize ?? 12, }; if (params.sortBy) { searchParams.sortBy = params.sortBy; } if (params.sortDir) { searchParams.sortDir = params.sortDir; } if (params.status) { searchParams.status = params.status; } if (params.assignee) { searchParams.assignee = params.assignee; } if (params.locationId) { searchParams.locationId = params.locationId; } if (params.dateFrom) { searchParams.dateFrom = params.dateFrom; } if (params.dateTo) { searchParams.dateTo = params.dateTo; } if (params.type) { searchParams.type = params.type; } if (params.assigneeIds) { searchParams.assigneeIds = params.assigneeIds; } return searchParams; } function toUrlSearchParams( params: Record, ): URLSearchParams { const sp = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (Array.isArray(value)) { for (const entry of value) { sp.append(key, String(entry)); } continue; } sp.append(key, String(value)); } return sp; } function mapUser(raw: unknown): WorkOrderUser { const item = (typeof raw === "object" && raw !== null ? raw : {}) as Record; const id = item.id ?? item.Id; const name = (typeof item.name === "string" ? item.name : undefined) ?? (typeof item.Name === "string" ? item.Name : undefined) ?? (typeof item.userName === "string" ? item.userName : "") ?? ""; return { id: typeof id === "string" || typeof id === "number" ? id : "", name, }; } export const workOrdersApi = { ...workOrderBoardDocumentsApi, getList: async (params: WorkOrdersListParams = {}): Promise => { const searchParams = buildSearchParams(params); const data = await apiGet(API_PATHS.workOrder.getList, { searchParams }); const paginated = handlePaginatedResponse(data, { page: params.page, pageSize: params.pageSize, }); return { items: mapWorkOrderTableRows(paginated.items), totalCount: paginated.totalCount, page: paginated.page, pageSize: paginated.pageSize, }; }, getBoard: async (params: WorkOrderBoardQueryParams): Promise => { const searchParams = toUrlSearchParams(buildBoardSearchParamsRecord(params)); const data = await apiGet(API_PATHS.workOrder.board, { searchParams }); const payload = ( typeof data === "object" && data !== null ? data : {} ) as WorkOrderBoardResponseApi; const scheduled = mapWorkOrderTableRows(payload.scheduled ?? []); const unscheduled = mapWorkOrderTableRows(payload.unscheduled ?? []); const returnedCount = payload.counts?.returned || scheduled.length; const totalCount = payload.counts?.total || scheduled.length; return { items: scheduled, scheduled, unscheduled, totalCount, returnedCount, page: 1, pageSize: scheduled.length, weekStart: payload.weekStart, weekEnd: payload.weekEnd, }; }, searchBoard: async ( params: WorkOrderBoardSearchQueryParams, ): Promise => { const searchParams = toUrlSearchParams(buildBoardSearchParamsRecord(params)); const data = await apiGet(API_PATHS.workOrder.boardSearch, { searchParams }); const paginated = handlePaginatedResponse(data, { page: params.page, pageSize: params.pageSize, }); const items = mapWorkOrderTableRows(paginated.items); return { items, scheduled: items.filter((row) => !!row.scheduledOn), unscheduled: items.filter((row) => !row.scheduledOn), totalCount: paginated.totalCount, returnedCount: items.length, page: paginated.page, pageSize: paginated.pageSize, }; }, findByWoNumber: async (woNumber: string): Promise => { const normalized = woNumber.trim(); // Search every non-empty WO# — backend advanced search accepts 1-character queries, and // wizard/create allow single-digit numbers (SH-189 must not bypass remote uniqueness). if (!normalized) { return undefined; } // Safety cap — if hit before exhausting results, fail closed (do not treat as unique). const safetyMaxPages = 50; // `board/search` is fuzzy and paginated (0-based); exhaust pages for an exact WO#. const pageSize = 100; let page = 0; let scanned = 0; let totalCount = Number.POSITIVE_INFINITY; while (scanned < totalCount) { if (page >= safetyMaxPages) { throw new Error( "Unable to verify the work order number: search result set exceeded the safety page limit.", ); } const result = await workOrdersApi.searchBoard({ search: normalized, page, pageSize, }); const exact = result.items.find((row) => row.woNumber === normalized); if (exact) { return exact; } scanned += result.items.length; totalCount = result.totalCount; if (result.items.length === 0) { break; } page += 1; } return undefined; }, getListByLocation: async ( locationId: string | number, search = "", ): Promise => { return workOrdersApi.getList({ locationId, search, page: 1, pageSize: DISPATCH_LOCATION_WO_PAGE_SIZE, sortBy: "number", sortDir: "asc", }); }, fetchUsers: async (): Promise => { const data = await apiGet(API_PATHS.user.list); const users = handleApiResponse(data); return Array.isArray(users) ? users.map(mapUser) : []; }, fetchDispatchers: async (): Promise => { try { const data = await apiGet(API_PATHS.workOrder.lookupsDispatchers); const users = handleApiResponse(data); const mapped = Array.isArray(users) ? users.map(mapUser) : []; if (mapped.length > 0) { return mapped; } } catch { // Fall back to User list when lookup is unavailable. } return workOrdersApi.fetchUsers(); }, getById: async (id: string | number): Promise => { const data = await apiGet(API_PATHS.workOrder.getById, { searchParams: { id }, }); return mapWorkOrderDetail(handleApiResponse(data)); }, getBoardDetail: async (id: string | number): Promise => { const data = await apiGet(API_PATHS.workOrder.boardDetail(id)); return mapWorkOrderBoardDetail(handleApiResponse(data)); }, create: async ( values: WorkOrderFormSchemaValues | WorkOrderFormValues, ): Promise => { const payload = mapWorkOrderFormToCreatePayload(values); const data = await apiPost(API_PATHS.workOrder.addWorkorder, payload); return mapWorkOrderDetail(handleApiResponse(data)); }, createFromBoard: async (draft: WorkOrderWizardDraft): Promise => { const payload = mapWizardDraftToBoardCreatePayload(draft); const data = await apiPost(API_PATHS.workOrder.boardCreate, payload); return mapWorkOrderDetail(handleApiResponse(data)); }, patchBoardField: async ( id: string | number, payload: WorkOrderBoardPatchPayload, ): Promise => { const response = await api.patch(API_PATHS.workOrder.boardPatch(id), { json: payload, throwHttpErrors: false, }); let data: unknown; try { data = await response.json(); } catch { data = undefined; } if (response.ok) { return mapWorkOrderTableRow(data); } const record = typeof data === "object" && data !== null ? (data as Record) : {}; if (response.status === 409) { const currentStateRaw = record.currentState ?? record.CurrentState; const currentState = currentStateRaw != null ? mapWorkOrderTableRow(currentStateRaw) : null; throw new BoardPatchConflictError( typeof record.message === "string" ? record.message : "The record was modified by another user. Refresh and retry.", currentState, ); } if (response.status === 422) { throw new BoardPatchValidationError( typeof record.code === "string" ? record.code : "ValidationError", typeof record.message === "string" ? record.message : mapHttpStatusToMessage(422, data), ); } throw new ApiError(mapHttpStatusToMessage(response.status, data), response.status); }, update: async ( id: string | number, values: WorkOrderFormSchemaValues | WorkOrderFormValues, ): Promise => { const formData = mapWorkOrderFormToUpdateFormData(id, values); await apiPostForm<{ message?: string; status?: string }>( API_PATHS.workOrder.editWorkorder, formData, ); }, delete: async (id: string | number): Promise => { await apiDelete(API_PATHS.workOrder.deleteWorkorder, { json: { Id: Number(id) }, }); }, changeStatus: async (id: string | number, status: string): Promise => { await apiPost(API_PATHS.workOrder.changeStatus, undefined, { searchParams: { id, status }, }); }, changeAssignment: async (id: string | number, assignTo: string | number): Promise => { await apiPost(API_PATHS.workOrder.changeAssignment, undefined, { searchParams: { id, assignTo: assignTo || "" }, }); }, fetchLocations: async (): Promise => { const data = await apiGet(API_PATHS.contact.getLocations); const items = handleApiResponse(data); return Array.isArray(items) ? mapLocationOptions(items) : []; }, fetchEmployeesDropdown: async (): Promise => { const data = await apiGet(API_PATHS.employee.dropdown); const items = handleApiResponse(data); return Array.isArray(items) ? mapEmployeeOptions(items) : []; }, fetchVendorsDropdown: async (trade?: string, siteZip?: string): Promise => { const searchParams: Record = {}; if (trade) { searchParams.trade = trade; } if (siteZip) { searchParams.siteZip = siteZip; } const data = await apiGet(API_PATHS.vendor.dropdown, { searchParams: Object.keys(searchParams).length > 0 ? searchParams : undefined, }); const items = handleApiResponse(data); return Array.isArray(items) ? mapVendorDropdownItems(items) : []; }, fetchTaskTemplates: async (): Promise => { const data = await apiGet(API_PATHS.taskListTemplate.list); const items = handleApiResponse(data); return Array.isArray(items) ? mapTaskListTemplates(items) : []; }, dispatchToVendor: async (payload: DispatchToVendorPayload): Promise => { await apiPost( API_PATHS.workOrder.dispatchToVendor, mapDispatchToVendorPayload(payload), ); }, getDispatchById: async (id: string | number): Promise => { const data = await apiGet(`${API_PATHS.workOrder.getDispatch}/${id}`); return mapDispatchDetail(handleApiResponse(data)); }, updateDispatch: async (payload: UpdateDispatchPayload): Promise => { await apiPost(API_PATHS.workOrder.updateDispatch, mapUpdateDispatchPayload(payload)); }, updateVendorFromWorkOrder: async ( vendorId: string | number, payload: WorkOrderVendorUpdatePayload, ): Promise => { await apiPut(`${API_PATHS.rest.vendors}/${vendorId}/work-order-update`, payload); }, addDispatchComment: async ( dispatchId: string | number, text: string, sendEmail = true, ): Promise => { const data = await apiPost(API_PATHS.workOrder.addDispatchComment, { DispatchId: dispatchId, Text: text, SendEmail: sendEmail, }); return mapDispatchComment(handleApiResponse(data)); }, updateChecklistItem: async ( id: string | number, isCompleted: boolean, completedBy: string, ): Promise => { await apiPost(API_PATHS.workOrder.updateChecklistItem, { Id: id, IsCompleted: isCompleted, CompletedBy: completedBy, }); }, addChecklistItem: async (dispatchId: string | number, itemText: string): Promise => { await apiPost(API_PATHS.workOrder.addChecklistItem, { DispatchId: dispatchId, ItemText: itemText, }); }, addDispatchSignoff: async ( dispatchId: string | number, options: { signoffType: string; name: string; signature: string; signatureMethod: string }, ): Promise => { const data = await apiPost(API_PATHS.workOrder.addDispatchSignoff, { DispatchId: dispatchId, SignoffType: options.signoffType, Name: options.name, Signature: options.signature, SignatureMethod: options.signatureMethod, }); return mapDispatchSignoff(handleApiResponse(data)); }, verifyDispatch: async (dispatchId: string | number): Promise => { await apiPost(API_PATHS.workOrder.verifyDispatch, undefined, { searchParams: { dispatchId }, }); }, };