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 1a6b51c5..aa6b8599 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 @@ -5,7 +5,6 @@ import type { } from "@/domain/work-orders/types/work-order-table-row"; import { applySchedulePatch } from "@/domain/work-orders/use-cases/use-work-order-table-mutations"; import { shouldNotifyCrossWeek } from "@/domain/work-orders/utils/should-notify-cross-week"; -import { mondayOf } from "@/domain/work-orders/utils/wizard-date-utils"; import type { ReturnTypeOfUseWorkOrdersTableData } from "./wo-table-types"; import { rescheduleNotify, weekMoveNotify } from "./reschedule-notify"; import type { WoTableRowHandlers } from "./wo-table-row"; @@ -13,7 +12,8 @@ import type { WoTableRowHandlers } from "./wo-table-row"; type UseWoTableRowHandlersOptions = { tableData: ReturnTypeOfUseWorkOrdersTableData; weekMonday: string; - onWeekChange: (mondayIso: string) => void; + onNavigateToDay: (dateIso: string) => void; + onOpenAdvancedFilters?: () => void; onOpenSlideOver: (row: WorkOrderTableRow, edit?: boolean) => void; onOpenCompDoc?: (row: WorkOrderTableRow) => void; onOpenUplifts?: (row: WorkOrderTableRow) => void; @@ -26,7 +26,8 @@ type UseWoTableRowHandlersOptions = { export function useWoTableRowHandlers({ tableData, weekMonday, - onWeekChange, + onNavigateToDay, + onOpenAdvancedFilters, onOpenSlideOver, onOpenCompDoc, onOpenUplifts, @@ -35,14 +36,7 @@ export function useWoTableRowHandlers({ onScheduleHighlight, onDuplicateFound, }: UseWoTableRowHandlersOptions): WoTableRowHandlers { - const { - dragState: dragStateRef, - setDragViz, - reorderRows, - endDrag, - setUnschedOpen, - triggerFlash, - } = tableData; + const { dragState: dragStateRef, setDragViz, reorderRows, endDrag, triggerFlash } = tableData; return useMemo( () => ({ @@ -80,14 +74,16 @@ export function useWoTableRowHandlers({ onScheduleDate: (id: string | number, date: string) => { onPatch(id, applySchedulePatch(date, false)); if (shouldNotifyCrossWeek(date, weekMonday)) { - const targetMonday = mondayOf(date); rescheduleNotify(date, { onNavigate: () => { - onWeekChange(targetMonday); + onNavigateToDay(date); setTimeout(() => triggerFlash(id), 180); }, }); + return; } + onNavigateToDay(date); + triggerFlash(id); }, onScheduleWeek: (id: string | number, week: string) => { onPatch(id, { @@ -97,12 +93,10 @@ export function useWoTableRowHandlers({ dayGroup: "", dayLabel: "", }); - setUnschedOpen(true); - triggerFlash(id); weekMoveNotify(week, { onNavigate: () => { - setUnschedOpen(true); - setTimeout(() => triggerFlash(id), 180); + onNavigateToDay(week); + onOpenAdvancedFilters?.(); }, }); }, @@ -138,10 +132,10 @@ export function useWoTableRowHandlers({ setDragViz, reorderRows, endDrag, - setUnschedOpen, triggerFlash, weekMonday, - onWeekChange, + onNavigateToDay, + onOpenAdvancedFilters, onOpenSlideOver, onOpenCompDoc, onOpenUplifts, diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table.tsx index 7e45824c..0ac11f1a 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table.tsx @@ -29,7 +29,7 @@ type WoTableProps = { vendors: VendorDropdownItem[]; pmTypes: string[]; weekMonday: string; - onWeekChange: (mondayIso: string) => void; + onNavigateToDay: (dateIso: string) => void; onOpenSlideOver: (row: WorkOrderTableRow, edit?: boolean) => void; /** Omit until completion-doc slice mounts CompDocDialog. */ onOpenCompDoc?: (row: WorkOrderTableRow) => void; @@ -75,7 +75,7 @@ export function WoTable({ vendors, pmTypes, weekMonday, - onWeekChange, + onNavigateToDay, onOpenSlideOver, onOpenCompDoc, onOpenUplifts, @@ -98,7 +98,8 @@ export function WoTable({ const handlers = useWoTableRowHandlers({ tableData, weekMonday, - onWeekChange, + onNavigateToDay, + onOpenAdvancedFilters, onOpenSlideOver, onOpenCompDoc, onOpenUplifts, diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-list-filters.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-list-filters.ts index 20adf650..581d1b0f 100644 --- a/src/app/(protected)/workorders/_hooks/use-work-orders-list-filters.ts +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-list-filters.ts @@ -20,6 +20,7 @@ import { compactWeekLabel, currentWeekMonday } from "@/domain/work-orders/utils/ import { firstWeekday, initialSelectedDay, + scheduledDayTab, weekdaysMonFri, } from "@/domain/work-orders/utils/week-day-nav-utils"; @@ -72,6 +73,13 @@ export function useWorkOrdersListFilters() { setSelectedDayIsoState(value); }; + const navigateToDay = (dateIso: string) => { + const tab = scheduledDayTab(dateIso); + setWeekMondayState(tab.weekMonday); + setSelectedDayIsoState(tab.selectedDayIso); + setPage(0); + }; + const setDispatcherIds = (value: Set) => { setDispatcherIdsState(new Set(value)); setPage(0); @@ -156,6 +164,7 @@ export function useWorkOrdersListFilters() { setPageSize, setWeekMonday, setSelectedDayIso, + navigateToDay, setDispatcherIds, setTypeFilter, setSortBy, diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-table-data.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-table-data.ts index 7d7556e9..b931237e 100644 --- a/src/app/(protected)/workorders/_hooks/use-work-orders-table-data.ts +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-table-data.ts @@ -5,7 +5,10 @@ import { useWorkOrdersTableLocalState } from "@/app/(protected)/workorders/_hook import { currentWeekMonday } from "@/domain/work-orders/utils/list-week-utils"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils"; -import { buildSelectedDayGroup } from "@/domain/work-orders/utils/week-day-nav-utils"; +import { + buildSelectedDayGroup, + isDayScopedSearchEmpty, +} from "@/domain/work-orders/utils/week-day-nav-utils"; import { composeBoardSections, filterByBarAndSearch, @@ -83,12 +86,17 @@ export function useWorkOrdersTableData(filters: FiltersReturn) { ); const searchActive = filters.search.trim() !== ""; - const visibleRows = advActive ? advResults : filtered; - const noSearchResults = visibleRows.length === 0 && searchActive; - const isEmptyWeek = - !advActive && scheduled.length === 0 && unassigned.length === 0 && !searchActive; const displayGroups = dayGroups; const dayVisibleCount = displayGroups[0]?.items.length ?? 0; + const noSearchResults = + !advActive && + isDayScopedSearchEmpty({ + searchActive, + unassignedCount: unassigned.length, + dayItemCount: dayVisibleCount, + }); + const isEmptyWeek = + !advActive && scheduled.length === 0 && unassigned.length === 0 && !searchActive; const todayKey = filters.weekMonday === currentWeekMonday() diff --git a/src/app/(protected)/workorders/index.tsx b/src/app/(protected)/workorders/index.tsx index 2f597485..8d5b1739 100644 --- a/src/app/(protected)/workorders/index.tsx +++ b/src/app/(protected)/workorders/index.tsx @@ -90,7 +90,7 @@ export default function WorkOrdersListPage() { vendors={page.vendors} pmTypes={page.pmTypes} weekMonday={filters.weekMonday} - onWeekChange={filters.setWeekMonday} + onNavigateToDay={filters.navigateToDay} onOpenSlideOver={page.handleOpenSlideOver} onOpenCompDoc={(row) => openCompletionDoc(row, page.setDocRow)} onOpenMedia={page.setMediaRow} diff --git a/src/domain/work-orders/utils/week-day-nav-utils.ts b/src/domain/work-orders/utils/week-day-nav-utils.ts index 37e6ac26..7eea7d4b 100644 --- a/src/domain/work-orders/utils/week-day-nav-utils.ts +++ b/src/domain/work-orders/utils/week-day-nav-utils.ts @@ -4,7 +4,7 @@ import type { } from "@/domain/work-orders/types/work-order-table-row"; import { isLiveWeek } from "@/domain/work-orders/utils/list-week-utils"; import { isDateInWeek } from "@/domain/work-orders/utils/table-day-utils"; -import { addDaysIso, todayIso } from "@/domain/work-orders/utils/wizard-date-utils"; +import { addDaysIso, mondayOf, todayIso } from "@/domain/work-orders/utils/wizard-date-utils"; const DAY_TAB_WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const; @@ -16,6 +16,25 @@ export function firstWeekday(weekMonday: string): string { return weekMonday; } +/** Maps a scheduled date onto the Mon–Fri tab that can show it (weekends fall back to Monday). */ +export function scheduledDayTab(dateIso: string): { weekMonday: string; selectedDayIso: string } { + const day = dateIso.slice(0, 10); + const weekMonday = mondayOf(day); + const days = weekdaysMonFri(weekMonday); + return { + weekMonday, + selectedDayIso: days.includes(day) ? day : firstWeekday(weekMonday), + }; +} + +export function isDayScopedSearchEmpty(args: { + searchActive: boolean; + unassignedCount: number; + dayItemCount: number; +}): boolean { + return args.searchActive && args.unassignedCount === 0 && args.dayItemCount === 0; +} + export function initialSelectedDay(weekMonday: string, today = todayIso()): string { if (isLiveWeek(weekMonday) && isDateInWeek(today, weekMonday)) { return today; diff --git a/src/test/app/(protected)/workorders/use-wo-table-row-handlers-duplicate.test.ts b/src/test/app/(protected)/workorders/use-wo-table-row-handlers-duplicate.test.ts index df563025..e9b52a4a 100644 --- a/src/test/app/(protected)/workorders/use-wo-table-row-handlers-duplicate.test.ts +++ b/src/test/app/(protected)/workorders/use-wo-table-row-handlers-duplicate.test.ts @@ -26,7 +26,7 @@ describe("useWoTableRowHandlers onDuplicateFound", () => { useWoTableRowHandlers({ tableData: buildTableData(), weekMonday: "2026-07-20", - onWeekChange: vi.fn(), + onNavigateToDay: vi.fn(), onOpenSlideOver, onOpenMedia: vi.fn(), onPatch: vi.fn(), 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 new file mode 100644 index 00000000..afbf434c --- /dev/null +++ b/src/test/app/(protected)/workorders/use-wo-table-row-handlers-schedule.test.ts @@ -0,0 +1,116 @@ +import { renderHook } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { useWoTableRowHandlers } from "@/app/(protected)/workorders/_components/list/table/use-wo-table-row-handlers"; +import { applySchedulePatch } from "@/domain/work-orders/use-cases/use-work-order-table-mutations"; + +const { rescheduleNotify, weekMoveNotify } = vi.hoisted(() => ({ + rescheduleNotify: vi.fn(), + weekMoveNotify: vi.fn(), +})); + +vi.mock("@/app/(protected)/workorders/_components/list/table/reschedule-notify", () => ({ + rescheduleNotify, + weekMoveNotify, +})); + +function buildTableData(triggerFlash = vi.fn()) { + return { + dragState: { current: { dragId: null, overId: null } }, + setDragViz: vi.fn(), + reorderRows: vi.fn(), + endDrag: vi.fn(), + triggerFlash, + setSelectedId: vi.fn(), + } as never; +} + +describe("useWoTableRowHandlers schedule navigation (SH-240)", () => { + it("selects the scheduled weekday in the same week so the row stays visible", () => { + const onNavigateToDay = vi.fn(); + const onPatch = vi.fn(); + const triggerFlash = vi.fn(); + + const { result } = renderHook(() => + useWoTableRowHandlers({ + tableData: buildTableData(triggerFlash), + weekMonday: "2026-05-18", + onNavigateToDay, + onOpenSlideOver: vi.fn(), + onOpenMedia: vi.fn(), + onPatch, + onScheduleHighlight: vi.fn(), + }), + ); + + result.current.onScheduleDate("wo-1", "2026-05-21"); + + expect(onPatch).toHaveBeenCalledWith("wo-1", applySchedulePatch("2026-05-21", false)); + expect(onNavigateToDay).toHaveBeenCalledWith("2026-05-21"); + expect(triggerFlash).toHaveBeenCalledWith("wo-1"); + expect(rescheduleNotify).not.toHaveBeenCalled(); + }); + + it("sends cross-week view-it navigation to the target day, not Monday", () => { + const onNavigateToDay = vi.fn(); + + const { result } = renderHook(() => + useWoTableRowHandlers({ + tableData: buildTableData(), + weekMonday: "2026-05-18", + onNavigateToDay, + onOpenSlideOver: vi.fn(), + onOpenMedia: vi.fn(), + onPatch: vi.fn(), + onScheduleHighlight: vi.fn(), + }), + ); + + result.current.onScheduleDate("wo-1", "2026-05-27"); + + expect(onNavigateToDay).not.toHaveBeenCalled(); + expect(rescheduleNotify).toHaveBeenCalledWith( + "2026-05-27", + expect.objectContaining({ onNavigate: expect.any(Function) }), + ); + + const { onNavigate } = rescheduleNotify.mock.calls[0][1] as { onNavigate: () => void }; + onNavigate(); + expect(onNavigateToDay).toHaveBeenCalledWith("2026-05-27"); + }); + + it("opens Advanced Filters for week-only moves instead of the removed Unscheduled section", () => { + 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.onScheduleWeek("wo-1", "2026-05-25"); + + expect(onPatch).toHaveBeenCalledWith( + "wo-1", + expect.objectContaining({ + scheduledOn: "", + scheduleWeekOnly: true, + targetWeek: "2026-05-25", + }), + ); + expect(weekMoveNotify).toHaveBeenCalled(); + + const { onNavigate } = weekMoveNotify.mock.calls[0][1] as { onNavigate: () => void }; + onNavigate(); + expect(onNavigateToDay).toHaveBeenCalledWith("2026-05-25"); + expect(onOpenAdvancedFilters).toHaveBeenCalled(); + }); +}); diff --git a/src/test/app/(protected)/workorders/week-day-nav.test.tsx b/src/test/app/(protected)/workorders/week-day-nav.test.tsx index 8b7151f4..935df45f 100644 --- a/src/test/app/(protected)/workorders/week-day-nav.test.tsx +++ b/src/test/app/(protected)/workorders/week-day-nav.test.tsx @@ -99,4 +99,15 @@ describe("useWorkOrdersListFilters day state (SH-240)", () => { expect(result.current.weekMonday).toBe("2026-08-10"); expect(result.current.selectedDayIso).toBe("2026-08-10"); }); + + it("follows a scheduled date onto that weekday instead of resetting to Monday", () => { + const { result } = renderHook(() => useWorkOrdersListFilters()); + + act(() => { + result.current.navigateToDay("2026-05-20"); + }); + + expect(result.current.weekMonday).toBe("2026-05-18"); + expect(result.current.selectedDayIso).toBe("2026-05-20"); + }); }); diff --git a/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx b/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx index 2d0f7acf..a8e0c15e 100644 --- a/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx +++ b/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx @@ -51,6 +51,7 @@ vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () = selectedDayIso: "2026-07-20", setWeekMonday: vi.fn(), setSelectedDayIso: vi.fn(), + navigateToDay: vi.fn(), dispatcherIds: new Set(), setDispatcherIds: vi.fn(), typeFilter: "All", diff --git a/src/test/app/(protected)/workorders/work-orders-board-core-affordances.test.tsx b/src/test/app/(protected)/workorders/work-orders-board-core-affordances.test.tsx index 6b9284ad..42074864 100644 --- a/src/test/app/(protected)/workorders/work-orders-board-core-affordances.test.tsx +++ b/src/test/app/(protected)/workorders/work-orders-board-core-affordances.test.tsx @@ -47,6 +47,7 @@ vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () = selectedDayIso: "2026-07-20", setWeekMonday: vi.fn(), setSelectedDayIso: vi.fn(), + navigateToDay: vi.fn(), dispatcherIds: new Set(), setDispatcherIds: vi.fn(), typeFilter: "All", diff --git a/src/test/domain/work-orders/utils/week-day-nav-utils.test.ts b/src/test/domain/work-orders/utils/week-day-nav-utils.test.ts index 7e7db075..bc56591d 100644 --- a/src/test/domain/work-orders/utils/week-day-nav-utils.test.ts +++ b/src/test/domain/work-orders/utils/week-day-nav-utils.test.ts @@ -6,7 +6,9 @@ import { firstWeekday, formatDayTabLabel, initialSelectedDay, + isDayScopedSearchEmpty, rowMatchesSelectedDay, + scheduledDayTab, weekdaysMonFri, } from "@/domain/work-orders/utils/week-day-nav-utils"; import { addDaysIso } from "@/domain/work-orders/utils/wizard-date-utils"; @@ -119,4 +121,26 @@ describe("week-day-nav-utils (SH-240)", () => { expect(group.label).toBe("Mon · 05/18"); expect(group.items.map((item) => item.id)).toEqual(["mon"]); }); + + it("maps a scheduled weekday onto its week and day tab", () => { + expect(scheduledDayTab("2026-05-20")).toEqual({ + weekMonday: "2026-05-18", + selectedDayIso: "2026-05-20", + }); + }); + + it("treats search as empty only when the selected day and Unassigned have no hits", () => { + expect( + isDayScopedSearchEmpty({ searchActive: true, unassignedCount: 0, dayItemCount: 0 }), + ).toBe(true); + expect( + isDayScopedSearchEmpty({ searchActive: true, unassignedCount: 0, dayItemCount: 1 }), + ).toBe(false); + expect( + isDayScopedSearchEmpty({ searchActive: true, unassignedCount: 2, dayItemCount: 0 }), + ).toBe(false); + expect( + isDayScopedSearchEmpty({ searchActive: false, unassignedCount: 0, dayItemCount: 0 }), + ).toBe(false); + }); });