fix(work-orders): exhaust fuzzy pages for exact WO number lookup

Prevent accepting duplicates that fall outside the first fuzzy search page.
This commit is contained in:
arthur.bassi 2026-08-04 14:37:20 -03:00
parent 6ac1b5cd42
commit 0153187a0b
3 changed files with 97 additions and 43 deletions

View file

@ -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 ? (
<input
autoFocus
disabled={isChecking}
inputMode="numeric"
value={draft}
onChange={(e) => setDraft(e.target.value.replace(/\D/g, "").slice(0, 11))}

View file

@ -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 (

View file

@ -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", () => {