diff --git a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx
index 7bfb16a3..c5955f15 100644
--- a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx
+++ b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx
@@ -18,7 +18,6 @@ import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-ta
import type { WizardWOStatus } from "@/domain/work-orders/types/work-order-wizard";
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
-import { withAutoScheduleStatus } from "@/domain/work-orders/utils/wo-lifecycle-transitions";
import { DocBadge } from "./slide-over-doc-badge";
type SlideOverInfoTabEditViewProps = {
@@ -52,58 +51,50 @@ function SlideOverScheduleOnField({
scheduleEnd={draft.scheduleEnd}
schedMode={draft.schedMode}
onSave={(scheduledOn) =>
- onDraftChange(
- withAutoScheduleStatus(draft, {
- scheduledOn,
- scheduleWeekOnly: false,
- targetWeek: scheduledOn ? "" : draft.targetWeek,
- scheduleEnd: "",
- schedMode: "date",
- isPastDue: false,
- ...deriveDayFields(scheduledOn),
- }),
- )
+ onDraftChange({
+ scheduledOn,
+ scheduleWeekOnly: false,
+ targetWeek: scheduledOn ? "" : draft.targetWeek,
+ scheduleEnd: "",
+ schedMode: "date",
+ isPastDue: false,
+ ...deriveDayFields(scheduledOn),
+ })
}
onSaveWeek={(weekStart) =>
- onDraftChange(
- withAutoScheduleStatus(draft, {
- scheduledOn: "",
- targetWeek: weekStart,
- scheduleWeekOnly: true,
- scheduleEnd: "",
- schedMode: "week",
- isPastDue: false,
- ...deriveDayFields(""),
- }),
- )
+ onDraftChange({
+ scheduledOn: "",
+ targetWeek: weekStart,
+ scheduleWeekOnly: true,
+ scheduleEnd: "",
+ schedMode: "week",
+ isPastDue: false,
+ ...deriveDayFields(""),
+ })
}
onSaveRange={(start, end) =>
- onDraftChange(
- withAutoScheduleStatus(draft, {
- scheduledOn: start,
- scheduleEnd: end,
- schedMode: "range",
- scheduleWeekOnly: false,
- targetWeek: "",
- isPastDue: false,
- ...deriveDayFields(start),
- }),
- )
+ onDraftChange({
+ scheduledOn: start,
+ scheduleEnd: end,
+ schedMode: "range",
+ scheduleWeekOnly: false,
+ targetWeek: "",
+ isPastDue: false,
+ ...deriveDayFields(start),
+ })
}
onClear={
hasSchedule
? () =>
- onDraftChange(
- withAutoScheduleStatus(draft, {
- scheduledOn: "",
- targetWeek: "",
- scheduleWeekOnly: false,
- scheduleEnd: "",
- schedMode: undefined,
- isPastDue: false,
- ...deriveDayFields(""),
- }),
- )
+ onDraftChange({
+ scheduledOn: "",
+ scheduleWeekOnly: false,
+ targetWeek: "",
+ scheduleEnd: "",
+ schedMode: undefined,
+ isPastDue: false,
+ ...deriveDayFields(""),
+ })
: undefined
}
/>
diff --git a/src/app/(protected)/workorders/_components/list/table/cells/date-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/date-cell.tsx
index 8c709385..12b5a42e 100644
--- a/src/app/(protected)/workorders/_components/list/table/cells/date-cell.tsx
+++ b/src/app/(protected)/workorders/_components/list/table/cells/date-cell.tsx
@@ -1,5 +1,5 @@
import { useState } from "react";
-import { ChevronDown } from "lucide-react";
+import { ChevronDown, X } from "lucide-react";
import { Popover } from "@mui/material";
import { DateCellValue } from "@/app/(protected)/workorders/_components/list/table/cells/date-cell-value";
import { MiniCalendar } from "@/app/(protected)/workorders/_components/wizard/mini-calendar";
@@ -111,6 +111,22 @@ export function DateCell({
onClick={(e) => e.stopPropagation()}
slotProps={{ paper: { className: "rounded-lg border border-border p-3" } }}
>
+ {Boolean(onClear) && (
+
+
+
+ )}
)}
- {Boolean(onClear) && (
-
-
-
- )}
>
);
diff --git a/src/app/(protected)/workorders/_components/list/table/reschedule-notify.ts b/src/app/(protected)/workorders/_components/list/table/reschedule-notify.ts
index 8953af82..1391cf32 100644
--- a/src/app/(protected)/workorders/_components/list/table/reschedule-notify.ts
+++ b/src/app/(protected)/workorders/_components/list/table/reschedule-notify.ts
@@ -18,11 +18,15 @@ type NotifyNavigate = {
onNavigate: () => void;
};
-function showRescheduleToast(message: string, onNavigate: () => void) {
+function showRescheduleToast(
+ message: string,
+ onNavigate: () => void,
+ linkLabel = "Click to view it.",
+) {
const toastId = toast(
createElement(RescheduleToast, {
message,
- linkLabel: "Click to view it.",
+ linkLabel,
onNavigate: () => {
toast.dismiss(toastId);
onNavigate();
@@ -35,6 +39,13 @@ function showRescheduleToast(message: string, onNavigate: () => void) {
}
export function weekMoveNotify(weekStart: string, { onNavigate }: NotifyNavigate) {
+ if (!weekStart) {
+ return showRescheduleToast(
+ "Work order moved to Unscheduled.",
+ onNavigate,
+ "View in Advanced Filters.",
+ );
+ }
return showRescheduleToast(
`Work order moved to Unscheduled · ${fmtWeekOf(weekStart)}.`,
onNavigate,
diff --git a/src/app/(protected)/workorders/_components/list/table/use-wo-table-row-handlers.ts b/src/app/(protected)/workorders/_components/list/table/use-wo-table-row-handlers.ts
index 08e594e6..ae33d713 100644
--- a/src/app/(protected)/workorders/_components/list/table/use-wo-table-row-handlers.ts
+++ b/src/app/(protected)/workorders/_components/list/table/use-wo-table-row-handlers.ts
@@ -122,27 +122,26 @@ export function useWoTableRowHandlers({
});
},
onScheduleWeek: (id: string | number, week: string) => {
- onPatch(id, {
- scheduledOn: "",
- targetWeek: week,
- scheduleWeekOnly: true,
- scheduleEnd: "",
- schedMode: "week",
- dayGroup: "",
- dayLabel: "",
- });
+ const isClear = week === "";
+ onPatch(
+ id,
+ isClear
+ ? applySchedulePatch("", false)
+ : {
+ scheduledOn: "",
+ targetWeek: week,
+ scheduleWeekOnly: true,
+ scheduleEnd: "",
+ schedMode: "week",
+ dayGroup: "",
+ dayLabel: "",
+ },
+ );
weekMoveNotify(week, {
onNavigate: () => {
- onNavigateToDay(week);
- onOpenAdvancedFilters?.();
- },
- });
- },
- onScheduleClear: (id: string | number) => {
- onPatch(id, applySchedulePatch("", false));
- weekMoveNotify(weekMonday, {
- onNavigate: () => {
- onNavigateToDay(weekMonday);
+ if (!isClear) {
+ onNavigateToDay(week);
+ }
onOpenAdvancedFilters?.();
},
});
diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx
index e6f69f55..01fea44a 100644
--- a/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx
+++ b/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx
@@ -43,6 +43,9 @@ export function WoTableRowCells({
handlers,
onPatchRow,
}: WoTableRowCellsProps) {
+ const hasSchedule =
+ Boolean(row.scheduledOn) || Boolean(row.targetWeek) || Boolean(row.scheduleEnd);
+
return (
<>
handlers.onScheduleDate(row.id, date)}
onSaveWeek={(week) => handlers.onScheduleWeek(row.id, week)}
onSaveRange={(start, end) => handlers.onScheduleRange(row.id, start, end)}
- onClear={
- Boolean(row.scheduledOn) || Boolean(row.targetWeek) || Boolean(row.scheduleEnd)
- ? () => handlers.onScheduleClear(row.id)
- : undefined
- }
+ onClear={hasSchedule && !viewOnly ? () => handlers.onScheduleWeek(row.id, "") : undefined}
/>
|
void;
onScheduleWeek: (id: string | number, week: string) => void;
onScheduleRange: (id: string | number, start: string, end: string) => void;
- onScheduleClear: (id: string | number) => void;
onDuplicateFound?: (existing: WorkOrderTableRow) => void;
onOpenSlideOver: (row: WorkOrderTableRow) => void;
onRequestComplete?: (row: WorkOrderTableRow) => void;
@@ -90,10 +89,6 @@ function guardScheduleHandlers(
if (!canApplyWorkOrderTablePatch(row.status, { scheduledOn: start })) return;
handlers.onScheduleRange(id, start, end);
},
- onScheduleClear: (id) => {
- if (!canApplyWorkOrderTablePatch(row.status, { scheduledOn: "" })) return;
- handlers.onScheduleClear(id);
- },
};
}
@@ -206,7 +201,7 @@ export function WoTableRow({
forceScheduleHighlight={forceScheduleHighlight}
forceDueDateHighlight={forceDueDateHighlight}
mutationCellStyle={coreLockStyle}
- viewOnly={isCoreLocked}
+ viewOnly={closedForEdit}
canceledLockStyle={canceledLockStyle}
handlers={guardedHandlers}
onPatchRow={patchRow}
@@ -217,7 +212,7 @@ export function WoTableRow({
pmTypes={pmTypes}
search={search}
mutationCellStyle={coreLockStyle}
- viewOnly={isCoreLocked}
+ viewOnly={closedForEdit}
canceledLockStyle={canceledLockStyle}
handlers={guardedHandlers}
onPatchRow={patchRow}
diff --git a/src/domain/work-orders/utils/slide-over-draft.ts b/src/domain/work-orders/utils/slide-over-draft.ts
index 615d9f53..926e1027 100644
--- a/src/domain/work-orders/utils/slide-over-draft.ts
+++ b/src/domain/work-orders/utils/slide-over-draft.ts
@@ -19,6 +19,8 @@ export const SLIDE_OVER_EDIT_KEYS = [
"scheduledOn",
"targetWeek",
"scheduleWeekOnly",
+ "scheduleEnd",
+ "schedMode",
"dueDate",
"completedDate",
"apptTime",
diff --git a/src/test/app/(protected)/workorders/date-cell-schedule-range.test.tsx b/src/test/app/(protected)/workorders/date-cell-schedule-range.test.tsx
index 3344a87a..c0e9544e 100644
--- a/src/test/app/(protected)/workorders/date-cell-schedule-range.test.tsx
+++ b/src/test/app/(protected)/workorders/date-cell-schedule-range.test.tsx
@@ -105,11 +105,12 @@ describe("DateCell — Schedule On range", () => {
expect(onSave).not.toHaveBeenCalled();
expect(onSaveRange).not.toHaveBeenCalled();
});
- it("shows Clear when a concrete schedule exists", async () => {
+
+ it("shows Clear above the mode tabs and closes after clearing", async () => {
const onClear = vi.fn();
renderWithProviders(
{
/>,
);
- fireEvent.click(screen.getByRole("button", { name: /06-01/ }));
- fireEvent.click(await screen.findByRole("button", { name: /clear/i }));
+ fireEvent.click(screen.getByRole("button", { name: /09-09/ }));
- expect(onClear).toHaveBeenCalledTimes(1);
+ const clear = await screen.findByRole("button", { name: "Clear" });
+ const week = screen.getByRole("button", { name: "Week" });
+ expect(clear.compareDocumentPosition(week) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
+
+ fireEvent.click(clear);
+ expect(onClear).toHaveBeenCalledOnce();
+ expect(screen.queryByRole("button", { name: "Clear" })).not.toBeInTheDocument();
});
});
diff --git a/src/test/app/(protected)/workorders/reschedule-notify.test.ts b/src/test/app/(protected)/workorders/reschedule-notify.test.ts
new file mode 100644
index 00000000..241d40e2
--- /dev/null
+++ b/src/test/app/(protected)/workorders/reschedule-notify.test.ts
@@ -0,0 +1,33 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { weekMoveNotify } from "@/app/(protected)/workorders/_components/list/table/reschedule-notify";
+
+const { toast } = vi.hoisted(() => {
+ const notify = Object.assign(
+ vi.fn((_content: unknown, _options?: unknown) => 1),
+ {
+ dismiss: vi.fn(),
+ },
+ );
+ return { toast: notify };
+});
+
+vi.mock("react-toastify", () => ({ toast }));
+
+describe("weekMoveNotify", () => {
+ beforeEach(() => {
+ toast.mockClear();
+ toast.dismiss.mockClear();
+ });
+
+ it("uses the AAP Unscheduled message and Advanced Filters action when clearing", () => {
+ weekMoveNotify("", { onNavigate: vi.fn() });
+
+ const content = toast.mock.calls[0][0] as {
+ props: { message: string; linkLabel: string };
+ };
+ expect(content.props).toMatchObject({
+ message: "Work order moved to Unscheduled.",
+ linkLabel: "View in Advanced Filters.",
+ });
+ });
+});
diff --git a/src/test/app/(protected)/workorders/slide-over-schedule-range.test.tsx b/src/test/app/(protected)/workorders/slide-over-schedule-range.test.tsx
index 2fb72672..0de9f938 100644
--- a/src/test/app/(protected)/workorders/slide-over-schedule-range.test.tsx
+++ b/src/test/app/(protected)/workorders/slide-over-schedule-range.test.tsx
@@ -121,4 +121,46 @@ describe("SlideOverInfoTabEditView — Schedule On range", () => {
}),
);
}, 10_000);
+
+ it("clears every schedule field in the draft", async () => {
+ const onDraftChange = vi.fn();
+ renderWithProviders(
+ ,
+ );
+
+ const scheduleRow = screen.getByText("Schedule On").closest("div");
+ fireEvent.click(scheduleRow!.querySelector("button")!);
+ fireEvent.click(await screen.findByRole("button", { name: /clear/i }));
+
+ expect(onDraftChange).toHaveBeenCalledWith(
+ expect.objectContaining({
+ scheduledOn: "",
+ scheduleWeekOnly: false,
+ targetWeek: "",
+ scheduleEnd: "",
+ schedMode: undefined,
+ dayGroup: "",
+ dayLabel: "",
+ isPastDue: false,
+ }),
+ );
+ });
});
diff --git a/src/test/app/(protected)/workorders/use-wo-table-row-handlers-schedule.test.ts b/src/test/app/(protected)/workorders/use-wo-table-row-handlers-schedule.test.ts
index 2b9fca2b..ff3f0312 100644
--- a/src/test/app/(protected)/workorders/use-wo-table-row-handlers-schedule.test.ts
+++ b/src/test/app/(protected)/workorders/use-wo-table-row-handlers-schedule.test.ts
@@ -27,7 +27,7 @@ function buildTableData(revealScheduledRow = vi.fn()) {
} as never;
}
-describe("useWoTableRowHandlers schedule navigation (SH-240)", () => {
+describe("useWoTableRowHandlers schedule navigation", () => {
beforeEach(() => {
rescheduleNotify.mockClear();
weekMoveNotify.mockClear();
@@ -152,7 +152,7 @@ describe("useWoTableRowHandlers schedule navigation (SH-240)", () => {
expect(onOpenAdvancedFilters).toHaveBeenCalled();
});
- it("clears Schedule On and offers Advanced Filters navigation", () => {
+ it("clears the concrete schedule and opens Advanced Filters from the Unscheduled toast", () => {
const onNavigateToDay = vi.fn();
const onOpenAdvancedFilters = vi.fn();
const onPatch = vi.fn();
@@ -170,17 +170,17 @@ describe("useWoTableRowHandlers schedule navigation (SH-240)", () => {
}),
);
- result.current.onScheduleClear("wo-1");
+ result.current.onScheduleWeek("wo-1", "");
expect(onPatch).toHaveBeenCalledWith("wo-1", applySchedulePatch("", false));
expect(weekMoveNotify).toHaveBeenCalledWith(
- "2026-05-18",
+ "",
expect.objectContaining({ onNavigate: expect.any(Function) }),
);
const { onNavigate } = weekMoveNotify.mock.calls[0][1] as { onNavigate: () => void };
onNavigate();
- expect(onNavigateToDay).toHaveBeenCalledWith("2026-05-18");
+ expect(onNavigateToDay).not.toHaveBeenCalled();
expect(onOpenAdvancedFilters).toHaveBeenCalled();
});
});
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 11c2d05d..4d70e893 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
@@ -111,4 +111,33 @@ describe("WizardStepTypeSchedule — Schedule On range selection", () => {
expect(screen.getByRole("button", { name: /^done$/i })).toBeDisabled();
expect(onPatch).not.toHaveBeenCalled();
});
+
+ it("does not promote Incomplete to Scheduled for week-only Schedule On", async () => {
+ const onUpdate = vi.fn();
+ const onPatch = vi.fn();
+
+ render(
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: /pick a date/i }));
+ fireEvent.click(await screen.findByRole("button", { name: "Week" }));
+ const weekButtons = screen
+ .getAllByRole("button")
+ .filter((button) => /–/.test(button.textContent ?? ""));
+ fireEvent.click(weekButtons[0]);
+
+ expect(onPatch).toHaveBeenCalledWith(
+ expect.objectContaining({
+ scheduleWeekOnly: true,
+ schedMode: "week",
+ }),
+ );
+ expect(onPatch.mock.calls.some(([patch]) => patch.status === "Scheduled")).toBe(false);
+ });
});
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
new file mode 100644
index 00000000..38e0014e
--- /dev/null
+++ b/src/test/app/(protected)/workorders/wo-table-inline-status-schedule.test.tsx
@@ -0,0 +1,73 @@
+import { fireEvent, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import { WoTableInlineRow } from "@/app/(protected)/workorders/_components/list/table/wo-table-inline-row";
+import { EMPTY_TABLE_DRAFT } from "@/domain/work-orders/types/work-order-table-row";
+import { renderWithProviders } from "@/test/test-utils";
+
+vi.mock("@/app/(protected)/workorders/_components/list/table/cells/site-cell", () => ({
+ SiteCell: () => null,
+}));
+vi.mock(
+ "@/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell",
+ () => ({
+ EditableWONumberCell: () => null,
+ }),
+);
+vi.mock("@/app/(protected)/workorders/_components/list/table/cells/type-cell", () => ({
+ TypeCell: () => null,
+}));
+vi.mock("@/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell", () => ({
+ AssignedToCell: () => null,
+}));
+vi.mock("@/app/(protected)/workorders/_components/list/table/cells/date-cell", () => ({
+ DateCell: () => null,
+}));
+vi.mock("@/app/(protected)/workorders/_components/list/table/cells/due-date-cell", () => ({
+ DueDateCell: () => null,
+}));
+vi.mock("@/app/(protected)/workorders/_components/list/table/cells/pm-type-cell", () => ({
+ PMTypeCell: () => null,
+}));
+vi.mock("@/app/(protected)/workorders/_components/list/table/cells/vendor-cell", () => ({
+ VendorCell: () => null,
+}));
+vi.mock("@/app/(protected)/workorders/_components/list/table/cells/appt-time-cell", () => ({
+ ApptTimeCell: () => null,
+}));
+
+describe("WoTableInlineRow StatusCell with a concrete schedule", () => {
+ it("keeps Scheduled when Incomplete is picked on a dated draft", () => {
+ const onChange = vi.fn();
+ const draft = {
+ ...EMPTY_TABLE_DRAFT,
+ status: "Incomplete" as const,
+ scheduledOn: "2026-06-01",
+ scheduleWeekOnly: false,
+ };
+
+ renderWithProviders(
+ ,
+ { withAuth: false },
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: /incomplete/i }));
+ fireEvent.click(screen.getByRole("button", { name: /^pending$/i }));
+
+ expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ status: "Scheduled" }));
+ });
+});
diff --git a/src/test/app/(protected)/workorders/wo-table-row-schedule-clear.test.tsx b/src/test/app/(protected)/workorders/wo-table-row-schedule-clear.test.tsx
new file mode 100644
index 00000000..33dd7d92
--- /dev/null
+++ b/src/test/app/(protected)/workorders/wo-table-row-schedule-clear.test.tsx
@@ -0,0 +1,119 @@
+import { fireEvent, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import { WoTableRowCells } from "@/app/(protected)/workorders/_components/list/table/wo-table-row-cells";
+import type { WoTableRowHandlers } from "@/app/(protected)/workorders/_components/list/table/wo-table-row";
+import type {
+ WorkOrderTablePatch,
+ WorkOrderTableRow,
+} from "@/domain/work-orders/types/work-order-table-row";
+import { applySchedulePatch } from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
+import { mergeAutoSchedulePatch } from "@/domain/work-orders/utils/wo-lifecycle-transitions";
+import { renderWithProviders } from "@/test/test-utils";
+
+vi.mock("@/app/(protected)/workorders/_components/list/table/cells/site-cell", () => ({
+ SiteCell: () => null,
+}));
+vi.mock(
+ "@/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell",
+ () => ({
+ EditableWONumberCell: () => null,
+ }),
+);
+vi.mock("@/app/(protected)/workorders/_components/list/table/cells/type-cell", () => ({
+ TypeCell: () => null,
+}));
+vi.mock("@/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell", () => ({
+ AssignedToCell: () => null,
+}));
+vi.mock("@/app/(protected)/workorders/_components/list/table/cells/date-cell", () => ({
+ DateCell: ({ onClear }: { onClear?: () => void }) => {
+ if (!onClear) return null;
+ return (
+
+ );
+ },
+}));
+vi.mock("@/app/(protected)/workorders/_components/list/table/cells/due-date-cell", () => ({
+ DueDateCell: () => null,
+}));
+
+function renderScheduledRow(
+ row: WorkOrderTableRow,
+ onPersist: (id: string | number, patch: WorkOrderTablePatch) => void,
+ viewOnly = false,
+) {
+ const handlers = {
+ onScheduleWeek: (id: string | number, week: string) => {
+ onPersist(id, mergeAutoSchedulePatch(row, applySchedulePatch(week, false)));
+ },
+ } as unknown as WoTableRowHandlers;
+
+ renderWithProviders(
+ ,
+ { withAuth: false },
+ );
+}
+
+describe("WoTableRowCells Schedule On clear", () => {
+ it.each([
+ { scheduledOn: "2026-09-09", targetWeek: "", scheduleEnd: "" },
+ { scheduledOn: "", targetWeek: "2026-09-07", scheduleEnd: "" },
+ { scheduledOn: "2026-09-09", targetWeek: "", scheduleEnd: "2026-09-11" },
+ ])("clears a persisted schedule and derives Pending from Scheduled", (schedule) => {
+ const onPersist = vi.fn();
+ const row = {
+ id: 42,
+ status: "Scheduled",
+ scheduleWeekOnly: Boolean(schedule.targetWeek),
+ schedMode: schedule.targetWeek ? "week" : schedule.scheduleEnd ? "range" : "date",
+ ...schedule,
+ } as WorkOrderTableRow;
+
+ renderScheduledRow(row, onPersist);
+ fireEvent.click(screen.getByRole("button", { name: "clear schedule" }));
+
+ expect(onPersist).toHaveBeenCalledWith(
+ 42,
+ expect.objectContaining({
+ scheduledOn: "",
+ scheduleWeekOnly: false,
+ targetWeek: "",
+ scheduleEnd: "",
+ schedMode: undefined,
+ status: "Pending",
+ }),
+ );
+ });
+
+ it.each(["Completed", "Canceled"])("does not expose Clear for a %s row", (status) => {
+ const row = {
+ id: 42,
+ status,
+ scheduledOn: "2026-09-09",
+ targetWeek: "",
+ scheduleEnd: "",
+ scheduleWeekOnly: false,
+ schedMode: "date",
+ } as WorkOrderTableRow;
+
+ renderScheduledRow(row, vi.fn(), true);
+
+ expect(screen.queryByRole("button", { name: "clear schedule" })).not.toBeInTheDocument();
+ });
+});
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 26991511..e5d0cd0e 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
@@ -7,7 +7,10 @@ import {
resolveSiteCode,
} from "@/domain/work-orders/mappers/work-order-board-mapper";
-import { WORK_ORDER_TYPE_API } from "@/domain/work-orders/types/work-order-board";
+import {
+ LIFECYCLE_STATUS_API,
+ WORK_ORDER_TYPE_API,
+} from "@/domain/work-orders/types/work-order-board";
import { EMPTY_WIZARD_DRAFT } from "@/domain/work-orders/types/work-order-wizard";
@@ -262,6 +265,22 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
expect(payload.scheduledDate).toBeUndefined();
});
+ it("keeps Incomplete on week-only create and omits scheduledDate", () => {
+ const payload = mapWizardDraftToBoardCreatePayload({
+ ...EMPTY_WIZARD_DRAFT,
+ type: "PM",
+ siteCode: "BK5",
+ status: "Incomplete",
+ scheduleWeekOnly: true,
+ targetWeek: "2026-06-22",
+ });
+
+ expect(payload.lifecycleStatus).toBe(LIFECYCLE_STATUS_API.Incomplete);
+ expect(payload.scheduleWeekOnly).toBe(true);
+ expect(payload.targetWeek).toBe("2026-06-22");
+ expect(payload.scheduledDate).toBeUndefined();
+ });
+
it("prefers explicit targetWeek when scheduleWeekOnly and scheduledOn is empty", () => {
const payload = mapWizardDraftToBoardCreatePayload({
...EMPTY_WIZARD_DRAFT,
diff --git a/src/test/domain/work-orders/utils/slide-over-draft.test.ts b/src/test/domain/work-orders/utils/slide-over-draft.test.ts
index ab31f5a5..615e1085 100644
--- a/src/test/domain/work-orders/utils/slide-over-draft.test.ts
+++ b/src/test/domain/work-orders/utils/slide-over-draft.test.ts
@@ -6,6 +6,7 @@ import {
} from "@/domain/work-orders/utils/slide-over-draft";
import { EMPTY_UPLIFT_SUMMARY } from "@/domain/work-orders/types/work-order-uplift";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
+import { mergeAutoSchedulePatch } from "@/domain/work-orders/utils/wo-lifecycle-transitions";
function baseRow(overrides: Partial = {}): WorkOrderTableRow {
return {
@@ -97,6 +98,24 @@ describe("slide-over-draft", () => {
expect(isSlideOverDraftDirty(draft, row)).toBe(true);
});
+ it("clears client schedule range overlays in the save patch", () => {
+ const row = baseRow({
+ scheduleEnd: "2026-07-18",
+ schedMode: "range",
+ });
+ const draft = baseRow({
+ scheduledOn: "",
+ scheduleEnd: "",
+ schedMode: undefined,
+ });
+
+ expect(buildSlideOverPatch(draft, row)).toEqual({
+ scheduledOn: "",
+ scheduleEnd: "",
+ schedMode: undefined,
+ });
+ });
+
it("is not dirty when values match", () => {
const row = baseRow();
expect(isSlideOverDraftDirty(row, row)).toBe(false);
@@ -116,6 +135,38 @@ describe("slide-over-draft", () => {
});
});
+ it("derives Pending on persist when a Scheduled draft clears Schedule On", () => {
+ const baseline = baseRow({
+ status: "Scheduled",
+ scheduledOn: "2026-07-16",
+ scheduleWeekOnly: false,
+ targetWeek: "",
+ scheduleEnd: "",
+ schedMode: "date",
+ });
+ const draft = baseRow({
+ status: "Scheduled",
+ scheduledOn: "",
+ scheduleWeekOnly: false,
+ targetWeek: "",
+ scheduleEnd: "",
+ schedMode: undefined,
+ });
+
+ const patch = buildSlideOverPatch(draft, baseline);
+ expect(patch).toEqual({
+ scheduledOn: "",
+ schedMode: undefined,
+ });
+ expect(patch.status).toBeUndefined();
+ expect(mergeAutoSchedulePatch(baseline, patch)).toEqual(
+ expect.objectContaining({
+ scheduledOn: "",
+ status: "Pending",
+ }),
+ );
+ });
+
it("tracks ordered additionalContacts for dirty state and patch (SH-218)", () => {
const row = baseRow();
const contacts = [{ name: "Backup", phone: "555-0101", notes: "Gate" }];
|