From b6d5ad8a1eeb28c158d38ee8a4a5915ac7370a03 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Tue, 29 Sep 2026 11:28:26 -0300 Subject: [PATCH 1/8] fix(work-orders): hide Pending from existing status menus --- src/domain/work-orders/utils/wo-lifecycle-transitions.ts | 6 ++++-- .../(protected)/workorders/status-cell-closability.test.tsx | 6 +++--- .../work-orders/utils/wo-lifecycle-transitions.test.ts | 2 -- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/domain/work-orders/utils/wo-lifecycle-transitions.ts b/src/domain/work-orders/utils/wo-lifecycle-transitions.ts index f0184ef9..e0351242 100644 --- a/src/domain/work-orders/utils/wo-lifecycle-transitions.ts +++ b/src/domain/work-orders/utils/wo-lifecycle-transitions.ts @@ -17,9 +17,11 @@ import { */ export const AUTOMATIC_LIFECYCLE_STATUSES: readonly WizardWOStatus[] = ["Incomplete", "Scheduled"]; -/** Manually selectable statuses for an existing WO — Canceled is kebab-only. */ +/** + * Manually selectable statuses for an existing WO. Pending is creation-only; Canceled is kebab-only. + */ export const BOARD_INLINE_STATUSES: readonly WizardWOStatus[] = ALL_WIZARD_STATUSES.filter( - (status) => !AUTOMATIC_LIFECYCLE_STATUSES.includes(status), + (status) => status !== "Pending" && !AUTOMATIC_LIFECYCLE_STATUSES.includes(status), ); /** Inline options for an existing WO never repeat its current status. */ diff --git a/src/test/app/(protected)/workorders/status-cell-closability.test.tsx b/src/test/app/(protected)/workorders/status-cell-closability.test.tsx index 7f42d257..63cd8eb8 100644 --- a/src/test/app/(protected)/workorders/status-cell-closability.test.tsx +++ b/src/test/app/(protected)/workorders/status-cell-closability.test.tsx @@ -70,10 +70,10 @@ describe("StatusCell closability gate", () => { expect(offered).not.toContain("Scheduled"); expect(offered).not.toContain("Past Due"); expect(offered).not.toContain("Canceled"); + expect(offered).not.toContain("Pending"); expect(offered).not.toContain(current); expect(offered).toEqual( [ - "Pending", "En Route", "On Site", "In Progress", @@ -84,7 +84,7 @@ describe("StatusCell closability gate", () => { ); }); - it("offers every manual status, and no automatic one, from Incomplete or Scheduled", () => { + it("omits Pending and automatic statuses from Incomplete or Scheduled", () => { for (const current of ["Incomplete", "Scheduled"] as const) { const { unmount } = render( { ); fireEvent.click(screen.getByRole("button", { name: new RegExp(current, "i") })); - expect(screen.getByRole("button", { name: /^pending$/i })).toBeTruthy(); + expect(screen.queryByRole("button", { name: /^pending$/i })).toBeNull(); expect(screen.queryByRole("button", { name: /^incomplete$/i })).toBeNull(); expect(screen.queryByRole("button", { name: /^scheduled$/i })).toBeNull(); unmount(); diff --git a/src/test/domain/work-orders/utils/wo-lifecycle-transitions.test.ts b/src/test/domain/work-orders/utils/wo-lifecycle-transitions.test.ts index 75ea23c6..b22e4357 100644 --- a/src/test/domain/work-orders/utils/wo-lifecycle-transitions.test.ts +++ b/src/test/domain/work-orders/utils/wo-lifecycle-transitions.test.ts @@ -21,7 +21,6 @@ import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-ta describe("wo-lifecycle-transitions", () => { it("lists only manual board statuses: no automatic states and no Canceled (SH-169)", () => { expect(BOARD_INLINE_STATUSES).toEqual([ - "Pending", "En Route", "On Site", "In Progress", @@ -37,7 +36,6 @@ describe("wo-lifecycle-transitions", () => { it("never offers the current status as a transition (SH-358)", () => { expect(getInlineStatusTransitions("On Site")).toEqual([ - "Pending", "En Route", "In Progress", "Completed", From c5d0429fa20ff1bd6d3494c6a25de3d5a7c80000 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Tue, 29 Sep 2026 11:46:12 -0300 Subject: [PATCH 2/8] style(work-orders): format status menu expectations --- .../workorders/status-cell-closability.test.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/test/app/(protected)/workorders/status-cell-closability.test.tsx b/src/test/app/(protected)/workorders/status-cell-closability.test.tsx index 63cd8eb8..77922f0d 100644 --- a/src/test/app/(protected)/workorders/status-cell-closability.test.tsx +++ b/src/test/app/(protected)/workorders/status-cell-closability.test.tsx @@ -73,14 +73,9 @@ describe("StatusCell closability gate", () => { expect(offered).not.toContain("Pending"); expect(offered).not.toContain(current); expect(offered).toEqual( - [ - "En Route", - "On Site", - "In Progress", - "Completed", - "Rescheduled", - "Pending Quote", - ].filter((status) => status !== current), + ["En Route", "On Site", "In Progress", "Completed", "Rescheduled", "Pending Quote"].filter( + (status) => status !== current, + ), ); }); From 123b1ec9b5fcf2bee538d0acdfcf13f23382df7d Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Tue, 29 Sep 2026 12:25:56 -0300 Subject: [PATCH 3/8] test: cover pending work order status workflows --- e2e/work-orders/pending-status-menu.spec.ts | 183 ++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 e2e/work-orders/pending-status-menu.spec.ts diff --git a/e2e/work-orders/pending-status-menu.spec.ts b/e2e/work-orders/pending-status-menu.spec.ts new file mode 100644 index 00000000..90c1beca --- /dev/null +++ b/e2e/work-orders/pending-status-menu.spec.ts @@ -0,0 +1,183 @@ +import { expect, test, type Page, type Route } from "@playwright/test"; + +const FROZEN_NOW = "2026-08-19T15:00:00.000Z"; + +const BOARD_ROW = { + id: 1, + woNumber: "WO-501", + workOrderType: 2, + siteCode: "STL-01", + locationName: "St. Louis HQ", + locationId: 10, + lifecycleStatusLabel: "In Progress", + scheduledDate: "2026-08-19", + dispatcherId: "1", + dispatcherName: "Pat Dispatcher", + pm: "HVAC", + rowVersion: "rv-1", +}; + +type Role = "Admin" | "Dispatcher"; + +async function fulfillJson(route: Route, body: unknown) { + await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(body) }); +} + +async function mockApi(page: Page, role: Role, createPayloads: unknown[] = []) { + await page.clock.install({ time: new Date(FROZEN_NOW) }); + await page.clock.resume(); + await page.addInitScript( + (user) => { + localStorage.setItem("auth", JSON.stringify(user)); + const style = document.createElement("style"); + style.textContent = ".tsqd-parent-container { display: none !important; }"; + document.documentElement.appendChild(style); + }, + { + token: `wo-pending-${role.toLowerCase()}-e2e-token`, + expiration: "2030-01-01T00:00:00.000Z", + email: `${role.toLowerCase()}@seahavenind.com`, + userRoles: role, + phoneNumber: "5551234567", + fullname: `Work Order E2E ${role}`, + id: "1", + }, + ); + + let row = { ...BOARD_ROW }; + let createdRow: typeof BOARD_ROW | undefined; + await page.route( + (url) => url.pathname.startsWith("/api/"), + async (route) => { + const request = route.request(); + const { pathname } = new URL(request.url()); + + if (pathname.includes("/workorders/board/search")) { + await fulfillJson(route, { items: [], totalCount: 0, page: 0, pageSize: 50 }); + return; + } + if (pathname === "/api/workorders/board" && request.method() === "GET") { + await fulfillJson(route, { + weekStart: "2026-08-17", + weekEnd: "2026-08-21", + counts: { returned: createdRow ? 2 : 1, total: createdRow ? 2 : 1 }, + unscheduled: createdRow ? [createdRow] : [], + scheduled: [row], + }); + return; + } + if (pathname === "/api/workorders/board" && request.method() === "POST") { + createPayloads.push(request.postDataJSON()); + createdRow = { + ...BOARD_ROW, + id: 900, + woNumber: "WO-900", + lifecycleStatusLabel: "Pending", + scheduledDate: "", + rowVersion: "rv-created", + }; + await fulfillJson(route, { data: { id: 900, internalWONumber: "WO-900" } }); + return; + } + if (pathname === "/api/workorders/1/board" && request.method() === "PATCH") { + const body = request.postDataJSON() as { field: string; value: string }; + row = { ...row, lifecycleStatusLabel: body.value, rowVersion: "rv-2" }; + await fulfillJson(route, row); + return; + } + if (pathname.includes("/locations/sites") || pathname.includes("/Contact/GetLocations")) { + await fulfillJson(route, { + data: [{ id: 10, name: "STL-01 · St. Louis, MO", siteCode: "STL-01", code: "STL-01" }], + }); + return; + } + if (pathname === "/api/locations/10") { + await fulfillJson(route, { + data: { + id: 10, + name: "St. Louis HQ", + siteCode: "STL-01", + contact: "Pat Site", + phone: "3145550111", + }, + }); + return; + } + await fulfillJson(route, { data: [], items: [] }); + }, + ); +} + +async function expectManualTransitionWithoutPending(page: Page) { + const row = page.locator("#wo-row-1"); + await expect(row.getByText("WO-501")).toBeVisible(); + await row.getByRole("button", { name: "In Progress" }).click(); + + const menu = page.locator(".MuiPopover-paper").last(); + await expect(menu.getByRole("button", { name: "Pending", exact: true })).toHaveCount(0); + await expect(menu.getByRole("button", { name: "On Site", exact: true })).toBeEnabled(); + await menu.getByRole("button", { name: "On Site", exact: true }).click(); + + await expect(row.getByRole("button", { name: "On Site" })).toBeVisible(); +} + +test.describe("existing work order status menu", () => { + test.setTimeout(60_000); + + test("Admin can choose a manual status but not Pending", async ({ page }) => { + await mockApi(page, "Admin"); + await page.goto("/workorders"); + await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({ + timeout: 20_000, + }); + + await expectManualTransitionWithoutPending(page); + }); + + test("Dispatcher can choose a manual status but not Pending", async ({ page }) => { + await mockApi(page, "Dispatcher"); + await page.goto("/workorders"); + await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({ + timeout: 20_000, + }); + + await expectManualTransitionWithoutPending(page); + }); + + test("Admin can select Pending for a new work order when Schedule On has no date", async ({ + page, + }) => { + const createPayloads: unknown[] = []; + await mockApi(page, "Admin", createPayloads); + await page.goto("/workorders"); + await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({ + timeout: 20_000, + }); + await page.getByRole("button", { name: "New WO" }).click(); + await expect(page.getByRole("heading", { name: "Type & schedule" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Pick a date" })).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 page.getByRole("button", { name: "Continue" }).click(); + + await page.getByRole("button", { name: "Incomplete", exact: true }).last().click(); + const menu = page.locator(".MuiPopover-paper").last(); + await expect(menu.getByRole("button", { name: "Pending", exact: true })).toBeEnabled(); + await menu.getByRole("button", { name: "Pending", exact: true }).click(); + await expect(page.getByRole("button", { name: "Pending", exact: true })).toBeVisible(); + + await page.getByRole("button", { name: "Create work order" }).click(); + await expect(page.getByRole("heading", { name: "Type & schedule" })).toBeHidden(); + await expect + .poll(() => createPayloads) + .toEqual([expect.objectContaining({ lifecycleStatus: 2 })]); + const createdRow = page.locator("#wo-row-900"); + await expect(createdRow.getByText("WO-900")).toBeVisible(); + await expect(createdRow.getByRole("button", { name: "Pending" })).toBeVisible(); + }); +}); From 52b682283c5c83cb74d5489dee24c8e0cee61a3b Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Tue, 29 Sep 2026 12:33:15 -0300 Subject: [PATCH 4/8] test: scope pending wizard selection assertion --- e2e/work-orders/pending-status-menu.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/e2e/work-orders/pending-status-menu.spec.ts b/e2e/work-orders/pending-status-menu.spec.ts index 90c1beca..8988c8b5 100644 --- a/e2e/work-orders/pending-status-menu.spec.ts +++ b/e2e/work-orders/pending-status-menu.spec.ts @@ -169,7 +169,9 @@ test.describe("existing work order status menu", () => { const menu = page.locator(".MuiPopover-paper").last(); await expect(menu.getByRole("button", { name: "Pending", exact: true })).toBeEnabled(); await menu.getByRole("button", { name: "Pending", exact: true }).click(); - await expect(page.getByRole("button", { name: "Pending", exact: true })).toBeVisible(); + await expect( + page.getByRole("dialog").getByRole("button", { name: "Pending", exact: true }), + ).toBeVisible(); await page.getByRole("button", { name: "Create work order" }).click(); await expect(page.getByRole("heading", { name: "Type & schedule" })).toBeHidden(); From a61ab1919a688fa2b2ef895bcd8cafaa41a90dc5 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Tue, 29 Sep 2026 12:43:38 -0300 Subject: [PATCH 5/8] test: find pending creation in unscheduled results --- e2e/work-orders/pending-status-menu.spec.ts | 29 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/e2e/work-orders/pending-status-menu.spec.ts b/e2e/work-orders/pending-status-menu.spec.ts index 8988c8b5..aeab3633 100644 --- a/e2e/work-orders/pending-status-menu.spec.ts +++ b/e2e/work-orders/pending-status-menu.spec.ts @@ -23,7 +23,12 @@ async function fulfillJson(route: Route, body: unknown) { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(body) }); } -async function mockApi(page: Page, role: Role, createPayloads: unknown[] = []) { +async function mockApi( + page: Page, + role: Role, + createPayloads: unknown[] = [], + searchUrls: string[] = [], +) { await page.clock.install({ time: new Date(FROZEN_NOW) }); await page.clock.resume(); await page.addInitScript( @@ -53,7 +58,13 @@ async function mockApi(page: Page, role: Role, createPayloads: unknown[] = []) { const { pathname } = new URL(request.url()); if (pathname.includes("/workorders/board/search")) { - await fulfillJson(route, { items: [], totalCount: 0, page: 0, pageSize: 50 }); + searchUrls.push(request.url()); + await fulfillJson(route, { + items: createdRow ? [createdRow] : [], + totalCount: createdRow ? 1 : 0, + page: 0, + pageSize: 50, + }); return; } if (pathname === "/api/workorders/board" && request.method() === "GET") { @@ -148,7 +159,8 @@ test.describe("existing work order status menu", () => { page, }) => { const createPayloads: unknown[] = []; - await mockApi(page, "Admin", createPayloads); + const searchUrls: string[] = []; + await mockApi(page, "Admin", createPayloads, searchUrls); await page.goto("/workorders"); await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({ timeout: 20_000, @@ -178,6 +190,17 @@ test.describe("existing work order status menu", () => { await expect .poll(() => createPayloads) .toEqual([expect.objectContaining({ lifecycleStatus: 2 })]); + + const filtersSheet = page.getByRole("dialog", { name: "Advanced Filters" }); + await page.getByRole("button", { name: "Advanced Filters" }).click(); + await expect(filtersSheet).toBeVisible(); + await filtersSheet.getByRole("button", { name: "Date range" }).click(); + await filtersSheet.getByRole("button", { name: "Unscheduled", exact: true }).click(); + await filtersSheet.getByRole("button", { name: "Apply filters" }).click(); + + await expect + .poll(() => searchUrls.some((url) => url.includes("unscheduledOnly=true"))) + .toBe(true); const createdRow = page.locator("#wo-row-900"); await expect(createdRow.getByText("WO-900")).toBeVisible(); await expect(createdRow.getByRole("button", { name: "Pending" })).toBeVisible(); From 4771f6f8eb7e3ac6349786994b85c6803d90eb9b Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Tue, 29 Sep 2026 12:52:02 -0300 Subject: [PATCH 6/8] test: cover pending schedule date promotion --- e2e/work-orders/pending-status-menu.spec.ts | 57 +++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/e2e/work-orders/pending-status-menu.spec.ts b/e2e/work-orders/pending-status-menu.spec.ts index aeab3633..3f2307be 100644 --- a/e2e/work-orders/pending-status-menu.spec.ts +++ b/e2e/work-orders/pending-status-menu.spec.ts @@ -78,13 +78,17 @@ async function mockApi( return; } if (pathname === "/api/workorders/board" && request.method() === "POST") { - createPayloads.push(request.postDataJSON()); + const payload = request.postDataJSON() as { + lifecycleStatus: number; + scheduledDate?: string; + }; + createPayloads.push(payload); createdRow = { ...BOARD_ROW, id: 900, woNumber: "WO-900", - lifecycleStatusLabel: "Pending", - scheduledDate: "", + lifecycleStatusLabel: payload.lifecycleStatus === 3 ? "Scheduled" : "Pending", + scheduledDate: payload.scheduledDate ?? "", rowVersion: "rv-created", }; await fulfillJson(route, { data: { id: 900, internalWONumber: "WO-900" } }); @@ -205,4 +209,51 @@ test.describe("existing work order status menu", () => { await expect(createdRow.getByText("WO-900")).toBeVisible(); await expect(createdRow.getByRole("button", { name: "Pending" })).toBeVisible(); }); + + test("Admin gets Scheduled when adding Schedule On after selecting Pending", async ({ page }) => { + const createPayloads: unknown[] = []; + await mockApi(page, "Admin", createPayloads); + await page.goto("/workorders"); + await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({ + timeout: 20_000, + }); + 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 page.getByRole("button", { name: "Continue" }).click(); + + await page.getByRole("button", { name: "Incomplete", exact: true }).last().click(); + const menu = page.locator(".MuiPopover-paper").last(); + await menu.getByRole("button", { name: "Pending", exact: true }).click(); + await expect( + page.getByRole("dialog").getByRole("button", { name: "Pending", exact: true }), + ).toBeVisible(); + + await page.getByRole("button", { name: "Back" }).click(); + await page.getByRole("button", { name: "Back" }).click(); + await page.getByRole("button", { name: "Pick a date" }).click(); + const dateTab = page.getByRole("button", { name: "Date", exact: true }); + if (await dateTab.isVisible()) { + await dateTab.click(); + } + await page.getByRole("button", { name: "20", exact: true }).click(); + await expect(page.getByRole("button", { name: /Aug 20/ })).toBeVisible(); + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); + + await expect( + page.getByRole("dialog").getByRole("button", { name: "Scheduled", exact: true }), + ).toBeVisible(); + await page.getByRole("button", { name: "Create work order" }).click(); + await expect(page.getByRole("heading", { name: "Type & schedule" })).toBeHidden(); + await expect + .poll(() => createPayloads) + .toEqual([expect.objectContaining({ lifecycleStatus: 3, scheduledDate: "2026-08-20" })]); + }); }); From 7d5d01560214c0eea31a777cf78491318abddab3 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Tue, 29 Sep 2026 13:04:51 -0300 Subject: [PATCH 7/8] fix: require undated drafts for Pending status --- e2e/work-orders/pending-status-menu.spec.ts | 15 +++++++++++--- .../utils/wo-lifecycle-transitions.ts | 5 +++++ .../status-cell-closability.test.tsx | 20 +++++++++++++++++++ ...zard-step-vendor-time-closability.test.tsx | 12 ++++++++--- .../utils/wo-lifecycle-transitions.test.ts | 20 +++++++++++++++++++ 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/e2e/work-orders/pending-status-menu.spec.ts b/e2e/work-orders/pending-status-menu.spec.ts index 3f2307be..43c86efa 100644 --- a/e2e/work-orders/pending-status-menu.spec.ts +++ b/e2e/work-orders/pending-status-menu.spec.ts @@ -247,9 +247,18 @@ test.describe("existing work order status menu", () => { await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click(); - await expect( - page.getByRole("dialog").getByRole("button", { name: "Scheduled", exact: true }), - ).toBeVisible(); + const wizardDialog = page.getByRole("dialog"); + const scheduledStatus = wizardDialog.getByRole("button", { + name: "Scheduled", + exact: true, + }); + await expect(scheduledStatus).toBeVisible(); + await scheduledStatus.click(); + const statusMenu = page.locator(".MuiPopover-paper").last(); + await expect(statusMenu.getByRole("button", { name: "Pending", exact: true })).toBeDisabled(); + await statusMenu.getByRole("button", { name: "Scheduled", exact: true }).click(); + await expect(scheduledStatus).toBeVisible(); + await page.getByRole("button", { name: "Create work order" }).click(); await expect(page.getByRole("heading", { name: "Type & schedule" })).toBeHidden(); await expect diff --git a/src/domain/work-orders/utils/wo-lifecycle-transitions.ts b/src/domain/work-orders/utils/wo-lifecycle-transitions.ts index e0351242..bbef896a 100644 --- a/src/domain/work-orders/utils/wo-lifecycle-transitions.ts +++ b/src/domain/work-orders/utils/wo-lifecycle-transitions.ts @@ -38,6 +38,8 @@ export const CREATION_INLINE_STATUSES: readonly WizardWOStatus[] = [ export const SCHEDULED_NEEDS_DATE_MSG = "Set a Schedule On date before marking this work order as Scheduled."; +export const PENDING_NEEDS_NO_DATE_MSG = + "Clear the Schedule On date before marking this work order as Pending."; export const PAST_DUE_BADGE_TOOLTIP = "This WO missed its service window. Reschedule to unlock status updates."; @@ -132,6 +134,9 @@ export function getStatusOptionState( status: WizardWOStatus, closabilityGaps: string[], ): StatusOptionState { + if (status === "Pending" && hasConcreteScheduleDate(row)) { + return { disabled: true, title: PENDING_NEEDS_NO_DATE_MSG }; + } if (status === "Scheduled" && !hasConcreteScheduleDate(row)) { return { disabled: true, title: SCHEDULED_NEEDS_DATE_MSG }; } diff --git a/src/test/app/(protected)/workorders/status-cell-closability.test.tsx b/src/test/app/(protected)/workorders/status-cell-closability.test.tsx index 77922f0d..be9a17a6 100644 --- a/src/test/app/(protected)/workorders/status-cell-closability.test.tsx +++ b/src/test/app/(protected)/workorders/status-cell-closability.test.tsx @@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vitest"; import { StatusCell } from "@/app/(protected)/workorders/_components/list/table/cells/status-cell"; import type { WizardWOStatus } from "@/domain/work-orders/types/work-order-wizard"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import { CREATION_INLINE_STATUSES } from "@/domain/work-orders/utils/wo-lifecycle-transitions"; const incompleteRow = { id: "wo-1", @@ -111,4 +112,23 @@ describe("StatusCell closability gate", () => { expect(screen.getByRole("button", { name: /^pending$/i })).toBeTruthy(); expect(screen.queryByRole("button", { name: /^en route$/i })).toBeNull(); }); + + it("disables Pending for inline creation when Schedule On has a concrete date", () => { + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: /scheduled/i })); + const pending = screen.getByRole("button", { name: /^pending$/i }); + expect(pending).toBeDisabled(); + expect(pending).toHaveAttribute( + "title", + "Clear the Schedule On date before marking this work order as Pending.", + ); + }); }); diff --git a/src/test/app/(protected)/workorders/wizard-step-vendor-time-closability.test.tsx b/src/test/app/(protected)/workorders/wizard-step-vendor-time-closability.test.tsx index 9280a735..265597e0 100644 --- a/src/test/app/(protected)/workorders/wizard-step-vendor-time-closability.test.tsx +++ b/src/test/app/(protected)/workorders/wizard-step-vendor-time-closability.test.tsx @@ -77,7 +77,7 @@ describe("WizardStepVendorTime creation statuses (AAP)", () => { expect(onPatch).toHaveBeenCalledWith({ status: "Pending" }); }); - it("promotes Pending to Scheduled when picked on a dated draft", () => { + it("disables Pending when Schedule On has a concrete date", () => { const onPatch = vi.fn(); render( { ); fireEvent.click(screen.getByRole("button", { name: /scheduled/i })); - fireEvent.click(screen.getByRole("button", { name: /^pending$/i })); - expect(onPatch).toHaveBeenCalledWith({ status: "Scheduled" }); + const pending = screen.getByRole("button", { name: /^pending$/i }); + expect(pending).toBeDisabled(); + expect(pending).toHaveAttribute( + "title", + "Clear the Schedule On date before marking this work order as Pending.", + ); + fireEvent.click(pending); + expect(onPatch).not.toHaveBeenCalled(); }); }); diff --git a/src/test/domain/work-orders/utils/wo-lifecycle-transitions.test.ts b/src/test/domain/work-orders/utils/wo-lifecycle-transitions.test.ts index b22e4357..5d0b4d77 100644 --- a/src/test/domain/work-orders/utils/wo-lifecycle-transitions.test.ts +++ b/src/test/domain/work-orders/utils/wo-lifecycle-transitions.test.ts @@ -6,6 +6,7 @@ import { COMPLETED_DOC_BLOCK_MSG, COMPLETED_MEDIA_BLOCK_MSG, CREATION_INLINE_STATUSES, + PENDING_NEEDS_NO_DATE_MSG, SCHEDULED_NEEDS_DATE_MSG, formatAapCompletedBlockTitle, getInlineStatusTransitions, @@ -65,6 +66,25 @@ describe("wo-lifecycle-transitions", () => { }); }); + it("disables Pending only when a concrete schedule date exists", () => { + expect(getStatusOptionState({ scheduledOn: "2026-06-01" }, "Pending", [])).toEqual({ + disabled: true, + title: PENDING_NEEDS_NO_DATE_MSG, + }); + expect( + getStatusOptionState({ scheduledOn: "", scheduleWeekOnly: false }, "Pending", []), + ).toEqual({ + disabled: false, + }); + expect( + getStatusOptionState( + { scheduledOn: "2026-06-01", scheduleWeekOnly: true, schedMode: "week" }, + "Pending", + [], + ), + ).toEqual({ disabled: false }); + }); + it("promotes Incomplete/Pending when a concrete date exists", () => { expect( maybeAutoSchedulePatch({ From 5ace29eda4d4632c359c063c54e68d7a9553461f Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Tue, 29 Sep 2026 13:12:11 -0300 Subject: [PATCH 8/8] test: expect Pending disabled on dated inline draft --- .../workorders/wo-table-inline-status-schedule.test.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/app/(protected)/workorders/wo-table-inline-status-schedule.test.tsx b/src/test/app/(protected)/workorders/wo-table-inline-status-schedule.test.tsx index 38e0014e..cb54296e 100644 --- a/src/test/app/(protected)/workorders/wo-table-inline-status-schedule.test.tsx +++ b/src/test/app/(protected)/workorders/wo-table-inline-status-schedule.test.tsx @@ -36,7 +36,7 @@ vi.mock("@/app/(protected)/workorders/_components/list/table/cells/appt-time-cel })); describe("WoTableInlineRow StatusCell with a concrete schedule", () => { - it("keeps Scheduled when Incomplete is picked on a dated draft", () => { + it("disables Pending on a dated draft", () => { const onChange = vi.fn(); const draft = { ...EMPTY_TABLE_DRAFT, @@ -66,8 +66,10 @@ describe("WoTableInlineRow StatusCell with a concrete schedule", () => { ); fireEvent.click(screen.getByRole("button", { name: /incomplete/i })); - fireEvent.click(screen.getByRole("button", { name: /^pending$/i })); + const pending = screen.getByRole("button", { name: /^pending$/i }); - expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ status: "Scheduled" })); + expect(pending).toBeDisabled(); + fireEvent.click(pending); + expect(onChange).not.toHaveBeenCalledWith(expect.objectContaining({ status: "Pending" })); }); });