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"; } from "@/domain/work-orders/types/work-order-table-row";
import { applySchedulePatch } from "@/domain/work-orders/use-cases/use-work-order-table-mutations"; 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 { 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 type { ReturnTypeOfUseWorkOrdersTableData } from "./wo-table-types";
import { rescheduleNotify, weekMoveNotify } from "./reschedule-notify"; import { rescheduleNotify, weekMoveNotify } from "./reschedule-notify";
import type { WoTableRowHandlers } from "./wo-table-row"; import type { WoTableRowHandlers } from "./wo-table-row";
@ -13,7 +12,8 @@ import type { WoTableRowHandlers } from "./wo-table-row";
type UseWoTableRowHandlersOptions = { type UseWoTableRowHandlersOptions = {
tableData: ReturnTypeOfUseWorkOrdersTableData; tableData: ReturnTypeOfUseWorkOrdersTableData;
weekMonday: string; weekMonday: string;
onWeekChange: (mondayIso: string) => void; onNavigateToDay: (dateIso: string) => void;
onOpenAdvancedFilters?: () => void;
onOpenSlideOver: (row: WorkOrderTableRow, edit?: boolean) => void; onOpenSlideOver: (row: WorkOrderTableRow, edit?: boolean) => void;
onOpenCompDoc?: (row: WorkOrderTableRow) => void; onOpenCompDoc?: (row: WorkOrderTableRow) => void;
onOpenUplifts?: (row: WorkOrderTableRow) => void; onOpenUplifts?: (row: WorkOrderTableRow) => void;
@ -26,7 +26,8 @@ type UseWoTableRowHandlersOptions = {
export function useWoTableRowHandlers({ export function useWoTableRowHandlers({
tableData, tableData,
weekMonday, weekMonday,
onWeekChange, onNavigateToDay,
onOpenAdvancedFilters,
onOpenSlideOver, onOpenSlideOver,
onOpenCompDoc, onOpenCompDoc,
onOpenUplifts, onOpenUplifts,
@ -35,14 +36,7 @@ export function useWoTableRowHandlers({
onScheduleHighlight, onScheduleHighlight,
onDuplicateFound, onDuplicateFound,
}: UseWoTableRowHandlersOptions): WoTableRowHandlers { }: UseWoTableRowHandlersOptions): WoTableRowHandlers {
const { const { dragState: dragStateRef, setDragViz, reorderRows, endDrag, triggerFlash } = tableData;
dragState: dragStateRef,
setDragViz,
reorderRows,
endDrag,
setUnschedOpen,
triggerFlash,
} = tableData;
return useMemo( return useMemo(
() => ({ () => ({
@ -80,14 +74,16 @@ export function useWoTableRowHandlers({
onScheduleDate: (id: string | number, date: string) => { onScheduleDate: (id: string | number, date: string) => {
onPatch(id, applySchedulePatch(date, false)); onPatch(id, applySchedulePatch(date, false));
if (shouldNotifyCrossWeek(date, weekMonday)) { if (shouldNotifyCrossWeek(date, weekMonday)) {
const targetMonday = mondayOf(date);
rescheduleNotify(date, { rescheduleNotify(date, {
onNavigate: () => { onNavigate: () => {
onWeekChange(targetMonday); onNavigateToDay(date);
setTimeout(() => triggerFlash(id), 180); setTimeout(() => triggerFlash(id), 180);
}, },
}); });
return;
} }
onNavigateToDay(date);
triggerFlash(id);
}, },
onScheduleWeek: (id: string | number, week: string) => { onScheduleWeek: (id: string | number, week: string) => {
onPatch(id, { onPatch(id, {
@ -97,12 +93,10 @@ export function useWoTableRowHandlers({
dayGroup: "", dayGroup: "",
dayLabel: "", dayLabel: "",
}); });
setUnschedOpen(true);
triggerFlash(id);
weekMoveNotify(week, { weekMoveNotify(week, {
onNavigate: () => { onNavigate: () => {
setUnschedOpen(true); onNavigateToDay(week);
setTimeout(() => triggerFlash(id), 180); onOpenAdvancedFilters?.();
}, },
}); });
}, },
@ -138,10 +132,10 @@ export function useWoTableRowHandlers({
setDragViz, setDragViz,
reorderRows, reorderRows,
endDrag, endDrag,
setUnschedOpen,
triggerFlash, triggerFlash,
weekMonday, weekMonday,
onWeekChange, onNavigateToDay,
onOpenAdvancedFilters,
onOpenSlideOver, onOpenSlideOver,
onOpenCompDoc, onOpenCompDoc,
onOpenUplifts, onOpenUplifts,

View file

@ -29,7 +29,7 @@ type WoTableProps = {
vendors: VendorDropdownItem[]; vendors: VendorDropdownItem[];
pmTypes: string[]; pmTypes: string[];
weekMonday: string; weekMonday: string;
onWeekChange: (mondayIso: string) => void; onNavigateToDay: (dateIso: string) => void;
onOpenSlideOver: (row: WorkOrderTableRow, edit?: boolean) => void; onOpenSlideOver: (row: WorkOrderTableRow, edit?: boolean) => void;
/** Omit until completion-doc slice mounts CompDocDialog. */ /** Omit until completion-doc slice mounts CompDocDialog. */
onOpenCompDoc?: (row: WorkOrderTableRow) => void; onOpenCompDoc?: (row: WorkOrderTableRow) => void;
@ -75,7 +75,7 @@ export function WoTable({
vendors, vendors,
pmTypes, pmTypes,
weekMonday, weekMonday,
onWeekChange, onNavigateToDay,
onOpenSlideOver, onOpenSlideOver,
onOpenCompDoc, onOpenCompDoc,
onOpenUplifts, onOpenUplifts,
@ -98,7 +98,8 @@ export function WoTable({
const handlers = useWoTableRowHandlers({ const handlers = useWoTableRowHandlers({
tableData, tableData,
weekMonday, weekMonday,
onWeekChange, onNavigateToDay,
onOpenAdvancedFilters,
onOpenSlideOver, onOpenSlideOver,
onOpenCompDoc, onOpenCompDoc,
onOpenUplifts, onOpenUplifts,

View file

@ -20,6 +20,7 @@ import { compactWeekLabel, currentWeekMonday } from "@/domain/work-orders/utils/
import { import {
firstWeekday, firstWeekday,
initialSelectedDay, initialSelectedDay,
scheduledDayTab,
weekdaysMonFri, weekdaysMonFri,
} from "@/domain/work-orders/utils/week-day-nav-utils"; } from "@/domain/work-orders/utils/week-day-nav-utils";
@ -72,6 +73,13 @@ export function useWorkOrdersListFilters() {
setSelectedDayIsoState(value); setSelectedDayIsoState(value);
}; };
const navigateToDay = (dateIso: string) => {
const tab = scheduledDayTab(dateIso);
setWeekMondayState(tab.weekMonday);
setSelectedDayIsoState(tab.selectedDayIso);
setPage(0);
};
const setDispatcherIds = (value: Set<string>) => { const setDispatcherIds = (value: Set<string>) => {
setDispatcherIdsState(new Set(value)); setDispatcherIdsState(new Set(value));
setPage(0); setPage(0);
@ -156,6 +164,7 @@ export function useWorkOrdersListFilters() {
setPageSize, setPageSize,
setWeekMonday, setWeekMonday,
setSelectedDayIso, setSelectedDayIso,
navigateToDay,
setDispatcherIds, setDispatcherIds,
setTypeFilter, setTypeFilter,
setSortBy, 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 { currentWeekMonday } from "@/domain/work-orders/utils/list-week-utils";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils"; 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 { import {
composeBoardSections, composeBoardSections,
filterByBarAndSearch, filterByBarAndSearch,
@ -83,12 +86,17 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
); );
const searchActive = filters.search.trim() !== ""; 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 displayGroups = dayGroups;
const dayVisibleCount = displayGroups[0]?.items.length ?? 0; 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 = const todayKey =
filters.weekMonday === currentWeekMonday() filters.weekMonday === currentWeekMonday()

View file

@ -90,7 +90,7 @@ export default function WorkOrdersListPage() {
vendors={page.vendors} vendors={page.vendors}
pmTypes={page.pmTypes} pmTypes={page.pmTypes}
weekMonday={filters.weekMonday} weekMonday={filters.weekMonday}
onWeekChange={filters.setWeekMonday} onNavigateToDay={filters.navigateToDay}
onOpenSlideOver={page.handleOpenSlideOver} onOpenSlideOver={page.handleOpenSlideOver}
onOpenCompDoc={(row) => openCompletionDoc(row, page.setDocRow)} onOpenCompDoc={(row) => openCompletionDoc(row, page.setDocRow)}
onOpenMedia={page.setMediaRow} onOpenMedia={page.setMediaRow}

View file

@ -4,7 +4,7 @@ import type {
} from "@/domain/work-orders/types/work-order-table-row"; } from "@/domain/work-orders/types/work-order-table-row";
import { isLiveWeek } from "@/domain/work-orders/utils/list-week-utils"; import { isLiveWeek } from "@/domain/work-orders/utils/list-week-utils";
import { isDateInWeek } from "@/domain/work-orders/utils/table-day-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; 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; 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 { export function initialSelectedDay(weekMonday: string, today = todayIso()): string {
if (isLiveWeek(weekMonday) && isDateInWeek(today, weekMonday)) { if (isLiveWeek(weekMonday) && isDateInWeek(today, weekMonday)) {
return today; return today;

View file

@ -26,7 +26,7 @@ describe("useWoTableRowHandlers onDuplicateFound", () => {
useWoTableRowHandlers({ useWoTableRowHandlers({
tableData: buildTableData(), tableData: buildTableData(),
weekMonday: "2026-07-20", weekMonday: "2026-07-20",
onWeekChange: vi.fn(), onNavigateToDay: vi.fn(),
onOpenSlideOver, onOpenSlideOver,
onOpenMedia: vi.fn(), onOpenMedia: vi.fn(),
onPatch: 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.weekMonday).toBe("2026-08-10");
expect(result.current.selectedDayIso).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", selectedDayIso: "2026-07-20",
setWeekMonday: vi.fn(), setWeekMonday: vi.fn(),
setSelectedDayIso: vi.fn(), setSelectedDayIso: vi.fn(),
navigateToDay: vi.fn(),
dispatcherIds: new Set<string>(), dispatcherIds: new Set<string>(),
setDispatcherIds: vi.fn(), setDispatcherIds: vi.fn(),
typeFilter: "All", typeFilter: "All",

View file

@ -47,6 +47,7 @@ vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () =
selectedDayIso: "2026-07-20", selectedDayIso: "2026-07-20",
setWeekMonday: vi.fn(), setWeekMonday: vi.fn(),
setSelectedDayIso: vi.fn(), setSelectedDayIso: vi.fn(),
navigateToDay: vi.fn(),
dispatcherIds: new Set<string>(), dispatcherIds: new Set<string>(),
setDispatcherIds: vi.fn(), setDispatcherIds: vi.fn(),
typeFilter: "All", typeFilter: "All",

View file

@ -6,7 +6,9 @@ import {
firstWeekday, firstWeekday,
formatDayTabLabel, formatDayTabLabel,
initialSelectedDay, initialSelectedDay,
isDayScopedSearchEmpty,
rowMatchesSelectedDay, rowMatchesSelectedDay,
scheduledDayTab,
weekdaysMonFri, weekdaysMonFri,
} from "@/domain/work-orders/utils/week-day-nav-utils"; } from "@/domain/work-orders/utils/week-day-nav-utils";
import { addDaysIso } from "@/domain/work-orders/utils/wizard-date-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.label).toBe("Mon · 05/18");
expect(group.items.map((item) => item.id)).toEqual(["mon"]); 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);
});
}); });