diff --git a/e2e/__screenshots__/work-orders/work-orders.visual.spec.ts/wo-empty.png b/e2e/__screenshots__/work-orders/work-orders.visual.spec.ts/wo-empty.png index e64d4c12..b21a8ded 100644 Binary files a/e2e/__screenshots__/work-orders/work-orders.visual.spec.ts/wo-empty.png and b/e2e/__screenshots__/work-orders/work-orders.visual.spec.ts/wo-empty.png differ diff --git a/src/app/(protected)/workorders/_components/list/work-orders-list-header.tsx b/src/app/(protected)/workorders/_components/list/work-orders-list-header.tsx index 8208d7de..3f52ea0f 100644 --- a/src/app/(protected)/workorders/_components/list/work-orders-list-header.tsx +++ b/src/app/(protected)/workorders/_components/list/work-orders-list-header.tsx @@ -5,31 +5,49 @@ import { ToolbarButton } from "@/components/ui/toolbar-button"; type WorkOrdersListHeaderProps = { search: string; + /** Compact label of the active board week (e.g. "5/4–5/8"). */ + weekLabel: string; onSearchChange: (value: string) => void; /** Omit until Advanced Search slice mounts the sheet. */ onOpenAdvancedFilters?: () => void; /** Omit until wizard shell slice mounts New WO. */ onNewWorkOrder?: () => void; activeFilterCount: number; + /** True while Advanced Filters are applied; results are global and the keyword box is inert. */ + advancedFiltersActive?: boolean; isFetching?: boolean; }; export function WorkOrdersListHeader({ search, + weekLabel, onSearchChange, onOpenAdvancedFilters, onNewWorkOrder, activeFilterCount, + advancedFiltersActive = false, isFetching = false, }: WorkOrdersListHeaderProps) { return ( <> - +
+ + {/* Keyword search is scoped to the active week; make that scope visible. */} + {!advancedFiltersActive && search.trim() !== "" && Boolean(weekLabel) && ( + + Searching within {weekLabel} + + )} +
{Boolean(onOpenAdvancedFilters) && ( buildResolvedBoardSearchParams({ advApplied: filters.advApplied, - search: filters.search, + // Advanced Filters are global by design; the week-scoped keyword box has no effect. + search: "", page: filters.page, pageSize: filters.pageSize, sortBy: filters.sortBy, @@ -43,7 +44,6 @@ export function useWorkOrdersBoardSource(filters: FiltersReturn): WorkOrdersBoar }), [ filters.advApplied, - filters.search, filters.page, filters.pageSize, filters.sortBy, diff --git a/src/app/(protected)/workorders/index.tsx b/src/app/(protected)/workorders/index.tsx index 571251cb..e12e040d 100644 --- a/src/app/(protected)/workorders/index.tsx +++ b/src/app/(protected)/workorders/index.tsx @@ -43,10 +43,12 @@ export default function WorkOrdersListPage() { actions={ filters.setAdvOpen(true)} onNewWorkOrder={() => page.setWizardOpen(true)} activeFilterCount={filters.activeFilterCount} + advancedFiltersActive={filters.advApplied != null} isFetching={tableData.isFetching && !tableData.isLoading} /> } 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 c3fb07b9..7e4df369 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 @@ -189,7 +189,12 @@ vi.mock("@/app/(protected)/workorders/_components/list/table/wo-table", () => ({ describe("WorkOrdersListHeader slice affordances", () => { it("hides New WO and Advanced Filters when callbacks are omitted", () => { renderWithProviders( - , + , { withAuth: false }, ); @@ -202,6 +207,7 @@ describe("WorkOrdersListHeader slice affordances", () => { renderWithProviders( ({ + searchBoard: vi.fn(), + getBoard: vi.fn(), + emptyList: [] as const, +})); + +vi.mock("@/hooks/use-debounce", () => ({ + useDebounce: (value: T) => value, +})); + +vi.mock("@/domain/work-orders/api/work-orders-api", () => ({ + workOrdersApi: { + searchBoard: (...args: unknown[]) => searchBoard(...args), + getBoard: (...args: unknown[]) => getBoard(...args), + }, +})); + +vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({ + useWorkOrderLocations: () => ({ data: emptyList }), + useWorkOrderVendors: () => ({ data: emptyList }), +})); + +import { WorkOrdersListHeader } from "@/app/(protected)/workorders/_components/list/work-orders-list-header"; +import { useWorkOrdersBoardSource } from "@/app/(protected)/workorders/_hooks/use-work-orders-board-source"; +import { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters"; + +const EMPTY_RESULT = { items: [], totalCount: 0, page: 0, pageSize: 25 }; + +function useBoardWithFilters() { + const filters = useWorkOrdersListFilters(); + useWorkOrdersBoardSource(filters); + return filters; +} + +function renderBoardSource() { + const client = createTestQueryClient(); + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ); + return renderHook(() => useBoardWithFilters(), { wrapper }); +} + +describe("keyword search scope indicator", () => { + it("shows no scope badge while the search box is empty", () => { + renderWithProviders( + , + { withAuth: false }, + ); + + expect(screen.queryByText(/searching within/i)).not.toBeInTheDocument(); + }); + + it("names the active week while a keyword is typed", () => { + renderWithProviders( + , + { withAuth: false }, + ); + + expect(screen.getByText("Searching within 5/4–5/8")).toBeInTheDocument(); + }); + + it("hides the scope label while Advanced Filters are active", () => { + renderWithProviders( + , + { withAuth: false }, + ); + + expect(screen.queryByText(/searching within/i)).not.toBeInTheDocument(); + }); +}); + +describe("keyword search vs Advanced Filters scope", () => { + beforeEach(() => { + searchBoard.mockReset().mockResolvedValue(EMPTY_RESULT); + getBoard.mockReset().mockResolvedValue(EMPTY_RESULT); + }); + + it("scopes keyword search to the active week", async () => { + const { result } = renderBoardSource(); + + act(() => { + result.current.setWeekMonday("2026-05-04"); + result.current.handleSearchChange("hvac"); + }); + + await waitFor(() => + expect(getBoard).toHaveBeenCalledWith( + expect.objectContaining({ weekStart: "2026-05-04", weekEnd: "2026-05-08", search: "hvac" }), + ), + ); + expect(searchBoard).not.toHaveBeenCalled(); + }); + + it("keeps Advanced Filters global and ignores the week-scoped keyword", async () => { + const { result } = renderBoardSource(); + + act(() => { + result.current.handleSearchChange("hvac"); + result.current.applyAdvancedFilters({ ...EMPTY_ADVANCED_FILTERS, rescheduled: true }); + }); + + await waitFor(() => expect(searchBoard).toHaveBeenCalled()); + const params = searchBoard.mock.lastCall?.[0] as Record; + expect(params.search).toBeUndefined(); + expect(params).not.toHaveProperty("weekStart"); + }); +});