diff --git a/e2e/work-orders/advanced-filters-sh244.spec.ts b/e2e/work-orders/advanced-filters-sh244.spec.ts
new file mode 100644
index 00000000..fe71ffec
--- /dev/null
+++ b/e2e/work-orders/advanced-filters-sh244.spec.ts
@@ -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);
+ });
+});
diff --git a/src/app/(protected)/workorders/_components/list/advanced-search/adv-filter-sections.tsx b/src/app/(protected)/workorders/_components/list/advanced-search/adv-filter-sections.tsx
index 88ba6ca3..b21120b7 100644
--- a/src/app/(protected)/workorders/_components/list/advanced-search/adv-filter-sections.tsx
+++ b/src/app/(protected)/workorders/_components/list/advanced-search/adv-filter-sections.tsx
@@ -13,10 +13,11 @@ import {
import { AdvDocPill, AdvStatusPill } from "./adv-filter-pill";
import { AdvDateRangeSection } from "./adv-date-range-section";
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 { AdvSeverityList } from "./adv-severity-list";
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 {
ALL_WIZARD_STATUSES,
@@ -124,20 +125,18 @@ export function AdvancedSearchFilterSections({
-
+
-
+
({ value: pmType, label: pmType }))}
selected={draft.pmTypes}
onToggle={(value) => toggleIn("pmTypes", value)}
onBulkToggle={(values, select) => setMany("pmTypes", values, select)}
- placeholder="Search pm type…"
+ placeholder="Search service…"
+ renderLeft={(pmType) => (
+
+ )}
/>
@@ -164,6 +163,13 @@ export function AdvancedSearchFilterSections({
))}
+
+
);
}
diff --git a/src/app/(protected)/workorders/_components/list/advanced-search/adv-parity-filter-sections.tsx b/src/app/(protected)/workorders/_components/list/advanced-search/adv-parity-filter-sections.tsx
index 559004c8..199cfcb8 100644
--- a/src/app/(protected)/workorders/_components/list/advanced-search/adv-parity-filter-sections.tsx
+++ b/src/app/(protected)/workorders/_components/list/advanced-search/adv-parity-filter-sections.tsx
@@ -1,7 +1,7 @@
-import type { Dispatch, SetStateAction } from "react";
-import { Check, DollarSign, Flag, Hash, RefreshCw } from "lucide-react";
+import type { Dispatch, ReactNode, SetStateAction } from "react";
+import { ArrowRight, Check, DollarSign, Flag, Hash, Plus, RefreshCw } from "lucide-react";
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 { AdvSection } from "./adv-section";
import { FLAG_COLORS } from "@/domain/work-orders/flag-colors";
@@ -15,9 +15,12 @@ import {
type UpliftFilterStatus,
} from "@/domain/work-orders/types/work-order-uplift-filter";
-type AdvParityFilterSectionsProps = {
+type DraftProps = {
draft: WorkOrderAdvancedFilters;
setDraft: Dispatch>;
+};
+
+type AdvFlagInternalUpliftSectionsProps = DraftProps & {
toggleIn: (key: AdvancedFilterSetKey, value: string) => 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 = {
+ 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 (
+
+ );
+}
+
+export function AdvIndicatorsSection({ draft, setDraft }: DraftProps) {
+ return (
+
+
+
setDraft((prev) => ({ ...prev, rescheduled: !prev.rescheduled }))}
+ >
+
+ Rescheduled ≥ 2
+
+
setDraft((prev) => ({ ...prev, carriedOver: !prev.carriedOver }))}
+ >
+
+ Carried Over ≥ 2
+
+
setDraft((prev) => ({ ...prev, addOn: !prev.addOn }))}
+ >
+
+ Add-On
+
+
+
+ );
+}
+
+export function AdvFlagInternalUpliftSections({
draft,
setDraft,
toggleIn,
setMany,
-}: AdvParityFilterSectionsProps) {
+}: AdvFlagInternalUpliftSectionsProps) {
const toggleUpliftStatus = (status: UpliftFilterStatus) => {
setDraft((prev) => {
const next = new Set(prev.upliftStatuses);
@@ -55,29 +134,6 @@ export function AdvParityFilterSections({
return (
<>
-
-
-
setDraft((prev) => ({ ...prev, rescheduled: !prev.rescheduled }))}
- className="inline-flex items-center gap-1.5"
- />
- setDraft((prev) => ({ ...prev, carriedOver: !prev.carriedOver }))}
- className="inline-flex items-center gap-1.5"
- />
- setDraft((prev) => ({ ...prev, addOn: !prev.addOn }))}
- className="inline-flex items-center gap-1.5"
- />
-
-
-
({ value: color.value, label: color.name }))}
diff --git a/src/domain/work-orders/utils/advanced-filter-chip-builders.ts b/src/domain/work-orders/utils/advanced-filter-chip-builders.ts
index 8150c9d2..c43d5d55 100644
--- a/src/domain/work-orders/utils/advanced-filter-chip-builders.ts
+++ b/src/domain/work-orders/utils/advanced-filter-chip-builders.ts
@@ -178,7 +178,7 @@ export function appendStatusPmVendorDocChips(
pushSetChip({
chips,
key: "pm",
- group: "PM type",
+ group: "Service",
set: filters.pmTypes,
nameOf: (v) => v,
clear: (current) => ({ ...current, pmTypes: new Set() }),
diff --git a/src/test/app/(protected)/workorders/wo-table-search-empty.test.tsx b/src/test/app/(protected)/workorders/wo-table-search-empty.test.tsx
index db7e5685..23dbc11b 100644
--- a/src/test/app/(protected)/workorders/wo-table-search-empty.test.tsx
+++ b/src/test/app/(protected)/workorders/wo-table-search-empty.test.tsx
@@ -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(getByText("queue unavailable")).toBeInTheDocument();
});
+
+ it("shows empty advanced-filter copy with Adjust filters", () => {
+ const onOpenAdvancedFilters = vi.fn();
+ const { getByText, getByRole } = renderWithProviders(
+ ,
+ { withAuth: false },
+ );
+
+ expect(getByText("No work orders match these filters")).toBeInTheDocument();
+ getByRole("button", { name: "Adjust filters" }).click();
+ expect(onOpenAdvancedFilters).toHaveBeenCalled();
+ });
});
diff --git a/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx b/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx
index a8e0c15e..e1339afd 100644
--- a/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx
+++ b/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx
@@ -140,7 +140,7 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => {
expect(within(sheet).getByRole("button", { name: /apply filters/i })).toBeInTheDocument();
// 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") }));
}
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");
});
+ it("applies Service and Completion doc through the public Advanced Filters sheet", () => {
+ const view = renderWithProviders(, {
+ route: "/workorders",
+ withAuth: false,
+ });
+
+ fireEvent.click(screen.getByRole("button", { name: "Advanced Filters" }));
+ advOpen = true;
+ view.rerender();
+
+ 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", () => {
const view = renderWithProviders(, {
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: /^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: /^Red$/i }));
diff --git a/src/test/domain/work-orders/utils/advanced-filters-utils.test.ts b/src/test/domain/work-orders/utils/advanced-filters-utils.test.ts
index c4f351fa..0e5126fd 100644
--- a/src/test/domain/work-orders/utils/advanced-filters-utils.test.ts
+++ b/src/test/domain/work-orders/utils/advanced-filters-utils.test.ts
@@ -120,6 +120,24 @@ describe("clearAdvancedFilterGroup", () => {
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", () => {
const filters = {
...cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS),
diff --git a/src/test/domain/work-orders/utils/board-query-params.test.ts b/src/test/domain/work-orders/utils/board-query-params.test.ts
index 5d80c1b4..2167cfcb 100644
--- a/src/test/domain/work-orders/utils/board-query-params.test.ts
+++ b/src/test/domain/work-orders/utils/board-query-params.test.ts
@@ -1,7 +1,11 @@
import { describe, expect, it } from "vitest";
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 { 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 {
BOARD_SEARCH_MAX_PAGE_SIZE,
buildBoardSearchParamsRecord,
@@ -163,6 +167,82 @@ describe("toBoardSearchQueryParams", () => {
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", () => {
const params = toBoardSearchQueryParams({
filters: {