diff --git a/src/domain/work-orders/utils/board-audit-action-rules.ts b/src/domain/work-orders/utils/board-audit-action-rules.ts index bae79b4b..c5c6a12a 100644 --- a/src/domain/work-orders/utils/board-audit-action-rules.ts +++ b/src/domain/work-orders/utils/board-audit-action-rules.ts @@ -26,11 +26,19 @@ export type AuditActionRuleContext = { export type AuditActionRule = (ctx: AuditActionRuleContext) => string | null; -function tryStatusRule({ fieldKey, actionKey, newValue }: AuditActionRuleContext): string | null { +function tryStatusRule({ + fieldKey, + actionKey, + oldValue, + newValue, +}: AuditActionRuleContext): string | null { const matches = fieldKey.includes("status") || fieldKey === "lifecyclestatus" || actionKey.includes("status"); if (!matches || !newValue.text) return null; + if (oldValue.text) { + return `Changed status from ${wrapAuditValue(oldValue)} to ${wrapAuditValue(newValue)}`; + } return `Changed status to ${wrapAuditValue(newValue)}`; } diff --git a/src/domain/work-orders/utils/wo-lifecycle-transitions.ts b/src/domain/work-orders/utils/wo-lifecycle-transitions.ts index 2a2ba7f5..264344eb 100644 --- a/src/domain/work-orders/utils/wo-lifecycle-transitions.ts +++ b/src/domain/work-orders/utils/wo-lifecycle-transitions.ts @@ -143,8 +143,9 @@ export function maybeAutoSchedulePatch( return {}; } -function patchTouchesSchedule(patch: WorkOrderTablePatch): boolean { +function patchTouchesScheduleOrStatus(patch: WorkOrderTablePatch): boolean { return ( + patch.status !== undefined || patch.scheduledOn !== undefined || patch.scheduleWeekOnly !== undefined || patch.targetWeek !== undefined @@ -155,7 +156,7 @@ export function mergeAutoSchedulePatch( row: WorkOrderTableRow | undefined, patch: WorkOrderTablePatch, ): WorkOrderTablePatch { - if (!row || !patchTouchesSchedule(patch)) { + if (!row || !patchTouchesScheduleOrStatus(patch)) { return patch; } const merged = { @@ -181,13 +182,10 @@ export function withAutoScheduleStatus { expect(payload).not.toHaveProperty("assignTo"); }); + it("promotes Pending to Scheduled when create has a concrete date", () => { + const draft = { + ...EMPTY_TABLE_DRAFT, + site: "BK5", + locationId: "9", + woNumber: "25006", + pocName: "Jane", + pocPhone: "555-0100", + status: "Pending" as const, + scheduledOn: "2026-06-01", + scheduleWeekOnly: false, + }; + + const input = buildInlineDraftCreateInput(draft, [], [{ id: 9, name: "BK5 · Dallas, TX" }]); + expect(input.status).toBe("Scheduled"); + }); + it("demotes Scheduled to Pending when create has no concrete date", () => { const draft = { ...EMPTY_TABLE_DRAFT, diff --git a/src/test/app/(protected)/workorders/wizard-schedule-date-range.test.tsx b/src/test/app/(protected)/workorders/wizard-schedule-date-range.test.tsx index a79ac2e6..11c2d05d 100644 --- a/src/test/app/(protected)/workorders/wizard-schedule-date-range.test.tsx +++ b/src/test/app/(protected)/workorders/wizard-schedule-date-range.test.tsx @@ -52,6 +52,7 @@ describe("WizardStepTypeSchedule — Schedule On range selection", () => { scheduleWeekOnly: false, targetWeek: "", isAddOnOverride: false, + status: "Scheduled", }); }); @@ -86,6 +87,7 @@ describe("WizardStepTypeSchedule — Schedule On range selection", () => { scheduleWeekOnly: false, targetWeek: "", isAddOnOverride: false, + status: "Scheduled", }); }); 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 6210a3b2..9280a735 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("keeps Pending when picked on a dated draft", () => { + it("promotes Pending to Scheduled when picked on a dated draft", () => { 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: "Pending" }); + expect(onPatch).toHaveBeenCalledWith({ status: "Scheduled" }); }); }); diff --git a/src/test/domain/work-orders/utils/format-board-audit-action.test.ts b/src/test/domain/work-orders/utils/format-board-audit-action.test.ts index 77cbb059..3ba0ad1b 100644 --- a/src/test/domain/work-orders/utils/format-board-audit-action.test.ts +++ b/src/test/domain/work-orders/utils/format-board-audit-action.test.ts @@ -21,19 +21,28 @@ describe("formatBoardAuditAction", () => { oldValue: "Scheduled", newValue: "Incomplete", }), - ).toBe('Changed status to "Incomplete"'); + ).toBe('Changed status from "Scheduled" to "Incomplete"'); }); - it("does not include the previous status in the audit sentence", () => { + it("includes the previous status in the audit sentence when present", () => { const sentence = formatBoardAuditAction({ action: "StatusChanged", fieldName: "LifecycleStatus", oldValue: "Pending", newValue: "En Route", }); - expect(sentence).toBe('Changed status to "En Route"'); - expect(sentence).not.toMatch(/from/i); - expect(sentence).not.toContain("Pending"); + expect(sentence).toBe('Changed status from "Pending" to "En Route"'); + }); + + it("omits from when the previous status is empty", () => { + expect( + formatBoardAuditAction({ + action: "StatusChanged", + fieldName: "LifecycleStatus", + oldValue: "", + newValue: "Scheduled", + }), + ).toBe('Changed status to "Scheduled"'); }); it("formats WeekRolled / CarriedOver with increment", () => { 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 4a31e4e7..5e99b7a1 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 @@ -11,6 +11,7 @@ import { hasConcreteScheduleDate, maybeAutoSchedulePatch, mergeAutoSchedulePatch, + resolveCreationStatus, withAutoScheduleStatus, } from "@/domain/work-orders/utils/wo-lifecycle-transitions"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; @@ -102,17 +103,20 @@ describe("wo-lifecycle-transitions", () => { status: "Scheduled", }); expect(mergeAutoSchedulePatch(row, { vendorId: "9" })).toEqual({ vendorId: "9" }); + expect( + mergeAutoSchedulePatch({ ...row, scheduledOn: "2026-06-01" }, { vendorId: "9" }), + ).toEqual({ vendorId: "9" }); }); - it("keeps an explicit wizard status pick on a dated draft", () => { + it("promotes a pre-schedule wizard pick to Scheduled on a dated draft", () => { const draft = { status: "Scheduled", scheduledOn: "2026-06-01", scheduleWeekOnly: false, }; - expect(withAutoScheduleStatus(draft, { status: "Pending" })).toEqual({ status: "Pending" }); + expect(withAutoScheduleStatus(draft, { status: "Pending" })).toEqual({ status: "Scheduled" }); expect(withAutoScheduleStatus(draft, { status: "Incomplete" })).toEqual({ - status: "Incomplete", + status: "Scheduled", }); }); @@ -131,15 +135,15 @@ describe("wo-lifecycle-transitions", () => { }); }); - it("keeps explicit status picks on dated rows", () => { + it("promotes pre-schedule picks on dated rows", () => { const row = { status: "Scheduled", scheduledOn: "2026-06-01", scheduleWeekOnly: false, } as WorkOrderTableRow; - expect(mergeAutoSchedulePatch(row, { status: "Pending" })).toEqual({ status: "Pending" }); + expect(mergeAutoSchedulePatch(row, { status: "Pending" })).toEqual({ status: "Scheduled" }); expect(mergeAutoSchedulePatch(row, { status: "Incomplete" })).toEqual({ - status: "Incomplete", + status: "Scheduled", }); }); @@ -156,6 +160,26 @@ describe("wo-lifecycle-transitions", () => { }); }); + it("promotes a Pending create pick to Scheduled when the draft is dated", () => { + expect( + resolveCreationStatus({ + status: "Pending", + scheduledOn: "2026-06-01", + scheduleWeekOnly: false, + }), + ).toBe("Scheduled"); + }); + + it("demotes Scheduled without a concrete date at create", () => { + expect( + resolveCreationStatus({ + status: "Scheduled", + scheduledOn: "", + scheduleWeekOnly: false, + }), + ).toBe("Pending"); + }); + it("uses AAP completed tooltip hierarchy", () => { expect(formatAapCompletedBlockTitle(["Dispatcher", "Company"])).toBe( "Fill in Assigned To, Vendor before completing this work order.",