mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-04 11:22:05 +00:00
feat(work-orders): add SH-244 advanced filter fields and AAP indicator styles
This commit is contained in:
parent
19a54e38c6
commit
93ab14abf3
8 changed files with 411 additions and 41 deletions
135
e2e/work-orders/advanced-filters-sh244.spec.ts
Normal file
135
e2e/work-orders/advanced-filters-sh244.spec.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
import { expect, test, type Page, type Route } from "@playwright/test";
|
||||||
|
|
||||||
|
const FROZEN_NOW = "2026-08-19T15:00:00.000Z";
|
||||||
|
|
||||||
|
const TEST_USER = {
|
||||||
|
token: "wo-adv-filters-e2e-token",
|
||||||
|
expiration: "2030-01-01T00:00:00.000Z",
|
||||||
|
email: "admin@seahavenind.com",
|
||||||
|
userRoles: "Admin",
|
||||||
|
phoneNumber: "5551234567",
|
||||||
|
fullname: "Work Order E2E Admin",
|
||||||
|
id: "1",
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fulfillJson(route: Route, body: unknown, status = 200) {
|
||||||
|
await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body) });
|
||||||
|
}
|
||||||
|
|
||||||
|
function boardRow(id: number, woNumber: string, scheduledDate: string | null) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
woNumber,
|
||||||
|
workOrderType: 2,
|
||||||
|
siteCode: woNumber,
|
||||||
|
lifecycleStatusLabel: "Scheduled",
|
||||||
|
scheduledDate,
|
||||||
|
dispatcherId: "1",
|
||||||
|
dispatcherName: "Pat Dispatcher",
|
||||||
|
pm: "HVAC",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mockWorkOrderApis(page: Page, searchUrls: string[]) {
|
||||||
|
await page.route("**/api/**", (route) => {
|
||||||
|
const pathname = new URL(route.request().url()).pathname;
|
||||||
|
if (!pathname.startsWith("/api/")) {
|
||||||
|
return route.fallback();
|
||||||
|
}
|
||||||
|
if (pathname.includes("/workorders/board") || pathname.includes("DropdownOptions")) {
|
||||||
|
return route.fallback();
|
||||||
|
}
|
||||||
|
return fulfillJson(route, { data: [] });
|
||||||
|
});
|
||||||
|
await page.route("**/api/**/DropdownOptions/ByCategory/**", (route) => {
|
||||||
|
const pathname = new URL(route.request().url()).pathname;
|
||||||
|
if (pathname.toLowerCase().includes("problem")) {
|
||||||
|
return fulfillJson(route, { data: [{ id: 1, value: "HVAC", category: "Problem" }] });
|
||||||
|
}
|
||||||
|
return fulfillJson(route, { data: [] });
|
||||||
|
});
|
||||||
|
await page.route("**/api/workorders/board*", (route) => {
|
||||||
|
if (route.request().url().includes("board/search")) {
|
||||||
|
return route.fallback();
|
||||||
|
}
|
||||||
|
return fulfillJson(route, {
|
||||||
|
weekStart: "2026-08-17",
|
||||||
|
weekEnd: "2026-08-21",
|
||||||
|
counts: { returned: 1, total: 1 },
|
||||||
|
unscheduled: [],
|
||||||
|
scheduled: [boardRow(1, "ADV-WO", "2026-08-19")],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await page.route("**/api/workorders/board/search*", (route) => {
|
||||||
|
searchUrls.push(route.request().url());
|
||||||
|
return fulfillJson(route, { items: [], totalCount: 0, page: 0, pageSize: 50 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signInAndOpenWorkOrders(page: Page) {
|
||||||
|
await page.clock.install({ time: new Date(FROZEN_NOW) });
|
||||||
|
await page.clock.resume();
|
||||||
|
await page.addInitScript((user) => localStorage.setItem("auth", JSON.stringify(user)), TEST_USER);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyAdvancedFilters(page: Page) {
|
||||||
|
await page.getByRole("button", { name: "Advanced Filters" }).click();
|
||||||
|
const sheet = page.getByRole("dialog", { name: "Advanced Filters" });
|
||||||
|
await expect(sheet).toBeVisible();
|
||||||
|
return sheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("Work Orders advanced filters (SH-244)", () => {
|
||||||
|
test("sends carriedOver on board/search and shows empty copy", async ({ page }) => {
|
||||||
|
const searchUrls: string[] = [];
|
||||||
|
await signInAndOpenWorkOrders(page);
|
||||||
|
await mockWorkOrderApis(page, searchUrls);
|
||||||
|
|
||||||
|
await page.goto("/workorders");
|
||||||
|
await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({
|
||||||
|
timeout: 15_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sheet = await applyAdvancedFilters(page);
|
||||||
|
await sheet.getByRole("button", { name: /Indicators/i }).click();
|
||||||
|
await sheet.getByRole("button", { name: /Carried Over ≥ 2/i }).click();
|
||||||
|
await sheet.getByRole("button", { name: /apply filters/i }).click();
|
||||||
|
|
||||||
|
await expect.poll(() => searchUrls.some((url) => url.includes("carriedOver=true"))).toBe(true);
|
||||||
|
await expect(page.getByText("No work orders match these filters")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sends combined SH-244 facets on the same board/search request", async ({ page }) => {
|
||||||
|
const searchUrls: string[] = [];
|
||||||
|
await signInAndOpenWorkOrders(page);
|
||||||
|
await mockWorkOrderApis(page, searchUrls);
|
||||||
|
|
||||||
|
await page.goto("/workorders");
|
||||||
|
await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({
|
||||||
|
timeout: 15_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sheet = await applyAdvancedFilters(page);
|
||||||
|
await sheet.getByRole("button", { name: /Severity/i }).click();
|
||||||
|
await sheet.getByRole("button", { name: /SEV 1/i }).click();
|
||||||
|
await sheet.getByRole("button", { name: /Indicators/i }).click();
|
||||||
|
await sheet.getByRole("button", { name: /^Add-On$/i }).click();
|
||||||
|
await sheet.getByRole("button", { name: /^Service/i }).click();
|
||||||
|
await sheet.getByText("HVAC").click();
|
||||||
|
await sheet.getByRole("button", { name: /Completion doc/i }).click();
|
||||||
|
await sheet.getByRole("button", { name: /^Pending$/i }).click();
|
||||||
|
await sheet.getByRole("button", { name: /apply filters/i }).click();
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(() => {
|
||||||
|
const url = searchUrls[searchUrls.length - 1] ?? "";
|
||||||
|
return (
|
||||||
|
url.includes("severities=1") &&
|
||||||
|
url.includes("addOn=true") &&
|
||||||
|
url.includes("pmTypes=HVAC") &&
|
||||||
|
(url.includes("docStatuses=2") || url.includes("DocStatuses=2"))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -13,10 +13,11 @@ import {
|
||||||
import { AdvDocPill, AdvStatusPill } from "./adv-filter-pill";
|
import { AdvDocPill, AdvStatusPill } from "./adv-filter-pill";
|
||||||
import { AdvDateRangeSection } from "./adv-date-range-section";
|
import { AdvDateRangeSection } from "./adv-date-range-section";
|
||||||
import { AdvMultiSelect } from "./adv-multi-select";
|
import { AdvMultiSelect } from "./adv-multi-select";
|
||||||
import { AdvParityFilterSections } from "./adv-parity-filter-sections";
|
import { AdvFlagInternalUpliftSections, AdvIndicatorsSection } from "./adv-parity-filter-sections";
|
||||||
import { AdvSection } from "./adv-section";
|
import { AdvSection } from "./adv-section";
|
||||||
import { AdvSeverityList } from "./adv-severity-list";
|
import { AdvSeverityList } from "./adv-severity-list";
|
||||||
import { AdvTypeGrid } from "./adv-type-grid";
|
import { AdvTypeGrid } from "./adv-type-grid";
|
||||||
|
import { PmTypeIcon } from "@/app/(protected)/workorders/_components/list/table/pm-type-icons";
|
||||||
import { SelectAllAction } from "@/components/ui/select-all-action";
|
import { SelectAllAction } from "@/components/ui/select-all-action";
|
||||||
import {
|
import {
|
||||||
ALL_WIZARD_STATUSES,
|
ALL_WIZARD_STATUSES,
|
||||||
|
|
@ -124,20 +125,18 @@ export function AdvancedSearchFilterSections({
|
||||||
</div>
|
</div>
|
||||||
</AdvSection>
|
</AdvSection>
|
||||||
|
|
||||||
<AdvParityFilterSections
|
<AdvIndicatorsSection draft={draft} setDraft={setDraft} />
|
||||||
draft={draft}
|
|
||||||
setDraft={setDraft}
|
|
||||||
toggleIn={toggleIn}
|
|
||||||
setMany={setMany}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AdvSection icon={Flame} label="PM type" count={draft.pmTypes.size}>
|
<AdvSection icon={Flame} label="Service" count={draft.pmTypes.size}>
|
||||||
<AdvMultiSelect
|
<AdvMultiSelect
|
||||||
options={pmTypes.map((pmType) => ({ value: pmType, label: pmType }))}
|
options={pmTypes.map((pmType) => ({ value: pmType, label: pmType }))}
|
||||||
selected={draft.pmTypes}
|
selected={draft.pmTypes}
|
||||||
onToggle={(value) => toggleIn("pmTypes", value)}
|
onToggle={(value) => toggleIn("pmTypes", value)}
|
||||||
onBulkToggle={(values, select) => setMany("pmTypes", values, select)}
|
onBulkToggle={(values, select) => setMany("pmTypes", values, select)}
|
||||||
placeholder="Search pm type…"
|
placeholder="Search service…"
|
||||||
|
renderLeft={(pmType) => (
|
||||||
|
<PmTypeIcon pmType={pmType} size={14} className="shrink-0 text-muted-foreground" />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</AdvSection>
|
</AdvSection>
|
||||||
|
|
||||||
|
|
@ -164,6 +163,13 @@ export function AdvancedSearchFilterSections({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</AdvSection>
|
</AdvSection>
|
||||||
|
|
||||||
|
<AdvFlagInternalUpliftSections
|
||||||
|
draft={draft}
|
||||||
|
setDraft={setDraft}
|
||||||
|
toggleIn={toggleIn}
|
||||||
|
setMany={setMany}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, ReactNode, SetStateAction } from "react";
|
||||||
import { Check, DollarSign, Flag, Hash, RefreshCw } from "lucide-react";
|
import { ArrowRight, Check, DollarSign, Flag, Hash, Plus, RefreshCw } from "lucide-react";
|
||||||
import { Checkbox } from "@mui/material";
|
import { Checkbox } from "@mui/material";
|
||||||
import { AdvDocPill, AdvFilterPill } from "./adv-filter-pill";
|
import { AdvDocPill } from "./adv-filter-pill";
|
||||||
import { AdvMultiSelect } from "./adv-multi-select";
|
import { AdvMultiSelect } from "./adv-multi-select";
|
||||||
import { AdvSection } from "./adv-section";
|
import { AdvSection } from "./adv-section";
|
||||||
import { FLAG_COLORS } from "@/domain/work-orders/flag-colors";
|
import { FLAG_COLORS } from "@/domain/work-orders/flag-colors";
|
||||||
|
|
@ -15,9 +15,12 @@ import {
|
||||||
type UpliftFilterStatus,
|
type UpliftFilterStatus,
|
||||||
} from "@/domain/work-orders/types/work-order-uplift-filter";
|
} from "@/domain/work-orders/types/work-order-uplift-filter";
|
||||||
|
|
||||||
type AdvParityFilterSectionsProps = {
|
type DraftProps = {
|
||||||
draft: WorkOrderAdvancedFilters;
|
draft: WorkOrderAdvancedFilters;
|
||||||
setDraft: Dispatch<SetStateAction<WorkOrderAdvancedFilters>>;
|
setDraft: Dispatch<SetStateAction<WorkOrderAdvancedFilters>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AdvFlagInternalUpliftSectionsProps = DraftProps & {
|
||||||
toggleIn: (key: AdvancedFilterSetKey, value: string) => void;
|
toggleIn: (key: AdvancedFilterSetKey, value: string) => void;
|
||||||
setMany: (key: AdvancedFilterSetKey, values: string[], select: boolean) => void;
|
setMany: (key: AdvancedFilterSetKey, values: string[], select: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
@ -35,12 +38,88 @@ function FlagColorDot({ value }: { value: string }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AdvParityFilterSections({
|
type IndicatorKind = "rescheduled" | "carriedOver" | "addOn";
|
||||||
|
|
||||||
|
const INDICATOR_SELECTED: Record<IndicatorKind, { background: string; color: string }> = {
|
||||||
|
rescheduled: { background: "#f5f3ff", color: "#7c3aed" },
|
||||||
|
carriedOver: { background: "var(--color-bg-muted)", color: "var(--color-text-primary)" },
|
||||||
|
addOn: { background: "var(--type-addon-bg)", color: "var(--type-addon-text)" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function IndicatorPill({
|
||||||
|
selected,
|
||||||
|
onClick,
|
||||||
|
kind,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
selected: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
kind: IndicatorKind;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const selectedLook = INDICATOR_SELECTED[kind];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
data-indicator={kind}
|
||||||
|
data-selected={selected ? "true" : "false"}
|
||||||
|
className="inline-flex items-center gap-1.5"
|
||||||
|
style={{
|
||||||
|
height: 26,
|
||||||
|
padding: "0 9px",
|
||||||
|
borderRadius: 9999,
|
||||||
|
border: `1px solid ${selected ? "var(--color-primary)" : "var(--color-border)"}`,
|
||||||
|
background: selected ? selectedLook.background : "var(--color-bg-surface)",
|
||||||
|
color: selected ? selectedLook.color : "var(--color-text-secondary)",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdvIndicatorsSection({ draft, setDraft }: DraftProps) {
|
||||||
|
return (
|
||||||
|
<AdvSection icon={RefreshCw} label="Indicators" count={indicatorCount(draft)}>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
<IndicatorPill
|
||||||
|
kind="rescheduled"
|
||||||
|
selected={draft.rescheduled}
|
||||||
|
onClick={() => setDraft((prev) => ({ ...prev, rescheduled: !prev.rescheduled }))}
|
||||||
|
>
|
||||||
|
<RefreshCw size={12} className="shrink-0" style={{ color: "#7c3aed" }} />
|
||||||
|
Rescheduled ≥ 2
|
||||||
|
</IndicatorPill>
|
||||||
|
<IndicatorPill
|
||||||
|
kind="carriedOver"
|
||||||
|
selected={draft.carriedOver}
|
||||||
|
onClick={() => setDraft((prev) => ({ ...prev, carriedOver: !prev.carriedOver }))}
|
||||||
|
>
|
||||||
|
<ArrowRight size={12} className="shrink-0" style={{ color: "var(--color-text-muted)" }} />
|
||||||
|
Carried Over ≥ 2
|
||||||
|
</IndicatorPill>
|
||||||
|
<IndicatorPill
|
||||||
|
kind="addOn"
|
||||||
|
selected={draft.addOn}
|
||||||
|
onClick={() => setDraft((prev) => ({ ...prev, addOn: !prev.addOn }))}
|
||||||
|
>
|
||||||
|
<Plus size={12} className="shrink-0" style={{ color: "var(--type-addon-text)" }} />
|
||||||
|
Add-On
|
||||||
|
</IndicatorPill>
|
||||||
|
</div>
|
||||||
|
</AdvSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdvFlagInternalUpliftSections({
|
||||||
draft,
|
draft,
|
||||||
setDraft,
|
setDraft,
|
||||||
toggleIn,
|
toggleIn,
|
||||||
setMany,
|
setMany,
|
||||||
}: AdvParityFilterSectionsProps) {
|
}: AdvFlagInternalUpliftSectionsProps) {
|
||||||
const toggleUpliftStatus = (status: UpliftFilterStatus) => {
|
const toggleUpliftStatus = (status: UpliftFilterStatus) => {
|
||||||
setDraft((prev) => {
|
setDraft((prev) => {
|
||||||
const next = new Set(prev.upliftStatuses);
|
const next = new Set(prev.upliftStatuses);
|
||||||
|
|
@ -55,29 +134,6 @@ export function AdvParityFilterSections({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AdvSection icon={RefreshCw} label="Indicators" count={indicatorCount(draft)}>
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
<AdvFilterPill
|
|
||||||
label="Rescheduled ≥ 2"
|
|
||||||
selected={draft.rescheduled}
|
|
||||||
onClick={() => setDraft((prev) => ({ ...prev, rescheduled: !prev.rescheduled }))}
|
|
||||||
className="inline-flex items-center gap-1.5"
|
|
||||||
/>
|
|
||||||
<AdvFilterPill
|
|
||||||
label="Carried Over ≥ 2"
|
|
||||||
selected={draft.carriedOver}
|
|
||||||
onClick={() => setDraft((prev) => ({ ...prev, carriedOver: !prev.carriedOver }))}
|
|
||||||
className="inline-flex items-center gap-1.5"
|
|
||||||
/>
|
|
||||||
<AdvFilterPill
|
|
||||||
label="Add-On"
|
|
||||||
selected={draft.addOn}
|
|
||||||
onClick={() => setDraft((prev) => ({ ...prev, addOn: !prev.addOn }))}
|
|
||||||
className="inline-flex items-center gap-1.5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AdvSection>
|
|
||||||
|
|
||||||
<AdvSection icon={Flag} label="Flag" count={draft.flagColors.size}>
|
<AdvSection icon={Flag} label="Flag" count={draft.flagColors.size}>
|
||||||
<AdvMultiSelect
|
<AdvMultiSelect
|
||||||
options={FLAG_COLORS.map((color) => ({ value: color.value, label: color.name }))}
|
options={FLAG_COLORS.map((color) => ({ value: color.value, label: color.name }))}
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ export function appendStatusPmVendorDocChips(
|
||||||
pushSetChip({
|
pushSetChip({
|
||||||
chips,
|
chips,
|
||||||
key: "pm",
|
key: "pm",
|
||||||
group: "PM type",
|
group: "Service",
|
||||||
set: filters.pmTypes,
|
set: filters.pmTypes,
|
||||||
nameOf: (v) => v,
|
nameOf: (v) => v,
|
||||||
clear: (current) => ({ ...current, pmTypes: new Set() }),
|
clear: (current) => ({ ...current, pmTypes: new Set() }),
|
||||||
|
|
|
||||||
|
|
@ -120,4 +120,33 @@ describe("WoTable search empty vs Unassigned queue (SH-165/SH-241)", () => {
|
||||||
expect(queryByText("No work orders match your search")).not.toBeInTheDocument();
|
expect(queryByText("No work orders match your search")).not.toBeInTheDocument();
|
||||||
expect(getByText("queue unavailable")).toBeInTheDocument();
|
expect(getByText("queue unavailable")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows empty advanced-filter copy with Adjust filters", () => {
|
||||||
|
const onOpenAdvancedFilters = vi.fn();
|
||||||
|
const { getByText, getByRole } = renderWithProviders(
|
||||||
|
<WoTable
|
||||||
|
tableData={tableData({
|
||||||
|
advActive: true,
|
||||||
|
advResults: [],
|
||||||
|
searchActive: false,
|
||||||
|
noSearchResults: false,
|
||||||
|
})}
|
||||||
|
users={[]}
|
||||||
|
sites={[]}
|
||||||
|
vendors={[]}
|
||||||
|
pmTypes={[]}
|
||||||
|
weekMonday="2026-05-18"
|
||||||
|
onNavigateToDay={vi.fn()}
|
||||||
|
onOpenSlideOver={vi.fn()}
|
||||||
|
onOpenMedia={vi.fn()}
|
||||||
|
onOpenAdvancedFilters={onOpenAdvancedFilters}
|
||||||
|
onPatch={vi.fn()}
|
||||||
|
/>,
|
||||||
|
{ withAuth: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByText("No work orders match these filters")).toBeInTheDocument();
|
||||||
|
getByRole("button", { name: "Adjust filters" }).click();
|
||||||
|
expect(onOpenAdvancedFilters).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => {
|
||||||
expect(within(sheet).getByRole("button", { name: /apply filters/i })).toBeInTheDocument();
|
expect(within(sheet).getByRole("button", { name: /apply filters/i })).toBeInTheDocument();
|
||||||
|
|
||||||
// Expand filter sections that start collapsed and assert option inputs are wired.
|
// Expand filter sections that start collapsed and assert option inputs are wired.
|
||||||
for (const label of ["Site", "Assigned to", "PM type", "Vendor / tech"]) {
|
for (const label of ["Site", "Assigned to", "Service", "Vendor / tech"]) {
|
||||||
fireEvent.click(within(sheet).getByRole("button", { name: new RegExp(label, "i") }));
|
fireEvent.click(within(sheet).getByRole("button", { name: new RegExp(label, "i") }));
|
||||||
}
|
}
|
||||||
expect(within(sheet).getByText("Seattle")).toBeInTheDocument();
|
expect(within(sheet).getByText("Seattle")).toBeInTheDocument();
|
||||||
|
|
@ -200,6 +200,40 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => {
|
||||||
expect(params.dateTo).toBe("2099-12-31");
|
expect(params.dateTo).toBe("2099-12-31");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("applies Service and Completion doc through the public Advanced Filters sheet", () => {
|
||||||
|
const view = renderWithProviders(<WorkOrdersListPage />, {
|
||||||
|
route: "/workorders",
|
||||||
|
withAuth: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Advanced Filters" }));
|
||||||
|
advOpen = true;
|
||||||
|
view.rerender(<WorkOrdersListPage />);
|
||||||
|
|
||||||
|
const sheet = screen.getByRole("dialog", { name: "Advanced Filters" });
|
||||||
|
fireEvent.click(within(sheet).getByRole("button", { name: /^Service/i }));
|
||||||
|
fireEvent.click(within(sheet).getByText("HVAC"));
|
||||||
|
|
||||||
|
fireEvent.click(within(sheet).getByRole("button", { name: /Completion doc/i }));
|
||||||
|
fireEvent.click(within(sheet).getByRole("button", { name: /^Pending$/i }));
|
||||||
|
fireEvent.click(within(sheet).getByRole("button", { name: /^Uploaded$/i }));
|
||||||
|
fireEvent.click(within(sheet).getByRole("button", { name: /^N\/N$/i }));
|
||||||
|
|
||||||
|
fireEvent.click(within(sheet).getByRole("button", { name: /apply filters/i }));
|
||||||
|
|
||||||
|
expect(applyAdvancedFilters).toHaveBeenCalled();
|
||||||
|
const appliedCalls = applyAdvancedFilters.mock.calls;
|
||||||
|
const applied = appliedCalls[appliedCalls.length - 1]?.[0] as typeof EMPTY_ADVANCED_FILTERS;
|
||||||
|
expect(applied.pmTypes.has("HVAC")).toBe(true);
|
||||||
|
expect(applied.docs.has("No")).toBe(true);
|
||||||
|
expect(applied.docs.has("Yes")).toBe(true);
|
||||||
|
expect(applied.docs.has("NN")).toBe(true);
|
||||||
|
|
||||||
|
const params = toBoardSearchQueryParams({ filters: applied });
|
||||||
|
expect(params.pmTypes).toEqual(["HVAC"]);
|
||||||
|
expect(params.docStatuses).toEqual([2, 1, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
it("applies Indicators, Flag, and Internal WO# through the public Advanced Filters sheet", () => {
|
it("applies Indicators, Flag, and Internal WO# through the public Advanced Filters sheet", () => {
|
||||||
const view = renderWithProviders(<WorkOrdersListPage />, {
|
const view = renderWithProviders(<WorkOrdersListPage />, {
|
||||||
route: "/workorders",
|
route: "/workorders",
|
||||||
|
|
@ -216,6 +250,18 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => {
|
||||||
fireEvent.click(within(sheet).getByRole("button", { name: /Carried Over ≥ 2/i }));
|
fireEvent.click(within(sheet).getByRole("button", { name: /Carried Over ≥ 2/i }));
|
||||||
fireEvent.click(within(sheet).getByRole("button", { name: /^Add-On$/i }));
|
fireEvent.click(within(sheet).getByRole("button", { name: /^Add-On$/i }));
|
||||||
|
|
||||||
|
const rescheduled = within(sheet).getByRole("button", { name: /Rescheduled ≥ 2/i });
|
||||||
|
const carriedOver = within(sheet).getByRole("button", { name: /Carried Over ≥ 2/i });
|
||||||
|
const addOn = within(sheet).getByRole("button", { name: /^Add-On$/i });
|
||||||
|
expect(rescheduled).toHaveAttribute("data-selected", "true");
|
||||||
|
expect(carriedOver).toHaveAttribute("data-selected", "true");
|
||||||
|
expect(addOn).toHaveAttribute("data-selected", "true");
|
||||||
|
expect(rescheduled.style.background).toBe("rgb(245, 243, 255)");
|
||||||
|
expect(rescheduled.style.color).toBe("rgb(124, 58, 237)");
|
||||||
|
expect(carriedOver.style.background).toBe("var(--color-bg-muted)");
|
||||||
|
expect(addOn.style.background).toBe("var(--type-addon-bg)");
|
||||||
|
expect(addOn.style.color).toBe("var(--type-addon-text)");
|
||||||
|
|
||||||
fireEvent.click(within(sheet).getByRole("button", { name: /^Flag$/i }));
|
fireEvent.click(within(sheet).getByRole("button", { name: /^Flag$/i }));
|
||||||
fireEvent.click(within(sheet).getByRole("button", { name: /^Red$/i }));
|
fireEvent.click(within(sheet).getByRole("button", { name: /^Red$/i }));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,24 @@ describe("clearAdvancedFilterGroup", () => {
|
||||||
expect(chips.some((c) => c.key === "uplift" && c.value === "Pending")).toBe(true);
|
expect(chips.some((c) => c.key === "uplift" && c.value === "Pending")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds chips for service, carried over, add-on, and completion doc", () => {
|
||||||
|
const filters = {
|
||||||
|
...cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS),
|
||||||
|
pmTypes: new Set(["HVAC"]),
|
||||||
|
carriedOver: true,
|
||||||
|
addOn: true,
|
||||||
|
docs: new Set(["No" as const, "Yes" as const]),
|
||||||
|
};
|
||||||
|
|
||||||
|
const chips = buildAdvancedFilterChips(filters);
|
||||||
|
expect(chips.some((c) => c.key === "pm" && c.group === "Service" && c.value === "HVAC")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(chips.some((c) => c.key === "carriedover" && c.value === "Carried Over")).toBe(true);
|
||||||
|
expect(chips.some((c) => c.key === "addon" && c.value === "Add-On")).toBe(true);
|
||||||
|
expect(chips.some((c) => c.key === "doc" && c.group === "Comp doc")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("clears uplift chip and drops hasUplift with statuses", () => {
|
it("clears uplift chip and drops hasUplift with statuses", () => {
|
||||||
const filters = {
|
const filters = {
|
||||||
...cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS),
|
...cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list";
|
import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list";
|
||||||
import { EMPTY_ADVANCED_FILTERS } from "@/domain/work-orders/types/work-order-advanced-filters";
|
import { EMPTY_ADVANCED_FILTERS } from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||||
import { WORK_ORDER_TYPE_API } from "@/domain/work-orders/types/work-order-board";
|
import {
|
||||||
|
DOC_STATUS_API,
|
||||||
|
LIFECYCLE_STATUS_API,
|
||||||
|
WORK_ORDER_TYPE_API,
|
||||||
|
} from "@/domain/work-orders/types/work-order-board";
|
||||||
import {
|
import {
|
||||||
BOARD_SEARCH_MAX_PAGE_SIZE,
|
BOARD_SEARCH_MAX_PAGE_SIZE,
|
||||||
buildBoardSearchParamsRecord,
|
buildBoardSearchParamsRecord,
|
||||||
|
|
@ -163,6 +167,82 @@ describe("toBoardSearchQueryParams", () => {
|
||||||
expect(params.upliftStatuses).toBeUndefined();
|
expect(params.upliftStatuses).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps service types and completion docs onto board search params", () => {
|
||||||
|
const params = toBoardSearchQueryParams({
|
||||||
|
filters: {
|
||||||
|
...EMPTY_ADVANCED_FILTERS,
|
||||||
|
pmTypes: new Set(["HVAC", "Electrical"]),
|
||||||
|
docs: new Set(["No", "Yes", "NN"]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(params.pmTypes).toEqual(["HVAC", "Electrical"]);
|
||||||
|
expect(params.docStatuses).toEqual([
|
||||||
|
DOC_STATUS_API.No,
|
||||||
|
DOC_STATUS_API.Yes,
|
||||||
|
DOC_STATUS_API.NotNeeded,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps each SH-244 facet alone onto board search params (AND-ready query)", () => {
|
||||||
|
const severity = toBoardSearchQueryParams({
|
||||||
|
filters: { ...EMPTY_ADVANCED_FILTERS, severities: new Set([4]) },
|
||||||
|
});
|
||||||
|
expect(severity.severities).toEqual([4]);
|
||||||
|
expect(severity.carriedOver).toBeUndefined();
|
||||||
|
expect(severity.addOn).toBeUndefined();
|
||||||
|
expect(severity.pmTypes).toBeUndefined();
|
||||||
|
expect(severity.docStatuses).toBeUndefined();
|
||||||
|
|
||||||
|
const carried = toBoardSearchQueryParams({
|
||||||
|
filters: { ...EMPTY_ADVANCED_FILTERS, carriedOver: true },
|
||||||
|
});
|
||||||
|
expect(carried.carriedOver).toBe(true);
|
||||||
|
expect(carried.severities).toBeUndefined();
|
||||||
|
|
||||||
|
const addOn = toBoardSearchQueryParams({
|
||||||
|
filters: { ...EMPTY_ADVANCED_FILTERS, addOn: true },
|
||||||
|
});
|
||||||
|
expect(addOn.addOn).toBe(true);
|
||||||
|
expect(addOn.carriedOver).toBeUndefined();
|
||||||
|
|
||||||
|
const service = toBoardSearchQueryParams({
|
||||||
|
filters: { ...EMPTY_ADVANCED_FILTERS, pmTypes: new Set(["HVAC"]) },
|
||||||
|
});
|
||||||
|
expect(service.pmTypes).toEqual(["HVAC"]);
|
||||||
|
|
||||||
|
const docs = toBoardSearchQueryParams({
|
||||||
|
filters: { ...EMPTY_ADVANCED_FILTERS, docs: new Set(["NN"]) },
|
||||||
|
});
|
||||||
|
expect(docs.docStatuses).toEqual([DOC_STATUS_API.NotNeeded]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("combines SH-244 facets with a SH-121 status facet on the same request", () => {
|
||||||
|
const params = toBoardSearchQueryParams({
|
||||||
|
filters: {
|
||||||
|
...EMPTY_ADVANCED_FILTERS,
|
||||||
|
severities: new Set([1]),
|
||||||
|
carriedOver: true,
|
||||||
|
addOn: true,
|
||||||
|
pmTypes: new Set(["HVAC"]),
|
||||||
|
docs: new Set(["Yes"]),
|
||||||
|
statuses: new Set(["Scheduled"]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(params.severities).toEqual([1]);
|
||||||
|
expect(params.carriedOver).toBe(true);
|
||||||
|
expect(params.addOn).toBe(true);
|
||||||
|
expect(params.pmTypes).toEqual(["HVAC"]);
|
||||||
|
expect(params.docStatuses).toEqual([DOC_STATUS_API.Yes]);
|
||||||
|
expect(params.statuses).toEqual([LIFECYCLE_STATUS_API.Scheduled]);
|
||||||
|
const record = buildBoardSearchParamsRecord(params);
|
||||||
|
expect(record.severities).toEqual([1]);
|
||||||
|
expect(record.carriedOver).toBe(true);
|
||||||
|
expect(record.addOn).toBe(true);
|
||||||
|
expect(record.pmTypes).toEqual(["HVAC"]);
|
||||||
|
expect(record.docStatuses).toEqual([DOC_STATUS_API.Yes]);
|
||||||
|
expect(record.statuses).toEqual([LIFECYCLE_STATUS_API.Scheduled]);
|
||||||
|
});
|
||||||
|
|
||||||
it("maps severity and uplift facets onto board search params", () => {
|
it("maps severity and uplift facets onto board search params", () => {
|
||||||
const params = toBoardSearchQueryParams({
|
const params = toBoardSearchQueryParams({
|
||||||
filters: {
|
filters: {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue