import { expect, test, type Page, type Request, type Route } from "@playwright/test"; // Technicians registered inline from work-order entry points // become real vendor records only on save, through the additive vendor-company roster // PATCH (never a whole-vendor POST that restates or blanks company contact fields). const FROZEN_NOW = "2026-08-19T15:00:00.000Z"; const TEST_USER = { token: "wo-inline-technician-e2e-token", expiration: "2030-01-01T00:00:00.000Z", email: "admin@seahavenind.com", userRoles: "Admin", phoneNumber: "5551234567", fullname: "Work Order E2E Admin", id: "1", }; const VENDORS = [ { id: 41, companyName: "Vinewood LLC", contactName: "Adam Whyte", tradeSpecialties: "HVAC", address: "1 Market St", }, ]; const COASTAL_VENDOR = { id: 50, companyName: "Coastal HVAC", contactName: "Avery Chen", tradeSpecialties: "HVAC", address: "9 Shore Rd", }; const ROSTER = { companyId: 7, rowVersion: "AAAAAAAAB9E=", name: "Vinewood LLC", companyPhone: "(314) 555-0100", email: "", address: "1 Market St", city: "St. Louis", state: "MO", zip: "63101", notes: "", technicians: [{ id: 41, contactName: "Adam Whyte", phone: "", email: "", isActive: true }], }; const BOARD_ROW = { id: 1, woNumber: "WO-501", workOrderType: 2, siteCode: "STL-01", locationName: "St. Louis HQ", locationId: 10, lifecycleStatusLabel: "Scheduled", scheduledDate: "2026-08-19", dispatcherId: "1", dispatcherName: "Pat Dispatcher", pm: "HVAC", vendorId: 41, vendorName: "Vinewood LLC", techName: "Adam Whyte", technicianAssigned: true, completedDate: "2026-08-19", docStatus: "Yes", mediaCount: 1, title: "Quarterly HVAC PM", rowVersion: "rv-1", }; type Captured = { method: string; pathname: string; body: unknown }; async function fulfillJson(route: Route, body: unknown, status = 200) { await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body) }); } function bodyOf(request: Request): unknown { try { return request.postDataJSON(); } catch { return undefined; } } function rosterAfterAdd(body: { addTechnicians?: Array> }) { const added = (body.addTechnicians ?? []).map((technician, index) => ({ ...technician, id: 42 + index, })); return { ...ROSTER, rowVersion: "AAAAAAAAB9F=", technicians: [...ROSTER.technicians, ...added] }; } async function mockApi( page: Page, writes: Captured[], initialRows: Array> = [BOARD_ROW], ) { const currentBoardRows = [...initialRows]; await page.clock.install({ time: new Date(FROZEN_NOW) }); await page.clock.resume(); await page.addInitScript((user) => localStorage.setItem("auth", JSON.stringify(user)), TEST_USER); await page.route("**/api/**", async (route) => { const request = route.request(); const pathname = new URL(request.url()).pathname; if (!pathname.startsWith("/api/")) return route.fallback(); const method = request.method(); if (method !== "GET") writes.push({ method, pathname, body: bodyOf(request) }); if (pathname.includes("/workorders/board/search")) { return fulfillJson(route, { items: [], totalCount: 0, page: 0, pageSize: 50 }); } if (pathname === "/api/workorders/board" && method === "GET") { return fulfillJson(route, { weekStart: "2026-08-17", weekEnd: "2026-08-21", counts: { returned: currentBoardRows.length, total: currentBoardRows.length }, unscheduled: [], 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" } }); } if (/^\/api\/workorders\/\d+\/board$/u.test(pathname) && method === "PATCH") { const body = bodyOf(request) as { field?: string; value?: string; technicianAssigned?: boolean; }; const id = Number(pathname.split("/")[3]); const index = currentBoardRows.findIndex((row) => row.id === id); if (index < 0) return fulfillJson(route, { message: "Missing fixture row" }, 404); const current = currentBoardRows[index]; if (current == null) return fulfillJson(route, { message: "Missing fixture row" }, 404); if (body.field === "vendorId") { const vendorId = Number(body.value); currentBoardRows[index] = { ...current, vendorId, vendorName: vendorId === 50 ? "Coastal HVAC" : "Vinewood LLC", ...(body.technicianAssigned === false ? { technicianAssigned: false, techName: null, techPhone: null } : body.technicianAssigned === true ? { technicianAssigned: true, techName: vendorId === 50 ? "Avery Chen" : "Adam Whyte", techPhone: "(314) 555-0141", } : {}), rowVersion: "rv-2", }; } else if (body.field === "lifecycleStatus") { currentBoardRows[index] = { ...current, lifecycleStatusLabel: body.value, ...(body.value === "Completed" ? { frozenCompany: { name: current.vendorName, contactName: current.vendorName === "Coastal HVAC" ? "Avery Chen" : "Adam Whyte", phone: "555-0100", technicianAssigned: current.technicianAssigned, }, } : {}), rowVersion: "rv-3", }; } return fulfillJson(route, currentBoardRows[index]); } if (pathname === "/api/vendor-company-roster" && method === "GET") { return fulfillJson(route, ROSTER); } if (pathname === "/api/vendor-company-roster/7" && method === "PATCH") { return fulfillJson(route, rosterAfterAdd(bodyOf(request) as never)); } if (pathname.toLowerCase().includes("/vendors/dropdown")) { return fulfillJson(route, { data: [...VENDORS, COASTAL_VENDOR] }); } if (pathname.includes("/locations/sites")) { return fulfillJson(route, { data: [{ id: 10, name: "STL-01 · St. Louis, MO", siteCode: "STL-01", code: "STL-01" }], }); } if (pathname === "/api/locations/10") { return fulfillJson(route, { data: { id: 10, name: "St. Louis HQ", siteCode: "STL-01", contact: "Pat Site", phone: "3145550111", }, }); } return fulfillJson(route, { data: [] }); }); } function vendorWrites(writes: Captured[]) { return writes.filter( (write) => write.pathname.startsWith("/api/vendors") || write.pathname.startsWith("/api/vendor-company-roster"), ); } async function openWizardVendorStep(page: Page) { await page.goto("/workorders"); await page.getByRole("button", { name: "New WO" }).click(); await expect(page.getByRole("heading", { name: "Type & schedule" })).toBeVisible(); await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: /select site/i }).click(); await page .getByRole("button", { name: /STL-01/ }) .last() .click(); await expect(page.getByRole("button", { name: "Continue" })).toBeEnabled(); await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: /select company/i }).click(); await page.getByRole("button", { name: "Vinewood LLC" }).last().click(); await page.getByRole("button", { name: "+ Add technician" }).click(); await page.getByLabel("New Technician 1 name").fill("Jordan Lee"); await page.getByLabel("New Technician 1 phone").fill("3145550199"); } test.describe("Inline technician registration", () => { test("wizard persists the technician under the company on create and assigns it", async ({ page, }) => { const writes: Captured[] = []; await mockApi(page, writes); await openWizardVendorStep(page); // Staging a technician is local only. expect(vendorWrites(writes)).toEqual([]); await page.getByRole("button", { name: "Create work order" }).click(); await expect .poll(() => writes.find((write) => write.pathname === "/api/workorders/board")) .toBeTruthy(); expect(vendorWrites(writes)).toEqual([ { method: "PATCH", pathname: "/api/vendor-company-roster/7", body: { rowVersion: "AAAAAAAAB9E=", addTechnicians: [ { contactName: "Jordan Lee", phone: "(314) 555-0199", email: "", tradeSpecialties: "HVAC", isActive: true, preferredContact: "Phone", }, ], }, }, ]); const create = writes.find((write) => write.pathname === "/api/workorders/board"); expect(create?.body).toEqual(expect.objectContaining({ vendorId: 42 })); }); test("company-only save returns no technician and stays empty after reopen and reload", async ({ page, }) => { const writes: Captured[] = []; await mockApi(page, writes); await page.goto("/workorders"); const row = page.locator("#wo-row-1"); await row.getByRole("button", { name: /Adam Whyte/ }).click(); const dialog = page.getByRole("dialog"); await dialog .getByRole("button", { name: /Gateway Plumbing/ }) .first() .click(); await dialog.getByRole("button", { name: "Coastal HVAC" }).click(); await dialog.getByRole("button", { name: /^save$/i }).click(); await expect(dialog).toBeHidden(); const boardPatch = writes.find((write) => write.pathname === "/api/workorders/1/board"); expect(boardPatch?.body).toEqual( expect.objectContaining({ field: "vendorId", value: "50", technicianAssigned: false }), ); await expect(row.getByRole("button", { name: /Coastal HVAC/ })).toBeVisible(); await row.getByRole("button", { name: /Coastal HVAC/ }).click(); 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 }) .click(); await page.reload(); const reloadedRow = page.locator("#wo-row-1"); await expect(reloadedRow.getByRole("button", { name: /Coastal HVAC/ })).toBeVisible(); await reloadedRow.getByRole("button", { name: /Coastal HVAC/ }).click(); await expect( page.getByRole("dialog").getByRole("button", { name: "No technician" }), ).toBeVisible(); await page .getByRole("dialog") .getByRole("button", { name: /cancel/i }) .click(); await reloadedRow.getByRole("button", { name: /in progress/i }).click(); await page.getByRole("button", { name: "Completed" }).click(); await page .getByRole("dialog", { name: "Mark as Complete?" }) .getByRole("button", { name: "Mark as Complete" }) .click(); await expect .poll(() => writes.some( (write) => write.pathname === "/api/workorders/1/board" && (write.body as { field?: string }).field === "lifecycleStatus", ), ) .toBe(true); await expect(reloadedRow.getByRole("button", { name: /completed/i })).toBeVisible(); await reloadedRow.getByRole("button", { name: /Coastal HVAC/ }).click(); const completedDialog = page.getByRole("dialog"); await expect(completedDialog.getByText("—", { exact: true })).toBeVisible(); await expect(completedDialog.getByRole("button", { name: /^save$/i })).toHaveCount(0); expect( writes.some( (write) => write.pathname === "/api/workorders/1/board" && (write.body as { field?: string }).field === "lifecycleStatus", ), ).toBe(true); expect(writes.some((write) => write.pathname.startsWith("/api/vendors"))).toBe(false); }); test("company absence blocks completion for PM, Emergency, Reactive, and Overdue", async ({ page, }) => { const writes: Captured[] = []; const withoutCompany = [ [1, 2], [2, 3], [3, 6], [4, 8], ].map(([id, workOrderType]) => ({ ...BOARD_ROW, id, woNumber: `WO-${600 + id}`, workOrderType, lifecycleStatusLabel: "In Progress", vendorId: null, vendorName: null, techName: "Avery Chen", technicianAssigned: true, docStatus: "Yes", mediaCount: 1, })); await mockApi(page, writes, withoutCompany); await page.goto("/workorders"); for (const id of [1, 2, 3, 4]) { const row = page.locator(`#wo-row-${id}`); await row.getByRole("button", { name: /in progress/i }).click(); const completed = page.getByRole("button", { name: "Completed" }); await expect(completed).toBeDisabled(); await expect(completed).toHaveAttribute( "title", "Fill in Vendor before completing this work order.", ); await page.keyboard.press("Escape"); } expect(writes.filter((write) => write.pathname.endsWith("/board"))).toEqual([]); }); test("explicit technician choice is retained and can complete after reload", async ({ page }) => { const writes: Captured[] = []; const unassigned = { ...BOARD_ROW, technicianAssigned: false, techName: null, techPhone: null, }; await mockApi(page, writes, [unassigned]); await page.goto("/workorders"); const row = page.locator("#wo-row-1"); await row.getByRole("button", { name: /Vinewood LLC/ }).click(); 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"); expect(assignmentWrite?.body).toEqual( expect.objectContaining({ field: "vendorId", value: "41", technicianAssigned: true }), ); await expect(row.getByRole("button", { name: /Adam Whyte/ })).toBeVisible(); await page.reload(); const reloadedRow = page.locator("#wo-row-1"); await expect(reloadedRow.getByRole("button", { name: /Adam Whyte/ })).toBeVisible(); await reloadedRow.getByRole("button", { name: /Adam Whyte/ }).click(); 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 }) .click(); await reloadedRow.getByRole("button", { name: /in progress/i }).click(); await page.getByRole("button", { name: "Completed" }).click(); await page .getByRole("dialog", { name: "Mark as Complete?" }) .getByRole("button", { name: "Mark as Complete" }) .click(); await expect .poll(() => writes.filter((write) => write.pathname === "/api/workorders/1/board").length) .toBe(2); await expect(reloadedRow.getByRole("button", { name: /completed/i })).toBeVisible(); expect( writes.some( (write) => write.pathname === "/api/workorders/1/board" && (write.body as { field?: string; value?: string }).field === "lifecycleStatus", ), ).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, }) => { const writes: Captured[] = []; await mockApi(page, writes); await openWizardVendorStep(page); await page.getByRole("button", { name: "Back" }).click(); await page.getByRole("button", { name: "Back" }).click(); await page.getByRole("button", { name: "Cancel" }).click(); await expect(page.getByRole("heading", { name: "Type & schedule" })).toBeHidden(); // Reopening starts clean: the staged technician was discarded, not persisted. await page.getByRole("button", { name: "New WO" }).click(); await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: /select site/i }).click(); await page .getByRole("button", { name: /STL-01/ }) .last() .click(); await page.getByRole("button", { name: "Continue" }).click(); await expect(page.getByRole("group", { name: "New Technician 1" })).toHaveCount(0); expect(writes).toEqual([]); }); test("vendor assignment modal saves an inline technician for an existing company (SH-365)", async ({ page, }) => { const writes: Captured[] = []; await mockApi(page, writes); await page.goto("/workorders"); await page .locator("#wo-row-1") .getByRole("button", { name: /Adam Whyte/ }) .click(); const dialog = page.getByRole("dialog"); await dialog.getByRole("button", { name: /\+ add technician/i }).click(); await dialog.getByPlaceholder("Full name").fill("Jordan Lee"); await dialog.getByPlaceholder("(000) 000-0000").fill("3145550199"); await dialog.getByRole("button", { name: /^add$/i }).click(); await dialog.getByRole("button", { name: /^save$/i }).click(); await expect(dialog).toBeHidden(); await expect .poll(() => writes.find((write) => write.pathname === "/api/workorders/1/board")) .toBeTruthy(); expect(writes.some((write) => write.pathname.startsWith("/api/vendors"))).toBe(false); const rosterPatch = writes.find((write) => write.pathname === "/api/vendor-company-roster/7"); expect(rosterPatch?.body).not.toHaveProperty("companyFields"); const boardPatch = writes.find((write) => write.pathname === "/api/workorders/1/board"); expect(JSON.stringify(boardPatch?.body)).toContain("42"); }); });