diff --git a/e2e/work-orders/inline-technician.spec.ts b/e2e/work-orders/inline-technician.spec.ts index bb1fcae5..f61412e0 100644 --- a/e2e/work-orders/inline-technician.spec.ts +++ b/e2e/work-orders/inline-technician.spec.ts @@ -121,6 +121,18 @@ async function mockApi( scheduled: currentBoardRows, }); } + if (/^\/api\/workorders\/\d+\/detail$/u.test(pathname) && method === "GET") { + const id = Number(pathname.split("/")[3]); + const detailRow = currentBoardRows.find((row) => row.id === id); + if (detailRow == null) return fulfillJson(route, { message: "Missing fixture row" }, 404); + return fulfillJson(route, { + info: detailRow, + completion: { docStatus: 1 }, + comments: [], + audit: [], + media: [], + }); + } if (pathname === "/api/workorders/board" && method === "POST") { return fulfillJson(route, { data: { id: 900, woNumber: "SH-900" } }); } @@ -147,6 +159,7 @@ async function mockApi( ? { technicianAssigned: true, techName: vendorId === 50 ? "Avery Chen" : "Adam Whyte", + techPhone: "(314) 555-0141", } : {}), rowVersion: "rv-2", @@ -294,6 +307,7 @@ test.describe("Inline technician registration", () => { await expect( page.getByRole("dialog").getByRole("button", { name: "No technician" }), ).toBeVisible(); + await expect(page.locator('input[type="tel"]')).toHaveCount(0); await page .getByRole("dialog") .getByRole("button", { name: /cancel/i }) @@ -395,6 +409,7 @@ test.describe("Inline technician registration", () => { const dialog = page.getByRole("dialog"); await dialog.getByRole("button", { name: "No technician" }).click(); await dialog.getByRole("button", { name: "Adam Whyte" }).click(); + await expect(page.locator('input[type="tel"]')).toBeVisible(); await dialog.getByRole("button", { name: /^save$/i }).click(); const assignmentWrite = writes.find((write) => write.pathname === "/api/workorders/1/board"); @@ -406,9 +421,9 @@ test.describe("Inline technician registration", () => { const reloadedRow = page.locator("#wo-row-1"); await expect(reloadedRow.getByRole("button", { name: /Adam Whyte/ })).toBeVisible(); await reloadedRow.getByRole("button", { name: /Adam Whyte/ }).click(); - await expect( - page.getByRole("dialog").getByRole("button", { name: "Adam Whyte" }), - ).toBeVisible(); + const reopenedDialog = page.getByRole("dialog"); + await expect(reopenedDialog.getByRole("button", { name: "Adam Whyte" })).toBeVisible(); + await expect(reopenedDialog.locator('input[type="tel"]')).toHaveValue("(314) 555-0141"); await page .getByRole("dialog") .getByRole("button", { name: /cancel/i }) @@ -432,6 +447,56 @@ test.describe("Inline technician registration", () => { ).toBe(true); }); + test("slide-over saves a same-company no-technician selection with its vendor anchor", async ({ + page, + }) => { + const writes: Captured[] = []; + const companyOnly = { + ...BOARD_ROW, + vendorId: 50, + vendorName: "Coastal HVAC", + technicianAssigned: false, + techName: null, + techPhone: null, + }; + await mockApi(page, writes, [companyOnly]); + await page.goto("/workorders"); + + const row = page.locator("#wo-row-1"); + await row.hover(); + await row.getByRole("button", { name: "View details" }).click(); + const details = page.getByRole("dialog", { name: "Work order details" }); + await expect(details).toBeVisible(); + await details.getByRole("button", { name: "Edit WO" }).click(); + + await details.getByRole("button", { name: /Coastal HVAC/ }).click(); + const assignment = page.getByRole("dialog").last(); + await assignment.getByRole("button", { name: "Coastal HVAC" }).click(); + await assignment.getByRole("button", { name: "Vinewood LLC" }).click(); + await assignment.getByRole("button", { name: /select technician/i }).click(); + await assignment.getByRole("button", { name: "No technician" }).click(); + await assignment.getByRole("button", { name: /^save$/i }).click(); + await details.getByRole("button", { name: "Save", exact: true }).click(); + + const slideOverPatch = writes.find( + (write) => + write.pathname === "/api/workorders/1/board" && + (write.body as { field?: string }).field === "vendorId", + ); + expect(slideOverPatch?.body).toEqual( + expect.objectContaining({ field: "vendorId", value: "41", technicianAssigned: false }), + ); + + await page.reload(); + const reloadedRow = page.locator("#wo-row-1"); + await reloadedRow.hover(); + await reloadedRow.getByRole("button", { name: "View details" }).click(); + const reloadedDetails = page.getByRole("dialog", { name: "Work order details" }); + await expect(reloadedDetails).toBeVisible(); + await reloadedDetails.getByRole("button", { name: /Vinewood LLC/ }).click(); + await expect(reloadedDetails.getByRole("button", { name: "No technician" })).toBeVisible(); + }); + test("cancelling the wizard after staging a technician writes no vendor (SH-366)", async ({ page, }) => { diff --git a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx index 248d7281..2d718b4f 100644 --- a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx +++ b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx @@ -213,6 +213,7 @@ export function SlideOverInfoTabEditView({ onPatch(toVendorTablePatch(p))} /> diff --git a/src/domain/work-orders/utils/slide-over-draft.ts b/src/domain/work-orders/utils/slide-over-draft.ts index 7135bd01..af8c2679 100644 --- a/src/domain/work-orders/utils/slide-over-draft.ts +++ b/src/domain/work-orders/utils/slide-over-draft.ts @@ -31,6 +31,7 @@ export const SLIDE_OVER_EDIT_KEYS = [ "avetaRequired", "additionalContacts", "vendorId", + "technicianAssigned", // company/tech are display companions; board persists vendor via vendorId only "status", "pocName", @@ -91,6 +92,7 @@ export function buildSlideOverPatch( const patch: WorkOrderTablePatch = {}; for (const key of SLIDE_OVER_EDIT_KEYS) { + if (key === "technicianAssigned") continue; if (key === "extraServices") { if (!sameExtraServices(draft.extraServices, baseline.extraServices)) { patch.extraServices = draft.extraServices; @@ -108,5 +110,16 @@ export function buildSlideOverPatch( } } + const assignmentChanged = draft.technicianAssigned !== baseline.technicianAssigned; + const vendorChanged = draft.vendorId !== baseline.vendorId; + if ( + typeof draft.technicianAssigned === "boolean" && + (assignmentChanged || vendorChanged) && + String(draft.vendorId).trim() !== "" + ) { + patch.vendorId = draft.vendorId; + patch.technicianAssigned = draft.technicianAssigned; + } + return patch; } diff --git a/src/test/app/(protected)/workorders/vendor-dialog-save-gate.test.tsx b/src/test/app/(protected)/workorders/vendor-dialog-save-gate.test.tsx index 967e1a3a..6eed4b1a 100644 --- a/src/test/app/(protected)/workorders/vendor-dialog-save-gate.test.tsx +++ b/src/test/app/(protected)/workorders/vendor-dialog-save-gate.test.tsx @@ -157,6 +157,35 @@ describe("VendorDialog save gate", () => { }); }); + it("shows the editable phone for a newly selected technician and hides it for No technician", async () => { + renderWithProviders( + , + ); + + expect(document.querySelector('input[type="tel"]')).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "No technician" })); + fireEvent.click(await screen.findByRole("button", { name: /jordan lee/i })); + + const phone = document.querySelector('input[type="tel"]'); + expect(phone).toBeVisible(); + fireEvent.change(phone!, { target: { value: "(314) 555-0141" } }); + expect(phone).toHaveValue("(314) 555-0141"); + + fireEvent.click(await screen.findByRole("button", { name: /jordan lee/i })); + fireEvent.click(await screen.findByRole("button", { name: "No technician" })); + expect(document.querySelector('input[type="tel"]')).not.toBeInTheDocument(); + }); + it("only offers technicians from the selected company", async () => { renderWithProviders( { + it("reopens a company-only inline assignment without inferring its anchor as technician", async () => { + renderWithProviders( + + + + + + +
, + ); + + fireEvent.click(screen.getByRole("button", { name: /Vinewood LLC/ })); + expect(await screen.findByRole("button", { name: "No technician" })).toBeVisible(); + expect(screen.queryByRole("button", { name: "Adam Whyte" })).not.toBeInTheDocument(); + }, 10_000); + it("patches schedMode range with start and end", async () => { const onPatch = vi.fn(); renderWithProviders( diff --git a/src/test/domain/work-orders/utils/slide-over-draft.test.ts b/src/test/domain/work-orders/utils/slide-over-draft.test.ts index 2bc85efc..d0bae108 100644 --- a/src/test/domain/work-orders/utils/slide-over-draft.test.ts +++ b/src/test/domain/work-orders/utils/slide-over-draft.test.ts @@ -65,6 +65,7 @@ describe("slide-over-draft", () => { expect(SLIDE_OVER_EDIT_KEYS).toContain("woNumber"); expect(SLIDE_OVER_EDIT_KEYS).toContain("status"); expect(SLIDE_OVER_EDIT_KEYS).toContain("vendorId"); + expect(SLIDE_OVER_EDIT_KEYS).toContain("technicianAssigned"); expect(SLIDE_OVER_EDIT_KEYS).toContain("additionalContacts"); expect(SLIDE_OVER_EDIT_KEYS).toContain("avetaRequired"); expect(SLIDE_OVER_EDIT_KEYS).toContain("pocName"); @@ -87,6 +88,37 @@ describe("slide-over-draft", () => { expect(buildSlideOverPatch(vendorDraft, row)).toEqual({ vendorId: "v2" }); }); + it("sends a same-vendor assignment toggle in the vendorId envelope", () => { + const row = baseRow({ technicianAssigned: true }); + const draft = baseRow({ technicianAssigned: false }); + + expect(isSlideOverDraftDirty(draft, row)).toBe(true); + expect(buildSlideOverPatch(draft, row)).toEqual({ + vendorId: row.vendorId, + technicianAssigned: false, + }); + }); + + it("includes assignment state when the slide-over changes company", () => { + const row = baseRow({ technicianAssigned: false }); + const draft = baseRow({ vendorId: "v2", company: "Other Co", technicianAssigned: false }); + + expect(isSlideOverDraftDirty(draft, row)).toBe(true); + expect(buildSlideOverPatch(draft, row)).toEqual({ + vendorId: "v2", + technicianAssigned: false, + }); + }); + + it("keeps an unchanged legacy assignment clean", () => { + const legacy = baseRow(); + delete (legacy as Partial).technicianAssigned; + const draft = { ...legacy }; + + expect(isSlideOverDraftDirty(draft, legacy)).toBe(false); + expect(buildSlideOverPatch(draft, legacy)).toEqual({}); + }); + it("tracks POC edits for dirty state and patch", () => { const row = baseRow(); const draft = baseRow({ pocName: "Jordan", pocPhone: "555-0100" });