fix(work-orders): promote dated pre-schedule picks to Scheduled

Align create, wizard, and board status patches with AAP maybeAutoSchedule.
Include the previous status in audit copy when the API sends it.
This commit is contained in:
Arthur Bassi 2026-09-04 14:31:08 -03:00
parent 3a8f41a444
commit 3ed8600beb
7 changed files with 78 additions and 20 deletions

View file

@ -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)}`;
}

View file

@ -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<T extends ScheduleDateInput & { status?:
? schedulePatch.scheduleWeekOnly
: current.scheduleWeekOnly,
};
if (!patchTouchesSchedule(schedulePatch as WorkOrderTablePatch)) {
return { ...schedulePatch };
}
return { ...schedulePatch, ...maybeAutoSchedulePatch(next) };
}
/** Normalize create-time status after schedule auto-derive rules. */
/** AAP create: apply maybeAutoSchedule to the draft before POST. */
export function resolveCreationStatus(row: ScheduleDateInput & { status: string }): WizardWOStatus {
const autoStatus = maybeAutoSchedulePatch(row).status;
if (autoStatus === "Scheduled" || autoStatus === "Pending") {

View file

@ -93,6 +93,23 @@ describe("buildInlineDraftCreateInput", () => {
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,

View file

@ -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",
});
});

View file

@ -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(
<WizardStepVendorTime
@ -96,6 +96,6 @@ describe("WizardStepVendorTime creation statuses (AAP)", () => {
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" });
});
});

View file

@ -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", () => {

View file

@ -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.",