From 64c53e9da0201f810629edef55ece634bc9d4797 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Tue, 29 Sep 2026 20:06:05 -0300 Subject: [PATCH] fix(workorders): preserve explicit no-technician choice --- e2e/work-orders/inline-technician.spec.ts | 51 ++++++++++++++++++ .../table/cells/use-vendor-dialog-form.ts | 4 +- .../use-wizard-create-with-technicians.ts | 1 + .../wizard/wizard-vendor-company-fields.tsx | 5 ++ .../work-orders/types/work-order-wizard.ts | 2 + .../wizard-vendor-inline-technician.test.tsx | 52 +++++++++++++++++++ .../mappers/work-order-board-mapper.test.ts | 14 +++++ 7 files changed, 126 insertions(+), 3 deletions(-) diff --git a/e2e/work-orders/inline-technician.spec.ts b/e2e/work-orders/inline-technician.spec.ts index 66b214fb..9fc93684 100644 --- a/e2e/work-orders/inline-technician.spec.ts +++ b/e2e/work-orders/inline-technician.spec.ts @@ -134,6 +134,19 @@ async function mockApi( }); } if (pathname === "/api/workorders/board" && method === "POST") { + const body = bodyOf(request) as { vendorId?: number; technicianAssigned?: boolean }; + const vendorId = body.vendorId ?? 0; + currentBoardRows.push({ + ...BOARD_ROW, + id: 900, + woNumber: "SH-900", + lifecycleStatusLabel: "Incomplete", + vendorId: vendorId || null, + vendorName: vendorId > 0 ? "Vinewood LLC" : null, + technicianAssigned: body.technicianAssigned, + techName: body.technicianAssigned === true ? "Jordan Lee" : null, + techPhone: body.technicianAssigned === true ? "(314) 555-0199" : null, + }); return fulfillJson(route, { data: { id: 900, woNumber: "SH-900" } }); } if (/^\/api\/workorders\/\d+\/board$/u.test(pathname) && method === "PATCH") { @@ -280,6 +293,44 @@ test.describe("Inline technician registration", () => { expect(create?.body).toEqual(expect.objectContaining({ vendorId: 42 })); }); + test("wizard No technician selection overrides a staged technician through create and reopen", async ({ + page, + }) => { + const writes: Captured[] = []; + await mockApi(page, writes); + await openWizardVendorStep(page); + + await page.getByRole("button", { name: "No technician" }).first().click(); + await page.getByRole("button", { name: "No technician" }).last().click(); + await page.getByRole("button", { name: "Create work order" }).click(); + + await expect + .poll(() => + writes.find( + (write) => write.pathname === "/api/workorders/board" && write.method === "POST", + ), + ) + .toBeTruthy(); + const create = writes.find( + (write) => write.pathname === "/api/workorders/board" && write.method === "POST", + ); + expect(create?.body).toEqual( + expect.objectContaining({ vendorId: 41, technicianAssigned: false }), + ); + expect(vendorWrites(writes)).toEqual([ + expect.objectContaining({ pathname: "/api/vendor-company-roster/7" }), + ]); + + await page.reload(); + const createdRow = page.locator("#wo-row-900"); + await expect(createdRow.getByRole("button", { name: /Vinewood LLC/ })).toBeVisible(); + await createdRow.getByRole("button", { name: /Vinewood LLC/ }).click(); + const vendorDialog = page.getByRole("dialog", { name: "Vendor" }); + await expect(vendorDialog.getByRole("button", { name: "No technician" })).toBeVisible(); + await expect(vendorDialog.locator('input[type="tel"]')).toHaveCount(0); + await expect(vendorDialog.getByRole("button", { name: "Jordan Lee" })).toHaveCount(0); + }); + test("company-only save returns no technician and stays empty after reopen and reload", async ({ page, }) => { diff --git a/src/app/(protected)/workorders/_components/list/table/cells/use-vendor-dialog-form.ts b/src/app/(protected)/workorders/_components/list/table/cells/use-vendor-dialog-form.ts index 4411f5ba..90f83218 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/use-vendor-dialog-form.ts +++ b/src/app/(protected)/workorders/_components/list/table/cells/use-vendor-dialog-form.ts @@ -172,9 +172,7 @@ export function useVendorDialogForm({ notes, pendingCreateCount: Object.keys(pendingCreates).length, }, - baseline: { - ...buildSubmitBaseline({ ...technicianKeyArgs, techPhone }), - }, + baseline: buildSubmitBaseline({ ...technicianKeyArgs, techPhone }), }); const { handleSave } = useVendorDialogSave({ diff --git a/src/app/(protected)/workorders/_components/wizard/use-wizard-create-with-technicians.ts b/src/app/(protected)/workorders/_components/wizard/use-wizard-create-with-technicians.ts index 7d6ef42e..d15ade1e 100644 --- a/src/app/(protected)/workorders/_components/wizard/use-wizard-create-with-technicians.ts +++ b/src/app/(protected)/workorders/_components/wizard/use-wizard-create-with-technicians.ts @@ -33,6 +33,7 @@ export function buildCreatedTechniciansPatch( if ( created.length === 1 && only != null && + draft.technicianSelectionCleared !== true && (draft.technicianAssigned === false || draft.vendorId === "") ) { patch.vendorId = String(only.id); diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-vendor-company-fields.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-vendor-company-fields.tsx index f63346d1..e0351b2b 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-vendor-company-fields.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-vendor-company-fields.tsx @@ -76,6 +76,7 @@ export function WizardVendorCompanyFields({ techPhone: "", vendorNotes: "", newTechnicians: [], + technicianSelectionCleared: false, }) } > @@ -96,6 +97,7 @@ export function WizardVendorCompanyFields({ vendorId: nextAnchor ? String(nextAnchor.id) : "", vendorName: nextAnchor ? nextCompany : "", technicianAssigned: false, + technicianSelectionCleared: false, techPhone: "", vendorNotes: "", newTechnicians: [], @@ -116,6 +118,7 @@ export function WizardVendorCompanyFields({ vendorId: noTechnicianVendorId, vendorName: companyAnchor ? company : "", technicianAssigned: false, + technicianSelectionCleared: true, techPhone: "", vendorNotes: "", }) @@ -147,6 +150,7 @@ export function WizardVendorCompanyFields({ vendorId: noTechnicianVendorId, vendorName: companyAnchor ? company : "", technicianAssigned: false, + technicianSelectionCleared: true, techPhone: "", }); return; @@ -157,6 +161,7 @@ export function WizardVendorCompanyFields({ vendorId: id, vendorName: vendor?.companyName ?? "", technicianAssigned: true, + technicianSelectionCleared: false, techPhone: "", }); }} diff --git a/src/domain/work-orders/types/work-order-wizard.ts b/src/domain/work-orders/types/work-order-wizard.ts index b5675c13..3a3416e1 100644 --- a/src/domain/work-orders/types/work-order-wizard.ts +++ b/src/domain/work-orders/types/work-order-wizard.ts @@ -56,6 +56,8 @@ export interface WorkOrderWizardDraft { vendorCompany?: string; /** Explicit technician choice; null/omitted retains legacy inference. */ technicianAssigned?: boolean | null; + /** UI intent: an explicit clear overrides staged single-technician autoassignment. */ + technicianSelectionCleared?: boolean; /** Technicians registered inline; persisted under `vendorCompany` only on create. */ newTechnicians?: WizardNewTechnician[]; techPhone: string; diff --git a/src/test/app/(protected)/workorders/wizard-vendor-inline-technician.test.tsx b/src/test/app/(protected)/workorders/wizard-vendor-inline-technician.test.tsx index dfe099f0..462a3ebe 100644 --- a/src/test/app/(protected)/workorders/wizard-vendor-inline-technician.test.tsx +++ b/src/test/app/(protected)/workorders/wizard-vendor-inline-technician.test.tsx @@ -116,6 +116,7 @@ describe("WizardStepVendorTime inline technician (SH-364)", () => { vendorId: "52", vendorName: "Gateway Plumbing", technicianAssigned: false, + technicianSelectionCleared: false, techPhone: "", vendorNotes: "", newTechnicians: [], @@ -141,6 +142,7 @@ describe("WizardStepVendorTime inline technician (SH-364)", () => { vendorId: "41", vendorName: "Vinewood LLC", technicianAssigned: false, + technicianSelectionCleared: false, techPhone: "", vendorNotes: "", newTechnicians: [], @@ -170,10 +172,41 @@ describe("WizardStepVendorTime inline technician (SH-364)", () => { vendorId: "41", vendorName: "Vinewood LLC", technicianAssigned: true, + technicianSelectionCleared: false, techPhone: "", }); }); + it("records explicit no-technician intent while leaving a staged technician untouched", async () => { + const onPatch = vi.fn(); + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "No technician" })); + const noTechnicianOptions = screen.getAllByRole("button", { name: "No technician" }); + fireEvent.click(noTechnicianOptions[noTechnicianOptions.length - 1]!); + + expect(onPatch).toHaveBeenCalledWith({ + vendorId: "41", + vendorName: "Vinewood LLC", + technicianAssigned: false, + technicianSelectionCleared: true, + techPhone: "", + }); + expect(screen.getByRole("group", { name: "New Technician 1" })).toBeInTheDocument(); + }); + it("retains the same company vendor id when clearing a second technician", async () => { const onPatch = vi.fn(); const secondVinewoodTech = { @@ -201,6 +234,7 @@ describe("WizardStepVendorTime inline technician (SH-364)", () => { vendorId: "42", vendorName: "Vinewood LLC", technicianAssigned: false, + technicianSelectionCleared: true, techPhone: "", }); }); @@ -327,6 +361,24 @@ describe("useWizardCreateWithTechnicians (SH-364)", () => { }); }); + it("does not autoassign a staged technician after No technician was explicitly chosen", () => { + const created = { ...vendors[0], id: 99, contactName: "Jordan Lee" }; + expect( + buildCreatedTechniciansPatch( + draftWith({ + vendorCompany: "Vinewood LLC", + vendorId: "41", + technicianAssigned: false, + technicianSelectionCleared: true, + newTechnicians: [{ name: "Jordan Lee", phone: "" }], + }), + "Vinewood LLC", + [{ name: "Jordan Lee", phone: "" }], + [created], + ), + ).toEqual({ newTechnicians: [] }); + }); + it("blocks Create and skips the roster save when a staged phone is not a valid number", async () => { const createMutation = { mutate: vi.fn(), isPending: false }; const { result } = renderHook(() => 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 ae62b6b0..cb5dca07 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 @@ -77,6 +77,20 @@ describe("mapWizardDraftToBoardCreatePayload", () => { expect(payload).not.toHaveProperty("customer"); }); + it("omits wizard-only technician selection intent from the create payload", () => { + const payload = mapWizardDraftToBoardCreatePayload({ + ...EMPTY_WIZARD_DRAFT, + type: "PM", + siteCode: "BK5", + vendorId: "41", + technicianAssigned: false, + technicianSelectionCleared: true, + }); + + expect(payload).toEqual(expect.objectContaining({ vendorId: 41, technicianAssigned: false })); + expect(payload).not.toHaveProperty("technicianSelectionCleared"); + }); + it("allows create mapping without a service", () => { const payload = mapWizardDraftToBoardCreatePayload({ ...EMPTY_WIZARD_DRAFT,