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 b21120b7..148aeee9 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 @@ -109,7 +109,11 @@ export function AdvancedSearchFilterSections({ /> - + {selectAllRow("statuses", [...ALL_WIZARD_STATUSES], draft.statuses.size)}
{ALL_WIZARD_STATUSES.map((status) => ( @@ -122,6 +126,14 @@ export function AdvancedSearchFilterSections({ onClick={() => toggleIn("statuses", status)} /> ))} + setDraft((prev) => ({ ...prev, pastDue: !prev.pastDue }))} + />
diff --git a/src/domain/services/api/services-api.ts b/src/domain/services/api/services-api.ts index d725d828..df781742 100644 --- a/src/domain/services/api/services-api.ts +++ b/src/domain/services/api/services-api.ts @@ -20,10 +20,13 @@ const SERVICE_WORK_ORDER_TYPE_API = { Reactive: 6, } as const; -const TYPE_TO_API: Record, number> = { +// The registry is keyed on PM, Reactive and Emergency; Overdue is PM work the +// client never confirmed, so it lists the PM catalog. +const TYPE_TO_API: Record = { PM: WORK_ORDER_TYPE_API.PM, Reactive: WORK_ORDER_TYPE_API.Reactive, Emergency: WORK_ORDER_TYPE_API.Emergency, + Overdue: WORK_ORDER_TYPE_API.PM, }; function toApiInput(input: ServiceInput) { @@ -51,7 +54,6 @@ export const servicesApi = { }, getActiveForWorkOrderType: async (workOrderType: WOType): Promise => { - if (workOrderType === "Overdue") return []; const data = await apiGet(API_PATHS.services.list, { searchParams: { isActive: true, workOrderType: TYPE_TO_API[workOrderType] }, }); diff --git a/src/domain/services/use-cases/use-services-for-work-order-type.ts b/src/domain/services/use-cases/use-services-for-work-order-type.ts index 6dd8d4ac..f712051e 100644 --- a/src/domain/services/use-cases/use-services-for-work-order-type.ts +++ b/src/domain/services/use-cases/use-services-for-work-order-type.ts @@ -11,7 +11,6 @@ export function useServicesForWorkOrderType( return useQuery({ queryKey: queryKeys.services.byWorkOrderType(effectiveType), queryFn: () => servicesApi.getActiveForWorkOrderType(effectiveType), - enabled: effectiveType !== "Overdue", staleTime: 60 * 1000, }); } diff --git a/src/domain/work-orders/mappers/assign-wizard-service-poc-fields.ts b/src/domain/work-orders/mappers/assign-wizard-service-poc-fields.ts index e45381e7..b4940692 100644 --- a/src/domain/work-orders/mappers/assign-wizard-service-poc-fields.ts +++ b/src/domain/work-orders/mappers/assign-wizard-service-poc-fields.ts @@ -1,4 +1,5 @@ import type { WorkOrderBoardCreatePayload } from "@/domain/work-orders/types/work-order-board"; +import { typeNeedsSeverity } from "@/domain/work-orders/types/work-order-severity"; import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard"; import { normalizeAdditionalContacts } from "@/domain/work-orders/mappers/additional-contacts-mapper"; @@ -64,7 +65,7 @@ export function assignWizardServicePocFields( payload.vendorNotes = vendorNotes; } - if (draft.severity != null) { + if (draft.severity != null && typeNeedsSeverity(draft.type)) { payload.severity = draft.severity; } diff --git a/src/domain/work-orders/mappers/work-order-board-mapper.ts b/src/domain/work-orders/mappers/work-order-board-mapper.ts index af00a55d..f1f046f9 100644 --- a/src/domain/work-orders/mappers/work-order-board-mapper.ts +++ b/src/domain/work-orders/mappers/work-order-board-mapper.ts @@ -28,7 +28,7 @@ const WO_TYPE_TO_API: Record = { Reactive: WORK_ORDER_TYPE_API.Reactive, - Overdue: WORK_ORDER_TYPE_API.Other, + Overdue: WORK_ORDER_TYPE_API.Overdue, }; /** Leading token of a location label, e.g. `BK5` from `BK5 · Dallas` or `CHI-1042` from `CHI-1042 — Chicago`. */ diff --git a/src/domain/work-orders/types/work-order-advanced-filters.ts b/src/domain/work-orders/types/work-order-advanced-filters.ts index 66f831dc..79f65b98 100644 --- a/src/domain/work-orders/types/work-order-advanced-filters.ts +++ b/src/domain/work-orders/types/work-order-advanced-filters.ts @@ -35,6 +35,8 @@ export interface WorkOrderAdvancedFilters { dispatchers: Set; regions: Set; statuses: Set; + /** Past Due overlay (any type); narrows the other filters, independent of the Overdue type. */ + pastDue: boolean; rescheduled: boolean; carriedOver: boolean; addOn: boolean; @@ -59,6 +61,7 @@ export const EMPTY_ADVANCED_FILTERS: WorkOrderAdvancedFilters = { dispatchers: new Set(), regions: new Set(), statuses: new Set(), + pastDue: false, rescheduled: false, carriedOver: false, addOn: false, diff --git a/src/domain/work-orders/types/work-order-board.ts b/src/domain/work-orders/types/work-order-board.ts index 317e7cad..88addefd 100644 --- a/src/domain/work-orders/types/work-order-board.ts +++ b/src/domain/work-orders/types/work-order-board.ts @@ -17,6 +17,7 @@ export const WORK_ORDER_TYPE_API = { Inspection: 5, Reactive: 6, AddOn: 7, + Overdue: 8, Other: 99, } as const; @@ -64,6 +65,7 @@ export const WORK_ORDER_TYPE_API_TO_FE: Record = { [WORK_ORDER_TYPE_API.PM]: "PM", [WORK_ORDER_TYPE_API.Reactive]: "Reactive", [WORK_ORDER_TYPE_API.Emergency]: "Emergency", + [WORK_ORDER_TYPE_API.Overdue]: "Overdue", }; export const LIFECYCLE_STATUS_API_TO_FE: Record = { 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 c7222b82..47703c1f 100644 --- a/src/domain/work-orders/utils/advanced-filter-chip-builders.ts +++ b/src/domain/work-orders/utils/advanced-filter-chip-builders.ts @@ -192,6 +192,15 @@ export function appendStatusPmVendorDocChips( clear: (current) => ({ ...current, statuses: new Set() }), }); + if (filters.pastDue) { + chips.push({ + key: "pastdue", + group: "Status", + value: "Past Due", + clear: (current) => ({ ...current, pastDue: false }), + }); + } + pushSetChip({ chips, key: "pm", diff --git a/src/domain/work-orders/utils/advanced-filter-predicates.ts b/src/domain/work-orders/utils/advanced-filter-predicates.ts index 45fb611e..530bbd70 100644 --- a/src/domain/work-orders/utils/advanced-filter-predicates.ts +++ b/src/domain/work-orders/utils/advanced-filter-predicates.ts @@ -45,12 +45,10 @@ const matchesDateRange: AdvancedFilterPredicate = (row, filters) => { const matchesSite: AdvancedFilterPredicate = (row, filters) => !filters.sites.size || filters.sites.has(row.site); -const matchesType: AdvancedFilterPredicate = (row, filters) => { - if (!filters.types.size) return true; - const overdueMatch = filters.types.has("Overdue") && row.isPastDue; - const typeMatch = filters.types.has(row.type as never); - return overdueMatch || typeMatch; -}; +const matchesType: AdvancedFilterPredicate = (row, filters) => + !filters.types.size || filters.types.has(row.type as never); + +const matchesPastDue: AdvancedFilterPredicate = (row, filters) => !filters.pastDue || row.isPastDue; const matchesDispatcher: AdvancedFilterPredicate = (row, filters) => { if (!filters.dispatchers.size) return true; @@ -95,6 +93,7 @@ export const ADVANCED_FILTER_PREDICATES: AdvancedFilterPredicate[] = [ matchesType, matchesDispatcher, matchesStatus, + matchesPastDue, matchesPmType, matchesVendorTech, matchesDoc, diff --git a/src/domain/work-orders/utils/advanced-filters-utils.ts b/src/domain/work-orders/utils/advanced-filters-utils.ts index 871bd0cd..9e0ccb90 100644 --- a/src/domain/work-orders/utils/advanced-filters-utils.ts +++ b/src/domain/work-orders/utils/advanced-filters-utils.ts @@ -34,6 +34,7 @@ function cloneFilters(filters: WorkOrderAdvancedFilters): WorkOrderAdvancedFilte flagColors: new Set(filters.flagColors ?? []), upliftStatuses: new Set(filters.upliftStatuses ?? []), unscheduledOnly: Boolean(filters.unscheduledOnly), + pastDue: Boolean(filters.pastDue), }; } @@ -45,6 +46,7 @@ const ADVANCED_FILTER_ACTIVE_SIGNALS = (filters: WorkOrderAdvancedFilters): bool Boolean(filters.dispatchers?.size), Boolean(filters.regions?.size), Boolean(filters.statuses?.size), + Boolean(filters.pastDue), filters.rescheduled, filters.carriedOver, filters.addOn, diff --git a/src/domain/work-orders/utils/board-query-params.ts b/src/domain/work-orders/utils/board-query-params.ts index 998d0605..df90e2fc 100644 --- a/src/domain/work-orders/utils/board-query-params.ts +++ b/src/domain/work-orders/utils/board-query-params.ts @@ -41,30 +41,19 @@ function mapDispatchers(ids: Iterable): string[] | undefined { return list.length > 0 ? list : undefined; } -/** Maps UI type filters to API `types` + separate `overdue` flag. Overdue is never sent as types=99. */ -function mapWoTypeFilters(types: Iterable): { - types?: number[]; - overdue?: boolean; -} { +/** Maps UI type filters to API `types`. The Overdue type is sent as its own enum; past due uses `overdue`. */ +function mapWoTypeFilters(types: Iterable): number[] | undefined { const list: number[] = []; - let overdue = false; for (const type of types) { if (type === "All") { continue; } - if (type === "Overdue") { - overdue = true; - continue; - } const api = mapWizardTypeToApi(type as WOType); if (api != null) { list.push(api); } } - return { - types: list.length > 0 ? list : undefined, - overdue: overdue || undefined, - }; + return list.length > 0 ? list : undefined; } function mapStatuses(statuses: Iterable): number[] | undefined { @@ -139,14 +128,11 @@ export function toBoardQueryParams( search: string, ): WorkOrderBoardQueryParams { const [, weekTo] = weekRange(bar.weekMonday); - const typeFilters = bar.types.size > 0 ? mapWoTypeFilters(bar.types) : {}; - return { weekStart: bar.weekMonday, weekEnd: weekTo, dispatchers: mapDispatchers(bar.dispatcherIds), - types: typeFilters.types, - overdue: typeFilters.overdue, + types: mapWoTypeFilters(bar.types), search: search.trim().length >= 2 ? search.trim() : undefined, }; } @@ -203,7 +189,6 @@ export function toBoardSearchQueryParams(input: { BOARD_SEARCH_MAX_PAGE_SIZE, ); - const typeFilters = mapWoTypeFilters(filters.types); const trimmedSearch = input.search?.trim(); const search = trimmedSearch && trimmedSearch.length >= 2 ? trimmedSearch : undefined; @@ -216,8 +201,8 @@ export function toBoardSearchQueryParams(input: { dateTo: date.dateTo, includeDateless: date.includeDateless, sites: filters.sites.size > 0 ? [...filters.sites] : undefined, - types: typeFilters.types, - overdue: typeFilters.overdue, + types: mapWoTypeFilters(filters.types), + overdue: filters.pastDue || undefined, dispatchers: mapDispatchers(filters.dispatchers), regions: filters.regions.size > 0 ? [...filters.regions] : undefined, statuses: mapStatuses(filters.statuses), @@ -252,10 +237,6 @@ export function buildBoardSearchParamsRecord( return out; } -export function woTypeFilterIsOverdue(type: string): boolean { - return type === "Overdue"; -} - /** Week-independent GET /board/search params for the pinned Unassigned queue. */ export function toUnassignedQueueQueryParams( page = 0, diff --git a/src/domain/work-orders/utils/table-filter-utils.ts b/src/domain/work-orders/utils/table-filter-utils.ts index 3d8eb132..2300335f 100644 --- a/src/domain/work-orders/utils/table-filter-utils.ts +++ b/src/domain/work-orders/utils/table-filter-utils.ts @@ -65,12 +65,7 @@ function rowMatchesSearch(row: WorkOrderTableRow, query: string): boolean { } function rowMatchesBarTypes(row: WorkOrderTableRow, types: Set): boolean { - if (types.size === 0) { - return true; - } - const overdueMatch = types.has("Overdue") && row.isPastDue; - const typeMatch = types.has(row.type as WOType); - return overdueMatch || typeMatch; + return types.size === 0 || types.has(row.type as WOType); } export function filterByBarAndSearch( diff --git a/src/test/app/(protected)/workorders/overdue-work-order-type.test.tsx b/src/test/app/(protected)/workorders/overdue-work-order-type.test.tsx new file mode 100644 index 00000000..5c429789 --- /dev/null +++ b/src/test/app/(protected)/workorders/overdue-work-order-type.test.tsx @@ -0,0 +1,154 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import { SlideOverCompletionTab } from "@/app/(protected)/workorders/_components/detail/tabs/slide-over-completion-tab"; +import { AdvancedSearchFilterSections } from "@/app/(protected)/workorders/_components/list/advanced-search/adv-filter-sections"; +import { TypeCell } from "@/app/(protected)/workorders/_components/list/table/cells/type-cell"; +import { WizardStepTypeSchedule } from "@/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule"; +import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-table-row-mapper"; +import { mapWizardDraftToBoardCreatePayload } from "@/domain/work-orders/mappers/work-order-board-mapper"; +import { + EMPTY_ADVANCED_FILTERS, + type WorkOrderAdvancedFilters, +} from "@/domain/work-orders/types/work-order-advanced-filters"; +import { WORK_ORDER_TYPE_API } from "@/domain/work-orders/types/work-order-board"; +import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import { + canContinueWizardStep1, + EMPTY_WIZARD_DRAFT, + type WorkOrderWizardDraft, +} from "@/domain/work-orders/types/work-order-wizard"; +import { buildAdvancedFilterChips } from "@/domain/work-orders/utils/advanced-filters-utils"; +import { getClosabilityGaps } from "@/domain/work-orders/utils/wo-closability"; + +const USERS = [{ id: "sarah", name: "Sarah Chen" }]; + +describe("Overdue work order type", () => { + it("offers Overdue in the Create WO wizard and never asks for a severity", () => { + const onPatch = vi.fn(); + const { rerender } = render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: /No client response — due date passed/ })); + expect(onPatch).toHaveBeenCalledWith( + expect.objectContaining({ type: "Overdue", severity: null }), + ); + + const overdueDraft: WorkOrderWizardDraft = { ...EMPTY_WIZARD_DRAFT, type: "Overdue" }; + rerender( + , + ); + expect(screen.queryByText(/Severity \(from APM\)/)).not.toBeInTheDocument(); + expect(canContinueWizardStep1(overdueDraft)).toBe(true); + }); + + it("creates the work order with the Overdue enum and no severity", () => { + const payload = mapWizardDraftToBoardCreatePayload({ + ...EMPTY_WIZARD_DRAFT, + type: "Overdue", + severity: 2, + locationId: "1", + siteCode: "BK5", + }); + + expect(payload.workOrderType).toBe(WORK_ORDER_TYPE_API.Overdue); + expect(payload.severity ?? null).toBeNull(); + }); + + it("corrects a type inline to Overdue without opening the severity dialog", async () => { + const onSave = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("button", { name: "Type of WO, Reactive" })); + await user.click(screen.getByRole("button", { name: /^Overdue$/ })); + + expect(onSave).toHaveBeenCalledWith("Overdue", null); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + + it("reads the Overdue enum back from the board as the Overdue type", () => { + const row = mapWorkOrderTableRow({ id: 1, workOrderType: WORK_ORDER_TYPE_API.Overdue }); + expect(row.type).toBe("Overdue"); + }); + + it("closes with the completion document, not photos", () => { + const base = { + id: "1", + dispatcherId: "sarah", + dispatcherName: "Sarah Chen", + company: "Gateway Plumbing", + vendorId: "", + tech: "", + completedDate: "2026-09-24", + woNumber: "24971", + pm: "Backflow Preventers", + mediaCount: 0, + }; + expect( + getClosabilityGaps({ ...base, type: "Overdue" as const, docStatus: "No" as const }), + ).toEqual(["Completion document"]); + expect( + getClosabilityGaps({ ...base, type: "Overdue" as const, docStatus: "Yes" as const }), + ).toEqual([]); + + const row = { + id: "1", + type: "Overdue", + pm: "Backflow Preventers", + docStatus: "No", + rowVersion: "1", + }; + render( + , + ); + expect(screen.getByText("Completion Document")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /open completion document/i })).toBeEnabled(); + }); + + it("keeps a separate Past Due status toggle in the advanced filters", () => { + let draft: WorkOrderAdvancedFilters = { ...EMPTY_ADVANCED_FILTERS }; + const setDraft = vi.fn( + (update: (prev: WorkOrderAdvancedFilters) => WorkOrderAdvancedFilters) => { + draft = update(draft); + }, + ); + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: /^Status/ })); + fireEvent.click(screen.getByRole("button", { name: /Past Due/ })); + + expect(draft.pastDue).toBe(true); + expect(draft.types.size).toBe(0); + expect(buildAdvancedFilterChips(draft)).toContainEqual( + expect.objectContaining({ group: "Status", value: "Past Due" }), + ); + }); +}); diff --git a/src/test/domain/services/api/services-api.test.ts b/src/test/domain/services/api/services-api.test.ts index be46e93b..7dc0b1b0 100644 --- a/src/test/domain/services/api/services-api.test.ts +++ b/src/test/domain/services/api/services-api.test.ts @@ -78,11 +78,14 @@ describe("servicesApi", () => { }); }); - it("does not request services for the unsupported Overdue type", async () => { - const services = await servicesApi.getActiveForWorkOrderType("Overdue"); + it("lists the PM catalog for the Overdue type", async () => { + vi.mocked(apiGet).mockResolvedValue([]); - expect(apiGet).not.toHaveBeenCalled(); - expect(services).toEqual([]); + await servicesApi.getActiveForWorkOrderType("Overdue"); + + expect(apiGet).toHaveBeenCalledWith(API_PATHS.services.list, { + searchParams: { isActive: true, workOrderType: 2 }, + }); }); it("creates, updates, and deactivates through the registry routes", async () => { diff --git a/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts b/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts index e5d0cd0e..b82d970c 100644 --- a/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts +++ b/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts @@ -36,7 +36,9 @@ describe("mapWizardTypeToApi", () => { expect(mapWizardTypeToApi("Reactive")).toBe(WORK_ORDER_TYPE_API.Reactive); - expect(mapWizardTypeToApi("Overdue")).toBe(WORK_ORDER_TYPE_API.Other); + expect(mapWizardTypeToApi("Overdue")).toBe(WORK_ORDER_TYPE_API.Overdue); + + expect(WORK_ORDER_TYPE_API.Overdue).toBe(8); expect(mapWizardTypeToApi("")).toBeNull(); }); 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 866de6f6..39fa0275 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 @@ -60,16 +60,16 @@ describe("toBoardQueryParams", () => { expect(params.overdue).toBeUndefined(); }); - it("maps Overdue bar filter to overdue=true without types", () => { + it("maps the Overdue bar filter to the Overdue type, not the past-due flag", () => { const params = toBoardQueryParams({ ...baseBar, types: new Set(["Overdue"]) }, ""); - expect(params.overdue).toBe(true); - expect(params.types).toBeUndefined(); + expect(params.types).toEqual([WORK_ORDER_TYPE_API.Overdue]); + expect(params.overdue).toBeUndefined(); }); - it("maps Overdue with real types as OR (types + overdue)", () => { + it("maps Overdue with other types as a plain type union", () => { const params = toBoardQueryParams({ ...baseBar, types: new Set(["PM", "Overdue"]) }, ""); - expect(params.types).toEqual([WORK_ORDER_TYPE_API.PM]); - expect(params.overdue).toBe(true); + expect(params.types).toEqual([WORK_ORDER_TYPE_API.PM, WORK_ORDER_TYPE_API.Overdue]); + expect(params.overdue).toBeUndefined(); }); it("omits types when the set is empty", () => { @@ -222,22 +222,23 @@ describe("toBoardSearchQueryParams", () => { expect(buildBoardSearchParamsRecord(params)).not.toHaveProperty("unscheduledOnly"); }); - it("maps Overdue-only advanced filter to overdue=true without types", () => { + it("maps the Overdue type advanced filter to types without the past-due flag", () => { const params = toBoardSearchQueryParams({ filters: { ...EMPTY_ADVANCED_FILTERS, types: new Set(["Overdue"]), }, }); - expect(params.overdue).toBe(true); - expect(params.types).toBeUndefined(); + expect(params.types).toEqual([WORK_ORDER_TYPE_API.Overdue]); + expect(params.overdue).toBeUndefined(); }); - it("maps Overdue with real types as OR (types + overdue)", () => { + it("maps the Past Due status toggle to overdue=true alongside types", () => { const params = toBoardSearchQueryParams({ filters: { ...EMPTY_ADVANCED_FILTERS, - types: new Set(["PM", "Overdue"]), + types: new Set(["PM"]), + pastDue: true, }, }); expect(params.types).toEqual([WORK_ORDER_TYPE_API.PM]); diff --git a/src/test/domain/work-orders/utils/table-filter-utils.test.ts b/src/test/domain/work-orders/utils/table-filter-utils.test.ts index cb7b10e5..9798afb8 100644 --- a/src/test/domain/work-orders/utils/table-filter-utils.test.ts +++ b/src/test/domain/work-orders/utils/table-filter-utils.test.ts @@ -172,26 +172,26 @@ describe("filterByBarAndSearch", () => { expect(ids).toEqual(["em", "rx"]); }); - it("matches Overdue via isPastDue even when row.type is not Overdue", () => { + it("matches the Overdue type only, never a past-due row of another type", () => { const pastDuePm = makeRow({ id: "pd", type: "PM", isPastDue: true, scheduledOn: "2026-06-03", }); - const currentPm = makeRow({ - id: "now", - type: "PM", + const overdue = makeRow({ + id: "ov", + type: "Overdue", isPastDue: false, scheduledOn: "2026-06-03", }); const ids = filterByBarAndSearch( - [pastDuePm, currentPm], + [pastDuePm, overdue], bar(weekMonday, { types: new Set(["Overdue"]) }), "", ).map((row) => row.id); - expect(ids).toEqual(["pd"]); + expect(ids).toEqual(["ov"]); }); it("hides a row after type correction when the bar filter no longer matches", () => { @@ -395,13 +395,8 @@ describe("matchesAdvancedFilters", () => { expect(matchesAdvancedFilters(row, noMatch)).toBe(false); }); - it("matches Overdue when isPastDue is true", () => { + it("matches the Overdue type filter by type, independent of isPastDue", () => { const weekMonday = mondayOf(todayIso()); - const pastDue = makeRow({ - scheduledOn: weekMonday, - ...deriveDayFields(weekMonday), - isPastDue: true, - }); const filters = { ...EMPTY_ADVANCED_FILTERS, dateRange: "custom" as const, @@ -409,9 +404,39 @@ describe("matchesAdvancedFilters", () => { customTo: "9999-12-31", types: new Set(["Overdue" as const]), }; - expect(matchesAdvancedFilters(pastDue, filters)).toBe(true); + const overdue = makeRow({ type: "Overdue", isPastDue: false, scheduledOn: weekMonday }); + const pastDuePm = makeRow({ type: "PM", isPastDue: true, scheduledOn: weekMonday }); + expect(matchesAdvancedFilters(overdue, filters)).toBe(true); + expect(matchesAdvancedFilters(pastDuePm, filters)).toBe(false); + }); + + it("narrows to past-due rows with the Past Due status toggle", () => { + const weekMonday = mondayOf(todayIso()); + const filters = { + ...EMPTY_ADVANCED_FILTERS, + dateRange: "custom" as const, + customFrom: "0000-01-01", + customTo: "9999-12-31", + types: new Set(["PM" as const]), + pastDue: true, + }; expect( - matchesAdvancedFilters(makeRow({ isPastDue: false, scheduledOn: weekMonday }), filters), + matchesAdvancedFilters( + makeRow({ type: "PM", isPastDue: true, scheduledOn: weekMonday }), + filters, + ), + ).toBe(true); + expect( + matchesAdvancedFilters( + makeRow({ type: "PM", isPastDue: false, scheduledOn: weekMonday }), + filters, + ), + ).toBe(false); + expect( + matchesAdvancedFilters( + makeRow({ type: "Overdue", isPastDue: true, scheduledOn: weekMonday }), + filters, + ), ).toBe(false); });