From dee1578efedc9658b4ef5c60ec5600e4c14de7f7 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Wed, 9 Sep 2026 10:23:23 -0300 Subject: [PATCH 1/3] feat(work-orders): persist Schedule On clear from board and slide-over Clearing a concrete date must go through the shared schedule patch so status can revert to Pending. --- .../tabs/slide-over-info-tab-edit-view.tsx | 80 ++++++++++++------- .../list/table/use-wo-table-row-handlers.ts | 9 +++ .../list/table/wo-table-row-cells.tsx | 5 ++ .../_components/list/table/wo-table-row.tsx | 5 ++ .../date-cell-schedule-range.test.tsx | 18 +++++ ...use-wo-table-row-handlers-schedule.test.ts | 32 ++++++++ .../wo-table-row-completed-lock.test.tsx | 1 + .../wo-table-row-reorder-keyboard.test.tsx | 1 + .../wo-table-unscheduled-highlight.test.tsx | 1 + 9 files changed, 125 insertions(+), 27 deletions(-) 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 1bc70fae..7bfb16a3 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,6 +18,7 @@ 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 = { @@ -39,6 +40,9 @@ function SlideOverScheduleOnField({ draft: WorkOrderTableRow; onDraftChange: (patch: Partial) => void; }) { + const hasSchedule = + Boolean(draft.scheduledOn) || Boolean(draft.targetWeek) || Boolean(draft.scheduleEnd); + return ( - onDraftChange({ - scheduledOn, - scheduleWeekOnly: false, - targetWeek: scheduledOn ? "" : draft.targetWeek, - scheduleEnd: "", - schedMode: "date", - isPastDue: false, - ...deriveDayFields(scheduledOn), - }) + onDraftChange( + withAutoScheduleStatus(draft, { + scheduledOn, + scheduleWeekOnly: false, + targetWeek: scheduledOn ? "" : draft.targetWeek, + scheduleEnd: "", + schedMode: "date", + isPastDue: false, + ...deriveDayFields(scheduledOn), + }), + ) } onSaveWeek={(weekStart) => - onDraftChange({ - scheduledOn: "", - targetWeek: weekStart, - scheduleWeekOnly: true, - scheduleEnd: "", - schedMode: "week", - isPastDue: false, - ...deriveDayFields(""), - }) + onDraftChange( + withAutoScheduleStatus(draft, { + scheduledOn: "", + targetWeek: weekStart, + scheduleWeekOnly: true, + scheduleEnd: "", + schedMode: "week", + isPastDue: false, + ...deriveDayFields(""), + }), + ) } onSaveRange={(start, end) => - onDraftChange({ - scheduledOn: start, - scheduleEnd: end, - schedMode: "range", - scheduleWeekOnly: false, - targetWeek: "", - isPastDue: false, - ...deriveDayFields(start), - }) + onDraftChange( + withAutoScheduleStatus(draft, { + 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(""), + }), + ) + : undefined } /> ); 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 20172baa..08e594e6 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 @@ -138,6 +138,15 @@ export function useWoTableRowHandlers({ }, }); }, + onScheduleClear: (id: string | number) => { + onPatch(id, applySchedulePatch("", false)); + weekMoveNotify(weekMonday, { + onNavigate: () => { + onNavigateToDay(weekMonday); + onOpenAdvancedFilters?.(); + }, + }); + }, onDuplicateFound: (existing: WorkOrderTableRow) => { onDuplicateFound?.(existing); }, 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 90b16533..e6f69f55 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 @@ -137,6 +137,11 @@ export function WoTableRowCells({ onSave={(date) => 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 + } /> 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; @@ -89,6 +90,10 @@ 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); + }, }; } 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 698918c4..3344a87a 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,6 +105,24 @@ describe("DateCell — Schedule On range", () => { expect(onSave).not.toHaveBeenCalled(); expect(onSaveRange).not.toHaveBeenCalled(); }); + it("shows Clear when a concrete schedule exists", async () => { + const onClear = vi.fn(); + renderWithProviders( + , + ); + + fireEvent.click(screen.getByRole("button", { name: /06-01/ })); + fireEvent.click(await screen.findByRole("button", { name: /clear/i })); + + expect(onClear).toHaveBeenCalledTimes(1); + }); }); describe("DueDateCell — no Schedule On range tabs", () => { 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 c4cc0867..2b9fca2b 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 @@ -151,4 +151,36 @@ describe("useWoTableRowHandlers schedule navigation (SH-240)", () => { expect(onNavigateToDay).toHaveBeenCalledWith("2026-05-25"); expect(onOpenAdvancedFilters).toHaveBeenCalled(); }); + + it("clears Schedule On and offers Advanced Filters navigation", () => { + const onNavigateToDay = vi.fn(); + const onOpenAdvancedFilters = vi.fn(); + const onPatch = vi.fn(); + + const { result } = renderHook(() => + useWoTableRowHandlers({ + tableData: buildTableData(), + weekMonday: "2026-05-18", + onNavigateToDay, + onOpenAdvancedFilters, + onOpenSlideOver: vi.fn(), + onOpenMedia: vi.fn(), + onPatch, + onScheduleHighlight: vi.fn(), + }), + ); + + result.current.onScheduleClear("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(onOpenAdvancedFilters).toHaveBeenCalled(); + }); }); diff --git a/src/test/app/(protected)/workorders/wo-table-row-completed-lock.test.tsx b/src/test/app/(protected)/workorders/wo-table-row-completed-lock.test.tsx index 1ce5a477..3e8ea166 100644 --- a/src/test/app/(protected)/workorders/wo-table-row-completed-lock.test.tsx +++ b/src/test/app/(protected)/workorders/wo-table-row-completed-lock.test.tsx @@ -66,6 +66,7 @@ const handlers: WoTableRowHandlers = { onScheduleDate: vi.fn(), onScheduleWeek: vi.fn(), onScheduleRange: vi.fn(), + onScheduleClear: vi.fn(), onOpenSlideOver: vi.fn(), onOpenCompDoc: vi.fn(), onOpenUplifts: vi.fn(), diff --git a/src/test/app/(protected)/workorders/wo-table-row-reorder-keyboard.test.tsx b/src/test/app/(protected)/workorders/wo-table-row-reorder-keyboard.test.tsx index 868d7412..89c0241e 100644 --- a/src/test/app/(protected)/workorders/wo-table-row-reorder-keyboard.test.tsx +++ b/src/test/app/(protected)/workorders/wo-table-row-reorder-keyboard.test.tsx @@ -66,6 +66,7 @@ function handlers(overrides: Partial = {}): WoTableRowHandle onScheduleDate: vi.fn(), onScheduleWeek: vi.fn(), onScheduleRange: vi.fn(), + onScheduleClear: vi.fn(), onOpenSlideOver: vi.fn(), onReorder: vi.fn(), onDragStart: vi.fn(), diff --git a/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx b/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx index 645154c8..048deb3b 100644 --- a/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx +++ b/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx @@ -65,6 +65,7 @@ const handlers: WoTableRowHandlers = { onScheduleDate: vi.fn(), onScheduleWeek: vi.fn(), onScheduleRange: vi.fn(), + onScheduleClear: vi.fn(), onOpenSlideOver: vi.fn(), onOpenCompDoc: vi.fn(), onOpenUplifts: vi.fn(), From c71d9482ec87ea3f1951ce01d9f7e84c0bb40746 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Wed, 9 Sep 2026 15:44:49 -0300 Subject: [PATCH 2/3] feat(work-orders): align Schedule On clear with shared demotion path Board Clear uses empty-week patch; slide-over Save derives Pending via mergeAutoSchedulePatch. --- .../tabs/slide-over-info-tab-edit-view.tsx | 81 ++++++------ .../list/table/cells/date-cell.tsx | 33 ++--- .../list/table/reschedule-notify.ts | 15 ++- .../list/table/use-wo-table-row-handlers.ts | 37 +++--- .../list/table/wo-table-row-cells.tsx | 9 +- .../_components/list/table/wo-table-row.tsx | 9 +- .../work-orders/utils/slide-over-draft.ts | 2 + .../date-cell-schedule-range.test.tsx | 16 ++- .../workorders/reschedule-notify.test.ts | 33 +++++ .../slide-over-schedule-range.test.tsx | 42 +++++++ ...use-wo-table-row-handlers-schedule.test.ts | 10 +- .../wizard-schedule-date-range.test.tsx | 29 +++++ .../wo-table-inline-status-schedule.test.tsx | 73 +++++++++++ .../wo-table-row-schedule-clear.test.tsx | 119 ++++++++++++++++++ .../mappers/work-order-board-mapper.test.ts | 21 +++- .../utils/slide-over-draft.test.ts | 51 ++++++++ 16 files changed, 475 insertions(+), 105 deletions(-) create mode 100644 src/test/app/(protected)/workorders/reschedule-notify.test.ts create mode 100644 src/test/app/(protected)/workorders/wo-table-inline-status-schedule.test.tsx create mode 100644 src/test/app/(protected)/workorders/wo-table-row-schedule-clear.test.tsx 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" }]; From 252c0d08568716369ccb36ca367ade300a4ab681 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Wed, 9 Sep 2026 17:09:26 -0300 Subject: [PATCH 3/3] fix(work-orders): remove stale schedule clear handlers --- .../(protected)/workorders/wo-table-row-completed-lock.test.tsx | 1 - .../workorders/wo-table-row-reorder-keyboard.test.tsx | 1 - .../workorders/wo-table-unscheduled-highlight.test.tsx | 1 - 3 files changed, 3 deletions(-) diff --git a/src/test/app/(protected)/workorders/wo-table-row-completed-lock.test.tsx b/src/test/app/(protected)/workorders/wo-table-row-completed-lock.test.tsx index 3e8ea166..1ce5a477 100644 --- a/src/test/app/(protected)/workorders/wo-table-row-completed-lock.test.tsx +++ b/src/test/app/(protected)/workorders/wo-table-row-completed-lock.test.tsx @@ -66,7 +66,6 @@ const handlers: WoTableRowHandlers = { onScheduleDate: vi.fn(), onScheduleWeek: vi.fn(), onScheduleRange: vi.fn(), - onScheduleClear: vi.fn(), onOpenSlideOver: vi.fn(), onOpenCompDoc: vi.fn(), onOpenUplifts: vi.fn(), diff --git a/src/test/app/(protected)/workorders/wo-table-row-reorder-keyboard.test.tsx b/src/test/app/(protected)/workorders/wo-table-row-reorder-keyboard.test.tsx index 89c0241e..868d7412 100644 --- a/src/test/app/(protected)/workorders/wo-table-row-reorder-keyboard.test.tsx +++ b/src/test/app/(protected)/workorders/wo-table-row-reorder-keyboard.test.tsx @@ -66,7 +66,6 @@ function handlers(overrides: Partial = {}): WoTableRowHandle onScheduleDate: vi.fn(), onScheduleWeek: vi.fn(), onScheduleRange: vi.fn(), - onScheduleClear: vi.fn(), onOpenSlideOver: vi.fn(), onReorder: vi.fn(), onDragStart: vi.fn(), diff --git a/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx b/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx index 048deb3b..645154c8 100644 --- a/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx +++ b/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx @@ -65,7 +65,6 @@ const handlers: WoTableRowHandlers = { onScheduleDate: vi.fn(), onScheduleWeek: vi.fn(), onScheduleRange: vi.fn(), - onScheduleClear: vi.fn(), onOpenSlideOver: vi.fn(), onOpenCompDoc: vi.fn(), onOpenUplifts: vi.fn(),