From cd4b8d27fda3d92d73a8a969e94817dbc66c9845 Mon Sep 17 00:00:00 2001 From: "arthur.bassi" Date: Thu, 6 Aug 2026 10:14:29 -0300 Subject: [PATCH] fix(work-orders): seed CompDoc address after location settles [SH-116] --- .../workorders/_hooks/use-comp-doc-dialog.ts | 42 ++++- .../use-comp-doc-dialog-address-seed.test.tsx | 148 ++++++++++++++++++ 2 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 src/test/app/(protected)/workorders/use-comp-doc-dialog-address-seed.test.tsx diff --git a/src/app/(protected)/workorders/_hooks/use-comp-doc-dialog.ts b/src/app/(protected)/workorders/_hooks/use-comp-doc-dialog.ts index bf84f59d..ad5c470b 100644 --- a/src/app/(protected)/workorders/_hooks/use-comp-doc-dialog.ts +++ b/src/app/(protected)/workorders/_hooks/use-comp-doc-dialog.ts @@ -46,22 +46,48 @@ export function useCompDocDialog({ const [editDateCompleted, setEditDateCompleted] = useState(""); const [editAddress, setEditAddress] = useState(""); const seedKeyRef = useRef(null); + const addressSeededRef = useRef(false); + const addressTouchedRef = useRef(false); // Seed once per open/WO — row/detail refetches must not wipe in-progress edits. + // Address waits for authoritative locationDetail (when a real location id exists) + // and still accepts that first response without overwriting later user edits. useEffect(() => { if (!open) { seedKeyRef.current = null; + addressSeededRef.current = false; + addressTouchedRef.current = false; return; } + const key = String(row.id); - if (seedKeyRef.current === key) return; - seedKeyRef.current = key; - setNotes(data?.notes ?? ""); - setDocState(docStateOf(row.docStatus)); - setGeneratedAt(data?.generatedAt ?? ""); - setEditDateCompleted(row.completedDate || ""); + if (seedKeyRef.current !== key) { + seedKeyRef.current = key; + addressSeededRef.current = false; + addressTouchedRef.current = false; + setNotes(data?.notes ?? ""); + setDocState(docStateOf(row.docStatus)); + setGeneratedAt(data?.generatedAt ?? ""); + setEditDateCompleted(row.completedDate || ""); + } + + if (addressTouchedRef.current || addressSeededRef.current) { + return; + } + + const awaitingLocation = Boolean(locId) && !isMockLocation && !locationDetail; + if (awaitingLocation) { + return; + } + setEditAddress(buildCompDocAddressLine(resolveCompDocSite(row, locationDetail))); - }, [open, row, data?.notes, data?.generatedAt, locationDetail]); + addressSeededRef.current = true; + }, [open, row, data?.notes, data?.generatedAt, locationDetail, locId, isMockLocation]); + + const handleAddressChange = (value: string) => { + addressTouchedRef.current = true; + setEditAddress(value); + }; const amazonRep = row.pocName || ""; const seaRep = row.tech || ""; @@ -118,7 +144,7 @@ export function useCompDocDialog({ docState, generatedAt, editAddress, - setEditAddress, + setEditAddress: handleAddressChange, editDateCompleted, setEditDateCompleted, amazonRep, diff --git a/src/test/app/(protected)/workorders/use-comp-doc-dialog-address-seed.test.tsx b/src/test/app/(protected)/workorders/use-comp-doc-dialog-address-seed.test.tsx new file mode 100644 index 00000000..3e734f4a --- /dev/null +++ b/src/test/app/(protected)/workorders/use-comp-doc-dialog-address-seed.test.tsx @@ -0,0 +1,148 @@ +import { act, renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { ReactNode } from "react"; +import { useCompDocDialog } from "@/app/(protected)/workorders/_hooks/use-comp-doc-dialog"; +import type { Location } from "@/domain/locations/types/location"; +import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; + +const locationDetailState = vi.hoisted(() => ({ + data: undefined as Location | undefined, +})); + +vi.mock("@/domain/locations/use-cases/use-location-detail", () => ({ + useLocationDetail: () => ({ data: locationDetailState.data }), +})); + +function baseRow(overrides: Partial = {}): WorkOrderTableRow { + return { + id: 42, + woNumber: "WO-42", + site: "SEA1", + type: "PM", + dispatcherId: "u1", + dispatcherName: "Dispatcher", + scheduledOn: "2026-06-01", + completedDate: "", + targetWeek: "", + dayGroup: "", + dayLabel: "", + location: "Seattle", + locationId: "loc-real-1", + pm: "HVAC", + serviceNotes: "", + extraServices: [], + vendorId: "", + company: "Vendor Co", + tech: "Tech One", + techPhone: "", + apptStart: "", + apptEnd: "", + apptTime: "", + status: "Scheduled", + docStatus: "No", + pocName: "Amazon Rep", + pocPhone: "", + pocNotes: "", + dueDate: "", + rescheduleCount: 0, + carriedOver: 0, + originalWeek: "", + originalDate: "", + isPastDue: false, + scheduleWeekOnly: false, + mediaCount: 0, + title: "PM — SEA1", + priority: "Sev 3", + primaryDispatchId: null, + rowVersion: "", + dispatchRowVersion: "", + flagColor: null, + ...overrides, + }; +} + +function wrapper({ children }: { children: ReactNode }) { + const client = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + return {children}; +} + +describe("useCompDocDialog address seeding", () => { + beforeEach(() => { + locationDetailState.data = undefined; + }); + + it("seeds address from the first authoritative locationDetail without locking on the empty fallback", async () => { + const row = baseRow(); + const { result, rerender } = renderHook( + () => + useCompDocDialog({ + row, + open: true, + onSave: vi.fn(), + }), + { wrapper }, + ); + + expect(result.current.editAddress).toBe(""); + + locationDetailState.data = { + id: "loc-real-1", + name: "SEA1", + address: "3811 Distribution Dr", + city: "Seattle", + state: "WA", + zipCode: "98101", + } as Location; + + rerender(); + + await waitFor(() => { + expect(result.current.editAddress).toContain("3811 Distribution Dr"); + }); + }); + + it("does not overwrite address after the dispatcher edits it", async () => { + locationDetailState.data = { + id: "loc-real-1", + name: "SEA1", + address: "3811 Distribution Dr", + city: "Seattle", + state: "WA", + zipCode: "98101", + } as Location; + + const row = baseRow(); + const { result, rerender } = renderHook( + () => + useCompDocDialog({ + row, + open: true, + onSave: vi.fn(), + }), + { wrapper }, + ); + + await waitFor(() => { + expect(result.current.editAddress).toContain("3811 Distribution Dr"); + }); + + act(() => { + result.current.setEditAddress("Dispatcher typed address"); + }); + + locationDetailState.data = { + id: "loc-real-1", + name: "SEA1", + address: "999 Other St", + city: "Seattle", + state: "WA", + zipCode: "98101", + } as Location; + rerender(); + + expect(result.current.editAddress).toBe("Dispatcher typed address"); + }); +});