fix(work-orders): keep search and reschedule on the selected day (SH-240)

Empty search and Schedule On must follow the day-tab board, not the whole week or Unscheduled.
This commit is contained in:
Arthur Bassi 2026-08-20 09:21:45 -03:00
parent 8ab56d9782
commit 0c75bd42f4
12 changed files with 214 additions and 30 deletions

View file

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

View file

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

View file

@ -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<string>) => {
setDispatcherIdsState(new Set(value));
setPage(0);
@ -156,6 +164,7 @@ export function useWorkOrdersListFilters() {
setPageSize,
setWeekMonday,
setSelectedDayIso,
navigateToDay,
setDispatcherIds,
setTypeFilter,
setSortBy,

View file

@ -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()

View file

@ -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}

View file

@ -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;

View file

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

View file

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

View file

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

View file

@ -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<string>(),
setDispatcherIds: vi.fn(),
typeFilter: "All",

View file

@ -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<string>(),
setDispatcherIds: vi.fn(),
typeFilter: "All",

View file

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