From 0153187a0b07765f58d0948511200f2fb4032d8d Mon Sep 17 00:00:00 2001 From: "arthur.bassi" Date: Tue, 4 Aug 2026 14:37:20 -0300 Subject: [PATCH] fix(work-orders): exhaust fuzzy pages for exact WO number lookup Prevent accepting duplicates that fall outside the first fuzzy search page. --- .../table/cells/editable-wo-number-cell.tsx | 63 +++++++++---------- src/domain/work-orders/api/work-orders-api.ts | 31 ++++++--- .../work-orders/api/work-orders-api.test.ts | 46 ++++++++++++-- 3 files changed, 97 insertions(+), 43 deletions(-) diff --git a/src/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell.tsx index ce4b3462..412a1c1e 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell.tsx @@ -2,7 +2,9 @@ import { useEffect, useState } from "react"; import { toast } from "react-toastify"; +import { workOrdersApi } from "@/domain/work-orders/api/work-orders-api"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import { findDuplicateWoNumberRow } from "@/domain/work-orders/utils/find-duplicate-wo-number-row"; import { HighlightText } from "./highlight-text"; @@ -67,17 +69,16 @@ export function EditableWONumberCell({ const [draft, setDraft] = useState(value); + const [isChecking, setIsChecking] = useState(false); + useEffect(() => { if (!editing) { setDraft(value); } }, [value, editing]); - const findDuplicate = (d: string) => - allOrders.find((o) => o.woNumber === d && String(o.id) !== String(currentId)); - const commit = () => { - if (draft === value) { + if (draft === value || isChecking) { setEditing(false); return; @@ -89,21 +90,26 @@ export function EditableWONumberCell({ return; } - const duplicate = findDuplicate(draft); - - if (duplicate) { - onDuplicateFound?.(duplicate); - - setDraft(value); - - setEditing(false); - - return; - } - - onSave(draft); - - setEditing(false); + setIsChecking(true); + void (async () => { + try { + const local = findDuplicateWoNumberRow(draft, allOrders, currentId); + const remote = local ?? (await workOrdersApi.findByWoNumber(draft)); + const duplicate = + remote && String(remote.id) !== String(currentId) ? remote : undefined; + if (duplicate) { + onDuplicateFound?.(duplicate); + setDraft(value); + return; + } + onSave(draft); + } catch { + toast.error("Unable to verify the work order number. Please try again."); + } finally { + setIsChecking(false); + setEditing(false); + } + })(); }; const cancel = () => { @@ -113,20 +119,12 @@ export function EditableWONumberCell({ }; const handleBlur = () => { + if (isChecking) { + return; + } if (draft !== value && draft.length === 11) { - const duplicate = findDuplicate(draft); - - if (duplicate) { - onDuplicateFound?.(duplicate); - - setDraft(value); - - setEditing(false); - - return; - } - - onSave(draft); + commit(); + return; } setEditing(false); @@ -139,6 +137,7 @@ export function EditableWONumberCell({ {editing ? ( setDraft(e.target.value.replace(/\D/g, "").slice(0, 11))} diff --git a/src/domain/work-orders/api/work-orders-api.ts b/src/domain/work-orders/api/work-orders-api.ts index 4398a4f6..e22cb9c3 100644 --- a/src/domain/work-orders/api/work-orders-api.ts +++ b/src/domain/work-orders/api/work-orders-api.ts @@ -197,14 +197,31 @@ export const workOrdersApi = { return undefined; } - // `board/search` without date or board filters searches the entire work-order collection. - const result = await workOrdersApi.searchBoard({ - search: normalized, - page: 1, - pageSize: 20, - }); + // `board/search` is fuzzy and paginated; exhaust pages until an exact WO# match appears. + const pageSize = 100; + let page = 1; + let scanned = 0; + let totalCount = Number.POSITIVE_INFINITY; - return result.items.find((row) => row.woNumber === normalized); + while (scanned < totalCount) { + 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 ( diff --git a/src/test/domain/work-orders/api/work-orders-api.test.ts b/src/test/domain/work-orders/api/work-orders-api.test.ts index 3683edf9..3d33441e 100644 --- a/src/test/domain/work-orders/api/work-orders-api.test.ts +++ b/src/test/domain/work-orders/api/work-orders-api.test.ts @@ -294,6 +294,9 @@ describe("workOrdersApi.searchBoard", () => { describe("workOrdersApi.findByWoNumber", () => { beforeEach(() => { apiGet.mockReset(); + }); + + it("searches globally without the active board filters", async () => { apiGet.mockResolvedValue({ items: [ { @@ -304,11 +307,9 @@ describe("workOrdersApi.findByWoNumber", () => { ], totalCount: 1, page: 1, - pageSize: 20, + pageSize: 100, }); - }); - it("searches globally without the active board filters", async () => { const result = await workOrdersApi.findByWoNumber("10000001235"); expect(apiGet).toHaveBeenCalledWith( @@ -321,10 +322,47 @@ describe("workOrdersApi.findByWoNumber", () => { expect(Object.fromEntries(searchParams)).toEqual({ search: "10000001235", page: "1", - pageSize: "20", + pageSize: "100", }); expect(result).toMatchObject({ id: 505, woNumber: "10000001235" }); }); + + it("pages through fuzzy search until the exact WO number is found", async () => { + apiGet + .mockResolvedValueOnce({ + items: Array.from({ length: 100 }, (_, index) => ({ + id: index + 1, + woNumber: `fuzzy-match-${index}`, + lifecycleStatusLabel: "Incomplete", + })), + totalCount: 101, + page: 1, + pageSize: 100, + }) + .mockResolvedValueOnce({ + items: [ + { + id: 999, + woNumber: "10000009999", + lifecycleStatusLabel: "Incomplete", + }, + ], + totalCount: 101, + page: 2, + pageSize: 100, + }); + + const result = await workOrdersApi.findByWoNumber("10000009999"); + + expect(apiGet).toHaveBeenCalledTimes(2); + const secondParams = apiGet.mock.calls[1]?.[1]?.searchParams as URLSearchParams; + expect(Object.fromEntries(secondParams)).toMatchObject({ + search: "10000009999", + page: "2", + pageSize: "100", + }); + expect(result).toMatchObject({ id: 999, woNumber: "10000009999" }); + }); }); describe("workOrdersApi.patchBoardField", () => {