From cbb2edf2a60c9d363934b134c32151423fc9a883 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 1 Oct 2026 16:10:46 -0300 Subject: [PATCH 1/2] fix(SH-357): disable Incomplete without a schedule date --- .../list/table/build-inline-draft-create-input.ts | 2 +- .../list/table/wo-table-inline-row-cells.tsx | 2 +- .../_components/wizard/use-new-wo-wizard-draft.ts | 3 +-- .../_components/wizard/use-new-wo-wizard-state.ts | 3 +-- .../_components/wizard/wizard-sidebar-summary-rows.ts | 2 +- .../_components/wizard/wizard-sidebar-summary.tsx | 4 ++-- .../_components/wizard/wizard-step-vendor-time.tsx | 4 ++-- .../_hooks/use-work-orders-table-ui-state.ts | 2 +- .../work-orders/mappers/work-order-board-mapper.ts | 9 +++------ src/domain/work-orders/types/work-order-table-row.ts | 2 +- src/domain/work-orders/types/work-order-wizard.ts | 2 +- .../work-orders/utils/wo-lifecycle-transitions.ts | 10 +++++++++- .../mappers/work-order-board-mapper.test.ts | 6 +++--- 13 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts b/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts index bebd1255..1724dc1e 100644 --- a/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts +++ b/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts @@ -19,7 +19,7 @@ function resolveInlineDraftSchedule(draft: WorkOrderTableDraft) { return { scheduleWeekOnly, status: resolveCreationStatus({ - status: draft.status || "Incomplete", + status: draft.status || "Pending", scheduledOn: draft.scheduledOn, scheduleWeekOnly, }), diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row-cells.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row-cells.tsx index 319333ac..a5a89611 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row-cells.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row-cells.tsx @@ -230,7 +230,7 @@ export function WoTableInlineRowCells({ dispatcherId={draft.dispatcherId} users={users} onSave={(id) => { - onPatch({ dispatcherId: id, status: draft.status || "Incomplete" }); + onPatch({ dispatcherId: id, status: draft.status || "Pending" }); }} /> diff --git a/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-draft.ts b/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-draft.ts index c0d45525..add130ff 100644 --- a/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-draft.ts +++ b/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-draft.ts @@ -9,7 +9,6 @@ export function useNewWoWizardDraft(open: boolean) { const [step, setStep] = useState(1); const [draft, setDraft] = useState({ ...EMPTY_WIZARD_DRAFT, - status: "Incomplete", }); const [svcOpen, setSvcOpen] = useState(false); const [svcSearch, setSvcSearch] = useState(""); @@ -18,7 +17,7 @@ export function useNewWoWizardDraft(open: boolean) { useEffect(() => { if (open) { setStep(1); - setDraft({ ...EMPTY_WIZARD_DRAFT, status: "Incomplete" }); + setDraft({ ...EMPTY_WIZARD_DRAFT }); setSvcOpen(false); setSvcSearch(""); setShowErrors(false); diff --git a/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-state.ts b/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-state.ts index 8f6b1e66..79c81e59 100644 --- a/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-state.ts +++ b/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-state.ts @@ -40,7 +40,6 @@ export function useNewWoWizardState({ const [showErrors, setShowErrors] = useState(false); const [draft, setDraft] = useState({ ...EMPTY_WIZARD_DRAFT, - status: "Incomplete", }); const [pocAutoFilledFor, setPocAutoFilledFor] = useState(""); @@ -83,7 +82,7 @@ export function useNewWoWizardState({ if (open) { setStep(1); setShowErrors(false); - setDraft({ ...EMPTY_WIZARD_DRAFT, status: "Incomplete" }); + setDraft({ ...EMPTY_WIZARD_DRAFT }); setPocAutoFilledFor(""); setDuplicateRow(null); } diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary-rows.ts b/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary-rows.ts index e49e267a..4db322aa 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary-rows.ts +++ b/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary-rows.ts @@ -53,6 +53,6 @@ export function buildWizardSidebarSummaryRows(draft: WorkOrderWizardDraft): [str ["Technician", draft.vendorName || "—"], ["Company", draft.vendorName || "—"], ["Appt time", draft.apptStart ? formatApptRange(draft.apptStart, draft.apptEnd) : "—"], - ["Status", draft.status || "Incomplete"], + ["Status", draft.status || "Pending"], ]; } diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary.tsx index e330f575..63529f73 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary.tsx @@ -8,9 +8,9 @@ import { ColorDot } from "@/app/(protected)/workorders/_components/wizard/wizard export function WizardSidebarSummary({ draft }: { draft: WorkOrderWizardDraft }) { const summary = buildWizardSidebarSummaryRows(draft); - const statusLabel = draft.status || "Incomplete"; + const statusLabel = draft.status || "Pending"; const statusValue = statusLabel as WizardWOStatus; - const statusDotColor = WIZARD_STATUS_DOTS[statusValue] ?? WIZARD_STATUS_DOTS.Incomplete; + const statusDotColor = WIZARD_STATUS_DOTS[statusValue] ?? WIZARD_STATUS_DOTS.Pending; return (
diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-step-vendor-time.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-step-vendor-time.tsx index 208bb513..5f386d20 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-step-vendor-time.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-step-vendor-time.tsx @@ -194,8 +194,8 @@ export function WizardStepVendorTime({ draft.technicianAssigned === false ? undefined : vendors.find((v) => String(v.id) === draft.vendorId); - const statusValue = (draft.status || "Incomplete") as WizardWOStatus; - const statusDotColor = WIZARD_STATUS_DOTS[statusValue] ?? WIZARD_STATUS_DOTS.Incomplete; + const statusValue = (draft.status || "Pending") as WizardWOStatus; + const statusDotColor = WIZARD_STATUS_DOTS[statusValue] ?? WIZARD_STATUS_DOTS.Pending; const selectedDistance = formatVendorDistance(selectedVendor?.distanceMiles); const closabilityGaps = getClosabilityGaps(wizardDraftToClosabilityInput(draft)); diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-table-ui-state.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-table-ui-state.ts index 196bfe69..da2bf21b 100644 --- a/src/app/(protected)/workorders/_hooks/use-work-orders-table-ui-state.ts +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-table-ui-state.ts @@ -60,7 +60,7 @@ export function useWorkOrdersTableUiState(weekMonday: string) { setDraft({ ...EMPTY_TABLE_DRAFT, scheduledOn, - status: scheduledOn ? "Scheduled" : "Incomplete", + status: scheduledOn ? "Scheduled" : "Pending", }); }, [weekMonday], diff --git a/src/domain/work-orders/mappers/work-order-board-mapper.ts b/src/domain/work-orders/mappers/work-order-board-mapper.ts index f1f046f9..4e837c84 100644 --- a/src/domain/work-orders/mappers/work-order-board-mapper.ts +++ b/src/domain/work-orders/mappers/work-order-board-mapper.ts @@ -8,11 +8,7 @@ import { WORK_ORDER_TYPE_API, } from "@/domain/work-orders/types/work-order-board"; -import type { - WizardWOStatus, - WOType, - WorkOrderWizardDraft, -} from "@/domain/work-orders/types/work-order-wizard"; +import type { WOType, WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard"; import { fmtDateRange } from "@/domain/work-orders/utils/wizard-date-utils"; @@ -20,6 +16,7 @@ import { assignWizardScheduleFields } from "@/domain/work-orders/mappers/assign- import { assignWizardVendorApptFields } from "@/domain/work-orders/mappers/assign-wizard-vendor-appt-fields"; import { assignWizardServicePocFields } from "@/domain/work-orders/mappers/assign-wizard-service-poc-fields"; import { resolveIsAddOnForCreate } from "@/domain/work-orders/utils/wo-addon-cutoff"; +import { resolveCreationStatus } from "@/domain/work-orders/utils/wo-lifecycle-transitions"; const WO_TYPE_TO_API: Record = { PM: WORK_ORDER_TYPE_API.PM, @@ -114,7 +111,7 @@ export function mapWizardDraftToBoardCreatePayload( }; if (draft.status) { - payload.lifecycleStatus = LIFECYCLE_STATUS_API[draft.status as WizardWOStatus]; + payload.lifecycleStatus = LIFECYCLE_STATUS_API[resolveCreationStatus(draft)]; } if (draft.woNumber.trim()) { diff --git a/src/domain/work-orders/types/work-order-table-row.ts b/src/domain/work-orders/types/work-order-table-row.ts index 9b973ebf..2e2c4464 100644 --- a/src/domain/work-orders/types/work-order-table-row.ts +++ b/src/domain/work-orders/types/work-order-table-row.ts @@ -175,7 +175,7 @@ export const EMPTY_TABLE_DRAFT: WorkOrderTableDraft = { techPhone: "", apptStart: "", apptEnd: "", - status: "Incomplete", + status: "Pending", pocName: "", pocPhone: "", pocNotes: "", diff --git a/src/domain/work-orders/types/work-order-wizard.ts b/src/domain/work-orders/types/work-order-wizard.ts index 3a3416e1..ae2b3ff3 100644 --- a/src/domain/work-orders/types/work-order-wizard.ts +++ b/src/domain/work-orders/types/work-order-wizard.ts @@ -109,7 +109,7 @@ export const EMPTY_WIZARD_DRAFT: WorkOrderWizardDraft = { vendorNotes: "", apptStart: "", apptEnd: "", - status: "Incomplete", + status: "Pending", pocName: "", pocPhone: "", pocNotes: "", diff --git a/src/domain/work-orders/utils/wo-lifecycle-transitions.ts b/src/domain/work-orders/utils/wo-lifecycle-transitions.ts index bbef896a..0a5a0064 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 INCOMPLETE_NEEDS_DATE_MSG = + "Set a Schedule On date before marking this work order as Incomplete."; export const PENDING_NEEDS_NO_DATE_MSG = "Clear the Schedule On date before marking this work order as Pending."; @@ -134,6 +136,9 @@ export function getStatusOptionState( status: WizardWOStatus, closabilityGaps: string[], ): StatusOptionState { + if (status === "Incomplete" && !hasConcreteScheduleDate(row)) { + return { disabled: true, title: INCOMPLETE_NEEDS_DATE_MSG }; + } if (status === "Pending" && hasConcreteScheduleDate(row)) { return { disabled: true, title: PENDING_NEEDS_NO_DATE_MSG }; } @@ -211,5 +216,8 @@ export function resolveCreationStatus(row: ScheduleDateInput & { status: string if (autoStatus === "Scheduled" || autoStatus === "Pending") { return autoStatus; } - return (row.status || "Incomplete") as WizardWOStatus; + if (!row.status || (row.status === "Incomplete" && !hasConcreteScheduleDate(row))) { + return "Pending"; + } + return row.status as WizardWOStatus; } 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 cb5dca07..a9d41ed6 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 @@ -61,7 +61,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => { isAddOn: false, - lifecycleStatus: 1, + lifecycleStatus: 2, }); }); @@ -283,7 +283,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => { expect(payload.scheduledDate).toBeUndefined(); }); - it("keeps Incomplete on week-only create and omits scheduledDate", () => { + it("normalizes Incomplete to Pending on week-only create and omits scheduledDate", () => { const payload = mapWizardDraftToBoardCreatePayload({ ...EMPTY_WIZARD_DRAFT, type: "PM", @@ -293,7 +293,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => { targetWeek: "2026-06-22", }); - expect(payload.lifecycleStatus).toBe(LIFECYCLE_STATUS_API.Incomplete); + expect(payload.lifecycleStatus).toBe(LIFECYCLE_STATUS_API.Pending); expect(payload.scheduleWeekOnly).toBe(true); expect(payload.targetWeek).toBe("2026-06-22"); expect(payload.scheduledDate).toBeUndefined(); From d1b53105a2fd9b3feb65400543d1b001a2a32c9b Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 1 Oct 2026 16:27:24 -0300 Subject: [PATCH 2/2] fix(SH-357): reconcile Incomplete after clearing Schedule On --- e2e/work-orders/pending-status-menu.spec.ts | 6 ++- .../utils/wo-lifecycle-transitions.ts | 2 +- .../work-orders/api/work-orders-api.test.ts | 2 +- .../mappers/work-order-board-mapper.test.ts | 1 + .../utils/wo-lifecycle-transitions.test.ts | 39 ++++++++++++++----- 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/e2e/work-orders/pending-status-menu.spec.ts b/e2e/work-orders/pending-status-menu.spec.ts index 43c86efa..c5b4d7f7 100644 --- a/e2e/work-orders/pending-status-menu.spec.ts +++ b/e2e/work-orders/pending-status-menu.spec.ts @@ -181,8 +181,9 @@ test.describe("existing work order status menu", () => { .click(); await page.getByRole("button", { name: "Continue" }).click(); - await page.getByRole("button", { name: "Incomplete", exact: true }).last().click(); + await page.getByRole("button", { name: "Pending", exact: true }).last().click(); const menu = page.locator(".MuiPopover-paper").last(); + await expect(menu.getByRole("button", { name: "Incomplete", exact: true })).toBeDisabled(); await expect(menu.getByRole("button", { name: "Pending", exact: true })).toBeEnabled(); await menu.getByRole("button", { name: "Pending", exact: true }).click(); await expect( @@ -228,8 +229,9 @@ test.describe("existing work order status menu", () => { .click(); await page.getByRole("button", { name: "Continue" }).click(); - await page.getByRole("button", { name: "Incomplete", exact: true }).last().click(); + await page.getByRole("button", { name: "Pending", exact: true }).last().click(); const menu = page.locator(".MuiPopover-paper").last(); + await expect(menu.getByRole("button", { name: "Incomplete", exact: true })).toBeDisabled(); await menu.getByRole("button", { name: "Pending", exact: true }).click(); await expect( page.getByRole("dialog").getByRole("button", { name: "Pending", exact: true }), diff --git a/src/domain/work-orders/utils/wo-lifecycle-transitions.ts b/src/domain/work-orders/utils/wo-lifecycle-transitions.ts index 0a5a0064..88a15ac6 100644 --- a/src/domain/work-orders/utils/wo-lifecycle-transitions.ts +++ b/src/domain/work-orders/utils/wo-lifecycle-transitions.ts @@ -162,7 +162,7 @@ export function maybeAutoSchedulePatch( if (hasDate && isPreScheduleStatus(row.status ?? "")) { return { status: "Scheduled" }; } - if (!hasDate && row.status === "Scheduled") { + if (!hasDate && (row.status === "Scheduled" || row.status === "Incomplete")) { return { status: "Pending" }; } return {}; diff --git a/src/test/domain/work-orders/api/work-orders-api.test.ts b/src/test/domain/work-orders/api/work-orders-api.test.ts index d6766be8..e8b97d15 100644 --- a/src/test/domain/work-orders/api/work-orders-api.test.ts +++ b/src/test/domain/work-orders/api/work-orders-api.test.ts @@ -134,7 +134,7 @@ describe("workOrdersApi.createFromBoard", () => { expect(apiPost).toHaveBeenCalledWith(API_PATHS.workOrder.boardCreate, { workOrderType: 2, siteCode: "BK5", - lifecycleStatus: 1, + lifecycleStatus: 2, woNumber: "12345", isAddOn: false, locationId: 9, 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 a9d41ed6..2ed6ba7a 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 @@ -162,6 +162,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => { type: "PM", siteCode: "BK5", status: "Scheduled", + scheduledOn: "2026-06-01", }); expect(payload.lifecycleStatus).toBe(3); 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 5d0b4d77..0bed9384 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, + INCOMPLETE_NEEDS_DATE_MSG, PENDING_NEEDS_NO_DATE_MSG, SCHEDULED_NEEDS_DATE_MSG, formatAapCompletedBlockTitle, @@ -59,7 +60,17 @@ describe("wo-lifecycle-transitions", () => { expect(hasConcreteScheduleDate({ scheduledOn: "2026-06-01" })).toBe(true); }); - it("disables Scheduled without a concrete date", () => { + it("disables Incomplete and Scheduled without a concrete date", () => { + expect(getStatusOptionState({ scheduledOn: "" }, "Incomplete", [])).toEqual({ + disabled: true, + title: INCOMPLETE_NEEDS_DATE_MSG, + }); + expect( + getStatusOptionState({ scheduledOn: "2026-06-01", scheduleWeekOnly: true }, "Incomplete", []), + ).toEqual({ disabled: true, title: INCOMPLETE_NEEDS_DATE_MSG }); + expect(getStatusOptionState({ scheduledOn: "2026-06-01" }, "Incomplete", [])).toEqual({ + disabled: false, + }); expect(getStatusOptionState({ scheduledOn: "" }, "Scheduled", [])).toEqual({ disabled: true, title: SCHEDULED_NEEDS_DATE_MSG, @@ -102,14 +113,15 @@ describe("wo-lifecycle-transitions", () => { ).toEqual({ status: "Scheduled" }); }); - it("reverts Scheduled without a concrete date to Pending", () => { - expect( - maybeAutoSchedulePatch({ - status: "Scheduled", - scheduledOn: "", - scheduleWeekOnly: false, - }), - ).toEqual({ status: "Pending" }); + it("reverts Scheduled or Incomplete without a concrete date to Pending", () => { + for (const status of ["Scheduled", "Incomplete"]) { + expect(maybeAutoSchedulePatch({ status, scheduledOn: "", scheduleWeekOnly: false })).toEqual({ + status: "Pending", + }); + expect( + maybeAutoSchedulePatch({ status, scheduledOn: "2026-06-01", scheduleWeekOnly: true }), + ).toEqual({ status: "Pending" }); + } }); it("does not auto-change In Progress when a date exists", () => { @@ -180,7 +192,7 @@ describe("wo-lifecycle-transitions", () => { }); }); - it("still demotes Scheduled when the schedule is cleared", () => { + it("demotes Scheduled or Incomplete when the schedule is cleared", () => { const row = { status: "Scheduled", scheduledOn: "2026-06-01", @@ -191,6 +203,13 @@ describe("wo-lifecycle-transitions", () => { scheduleWeekOnly: false, status: "Pending", }); + expect(withAutoScheduleStatus({ ...row, status: "Incomplete" }, { scheduledOn: "" })).toEqual({ + scheduledOn: "", + status: "Pending", + }); + expect( + withAutoScheduleStatus({ ...row, status: "Incomplete" }, { scheduleWeekOnly: true }), + ).toEqual({ scheduleWeekOnly: true, status: "Pending" }); }); it("promotes a Pending create pick to Scheduled when the draft is dated", () => {