From b3d8be571e66d8c4f00df9266888f362d441d1fb Mon Sep 17 00:00:00 2001 From: "arthur.bassi" Date: Wed, 5 Aug 2026 10:39:28 -0300 Subject: [PATCH] fix(work-orders): preserve wizard POC edits, clear stale vendor, persist range start --- .../wizard/use-wizard-poc-autofill.ts | 14 ++++- .../wizard-step-location-service-select.tsx | 8 ++- .../wizard-step-location-site-select.tsx | 4 ++ .../mappers/assign-wizard-schedule-fields.ts | 8 ++- .../use-wizard-poc-autofill.test.tsx | 56 +++++++++++++++++++ .../wizard-service-clears-vendor.test.tsx | 48 ++++++++++++++++ .../mappers/work-order-board-mapper.test.ts | 4 +- 7 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 src/test/app/(protected)/workorders/use-wizard-poc-autofill.test.tsx create mode 100644 src/test/app/(protected)/workorders/wizard-service-clears-vendor.test.tsx diff --git a/src/app/(protected)/workorders/_components/wizard/use-wizard-poc-autofill.ts b/src/app/(protected)/workorders/_components/wizard/use-wizard-poc-autofill.ts index a4b08132..5efd24e6 100644 --- a/src/app/(protected)/workorders/_components/wizard/use-wizard-poc-autofill.ts +++ b/src/app/(protected)/workorders/_components/wizard/use-wizard-poc-autofill.ts @@ -4,7 +4,11 @@ import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order type SetDraft = (updater: (current: WorkOrderWizardDraft) => WorkOrderWizardDraft) => void; -/** Auto-fills POC name/phone from the fetched location, once per selected location. */ +/** + * Auto-fills POC name/phone from the fetched location, once per selected location. + * Never clobbers fields the dispatcher already typed while detail was loading + * (prototype: autofill from site records, then edit if outdated). + */ export function useWizardPocAutofill( open: boolean, locationId: string, @@ -25,8 +29,12 @@ export function useWizardPocAutofill( } setDraft((current) => ({ ...current, - pocName: locationDetail.contact ?? current.pocName, - pocPhone: locationDetail.phone ?? current.pocPhone, + pocName: current.pocName.trim() + ? current.pocName + : (locationDetail.contact ?? current.pocName), + pocPhone: current.pocPhone.trim() + ? current.pocPhone + : (locationDetail.phone ?? current.pocPhone), })); setPocAutoFilledFor(locationId); }, [locationId, locationDetail, pocAutoFilledFor, setDraft]); diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select.tsx index 03101387..71d6ace7 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select.tsx @@ -30,11 +30,16 @@ export function WizardStepLocationServiceSelect({ }: WizardStepLocationServiceSelectProps) { const svcSel = [draft.pm, ...draft.extraServices].filter((p, i, a) => !!p && a.indexOf(p) === i); + const clearVendorIfPrimaryChanged = (nextPm: string): Partial => + nextPm !== draft.pm ? { vendorId: "", vendorName: "", techPhone: "" } : {}; + const toggleSvc = (p: string) => { const next = svcSel.includes(p) ? svcSel.filter((x) => x !== p) : [...svcSel, p]; + const nextPm = next[0] ?? ""; onPatch({ - pm: next[0] ?? "", + pm: nextPm, extraServices: next.slice(1), + ...clearVendorIfPrimaryChanged(nextPm), }); }; @@ -62,6 +67,7 @@ export function WizardStepLocationServiceSelect({ onPatch({ pm: p, extraServices: draft.extraServices.filter((x) => x !== p), + ...clearVendorIfPrimaryChanged(p), }) } /> diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-site-select.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-site-select.tsx index 537c8722..3cc42c58 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-site-select.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-site-select.tsx @@ -33,10 +33,14 @@ export function WizardStepLocationSiteSelect({ }))} onPick={(id) => { const loc = locations.find((l) => String(l.id) === id); + // Switching site invalidates the previous POC — clear so autofill can + // seed from the new location without clobbering mid-load edits later. onPatch({ locationId: id, locationName: loc?.name ?? "", siteCode: loc ? resolveSiteCode(loc) : "", + pocName: "", + pocPhone: "", }); }} /> diff --git a/src/domain/work-orders/mappers/assign-wizard-schedule-fields.ts b/src/domain/work-orders/mappers/assign-wizard-schedule-fields.ts index 3a6c656d..c2db6670 100644 --- a/src/domain/work-orders/mappers/assign-wizard-schedule-fields.ts +++ b/src/domain/work-orders/mappers/assign-wizard-schedule-fields.ts @@ -2,7 +2,11 @@ import type { WorkOrderBoardCreatePayload } from "@/domain/work-orders/types/wor import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard"; import { mondayOf } from "@/domain/work-orders/utils/wizard-date-utils"; -/** Mutates `payload` with `dueDate` and day/week schedule fields (range mode has no schedule fields). */ +/** + * Mutates `payload` with `dueDate` and schedule fields. + * Range mode lands on the board using the range start as `scheduledDate` + * (prototype `hasScheduleDate`: a range start qualifies as scheduled). + */ export function assignWizardScheduleFields( payload: WorkOrderBoardCreatePayload, draft: WorkOrderWizardDraft, @@ -21,7 +25,7 @@ export function assignWizardScheduleFields( if (week) { payload.targetWeek = week; } - } else if (!isRange && draft.scheduledOn) { + } else if (draft.scheduledOn && (isRange || !draft.scheduleWeekOnly)) { payload.scheduledDate = draft.scheduledOn; } } diff --git a/src/test/app/(protected)/workorders/use-wizard-poc-autofill.test.tsx b/src/test/app/(protected)/workorders/use-wizard-poc-autofill.test.tsx new file mode 100644 index 00000000..3f97b935 --- /dev/null +++ b/src/test/app/(protected)/workorders/use-wizard-poc-autofill.test.tsx @@ -0,0 +1,56 @@ +import { act, renderHook } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { useWizardPocAutofill } from "@/app/(protected)/workorders/_components/wizard/use-wizard-poc-autofill"; +import type { Location } from "@/domain/locations/types/location"; +import { + EMPTY_WIZARD_DRAFT, + type WorkOrderWizardDraft, +} from "@/domain/work-orders/types/work-order-wizard"; +import { useState } from "react"; + +function baseLocation(overrides: Partial = {}): Location { + return { + id: "loc-1", + name: "BK5", + contact: "Janelle Pickett", + phone: "214-555-0111", + ...overrides, + } as Location; +} + +describe("useWizardPocAutofill", () => { + it("fills empty POC fields from location detail once", () => { + const { result } = renderHook(() => { + const [draft, setDraft] = useState({ ...EMPTY_WIZARD_DRAFT }); + useWizardPocAutofill(true, "loc-1", baseLocation(), setDraft); + return draft; + }); + + expect(result.current.pocName).toBe("Janelle Pickett"); + expect(result.current.pocPhone).toBe("214-555-0111"); + }); + + it("does not overwrite POC edits made before location detail arrives", () => { + const { result, rerender } = renderHook( + ({ detail }: { detail: Location | undefined }) => { + const [draft, setDraft] = useState({ + ...EMPTY_WIZARD_DRAFT, + pocName: "Dispatcher Typed", + pocPhone: "999-000-1111", + }); + useWizardPocAutofill(true, "loc-1", detail, setDraft); + return draft; + }, + { initialProps: { detail: undefined as Location | undefined } }, + ); + + expect(result.current.pocName).toBe("Dispatcher Typed"); + + act(() => { + rerender({ detail: baseLocation() }); + }); + + expect(result.current.pocName).toBe("Dispatcher Typed"); + expect(result.current.pocPhone).toBe("999-000-1111"); + }); +}); diff --git a/src/test/app/(protected)/workorders/wizard-service-clears-vendor.test.tsx b/src/test/app/(protected)/workorders/wizard-service-clears-vendor.test.tsx new file mode 100644 index 00000000..31007cda --- /dev/null +++ b/src/test/app/(protected)/workorders/wizard-service-clears-vendor.test.tsx @@ -0,0 +1,48 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { WizardStepLocationServiceSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select"; +import { + EMPTY_WIZARD_DRAFT, + type WorkOrderWizardDraft, +} from "@/domain/work-orders/types/work-order-wizard"; + +describe("WizardStepLocationServiceSelect — vendor clear on service change", () => { + it("clears vendor fields when the primary service changes", () => { + const onPatch = vi.fn(); + const draft: WorkOrderWizardDraft = { + ...EMPTY_WIZARD_DRAFT, + pm: "HVAC", + vendorId: "9", + vendorName: "Old Vendor", + techPhone: "555-0100", + }; + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "HVAC" })); + fireEvent.click(screen.getByRole("button", { name: "Plumbing" })); + + expect(onPatch).toHaveBeenCalledWith( + expect.objectContaining({ + pm: "Plumbing", + vendorId: "", + vendorName: "", + techPhone: "", + }), + ); + }); +}); diff --git a/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts b/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts index 3d8034a7..fcb12c9a 100644 --- a/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts +++ b/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts @@ -254,7 +254,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => { ).toThrow("Site code is required"); }); - it("omits scheduledDate for range schedule and puts range in description only", () => { + it("sets scheduledDate from range start and keeps range text in description", () => { const payload = mapWizardDraftToBoardCreatePayload({ ...EMPTY_WIZARD_DRAFT, @@ -273,7 +273,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => { vendorNotes: "Also not in description", }); - expect(payload.scheduledDate).toBeUndefined(); + expect(payload.scheduledDate).toBe("2026-06-08"); expect(payload.scheduleWeekOnly).toBeUndefined();