diff --git a/e2e/work-orders/week-day-navigation.spec.ts b/e2e/work-orders/week-day-navigation.spec.ts index 5ec5402b..8a8e1245 100644 --- a/e2e/work-orders/week-day-navigation.spec.ts +++ b/e2e/work-orders/week-day-navigation.spec.ts @@ -80,6 +80,19 @@ test.describe("Work Orders week/day navigation (SH-240)", () => { await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({ timeout: 15_000, }); + const dispatcher = page.getByRole("button", { name: "All dispatchers" }); + await expect(dispatcher).toBeVisible(); + await expect( + page.getByLabel("Week and day").getByRole("button", { name: "All dispatchers" }), + ).toHaveCount(0); + const titleBox = await page.getByRole("heading", { name: "Work Orders" }).boundingBox(); + const dispatcherBox = await dispatcher.boundingBox(); + const weekBox = await page.getByRole("button", { name: "Week 8/17–8/21" }).boundingBox(); + expect(titleBox).toBeTruthy(); + expect(dispatcherBox).toBeTruthy(); + expect(weekBox).toBeTruthy(); + expect(dispatcherBox!.y).toBeGreaterThan(titleBox!.y); + expect(weekBox!.y).toBeGreaterThan(dispatcherBox!.y); await expect(page.getByRole("button", { name: "Week 8/17–8/21" })).toBeVisible(); await expect(page.getByRole("button", { name: "Week 8/17–8/21" })).toContainText("Live"); diff --git a/src/app/(protected)/workorders/_components/list/filter-bar/dispatcher-filter.tsx b/src/app/(protected)/workorders/_components/list/filter-bar/dispatcher-filter.tsx index 6bee3519..3998b53f 100644 --- a/src/app/(protected)/workorders/_components/list/filter-bar/dispatcher-filter.tsx +++ b/src/app/(protected)/workorders/_components/list/filter-bar/dispatcher-filter.tsx @@ -27,6 +27,7 @@ function userInitials(name: string): string { function DispatcherAvatar({ name, color }: { name: string; color: string }) { return ( @@ -75,8 +76,8 @@ export function DispatcherFilter({ return ( @@ -89,35 +90,13 @@ export function DispatcherFilter({ onAnchor={setAnchor} open={open} disabled={disabled} - className="flex w-auto shrink-0 items-center gap-2 border whitespace-nowrap transition-colors" - style={ - !allSelected - ? { - height: 38, - padding: "0 12px", - borderRadius: 6, - fontSize: 13, - fontWeight: 600, - background: "var(--color-primary-tint)", - borderColor: "#bfe2f5", - color: "var(--primary)", - } - : { - height: 38, - padding: "0 12px", - borderRadius: 6, - fontSize: 13, - fontWeight: 600, - background: "var(--card)", - borderColor: "var(--border)", - color: "var(--foreground)", - } - } + className="flex w-auto shrink-0 items-center gap-1.5 whitespace-nowrap transition-colors" + style={{ fontSize: 13, fontWeight: 500, color: "var(--muted-foreground)" }} aria-haspopup="listbox" > - + {label} - + void; selectedDayIso: string; onSelectedDayChange: (dayIso: string) => void; - dispatcherIds: Set; - onDispatcherChange: (ids: Set) => void; typeFilter: WorkOrderTypeFilter; onTypeChange: (value: WorkOrderTypeFilter) => void; - users: WorkOrderUser[]; - usersLoading?: boolean; - currentUserId?: string | number; filteredCount: number; scopeTotalCount: number; disabled?: boolean; @@ -28,13 +21,8 @@ export function WorkOrdersFilterBar({ onWeekChange, selectedDayIso, onSelectedDayChange, - dispatcherIds, - onDispatcherChange, typeFilter, onTypeChange, - users, - usersLoading = false, - currentUserId, filteredCount, scopeTotalCount, disabled = false, @@ -46,29 +34,21 @@ export function WorkOrdersFilterBar({ className="-mx-2.5 shrink-0 border-b border-(--color-border) bg-(--color-bg-surface)" aria-disabled={disabled} > -
- +
+
+ + +
{resultLabel}
-
- - -
diff --git a/src/app/(protected)/workorders/index.tsx b/src/app/(protected)/workorders/index.tsx index 8d5b1739..5707a4ef 100644 --- a/src/app/(protected)/workorders/index.tsx +++ b/src/app/(protected)/workorders/index.tsx @@ -2,6 +2,7 @@ import { Box } from "@mui/material"; import { PageHeader } from "@/components/ui/page-header"; import { QueryErrorPanel } from "@/components/ui/query-error-panel"; import { WorkOrdersAdvancedFiltersBanner } from "@/app/(protected)/workorders/_components/list/work-orders-advanced-filters-banner"; +import { DispatcherFilter } from "@/app/(protected)/workorders/_components/list/filter-bar/dispatcher-filter"; import { WorkOrdersFilterBar } from "@/app/(protected)/workorders/_components/list/filter-bar/work-orders-filter-bar"; import { WorkOrdersListHeader } from "@/app/(protected)/workorders/_components/list/work-orders-list-header"; import { WorkOrdersListPagePanels } from "@/app/(protected)/workorders/_components/list/work-orders-list-page-panels"; @@ -30,6 +31,16 @@ export default function WorkOrdersListPage() { surface eyebrow="OPERATIONS" title="Work Orders" + belowTitle={ + + } actions={ {subtitle} + {Boolean(belowTitle) &&
{belowTitle}
} {Boolean(actions) && ( { + it("renders a plain trigger without a boxed border", () => { + renderWithProviders( + , + { withAuth: false }, + ); + + const trigger = screen.getByRole("button", { name: "All dispatchers" }); + expect(trigger.className.split(/\s+/)).not.toContain("border"); + expect(trigger.style.height).not.toBe("38px"); + expect(trigger.style.borderColor).toBe(""); + expect(trigger.style.background).toBe(""); + expect(trigger.style.fontWeight).toBe("500"); + }); + + it("opens the menu from the keyboard", async () => { + const user = userEvent.setup(); + renderWithProviders( + , + { withAuth: false }, + ); + + screen.getByRole("button", { name: "All dispatchers" }).focus(); + await user.keyboard("{Enter}"); + expect(screen.getByRole("button", { name: "My WOs only" })).toBeInTheDocument(); + }); + + it("shows loading skeleton while dispatchers are fetching", () => { + renderWithProviders( + , + { withAuth: false }, + ); + + expect(screen.getByLabelText("Loading dispatchers")).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "All dispatchers" })).not.toBeInTheDocument(); + }); + + it("disables the trigger when requested", () => { + renderWithProviders( + , + { withAuth: false }, + ); + + expect(screen.getByRole("button", { name: "All dispatchers" })).toBeDisabled(); + }); + + it("labels My WOs, Unassigned, and a multi-select count", () => { + const { rerender } = renderWithProviders( + , + { withAuth: false }, + ); + expect(screen.getByRole("button", { name: "My WOs" })).toBeInTheDocument(); + + rerender( + , + ); + expect(screen.getByRole("button", { name: "Unassigned" })).toBeInTheDocument(); + + rerender( + , + ); + expect(screen.getByRole("button", { name: "2 selected" })).toBeInTheDocument(); + }); + + it("scopes All, My WOs, Unassigned, and a named dispatcher", async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + + renderWithProviders( + , + { withAuth: false }, + ); + + await user.click(screen.getByRole("button", { name: "All dispatchers" })); + await user.click(screen.getByRole("button", { name: "My WOs only" })); + expect(onChange).toHaveBeenLastCalledWith(new Set(["u1"])); + + await user.click(screen.getByRole("button", { name: "Unassigned" })); + expect(onChange).toHaveBeenLastCalledWith(new Set([ASSIGNEE_FILTER_UNASSIGNED])); + + await user.click(screen.getByRole("button", { name: "Blair Cho" })); + expect(onChange).toHaveBeenLastCalledWith(new Set(["u2"])); + + const menu = screen.getByRole("presentation"); + await user.click(within(menu).getByRole("button", { name: "All dispatchers" })); + expect(onChange).toHaveBeenLastCalledWith(new Set()); + }); +}); diff --git a/src/test/app/(protected)/workorders/work-orders-dispatcher-layout.test.tsx b/src/test/app/(protected)/workorders/work-orders-dispatcher-layout.test.tsx new file mode 100644 index 00000000..2ab48cfe --- /dev/null +++ b/src/test/app/(protected)/workorders/work-orders-dispatcher-layout.test.tsx @@ -0,0 +1,112 @@ +import { screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import WorkOrdersListPage from "@/app/(protected)/workorders/index"; +import { EMPTY_ADVANCED_FILTERS } from "@/domain/work-orders/types/work-order-advanced-filters"; +import { renderWithProviders } from "@/test/test-utils"; + +vi.mock("react-router", async () => { + const actual = await vi.importActual("react-router"); + return { + ...actual, + useNavigate: () => vi.fn(), + }; +}); + +vi.mock("@/providers/auth-context", () => ({ + useAuthContext: () => ({ user: { id: "u1" } }), +})); + +vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () => ({ + useWorkOrdersListFilters: () => ({ + search: "", + handleSearchChange: vi.fn(), + advOpen: false, + setAdvOpen: vi.fn(), + advApplied: null, + advInitial: EMPTY_ADVANCED_FILTERS, + applyAdvancedFilters: vi.fn(), + clearAdvancedFilters: vi.fn(), + clearAdvancedFilterChip: vi.fn(), + activeFilterCount: 0, + weekMonday: "2026-07-20", + selectedDayIso: "2026-07-20", + setWeekMonday: vi.fn(), + setSelectedDayIso: vi.fn(), + navigateToDay: vi.fn(), + dispatcherIds: new Set(), + setDispatcherIds: vi.fn(), + typeFilter: "All", + setTypeFilter: vi.fn(), + }), +})); + +vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-table-data", () => ({ + useWorkOrdersTableData: () => ({ + rows: [], + totalCount: 0, + scopeTotalCount: 0, + isFetching: false, + isLoading: false, + setSelectedId: vi.fn(), + patchRow: vi.fn(), + }), +})); + +vi.mock("@/domain/work-orders/use-cases/use-work-order-table-mutations", () => ({ + useWorkOrderTableMutations: () => ({ patchField: vi.fn() }), +})); + +vi.mock("@/domain/work-orders/use-cases/use-work-orders-list", () => ({ + useWorkOrderUsers: () => ({ + data: [{ id: "u1", name: "Dispatcher One" }], + }), +})); + +vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({ + useWorkOrderLocations: () => ({ data: [] }), + useWorkOrderVendors: () => ({ data: [] }), + useWorkOrderBoardDetail: () => ({ + data: undefined, + isLoading: false, + isFetching: false, + error: null, + refetch: vi.fn(), + }), +})); + +vi.mock("@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category", () => ({ + useDropdownOptionsByCategory: () => ({ data: [] }), +})); + +vi.mock("@/domain/work-orders/mocks/is-wo-table-mock-enabled", () => ({ + isWoTableMockEnabled: () => false, +})); + +vi.mock("@/app/(protected)/workorders/_components/list/table/wo-table", () => ({ + WoTable: () => null, +})); + +vi.mock("@/app/(protected)/workorders/_components/media/media-upload-modal", () => ({ + MediaUploadModal: () => null, +})); + +describe("Work Orders dispatcher filter layout (SH-242)", () => { + it("places the dispatcher trigger under the title and above week/day controls", () => { + renderWithProviders(, { route: "/workorders", withAuth: false }); + + const title = screen.getByRole("heading", { name: "Work Orders" }); + const dispatcher = screen.getByRole("button", { name: "All dispatchers" }); + const weekAndDay = screen.getByLabelText("Week and day"); + const week = screen.getByRole("button", { name: "Week 7/20–7/24" }); + + expect(title.compareDocumentPosition(dispatcher) & Node.DOCUMENT_POSITION_FOLLOWING).toBe( + Node.DOCUMENT_POSITION_FOLLOWING, + ); + expect(dispatcher.compareDocumentPosition(weekAndDay) & Node.DOCUMENT_POSITION_FOLLOWING).toBe( + Node.DOCUMENT_POSITION_FOLLOWING, + ); + expect(weekAndDay.contains(dispatcher)).toBe(false); + expect(weekAndDay.contains(week)).toBe(true); + expect(screen.getByText("0 of 0")).toBeInTheDocument(); + }); +});