mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-04 07:52:08 +00:00
Merge branch 'dev' into feat/ab/sh-195-date-header-tooltips
This commit is contained in:
commit
c6ed6072ff
6 changed files with 165 additions and 9 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 99 KiB |
|
|
@ -5,31 +5,49 @@ import { ToolbarButton } from "@/components/ui/toolbar-button";
|
||||||
|
|
||||||
type WorkOrdersListHeaderProps = {
|
type WorkOrdersListHeaderProps = {
|
||||||
search: string;
|
search: string;
|
||||||
|
/** Compact label of the active board week (e.g. "5/4–5/8"). */
|
||||||
|
weekLabel: string;
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
/** Omit until Advanced Search slice mounts the sheet. */
|
/** Omit until Advanced Search slice mounts the sheet. */
|
||||||
onOpenAdvancedFilters?: () => void;
|
onOpenAdvancedFilters?: () => void;
|
||||||
/** Omit until wizard shell slice mounts New WO. */
|
/** Omit until wizard shell slice mounts New WO. */
|
||||||
onNewWorkOrder?: () => void;
|
onNewWorkOrder?: () => void;
|
||||||
activeFilterCount: number;
|
activeFilterCount: number;
|
||||||
|
/** True while Advanced Filters are applied; results are global and the keyword box is inert. */
|
||||||
|
advancedFiltersActive?: boolean;
|
||||||
isFetching?: boolean;
|
isFetching?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function WorkOrdersListHeader({
|
export function WorkOrdersListHeader({
|
||||||
search,
|
search,
|
||||||
|
weekLabel,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onOpenAdvancedFilters,
|
onOpenAdvancedFilters,
|
||||||
onNewWorkOrder,
|
onNewWorkOrder,
|
||||||
activeFilterCount,
|
activeFilterCount,
|
||||||
|
advancedFiltersActive = false,
|
||||||
isFetching = false,
|
isFetching = false,
|
||||||
}: WorkOrdersListHeaderProps) {
|
}: WorkOrdersListHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SearchInput
|
<div className="relative">
|
||||||
value={search}
|
<SearchInput
|
||||||
onChange={onSearchChange}
|
value={search}
|
||||||
placeholder="Search this week…"
|
onChange={onSearchChange}
|
||||||
aria-label="Search work orders"
|
placeholder="Search this week…"
|
||||||
/>
|
aria-label="Search work orders"
|
||||||
|
/>
|
||||||
|
{/* Keyword search is scoped to the active week; make that scope visible. */}
|
||||||
|
{!advancedFiltersActive && search.trim() !== "" && Boolean(weekLabel) && (
|
||||||
|
<span
|
||||||
|
role="status"
|
||||||
|
className="absolute left-0 whitespace-nowrap text-(length:--text-xs) text-muted-foreground"
|
||||||
|
style={{ top: "calc(100% + 4px)" }}
|
||||||
|
>
|
||||||
|
Searching within {weekLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{Boolean(onOpenAdvancedFilters) && (
|
{Boolean(onOpenAdvancedFilters) && (
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ export function useWorkOrdersBoardSource(filters: FiltersReturn): WorkOrdersBoar
|
||||||
() =>
|
() =>
|
||||||
buildResolvedBoardSearchParams({
|
buildResolvedBoardSearchParams({
|
||||||
advApplied: filters.advApplied,
|
advApplied: filters.advApplied,
|
||||||
search: filters.search,
|
// Advanced Filters are global by design; the week-scoped keyword box has no effect.
|
||||||
|
search: "",
|
||||||
page: filters.page,
|
page: filters.page,
|
||||||
pageSize: filters.pageSize,
|
pageSize: filters.pageSize,
|
||||||
sortBy: filters.sortBy,
|
sortBy: filters.sortBy,
|
||||||
|
|
@ -43,7 +44,6 @@ export function useWorkOrdersBoardSource(filters: FiltersReturn): WorkOrdersBoar
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
filters.advApplied,
|
filters.advApplied,
|
||||||
filters.search,
|
|
||||||
filters.page,
|
filters.page,
|
||||||
filters.pageSize,
|
filters.pageSize,
|
||||||
filters.sortBy,
|
filters.sortBy,
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,12 @@ export default function WorkOrdersListPage() {
|
||||||
actions={
|
actions={
|
||||||
<WorkOrdersListHeader
|
<WorkOrdersListHeader
|
||||||
search={filters.search}
|
search={filters.search}
|
||||||
|
weekLabel={filters.weekLabel}
|
||||||
onSearchChange={filters.handleSearchChange}
|
onSearchChange={filters.handleSearchChange}
|
||||||
onOpenAdvancedFilters={() => filters.setAdvOpen(true)}
|
onOpenAdvancedFilters={() => filters.setAdvOpen(true)}
|
||||||
onNewWorkOrder={() => page.setWizardOpen(true)}
|
onNewWorkOrder={() => page.setWizardOpen(true)}
|
||||||
activeFilterCount={filters.activeFilterCount}
|
activeFilterCount={filters.activeFilterCount}
|
||||||
|
advancedFiltersActive={filters.advApplied != null}
|
||||||
isFetching={tableData.isFetching && !tableData.isLoading}
|
isFetching={tableData.isFetching && !tableData.isLoading}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,12 @@ vi.mock("@/app/(protected)/workorders/_components/list/table/wo-table", () => ({
|
||||||
describe("WorkOrdersListHeader slice affordances", () => {
|
describe("WorkOrdersListHeader slice affordances", () => {
|
||||||
it("hides New WO and Advanced Filters when callbacks are omitted", () => {
|
it("hides New WO and Advanced Filters when callbacks are omitted", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<WorkOrdersListHeader search="" onSearchChange={vi.fn()} activeFilterCount={0} />,
|
<WorkOrdersListHeader
|
||||||
|
search=""
|
||||||
|
weekLabel="5/4–5/8"
|
||||||
|
onSearchChange={vi.fn()}
|
||||||
|
activeFilterCount={0}
|
||||||
|
/>,
|
||||||
{ withAuth: false },
|
{ withAuth: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -202,6 +207,7 @@ describe("WorkOrdersListHeader slice affordances", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<WorkOrdersListHeader
|
<WorkOrdersListHeader
|
||||||
search=""
|
search=""
|
||||||
|
weekLabel="5/4–5/8"
|
||||||
onSearchChange={vi.fn()}
|
onSearchChange={vi.fn()}
|
||||||
onNewWorkOrder={vi.fn()}
|
onNewWorkOrder={vi.fn()}
|
||||||
onOpenAdvancedFilters={vi.fn()}
|
onOpenAdvancedFilters={vi.fn()}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { act, renderHook, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { EMPTY_ADVANCED_FILTERS } from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||||
|
import { createTestQueryClient, renderWithProviders } from "@/test/test-utils";
|
||||||
|
|
||||||
|
const { searchBoard, getBoard, emptyList } = vi.hoisted(() => ({
|
||||||
|
searchBoard: vi.fn(),
|
||||||
|
getBoard: vi.fn(),
|
||||||
|
emptyList: [] as const,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/hooks/use-debounce", () => ({
|
||||||
|
useDebounce: <T,>(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 }) => (
|
||||||
|
<QueryClientProvider client={client}>{children}</QueryClientProvider>
|
||||||
|
);
|
||||||
|
return renderHook(() => useBoardWithFilters(), { wrapper });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("keyword search scope indicator", () => {
|
||||||
|
it("shows no scope badge while the search box is empty", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<WorkOrdersListHeader
|
||||||
|
search=""
|
||||||
|
weekLabel="5/4–5/8"
|
||||||
|
onSearchChange={vi.fn()}
|
||||||
|
activeFilterCount={0}
|
||||||
|
/>,
|
||||||
|
{ withAuth: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText(/searching within/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("names the active week while a keyword is typed", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<WorkOrdersListHeader
|
||||||
|
search="hvac"
|
||||||
|
weekLabel="5/4–5/8"
|
||||||
|
onSearchChange={vi.fn()}
|
||||||
|
activeFilterCount={0}
|
||||||
|
/>,
|
||||||
|
{ withAuth: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Searching within 5/4–5/8")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides the scope label while Advanced Filters are active", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<WorkOrdersListHeader
|
||||||
|
search="hvac"
|
||||||
|
weekLabel="5/4–5/8"
|
||||||
|
onSearchChange={vi.fn()}
|
||||||
|
activeFilterCount={1}
|
||||||
|
advancedFiltersActive
|
||||||
|
/>,
|
||||||
|
{ 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<string, unknown>;
|
||||||
|
expect(params.search).toBeUndefined();
|
||||||
|
expect(params).not.toHaveProperty("weekStart");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue