mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 20:32:05 +00:00
Merge pull request #125 from Sea-Haven-Industries/feat/sh-242-dispatcher-filter-title
feat(work-orders): relocate dispatcher filter under the title (SH-242)
This commit is contained in:
commit
19a54e38c6
7 changed files with 282 additions and 64 deletions
|
|
@ -80,6 +80,19 @@ test.describe("Work Orders week/day navigation (SH-240)", () => {
|
||||||
await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({
|
await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({
|
||||||
timeout: 15_000,
|
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" })).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: "Week 8/17–8/21" })).toContainText("Live");
|
await expect(page.getByRole("button", { name: "Week 8/17–8/21" })).toContainText("Live");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ function userInitials(name: string): string {
|
||||||
function DispatcherAvatar({ name, color }: { name: string; color: string }) {
|
function DispatcherAvatar({ name, color }: { name: string; color: string }) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
|
aria-hidden
|
||||||
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[9px] font-semibold"
|
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[9px] font-semibold"
|
||||||
style={{ background: `${color}25`, color }}
|
style={{ background: `${color}25`, color }}
|
||||||
>
|
>
|
||||||
|
|
@ -75,8 +76,8 @@ export function DispatcherFilter({
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
width={160}
|
width={140}
|
||||||
height={38}
|
height={18}
|
||||||
animation="wave"
|
animation="wave"
|
||||||
aria-label="Loading dispatchers"
|
aria-label="Loading dispatchers"
|
||||||
/>
|
/>
|
||||||
|
|
@ -89,35 +90,13 @@ export function DispatcherFilter({
|
||||||
onAnchor={setAnchor}
|
onAnchor={setAnchor}
|
||||||
open={open}
|
open={open}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="flex w-auto shrink-0 items-center gap-2 border whitespace-nowrap transition-colors"
|
className="flex w-auto shrink-0 items-center gap-1.5 whitespace-nowrap transition-colors"
|
||||||
style={
|
style={{ fontSize: 13, fontWeight: 500, color: "var(--muted-foreground)" }}
|
||||||
!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)",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
>
|
>
|
||||||
<Users size={15} style={{ color: "var(--muted-foreground)" }} />
|
<Users size={13} aria-hidden style={{ color: "var(--muted-foreground)" }} />
|
||||||
{label}
|
{label}
|
||||||
<ChevronDown size={14} style={{ color: "var(--muted-foreground)" }} />
|
<ChevronDown size={13} aria-hidden style={{ color: "var(--muted-foreground)" }} />
|
||||||
</MenuAnchorButton>
|
</MenuAnchorButton>
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
|
||||||
import type { WorkOrderTypeFilter } from "@/domain/work-orders/types/work-order-list-filters";
|
import type { WorkOrderTypeFilter } from "@/domain/work-orders/types/work-order-list-filters";
|
||||||
import { DayTabs } from "@/app/(protected)/workorders/_components/list/filter-bar/day-tabs";
|
import { DayTabs } from "@/app/(protected)/workorders/_components/list/filter-bar/day-tabs";
|
||||||
import { DispatcherFilter } from "@/app/(protected)/workorders/_components/list/filter-bar/dispatcher-filter";
|
|
||||||
import { WeekSelector } from "@/app/(protected)/workorders/_components/list/filter-bar/week-selector";
|
import { WeekSelector } from "@/app/(protected)/workorders/_components/list/filter-bar/week-selector";
|
||||||
import { WoTypeSegment } from "@/app/(protected)/workorders/_components/list/filter-bar/wo-type-segment";
|
import { WoTypeSegment } from "@/app/(protected)/workorders/_components/list/filter-bar/wo-type-segment";
|
||||||
import { Text } from "@/components/ui/text";
|
import { Text } from "@/components/ui/text";
|
||||||
|
|
@ -11,13 +9,8 @@ type WorkOrdersFilterBarProps = {
|
||||||
onWeekChange: (mondayIso: string) => void;
|
onWeekChange: (mondayIso: string) => void;
|
||||||
selectedDayIso: string;
|
selectedDayIso: string;
|
||||||
onSelectedDayChange: (dayIso: string) => void;
|
onSelectedDayChange: (dayIso: string) => void;
|
||||||
dispatcherIds: Set<string>;
|
|
||||||
onDispatcherChange: (ids: Set<string>) => void;
|
|
||||||
typeFilter: WorkOrderTypeFilter;
|
typeFilter: WorkOrderTypeFilter;
|
||||||
onTypeChange: (value: WorkOrderTypeFilter) => void;
|
onTypeChange: (value: WorkOrderTypeFilter) => void;
|
||||||
users: WorkOrderUser[];
|
|
||||||
usersLoading?: boolean;
|
|
||||||
currentUserId?: string | number;
|
|
||||||
filteredCount: number;
|
filteredCount: number;
|
||||||
scopeTotalCount: number;
|
scopeTotalCount: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
@ -28,13 +21,8 @@ export function WorkOrdersFilterBar({
|
||||||
onWeekChange,
|
onWeekChange,
|
||||||
selectedDayIso,
|
selectedDayIso,
|
||||||
onSelectedDayChange,
|
onSelectedDayChange,
|
||||||
dispatcherIds,
|
|
||||||
onDispatcherChange,
|
|
||||||
typeFilter,
|
typeFilter,
|
||||||
onTypeChange,
|
onTypeChange,
|
||||||
users,
|
|
||||||
usersLoading = false,
|
|
||||||
currentUserId,
|
|
||||||
filteredCount,
|
filteredCount,
|
||||||
scopeTotalCount,
|
scopeTotalCount,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
|
@ -46,29 +34,21 @@ export function WorkOrdersFilterBar({
|
||||||
className="-mx-2.5 shrink-0 border-b border-(--color-border) bg-(--color-bg-surface)"
|
className="-mx-2.5 shrink-0 border-b border-(--color-border) bg-(--color-bg-surface)"
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 px-4 pt-2">
|
<div className="flex items-center gap-3 px-4 pt-2" aria-label="Week and day">
|
||||||
<DispatcherFilter
|
<div className="flex min-w-0 flex-col gap-1">
|
||||||
selectedIds={dispatcherIds}
|
<WeekSelector weekMonday={weekMonday} onChange={onWeekChange} disabled={disabled} />
|
||||||
onChange={onDispatcherChange}
|
<DayTabs
|
||||||
users={users}
|
weekMonday={weekMonday}
|
||||||
usersLoading={usersLoading}
|
selectedDayIso={selectedDayIso}
|
||||||
currentUserId={currentUserId}
|
onChange={onSelectedDayChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<Text variant="caption" tone="muted" className="shrink-0">
|
<Text variant="caption" tone="muted" className="shrink-0">
|
||||||
{resultLabel}
|
{resultLabel}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1 px-4 pt-1" aria-label="Week and day">
|
|
||||||
<WeekSelector weekMonday={weekMonday} onChange={onWeekChange} disabled={disabled} />
|
|
||||||
<DayTabs
|
|
||||||
weekMonday={weekMonday}
|
|
||||||
selectedDayIso={selectedDayIso}
|
|
||||||
onChange={onSelectedDayChange}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 px-4 pt-1 pb-2">
|
<div className="flex items-center gap-3 px-4 pt-1 pb-2">
|
||||||
<WoTypeSegment value={typeFilter} onChange={onTypeChange} disabled={disabled} />
|
<WoTypeSegment value={typeFilter} onChange={onTypeChange} disabled={disabled} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Box } from "@mui/material";
|
||||||
import { PageHeader } from "@/components/ui/page-header";
|
import { PageHeader } from "@/components/ui/page-header";
|
||||||
import { QueryErrorPanel } from "@/components/ui/query-error-panel";
|
import { QueryErrorPanel } from "@/components/ui/query-error-panel";
|
||||||
import { WorkOrdersAdvancedFiltersBanner } from "@/app/(protected)/workorders/_components/list/work-orders-advanced-filters-banner";
|
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 { 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 { WorkOrdersListHeader } from "@/app/(protected)/workorders/_components/list/work-orders-list-header";
|
||||||
import { WorkOrdersListPagePanels } from "@/app/(protected)/workorders/_components/list/work-orders-list-page-panels";
|
import { WorkOrdersListPagePanels } from "@/app/(protected)/workorders/_components/list/work-orders-list-page-panels";
|
||||||
|
|
@ -30,6 +31,16 @@ export default function WorkOrdersListPage() {
|
||||||
surface
|
surface
|
||||||
eyebrow="OPERATIONS"
|
eyebrow="OPERATIONS"
|
||||||
title="Work Orders"
|
title="Work Orders"
|
||||||
|
belowTitle={
|
||||||
|
<DispatcherFilter
|
||||||
|
selectedIds={filters.dispatcherIds}
|
||||||
|
onChange={filters.setDispatcherIds}
|
||||||
|
users={page.users}
|
||||||
|
usersLoading={page.usersLoading}
|
||||||
|
currentUserId={user?.id}
|
||||||
|
disabled={filters.advApplied != null}
|
||||||
|
/>
|
||||||
|
}
|
||||||
actions={
|
actions={
|
||||||
<WorkOrdersListHeader
|
<WorkOrdersListHeader
|
||||||
search={filters.search}
|
search={filters.search}
|
||||||
|
|
@ -47,13 +58,8 @@ export default function WorkOrdersListPage() {
|
||||||
onWeekChange={filters.setWeekMonday}
|
onWeekChange={filters.setWeekMonday}
|
||||||
selectedDayIso={filters.selectedDayIso}
|
selectedDayIso={filters.selectedDayIso}
|
||||||
onSelectedDayChange={filters.setSelectedDayIso}
|
onSelectedDayChange={filters.setSelectedDayIso}
|
||||||
dispatcherIds={filters.dispatcherIds}
|
|
||||||
onDispatcherChange={filters.setDispatcherIds}
|
|
||||||
typeFilter={filters.typeFilter}
|
typeFilter={filters.typeFilter}
|
||||||
onTypeChange={filters.setTypeFilter}
|
onTypeChange={filters.setTypeFilter}
|
||||||
users={page.users}
|
|
||||||
usersLoading={page.usersLoading}
|
|
||||||
currentUserId={user?.id}
|
|
||||||
filteredCount={tableData.totalCount}
|
filteredCount={tableData.totalCount}
|
||||||
scopeTotalCount={tableData.scopeTotalCount}
|
scopeTotalCount={tableData.scopeTotalCount}
|
||||||
disabled={filters.advApplied != null}
|
disabled={filters.advApplied != null}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
import { Stack } from "@mui/material";
|
import { Stack } from "@mui/material";
|
||||||
|
|
||||||
import { Text } from "@/components/ui/text";
|
import { Text } from "@/components/ui/text";
|
||||||
|
|
@ -8,6 +8,7 @@ type PageHeaderProps = {
|
||||||
eyebrow?: string;
|
eyebrow?: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
|
belowTitle?: ReactNode;
|
||||||
actions?: ReactElement;
|
actions?: ReactElement;
|
||||||
className?: string;
|
className?: string;
|
||||||
/** White surface bar (list page headers). */
|
/** White surface bar (list page headers). */
|
||||||
|
|
@ -18,6 +19,7 @@ export function PageHeader({
|
||||||
eyebrow,
|
eyebrow,
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
belowTitle,
|
||||||
actions,
|
actions,
|
||||||
className,
|
className,
|
||||||
surface = false,
|
surface = false,
|
||||||
|
|
@ -63,6 +65,7 @@ export function PageHeader({
|
||||||
>
|
>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</Text>
|
</Text>
|
||||||
|
{Boolean(belowTitle) && <div className="mt-1">{belowTitle}</div>}
|
||||||
</Stack>
|
</Stack>
|
||||||
{Boolean(actions) && (
|
{Boolean(actions) && (
|
||||||
<Stack
|
<Stack
|
||||||
|
|
|
||||||
125
src/test/app/(protected)/workorders/dispatcher-filter.test.tsx
Normal file
125
src/test/app/(protected)/workorders/dispatcher-filter.test.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { screen, within } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { DispatcherFilter } from "@/app/(protected)/workorders/_components/list/filter-bar/dispatcher-filter";
|
||||||
|
import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list";
|
||||||
|
import { renderWithProviders } from "@/test/test-utils";
|
||||||
|
|
||||||
|
const users = [
|
||||||
|
{ id: "u1", name: "Alex Rivera" },
|
||||||
|
{ id: "u2", name: "Blair Cho" },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("DispatcherFilter (SH-242)", () => {
|
||||||
|
it("renders a plain trigger without a boxed border", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<DispatcherFilter selectedIds={new Set()} onChange={vi.fn()} users={users} />,
|
||||||
|
{ 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(
|
||||||
|
<DispatcherFilter
|
||||||
|
selectedIds={new Set()}
|
||||||
|
onChange={vi.fn()}
|
||||||
|
users={users}
|
||||||
|
currentUserId="u1"
|
||||||
|
/>,
|
||||||
|
{ 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(
|
||||||
|
<DispatcherFilter selectedIds={new Set()} onChange={vi.fn()} users={[]} usersLoading />,
|
||||||
|
{ withAuth: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText("Loading dispatchers")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("button", { name: "All dispatchers" })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables the trigger when requested", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<DispatcherFilter selectedIds={new Set()} onChange={vi.fn()} users={users} disabled />,
|
||||||
|
{ withAuth: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: "All dispatchers" })).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("labels My WOs, Unassigned, and a multi-select count", () => {
|
||||||
|
const { rerender } = renderWithProviders(
|
||||||
|
<DispatcherFilter
|
||||||
|
selectedIds={new Set(["u1"])}
|
||||||
|
onChange={vi.fn()}
|
||||||
|
users={users}
|
||||||
|
currentUserId="u1"
|
||||||
|
/>,
|
||||||
|
{ withAuth: false },
|
||||||
|
);
|
||||||
|
expect(screen.getByRole("button", { name: "My WOs" })).toBeInTheDocument();
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<DispatcherFilter
|
||||||
|
selectedIds={new Set([ASSIGNEE_FILTER_UNASSIGNED])}
|
||||||
|
onChange={vi.fn()}
|
||||||
|
users={users}
|
||||||
|
currentUserId="u1"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByRole("button", { name: "Unassigned" })).toBeInTheDocument();
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<DispatcherFilter
|
||||||
|
selectedIds={new Set(["u1", "u2"])}
|
||||||
|
onChange={vi.fn()}
|
||||||
|
users={users}
|
||||||
|
currentUserId="u1"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<DispatcherFilter
|
||||||
|
selectedIds={new Set()}
|
||||||
|
onChange={onChange}
|
||||||
|
users={users}
|
||||||
|
currentUserId="u1"
|
||||||
|
/>,
|
||||||
|
{ 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());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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<typeof import("react-router")>("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<string>(),
|
||||||
|
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(<WorkOrdersListPage />, { 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue