import { expect, test, type Page, type Route } from "@playwright/test"; const TEST_USER = { token: "vendor-operations-e2e-token", expiration: new Date(Date.now() + 60 * 60 * 1000).toISOString(), email: "admin@seahavenind.com", userRoles: "Admin", phoneNumber: "5551234567", fullname: "Vendor Operations Admin", id: "1", }; async function fulfillJson(route: Route, body: unknown, status = 200) { await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body) }); } async function authenticate(page: Page) { await page.addInitScript((user) => localStorage.setItem("auth", JSON.stringify(user)), TEST_USER); await page.route("**/api/Dashboard/Stats", (route) => fulfillJson(route, { data: { openWorkOrders: 0, pendingApprovals: 0, scheduledPMs: 0, activeAssets: 0, }, }), ); await page.route("**/api/User*", (route) => fulfillJson(route, { data: [] })); } test.describe("Vendor operations", () => { test("keeps the most specific vendor navigation item exclusively current", async ({ page }) => { await authenticate(page); await page.route("**/api/vendor-operations/insights*", (route) => fulfillJson(route, { data: [] }), ); await page.goto("/vendors/insights"); const vendorLinks = page.locator('a[href^="/vendors"]'); await expect( vendorLinks.filter({ has: page.getByText("Vendor Insights", { exact: true }) }), ).toHaveAttribute("aria-current", "page"); await expect( vendorLinks.filter({ has: page.getByText("Vendors", { exact: true }) }), ).not.toHaveAttribute("aria-current", "page"); await expect( vendorLinks.filter({ has: page.getByText("Site Preferences", { exact: true }) }), ).not.toHaveAttribute("aria-current", "page"); }); test("keeps Vendor PO controls usable at a constrained viewport", async ({ page }) => { await page.setViewportSize({ width: 700, height: 900 }); await authenticate(page); await page.route("**/api/dispatches*", (route) => fulfillJson(route, { items: [], total: 0 })); await page.route("**/api/Vendor/GetVendorList*", (route) => fulfillJson(route, { Data: [], TotalCount: 0 }), ); await page.goto("/vendor-pos"); await expect(page.getByRole("heading", { name: "Vendor POs" })).toBeVisible(); await expect(page.getByText("0 total", { exact: true })).toBeVisible(); await expect(page.getByPlaceholder("Search PO #, dispatch, vendor, WO...")).toBeVisible(); await expect(page.locator("label").filter({ hasText: /^Status$/ })).toBeVisible(); await expect(page.locator("label").filter({ hasText: /^Vendor$/ })).toBeVisible(); await expect(page.getByLabel("From", { exact: true })).toBeVisible(); await expect(page.getByLabel("To", { exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: "Clear filters" })).toBeVisible(); await expect .poll(() => page .getByPlaceholder("Search PO #, dispatch, vendor, WO...") .evaluate((element) => element.clientWidth), ) .toBeGreaterThanOrEqual(280); }); test("groups 48-hour reminders and overlap conflicts with direct work-order actions", async ({ page, }) => { await authenticate(page); await page.route("**/api/vendor-operations/notifications", (route) => fulfillJson(route, { items: [ { id: "no-vendor-501", type: "NoVendor", severity: "Medium", group: "No Vendor", title: "No vendor assigned — 48h to service", workOrderId: 501, workOrderNumber: "WO-501", triggeredAt: "2026-07-23T12:00:00Z", scheduledAt: "2026-07-25T12:00:00Z", action: { label: "Choose vendor", href: "/workorders/edit/501?assignVendor=1" }, }, { id: "vendor-conflict-801-802", type: "VendorConflict", severity: "Medium", group: "Vendor Conflict", title: "Vendor has overlapping appointments", triggeredAt: "2026-07-23T12:00:00Z", workOrders: [ { workOrderId: 501, workOrderNumber: "WO-501", start: "2026-07-25T12:00:00Z", end: "2026-07-25T14:00:00Z", }, { workOrderId: 502, workOrderNumber: "WO-502", start: "2026-07-25T13:00:00Z", end: "2026-07-25T15:00:00Z", }, ], actions: [ { label: "WO-501", href: "/workorders/501" }, { label: "WO-502", href: "/workorders/502" }, ], }, ], }), ); await page.goto("/notifications"); await expect(page.getByRole("heading", { name: "Items requiring attention" })).toBeVisible(); await expect(page.getByRole("heading", { name: "No Vendor" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Vendor Conflict" })).toBeVisible(); await expect(page.getByRole("link", { name: "Choose vendor" })).toHaveAttribute( "href", "/workorders/edit/501?assignVendor=1", ); await expect(page.getByRole("link", { name: "WO-501" })).toHaveAttribute( "href", "/workorders/501", ); await expect(page.getByRole("link", { name: "WO-502" })).toHaveAttribute( "href", "/workorders/502", ); }); test("filters insights and exposes both export formats", async ({ page }) => { await authenticate(page); const requestedUrls: string[] = []; await page.route("**/api/vendor-operations/insights*", async (route) => { requestedUrls.push(route.request().url()); const { pathname } = new URL(route.request().url()); if (pathname.endsWith(".csv")) { await route.fulfill({ status: 200, contentType: "text/csv", body: "Vendor,Completed\n" }); return; } if (pathname.endsWith(".pdf")) { await route.fulfill({ status: 200, contentType: "application/pdf", body: "%PDF-1.4" }); return; } await fulfillJson(route, { data: [ { vendorId: 7, vendor: "Gateway Plumbing", completedJobs: 12, cancellationRefusalRate: 0.1, onTimeArrivalRate: 0.9, onTimeCompletionRate: 0.8, averageCycleHours: 5.5, }, ], }); }); await page.goto("/vendors/insights"); await expect(page.getByRole("cell", { name: "Gateway Plumbing" })).toBeVisible(); await page.getByLabel("From", { exact: true }).fill("2026-01-01"); await page.getByLabel("To", { exact: true }).fill("2026-01-31"); await page.getByLabel("Trade", { exact: true }).fill("Plumbing"); const insightsQueryHas = (name: string, value: string) => requestedUrls.some( (url) => new URL(url).pathname.endsWith("/insights") && new URL(url).searchParams.get(name) === value, ); await expect.poll(() => insightsQueryHas("from", "2026-01-01")).toBe(true); await expect.poll(() => insightsQueryHas("to", "2026-01-31")).toBe(true); await expect.poll(() => insightsQueryHas("trade", "Plumbing")).toBe(true); await page.getByRole("button", { name: "Export CSV" }).click(); await page.getByRole("button", { name: "Export PDF" }).click(); const exportRequest = (format: "csv" | "pdf") => requestedUrls.find((url) => new URL(url).pathname.endsWith(`/insights.${format}`)); const exportHasFilters = (url: string | undefined) => { if (!url) return false; const params = new URL(url).searchParams; return ( params.get("from") === "2026-01-01" && params.get("to") === "2026-01-31" && params.get("trade") === "Plumbing" ); }; await expect.poll(() => exportHasFilters(exportRequest("csv"))).toBe(true); await expect.poll(() => exportHasFilters(exportRequest("pdf"))).toBe(true); }); test("reorders and saves preferred vendors for a site", async ({ page }) => { await authenticate(page); let savedPreferences: unknown; await page.route("**/api/Contact/GetLocations", (route) => fulfillJson(route, { data: [{ Id: 10, Name: "St. Louis HQ" }] }), ); await page.route("**/api/Vendor/Dropdown*", (route) => fulfillJson(route, { data: [ { Id: 7, CompanyName: "Gateway Plumbing", TradeSpecialties: "Plumbing" }, { Id: 8, CompanyName: "Metro HVAC", TradeSpecialties: "HVAC" }, ], }), ); await page.route("**/api/vendor-operations/sites/10/preferred-vendors", async (route) => { if (route.request().method() === "PUT") { savedPreferences = route.request().postDataJSON(); await fulfillJson(route, savedPreferences); return; } await fulfillJson(route, [ { id: 1, locationId: 10, vendorId: 7, vendorName: "Gateway Plumbing", trade: "Plumbing", sortOrder: 0, isStale: false, }, { id: 2, locationId: 10, vendorId: 8, vendorName: "Metro HVAC", trade: "HVAC", sortOrder: 1, isStale: false, }, ]); }); await page.goto("/vendors/preferences"); await page.getByLabel("Site").click(); await page.getByRole("option", { name: "St. Louis HQ" }).click(); await expect(page.getByText(/1\. Gateway Plumbing/)).toBeVisible(); await page.getByRole("button", { name: "Move Metro HVAC up" }).click(); await expect(page.getByText(/1\. Metro HVAC/)).toBeVisible(); await page.getByRole("button", { name: "Save preferences" }).click(); await expect .poll(() => savedPreferences) .toEqual([ { vendorId: 8, trade: "HVAC", sortOrder: 0 }, { vendorId: 7, trade: "Plumbing", sortOrder: 1 }, ]); }); test("creates a work order and assigns the selected technician with ETA", async ({ page }) => { await authenticate(page); let assignment: unknown; await page.route("**/api/Contact/GetLocations", (route) => fulfillJson(route, { data: [{ Id: 10, Name: "St. Louis HQ" }] }), ); await page.route("**/api/Employee/Dropdown", (route) => fulfillJson(route, { data: [] })); await page.route("**/api/DropdownOptions/ByCategory/**", (route) => fulfillJson(route, { data: [] }), ); await page.route("**/api/vendor-operations/availability*", (route) => fulfillJson(route, { data: [ { id: 7, companyName: "Gateway Plumbing", contactName: "Taylor Tech", availabilityStatus: "Available", isPreferred: true, conflicts: [], }, ], }), ); await page.route("**/api/WorkOrder/AddWorkorder", (route) => fulfillJson(route, { data: { Id: 900, WorkerOrderTitle: "Boiler repair", LocationId: 10, Priority: "Sev 1", Status: "Open", Comments: [], AuditLog: [], Dispatches: [], }, }), ); await page.route("**/api/vendor-operations/work-orders/900/assignment", async (route) => { assignment = route.request().postDataJSON(); await fulfillJson(route, { availabilityStatus: "Available", conflicts: [ { workOrderId: 502, workOrderNumber: "WO-502", start: "2026-07-25T12:00:00Z", end: "2026-07-25T14:00:00Z", }, ], }); }); await page.route("**/api/WorkOrder/GetWorkorderById*", (route) => fulfillJson(route, { data: { Id: 900, WorkerOrderTitle: "Boiler repair", LocationId: 10, Priority: "Sev 1", Status: "Open", Comments: [], AuditLog: [], Dispatches: [], }, }), ); await page.goto("/workorders/new"); await page.getByLabel("Title").fill("Boiler repair"); await page.getByLabel("Location").click(); await page.getByRole("option", { name: "St. Louis HQ" }).click(); await page.getByLabel("Priority").click(); await page.getByRole("option", { name: "Sev 1" }).click(); await page.getByLabel("Scheduled").fill("2026-07-25"); await page.getByLabel("Appointment start").fill("2026-07-25T10:00"); await page.getByLabel("Appointment end").fill("2026-07-25T12:00"); await page.getByLabel("Company + Technician").click(); await page.getByRole("option", { name: /Gateway Plumbing · Taylor Tech/ }).click(); await page.getByLabel("Assignment ETA").fill("2026-07-25T08:30"); await page.getByLabel("Manual ETA override").check(); await page.getByRole("button", { name: "Create" }).click(); await expect .poll(() => assignment) .toEqual({ vendorId: 7, scheduledStart: "2026-07-25T10:00:00.000Z", scheduledEnd: "2026-07-25T12:00:00.000Z", estimatedArrivalAt: "2026-07-25T08:30:00.000Z", etaManualOverride: true, }); await expect(page.getByText(/scheduling conflict/i)).toBeVisible(); await expect(page).toHaveURL(/\/workorders\/900$/); }); test("preserves an existing ETA when editing in a non-UTC timezone", async ({ browser }) => { const context = await browser.newContext({ timezoneId: "America/New_York" }); const page = await context.newPage(); await authenticate(page); let assignment: Record | undefined; const workOrder = { Id: 901, WorkerOrderTitle: "Boiler repair", LocationId: 10, Priority: "Sev 1", Status: "Open", CreatedDate: "2026-07-20", ScheduledDate: "2026-07-25", Comments: [], AuditLog: [], Dispatches: [ { Id: 44, VendorId: 7, Status: "Acknowledged", EstimatedArrivalAt: "2026-07-25T12:30:00.000Z", EtaManualOverride: true, }, ], }; await page.route("**/api/Contact/GetLocations", (route) => fulfillJson(route, { data: [{ Id: 10, Name: "St. Louis HQ" }] }), ); await page.route("**/api/Employee/Dropdown", (route) => fulfillJson(route, { data: [] })); await page.route("**/api/DropdownOptions/ByCategory/**", (route) => fulfillJson(route, { data: [] }), ); await page.route("**/api/vendor-operations/availability*", (route) => fulfillJson(route, { data: [ { id: 7, companyName: "Gateway Plumbing", contactName: "Taylor Tech", availabilityStatus: "Available", isPreferred: true, conflicts: [], }, ], }), ); await page.route("**/api/WorkOrder/GetWorkorderById*", (route) => fulfillJson(route, { data: workOrder }), ); await page.route("**/api/WorkOrder/EditWorkorder", (route) => fulfillJson(route, { data: workOrder }), ); await page.route("**/api/vendor-operations/work-orders/901/assignment", async (route) => { assignment = route.request().postDataJSON() as Record; await fulfillJson(route, { availabilityStatus: "Available", conflicts: [] }); }); await page.goto("/workorders/edit/901"); await expect(page.getByLabel("Assignment ETA")).toHaveValue("2026-07-25T08:30"); await page.getByRole("button", { name: "Update" }).click(); await expect.poll(() => assignment?.estimatedArrivalAt).toBe("2026-07-25T12:30:00.000Z"); await context.close(); }); test("converts explicit local schedule bounds to the correct UTC instants across a DST transition", async ({ browser, }) => { // America/New_York springs forward on 2026-03-08 at 02:00 EST -> 03:00 EDT. // The window below straddles the transition: 01:00 is EST (UTC-5) while // 04:00 is EDT (UTC-4). Each bound must resolve its own offset, proving no // fixed-window assumption remains. const context = await browser.newContext({ timezoneId: "America/New_York" }); const page = await context.newPage(); await authenticate(page); let assignment: Record | undefined; await page.route("**/api/Contact/GetLocations", (route) => fulfillJson(route, { data: [{ Id: 10, Name: "St. Louis HQ" }] }), ); await page.route("**/api/Employee/Dropdown", (route) => fulfillJson(route, { data: [] })); await page.route("**/api/DropdownOptions/ByCategory/**", (route) => fulfillJson(route, { data: [] }), ); await page.route("**/api/vendor-operations/availability*", (route) => fulfillJson(route, { data: [ { id: 7, companyName: "Gateway Plumbing", contactName: "Taylor Tech", availabilityStatus: "Available", isPreferred: true, conflicts: [], }, ], }), ); await page.route("**/api/WorkOrder/AddWorkorder", (route) => fulfillJson(route, { data: { Id: 902, WorkerOrderTitle: "Boiler repair", LocationId: 10, Priority: "Sev 1", Status: "Open", Comments: [], AuditLog: [], Dispatches: [], }, }), ); await page.route("**/api/vendor-operations/work-orders/902/assignment", async (route) => { assignment = route.request().postDataJSON() as Record; await fulfillJson(route, { availabilityStatus: "Available", conflicts: [] }); }); await page.route("**/api/WorkOrder/GetWorkorderById*", (route) => fulfillJson(route, { data: { Id: 902, WorkerOrderTitle: "Boiler repair", LocationId: 10, Priority: "Sev 1", Status: "Open", Comments: [], AuditLog: [], Dispatches: [], }, }), ); await page.goto("/workorders/new"); await page.getByLabel("Title").fill("Boiler repair"); await page.getByLabel("Location").click(); await page.getByRole("option", { name: "St. Louis HQ" }).click(); await page.getByLabel("Priority").click(); await page.getByRole("option", { name: "Sev 1" }).click(); await page.getByLabel("Appointment start").fill("2026-03-08T01:00"); await page.getByLabel("Appointment end").fill("2026-03-08T04:00"); await page.getByLabel("Company + Technician").click(); await page.getByRole("option", { name: /Gateway Plumbing · Taylor Tech/ }).click(); await page.getByRole("button", { name: "Create" }).click(); await expect.poll(() => assignment?.scheduledStart).toBe("2026-03-08T06:00:00.000Z"); await expect.poll(() => assignment?.scheduledEnd).toBe("2026-03-08T08:00:00.000Z"); await context.close(); }); test("focuses the Vendor & Time section when arriving from a no-vendor alert", async ({ page, }) => { await authenticate(page); await page.route("**/api/Contact/GetLocations", (route) => fulfillJson(route, { data: [{ Id: 10, Name: "St. Louis HQ" }] }), ); await page.route("**/api/Employee/Dropdown", (route) => fulfillJson(route, { data: [] })); await page.route("**/api/DropdownOptions/ByCategory/**", (route) => fulfillJson(route, { data: [] }), ); await page.route("**/api/vendor-operations/availability*", (route) => fulfillJson(route, { data: [ { id: 7, companyName: "Gateway Plumbing", contactName: "Taylor Tech", availabilityStatus: "Available", isPreferred: true, conflicts: [], }, ], }), ); await page.route("**/api/WorkOrder/GetWorkorderById*", (route) => fulfillJson(route, { data: { Id: 501, WorkerOrderTitle: "Boiler repair", LocationId: 10, Priority: "Sev 1", Status: "Open", Comments: [], AuditLog: [], Dispatches: [], }, }), ); await page.goto("/workorders/edit/501?assignVendor=1"); const vendorSection = page.getByRole("region", { name: "Vendor & Time" }); await expect(vendorSection).toBeVisible(); await expect(vendorSection).toBeFocused(); await page.getByLabel("Company + Technician").click(); const option = page.getByRole("option", { name: /Gateway Plumbing · Taylor Tech/ }); await expect(option).toContainText("Unknown"); }); });