From 0d6af0da592b04f7a88ca91aa58460a6c6bac0fd Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 24 Sep 2026 20:55:59 -0300 Subject: [PATCH 1/5] fix(workorders): make the Advanced Filters date range narrow results (SH-391) The Advanced Filters default was This week, and that value doubled as no range, so an explicit This week searched every week. Other ranges passed every undated WO through on the server. No range is now its own state (nothing selected, like the prototype; clicking a selected range clears it). It searches every week with undated rows, via includeDateless. Any selected range narrows strictly. The pinned Unassigned queue and the WO# duplicate lookup keep spanning every week. --- .../adv-date-range-section.tsx | 7 ++- src/domain/work-orders/api/work-orders-api.ts | 11 +++- .../types/work-order-advanced-filters.ts | 5 +- .../work-orders/types/work-order-board.ts | 5 ++ .../utils/advanced-filter-chip-builders.ts | 4 +- .../utils/advanced-filter-predicates.ts | 16 ++++- .../utils/advanced-filters-utils.ts | 10 +-- .../work-orders/utils/board-query-params.ts | 13 ++-- .../adv-date-range-section.test.tsx | 63 +++++++++++++++++++ ...ork-orders-advanced-search-wiring.test.tsx | 2 +- .../work-orders/api/work-orders-api.test.ts | 10 +++ .../utils/advanced-filters-utils.test.ts | 4 +- .../utils/board-query-params.test.ts | 40 +++++++++++- 13 files changed, 167 insertions(+), 23 deletions(-) create mode 100644 src/test/app/(protected)/workorders/adv-date-range-section.test.tsx diff --git a/src/app/(protected)/workorders/_components/list/advanced-search/adv-date-range-section.tsx b/src/app/(protected)/workorders/_components/list/advanced-search/adv-date-range-section.tsx index ee9f1b25..284c3707 100644 --- a/src/app/(protected)/workorders/_components/list/advanced-search/adv-date-range-section.tsx +++ b/src/app/(protected)/workorders/_components/list/advanced-search/adv-date-range-section.tsx @@ -27,7 +27,12 @@ export function AdvDateRangeSection({ draft, setDraft }: AdvDateRangeSectionProp key={option.key} label={option.label} selected={draft.dateRange === option.key} - onClick={() => setDraft((prev) => ({ ...prev, dateRange: option.key }))} + onClick={() => + setDraft((prev) => ({ + ...prev, + dateRange: prev.dateRange === option.key ? null : option.key, + })) + } /> ))} { const searchParams: Record = { @@ -223,8 +227,13 @@ export const workOrdersApi = { "Unable to verify the work order number: search result set exceeded the safety page limit.", ); } + // Every week plus undated rows: a WO# must be unique regardless of schedule (SH-391). const result = await workOrdersApi.searchBoard({ search: normalized, + datePreset: "Custom", + dateFrom: ADVANCED_SEARCH_ALL_WEEKS_FROM, + dateTo: ADVANCED_SEARCH_ALL_WEEKS_TO, + includeDateless: true, page, pageSize, }); 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 c187aee8..62719c8d 100644 --- a/src/domain/work-orders/types/work-order-advanced-filters.ts +++ b/src/domain/work-orders/types/work-order-advanced-filters.ts @@ -23,7 +23,8 @@ export type DateRangeKey = export type CompletionDocFilter = "No" | "Yes" | "NN"; export interface WorkOrderAdvancedFilters { - dateRange: DateRangeKey; + /** `null` = no date range selected: search every week (SH-391). */ + dateRange: DateRangeKey | null; customFrom: string; customTo: string; /** invariant: independent of DateRangeKey; never sent as datePreset=Unscheduled. */ @@ -48,7 +49,7 @@ export interface WorkOrderAdvancedFilters { } export const EMPTY_ADVANCED_FILTERS: WorkOrderAdvancedFilters = { - dateRange: "this-week", + dateRange: null, customFrom: "", customTo: "", unscheduledOnly: false, diff --git a/src/domain/work-orders/types/work-order-board.ts b/src/domain/work-orders/types/work-order-board.ts index 3b06e4fb..803495ba 100644 --- a/src/domain/work-orders/types/work-order-board.ts +++ b/src/domain/work-orders/types/work-order-board.ts @@ -318,6 +318,11 @@ export interface WorkOrderBoardSearchQueryParams { * legacy wire behavior. */ unscheduledOnly?: boolean; + /** + * SH-391: `true` adds open rows with no date to the range. Sent only when no + * date range is selected, so an explicit range strictly narrows the results. + */ + includeDateless?: boolean; hasUplift?: boolean; upliftStatuses?: string[]; myWorkOrders?: boolean; 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 edc707a0..c7222b82 100644 --- a/src/domain/work-orders/utils/advanced-filter-chip-builders.ts +++ b/src/domain/work-orders/utils/advanced-filter-chip-builders.ts @@ -60,7 +60,7 @@ function severityLabel(level: WorkOrderSeverity): string { } export function appendDateChip(chips: AdvancedFilterChip[], filters: WorkOrderAdvancedFilters) { - if (filters.dateRange !== "this-week") { + if (filters.dateRange !== null) { const value = filters.dateRange === "custom" ? `${fmtChipDate(filters.customFrom) || "Any"} – ${fmtChipDate(filters.customTo) || "Any"}` @@ -71,7 +71,7 @@ export function appendDateChip(chips: AdvancedFilterChip[], filters: WorkOrderAd value, clear: (current) => ({ ...current, - dateRange: "this-week", + dateRange: null, customFrom: "", customTo: "", }), diff --git a/src/domain/work-orders/utils/advanced-filter-predicates.ts b/src/domain/work-orders/utils/advanced-filter-predicates.ts index 5217261b..75a6a40f 100644 --- a/src/domain/work-orders/utils/advanced-filter-predicates.ts +++ b/src/domain/work-orders/utils/advanced-filter-predicates.ts @@ -2,6 +2,7 @@ import type { WorkOrderAdvancedFilters } from "@/domain/work-orders/types/work-o import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import { dateRangeFor } from "@/domain/work-orders/utils/advanced-filters-utils"; +import { addDaysIso } from "@/domain/work-orders/utils/wizard-date-utils"; type AdvancedFilterPredicate = ( row: WorkOrderTableRow, @@ -12,8 +13,19 @@ const matchesDateRange: AdvancedFilterPredicate = (row, filters) => { if (filters.unscheduledOnly) { return !row.scheduledOn; } - const [from, to] = dateRangeFor(filters); - return !row.scheduledOn || (row.scheduledOn >= from && row.scheduledOn <= to); + const range = dateRangeFor(filters); + if (!range) return true; + const [from, to] = range; + if (row.scheduledOn) { + return row.scheduledOn >= from && row.scheduledOn <= to; + } + // Week-only rows match when their target week overlaps the range (SH-391). + return ( + row.scheduleWeekOnly && + Boolean(row.targetWeek) && + row.targetWeek <= to && + addDaysIso(row.targetWeek, 6) >= from + ); }; const matchesSite: AdvancedFilterPredicate = (row, filters) => diff --git a/src/domain/work-orders/utils/advanced-filters-utils.ts b/src/domain/work-orders/utils/advanced-filters-utils.ts index 3f8f5ce5..871bd0cd 100644 --- a/src/domain/work-orders/utils/advanced-filters-utils.ts +++ b/src/domain/work-orders/utils/advanced-filters-utils.ts @@ -38,7 +38,7 @@ function cloneFilters(filters: WorkOrderAdvancedFilters): WorkOrderAdvancedFilte } const ADVANCED_FILTER_ACTIVE_SIGNALS = (filters: WorkOrderAdvancedFilters): boolean[] => [ - filters.dateRange !== "this-week", + filters.dateRange !== null, Boolean(filters.sites?.size), Boolean(filters.types?.size), Boolean(filters.severities?.size), @@ -70,7 +70,9 @@ function monthEnd(y: number, m: number): string { return `${y}-${pad2(m + 1)}-${pad2(new Date(y, m + 1, 0).getDate())}`; } -export function dateRangeFor(filters: WorkOrderAdvancedFilters): [string, string] { +/** Inclusive [from, to] for the selected range; `null` when no range is selected. */ +export function dateRangeFor(filters: WorkOrderAdvancedFilters): [string, string] | null { + if (filters.dateRange === null) return null; const today = todayIso(); const ty = Number(today.slice(0, 4)); const tm = Number(today.slice(5, 7)) - 1; @@ -144,6 +146,6 @@ export function cloneAdvancedFilters(filters: WorkOrderAdvancedFilters): WorkOrd return cloneFilters(filters); } -export function isDefaultDateRange(dateRange: DateRangeKey): boolean { - return dateRange === "this-week"; +export function isDefaultDateRange(dateRange: DateRangeKey | null): boolean { + return dateRange === null; } diff --git a/src/domain/work-orders/utils/board-query-params.ts b/src/domain/work-orders/utils/board-query-params.ts index c10519b8..c5c21260 100644 --- a/src/domain/work-orders/utils/board-query-params.ts +++ b/src/domain/work-orders/utils/board-query-params.ts @@ -1,8 +1,5 @@ import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list"; -import { - EMPTY_ADVANCED_FILTERS, - type WorkOrderAdvancedFilters, -} from "@/domain/work-orders/types/work-order-advanced-filters"; +import type { WorkOrderAdvancedFilters } from "@/domain/work-orders/types/work-order-advanced-filters"; import type { WorkOrderBarFilters } from "@/domain/work-orders/types/work-order-list-filters"; import { BOARD_DISPATCHER_UNASSIGNED, @@ -158,17 +155,21 @@ function resolveAdvancedSearchDate(filters: WorkOrderAdvancedFilters): { datePreset?: WorkOrderBoardSearchQueryParams["datePreset"]; dateFrom?: string; dateTo?: string; + includeDateless?: boolean; } { // SH-338: server-side unscheduledOnly owns the date-range exemption; the // client must not fake a Custom window and sends no date params. if (filters.unscheduledOnly) { return {}; } - if (filters.dateRange === EMPTY_ADVANCED_FILTERS.dateRange) { + // SH-391: no range selected searches every week, undated rows included. Any + // selected range (This week included) strictly narrows the results. + if (filters.dateRange === null) { return { datePreset: "Custom", dateFrom: ADVANCED_SEARCH_ALL_WEEKS_FROM, dateTo: ADVANCED_SEARCH_ALL_WEEKS_TO, + includeDateless: true, }; } @@ -213,6 +214,7 @@ export function toBoardSearchQueryParams(input: { datePreset: date.datePreset, dateFrom: date.dateFrom, dateTo: date.dateTo, + includeDateless: date.includeDateless, sites: filters.sites.size > 0 ? [...filters.sites] : undefined, types: typeFilters.types, overdue: typeFilters.overdue, @@ -267,5 +269,6 @@ export function toUnassignedQueueQueryParams( datePreset: "Custom", dateFrom: UNASSIGNED_QUEUE_DATE_FROM, dateTo: UNASSIGNED_QUEUE_DATE_TO, + includeDateless: true, }; } diff --git a/src/test/app/(protected)/workorders/adv-date-range-section.test.tsx b/src/test/app/(protected)/workorders/adv-date-range-section.test.tsx new file mode 100644 index 00000000..8ffe1f60 --- /dev/null +++ b/src/test/app/(protected)/workorders/adv-date-range-section.test.tsx @@ -0,0 +1,63 @@ +import { useEffect, useState } from "react"; +import { fireEvent, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { AdvDateRangeSection } from "@/app/(protected)/workorders/_components/list/advanced-search/adv-date-range-section"; +import { + EMPTY_ADVANCED_FILTERS, + type WorkOrderAdvancedFilters, +} from "@/domain/work-orders/types/work-order-advanced-filters"; +import { cloneAdvancedFilters } from "@/domain/work-orders/utils/advanced-filters-utils"; +import { toBoardSearchQueryParams } from "@/domain/work-orders/utils/board-query-params"; +import { renderWithProviders } from "@/test/test-utils"; + +const onDraft = vi.fn<(draft: WorkOrderAdvancedFilters) => void>(); + +function latestDraft(): WorkOrderAdvancedFilters { + return onDraft.mock.calls[onDraft.mock.calls.length - 1][0]; +} + +function Harness() { + const [draft, setDraft] = useState(() => cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS)); + useEffect(() => { + onDraft(draft); + }, [draft]); + return ; +} + +function openSection() { + onDraft.mockClear(); + renderWithProviders(); + const header = screen.queryByRole("button", { name: /Date range/i }); + if (header && !screen.queryByRole("button", { name: /^This week$/i })) { + fireEvent.click(header); + } +} + +describe("Advanced Filters date range (SH-391)", () => { + it("starts with no range selected, so the search spans every week", () => { + openSection(); + + for (const label of ["This week", "Last week", "Last 3 months", "Custom"]) { + expect(screen.getByRole("button", { name: new RegExp(`^${label}$`, "i") })).toHaveAttribute( + "aria-pressed", + "false", + ); + } + expect(toBoardSearchQueryParams({ filters: latestDraft() }).includeDateless).toBe(true); + }); + + it("narrows to This week once picked, and clicking it again clears the range", () => { + openSection(); + const thisWeek = screen.getByRole("button", { name: /^This week$/i }); + + fireEvent.click(thisWeek); + expect(thisWeek).toHaveAttribute("aria-pressed", "true"); + const narrowed = toBoardSearchQueryParams({ filters: latestDraft() }); + expect(narrowed.datePreset).toBe("ThisWeek"); + expect(narrowed.includeDateless).toBeUndefined(); + + fireEvent.click(thisWeek); + expect(thisWeek).toHaveAttribute("aria-pressed", "false"); + expect(latestDraft().dateRange).toBeNull(); + }); +}); 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 6e64219f..3927bfaf 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 @@ -367,7 +367,7 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => { expect(unscheduled).toHaveAttribute("aria-pressed", "true"); expect(within(sheet).getByRole("button", { name: /^This week$/i })).toHaveAttribute( "aria-pressed", - "true", + "false", ); expect(within(sheet).queryByLabelText(/^From$/i)).not.toBeInTheDocument(); expect(within(sheet).queryByLabelText(/^To$/i)).not.toBeInTheDocument(); diff --git a/src/test/domain/work-orders/api/work-orders-api.test.ts b/src/test/domain/work-orders/api/work-orders-api.test.ts index 27ce9129..7ea19716 100644 --- a/src/test/domain/work-orders/api/work-orders-api.test.ts +++ b/src/test/domain/work-orders/api/work-orders-api.test.ts @@ -420,6 +420,14 @@ describe("workOrdersApi.searchBoard", () => { }); }); +/** A WO# lookup spans every week and undated rows (SH-391). */ +const ALL_WEEKS_SEARCH = { + datePreset: "Custom", + dateFrom: "2000-01-01", + dateTo: "2099-12-31", + includeDateless: "true", +}; + describe("workOrdersApi.findByWoNumber", () => { beforeEach(() => { apiGet.mockReset(); @@ -450,6 +458,7 @@ describe("workOrdersApi.findByWoNumber", () => { const searchParams = apiGet.mock.calls[0]?.[1]?.searchParams as URLSearchParams; expect(Object.fromEntries(searchParams)).toEqual({ search: "10000001235", + ...ALL_WEEKS_SEARCH, page: "0", pageSize: "100", }); @@ -519,6 +528,7 @@ describe("workOrdersApi.findByWoNumber", () => { const searchParams = apiGet.mock.calls[0]?.[1]?.searchParams as URLSearchParams; expect(Object.fromEntries(searchParams)).toEqual({ search: "9", + ...ALL_WEEKS_SEARCH, page: "0", pageSize: "100", }); 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 e26229d7..f065e4e0 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 @@ -40,12 +40,12 @@ describe("countAdvancedFilters", () => { expect(countAdvancedFilters(filters)).toBe(3); }); - it("counts Unscheduled +1 with the this-week default still in place", () => { + it("counts Unscheduled +1 with no date range selected", () => { const filters = { ...cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS), unscheduledOnly: true, }; - expect(filters.dateRange).toBe("this-week"); + expect(filters.dateRange).toBeNull(); expect(countAdvancedFilters(filters)).toBe(1); }); 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 486cd60e..3daaf798 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 @@ -130,9 +130,43 @@ describe("toBoardSearchQueryParams", () => { customTo: "2026-06-30", }, }); - expect(thisWeek.datePreset).toBe("Custom"); - expect(thisWeek.dateFrom).toBe("2000-01-01"); - expect(thisWeek.dateTo).toBe("2099-12-31"); + expect(thisWeek.datePreset).toBe("ThisWeek"); + expect(thisWeek.dateFrom).toBeUndefined(); + expect(thisWeek.dateTo).toBeUndefined(); + }); + + it("narrows strictly for every selected range, This week included (SH-391)", () => { + for (const dateRange of ["this-week", "last-week", "last-3-months", "custom"] as const) { + const params = toBoardSearchQueryParams({ + filters: { + ...EMPTY_ADVANCED_FILTERS, + dateRange, + customFrom: "2026-09-21", + customTo: "2026-09-25", + dispatchers: new Set(["__unassigned__"]), + }, + }); + expect(params.includeDateless).toBeUndefined(); + expect(params.dateFrom).not.toBe("2000-01-01"); + } + }); + + it("searches every week, undated rows included, when no range is selected (SH-391)", () => { + const params = toBoardSearchQueryParams({ + filters: { ...EMPTY_ADVANCED_FILTERS, dispatchers: new Set(["__unassigned__"]) }, + }); + expect(EMPTY_ADVANCED_FILTERS.dateRange).toBeNull(); + expect(params).toMatchObject({ + datePreset: "Custom", + dateFrom: "2000-01-01", + dateTo: "2099-12-31", + includeDateless: true, + dispatchers: ["__unassigned__"], + }); + }); + + it("keeps undated rows in the pinned Unassigned queue (SH-391)", () => { + expect(toUnassignedQueueQueryParams().includeDateless).toBe(true); }); it("sends unscheduledOnly=true with no faked Custom date window (SH-338)", () => { From f69c8a04ebe4da37fc98ae38097724c9d3423da8 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 24 Sep 2026 21:10:40 -0300 Subject: [PATCH 2/5] fix(workorders): match a range on the board the same way the search does (SH-391) A week-only row that still carries a scheduled date outside the range passed the board search (its target week overlaps) but failed the on-page filter, which judged it by the date alone. Both now accept a row dated in the range or a week-only row whose target week overlaps it. --- .../utils/advanced-filter-predicates.ts | 7 ++-- .../utils/table-filter-utils.test.ts | 36 +++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/domain/work-orders/utils/advanced-filter-predicates.ts b/src/domain/work-orders/utils/advanced-filter-predicates.ts index 75a6a40f..9fa0b3b4 100644 --- a/src/domain/work-orders/utils/advanced-filter-predicates.ts +++ b/src/domain/work-orders/utils/advanced-filter-predicates.ts @@ -16,10 +16,9 @@ const matchesDateRange: AdvancedFilterPredicate = (row, filters) => { const range = dateRangeFor(filters); if (!range) return true; const [from, to] = range; - if (row.scheduledOn) { - return row.scheduledOn >= from && row.scheduledOn <= to; - } - // Week-only rows match when their target week overlaps the range (SH-391). + if (row.scheduledOn && row.scheduledOn >= from && row.scheduledOn <= to) return true; + // Week-only rows also match when their target week overlaps the range, the + // same rule as the board search (SH-391). return ( row.scheduleWeekOnly && Boolean(row.targetWeek) && 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 022d36fe..8471849b 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 @@ -454,6 +454,42 @@ describe("matchesAdvancedFilters", () => { expect(matchesAdvancedFilters(weekOnly, filters)).toBe(true); expect(matchesAdvancedFilters(undatedAssigned, filters)).toBe(true); }); + + it("matches a range the same way the board search does (SH-391)", () => { + const filters = { + ...EMPTY_ADVANCED_FILTERS, + dateRange: "custom" as const, + customFrom: "2026-09-23", + customTo: "2026-09-25", + }; + + // Week-only: a target week overlapping the range matches, whatever date the + // row still carries. + const weekOnlyStaleDate = makeRow({ + scheduledOn: "2026-10-02", + targetWeek: "2026-09-21", + scheduleWeekOnly: true, + }); + const weekOnlyPreviousWeek = makeRow({ + scheduledOn: "", + targetWeek: "2026-09-14", + scheduleWeekOnly: true, + }); + const weekOnlyOutside = makeRow({ + scheduledOn: "", + targetWeek: "2026-09-28", + scheduleWeekOnly: true, + }); + const datedOutsideWithWeek = makeRow({ scheduledOn: "2026-10-02", targetWeek: "2026-09-21" }); + const undated = makeRow({ scheduledOn: "", targetWeek: "" }); + + expect(matchesAdvancedFilters(weekOnlyStaleDate, filters)).toBe(true); + expect(matchesAdvancedFilters(weekOnlyPreviousWeek, filters)).toBe(false); + expect(matchesAdvancedFilters(weekOnlyOutside, filters)).toBe(false); + expect(matchesAdvancedFilters(datedOutsideWithWeek, filters)).toBe(false); + expect(matchesAdvancedFilters(undated, filters)).toBe(false); + expect(matchesAdvancedFilters(undated, { ...filters, dateRange: null })).toBe(true); + }); }); describe("computeIsPastDue", () => { From 51ca244524eeaf3d62b1e3d2f68e9d208c6e58b6 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 24 Sep 2026 21:34:24 -0300 Subject: [PATCH 3/5] fix(workorders): keep closed undated WOs out of the board page like the search (SH-391) With no range selected, or with Unscheduled, the board search adds only open undated work orders; completed and canceled ones without a date are never unscheduled. The board page kept them. It now applies the same rule, so both return the same rows. --- .../utils/advanced-filter-predicates.ts | 31 ++++++++++++++----- .../utils/table-filter-utils.test.ts | 16 +++++++++- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/domain/work-orders/utils/advanced-filter-predicates.ts b/src/domain/work-orders/utils/advanced-filter-predicates.ts index 9fa0b3b4..6b40d48e 100644 --- a/src/domain/work-orders/utils/advanced-filter-predicates.ts +++ b/src/domain/work-orders/utils/advanced-filter-predicates.ts @@ -2,6 +2,11 @@ import type { WorkOrderAdvancedFilters } from "@/domain/work-orders/types/work-o import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import { dateRangeFor } from "@/domain/work-orders/utils/advanced-filters-utils"; +import { + ADVANCED_SEARCH_ALL_WEEKS_FROM, + ADVANCED_SEARCH_ALL_WEEKS_TO, +} from "@/domain/work-orders/utils/board-query-params"; +import { isTerminalCarryOverStatus } from "@/domain/work-orders/utils/carry-over-display"; import { addDaysIso } from "@/domain/work-orders/utils/wizard-date-utils"; type AdvancedFilterPredicate = ( @@ -9,22 +14,32 @@ type AdvancedFilterPredicate = ( filters: WorkOrderAdvancedFilters, ) => boolean; +const isOpenUndated = (row: WorkOrderTableRow): boolean => + !row.scheduledOn && !isTerminalCarryOverStatus(row.status); + +// Mirrors the board search (ApplyDateRangeFilter and ApplyUnscheduledOnlyFilter) +// so the page and /board/search keep the same rows (SH-391): a row is in a range +// when its date is, or when it is week-only and its target week overlaps. With +// no range, every week is searched and open undated rows are added. Closed +// undated rows are never unscheduled. const matchesDateRange: AdvancedFilterPredicate = (row, filters) => { if (filters.unscheduledOnly) { - return !row.scheduledOn; + return isOpenUndated(row); } - const range = dateRangeFor(filters); - if (!range) return true; - const [from, to] = range; + const [from, to] = dateRangeFor(filters) ?? [ + ADVANCED_SEARCH_ALL_WEEKS_FROM, + ADVANCED_SEARCH_ALL_WEEKS_TO, + ]; if (row.scheduledOn && row.scheduledOn >= from && row.scheduledOn <= to) return true; - // Week-only rows also match when their target week overlaps the range, the - // same rule as the board search (SH-391). - return ( + if ( row.scheduleWeekOnly && Boolean(row.targetWeek) && row.targetWeek <= to && addDaysIso(row.targetWeek, 6) >= from - ); + ) { + return true; + } + return filters.dateRange === null && isOpenUndated(row); }; const matchesSite: AdvancedFilterPredicate = (row, filters) => 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 8471849b..296c7b6d 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 @@ -488,7 +488,21 @@ describe("matchesAdvancedFilters", () => { expect(matchesAdvancedFilters(weekOnlyOutside, filters)).toBe(false); expect(matchesAdvancedFilters(datedOutsideWithWeek, filters)).toBe(false); expect(matchesAdvancedFilters(undated, filters)).toBe(false); - expect(matchesAdvancedFilters(undated, { ...filters, dateRange: null })).toBe(true); + // No range: every week, plus open undated rows. Closed undated rows are + // never unscheduled, with or without the Unscheduled filter. + const noRange = { ...filters, dateRange: null }; + const unscheduledOnly = { ...filters, unscheduledOnly: true }; + const completedUndated = makeRow({ scheduledOn: "", targetWeek: "", status: "Completed" }); + const canceledUndated = makeRow({ scheduledOn: "", targetWeek: "", status: "Canceled" }); + + expect(matchesAdvancedFilters(undated, noRange)).toBe(true); + expect(matchesAdvancedFilters(datedOutsideWithWeek, noRange)).toBe(true); + expect(matchesAdvancedFilters(weekOnlyOutside, noRange)).toBe(true); + expect(matchesAdvancedFilters(completedUndated, noRange)).toBe(false); + expect(matchesAdvancedFilters(canceledUndated, noRange)).toBe(false); + expect(matchesAdvancedFilters(undated, unscheduledOnly)).toBe(true); + expect(matchesAdvancedFilters(completedUndated, unscheduledOnly)).toBe(false); + expect(matchesAdvancedFilters(canceledUndated, unscheduledOnly)).toBe(false); }); }); From c1fd8f6d8893eb83b52d8e9216e48bf65cad02c5 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 24 Sep 2026 22:03:23 -0300 Subject: [PATCH 4/5] chore(workorders): keep ticket keys out of source comments --- src/domain/work-orders/api/work-orders-api.ts | 2 +- src/domain/work-orders/types/work-order-advanced-filters.ts | 2 +- src/domain/work-orders/types/work-order-board.ts | 2 +- src/domain/work-orders/utils/advanced-filter-predicates.ts | 2 +- src/domain/work-orders/utils/board-query-params.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/domain/work-orders/api/work-orders-api.ts b/src/domain/work-orders/api/work-orders-api.ts index 19e1ce76..289187bf 100644 --- a/src/domain/work-orders/api/work-orders-api.ts +++ b/src/domain/work-orders/api/work-orders-api.ts @@ -227,7 +227,7 @@ export const workOrdersApi = { "Unable to verify the work order number: search result set exceeded the safety page limit.", ); } - // Every week plus undated rows: a WO# must be unique regardless of schedule (SH-391). + // Every week plus undated rows: a WO# must be unique regardless of schedule. const result = await workOrdersApi.searchBoard({ search: normalized, datePreset: "Custom", 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 62719c8d..66f831dc 100644 --- a/src/domain/work-orders/types/work-order-advanced-filters.ts +++ b/src/domain/work-orders/types/work-order-advanced-filters.ts @@ -23,7 +23,7 @@ export type DateRangeKey = export type CompletionDocFilter = "No" | "Yes" | "NN"; export interface WorkOrderAdvancedFilters { - /** `null` = no date range selected: search every week (SH-391). */ + /** `null` = no date range selected: search every week. */ dateRange: DateRangeKey | null; customFrom: string; customTo: string; diff --git a/src/domain/work-orders/types/work-order-board.ts b/src/domain/work-orders/types/work-order-board.ts index 803495ba..317e7cad 100644 --- a/src/domain/work-orders/types/work-order-board.ts +++ b/src/domain/work-orders/types/work-order-board.ts @@ -319,7 +319,7 @@ export interface WorkOrderBoardSearchQueryParams { */ unscheduledOnly?: boolean; /** - * SH-391: `true` adds open rows with no date to the range. Sent only when no + * `true` adds open rows with no date to the range. Sent only when no * date range is selected, so an explicit range strictly narrows the results. */ includeDateless?: boolean; diff --git a/src/domain/work-orders/utils/advanced-filter-predicates.ts b/src/domain/work-orders/utils/advanced-filter-predicates.ts index 6b40d48e..45fb611e 100644 --- a/src/domain/work-orders/utils/advanced-filter-predicates.ts +++ b/src/domain/work-orders/utils/advanced-filter-predicates.ts @@ -18,7 +18,7 @@ const isOpenUndated = (row: WorkOrderTableRow): boolean => !row.scheduledOn && !isTerminalCarryOverStatus(row.status); // Mirrors the board search (ApplyDateRangeFilter and ApplyUnscheduledOnlyFilter) -// so the page and /board/search keep the same rows (SH-391): a row is in a range +// so the page and /board/search keep the same rows: a row is in a range // when its date is, or when it is week-only and its target week overlaps. With // no range, every week is searched and open undated rows are added. Closed // undated rows are never unscheduled. diff --git a/src/domain/work-orders/utils/board-query-params.ts b/src/domain/work-orders/utils/board-query-params.ts index c5c21260..998d0605 100644 --- a/src/domain/work-orders/utils/board-query-params.ts +++ b/src/domain/work-orders/utils/board-query-params.ts @@ -162,7 +162,7 @@ function resolveAdvancedSearchDate(filters: WorkOrderAdvancedFilters): { if (filters.unscheduledOnly) { return {}; } - // SH-391: no range selected searches every week, undated rows included. Any + // No range selected searches every week, undated rows included. Any // selected range (This week included) strictly narrows the results. if (filters.dateRange === null) { return { From 62e38e4b81b16d93b0da2db2653c57853c15ed0a Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 24 Sep 2026 22:41:05 -0300 Subject: [PATCH 5/5] chore(workorders): keep ticket keys out of test names The review contract bars Jira keys in source comments and test names; ticket identity lives in the PR, commit and branch. The previous commit cleaned the source files; this strips the six (SH-391) suffixes the branch added to test names and a test comment, keeping the behavioural wording. --- .../(protected)/workorders/adv-date-range-section.test.tsx | 2 +- src/test/domain/work-orders/api/work-orders-api.test.ts | 2 +- .../domain/work-orders/utils/board-query-params.test.ts | 6 +++--- .../domain/work-orders/utils/table-filter-utils.test.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/app/(protected)/workorders/adv-date-range-section.test.tsx b/src/test/app/(protected)/workorders/adv-date-range-section.test.tsx index 8ffe1f60..e3640e91 100644 --- a/src/test/app/(protected)/workorders/adv-date-range-section.test.tsx +++ b/src/test/app/(protected)/workorders/adv-date-range-section.test.tsx @@ -33,7 +33,7 @@ function openSection() { } } -describe("Advanced Filters date range (SH-391)", () => { +describe("Advanced Filters date range", () => { it("starts with no range selected, so the search spans every week", () => { openSection(); diff --git a/src/test/domain/work-orders/api/work-orders-api.test.ts b/src/test/domain/work-orders/api/work-orders-api.test.ts index 7ea19716..d6766be8 100644 --- a/src/test/domain/work-orders/api/work-orders-api.test.ts +++ b/src/test/domain/work-orders/api/work-orders-api.test.ts @@ -420,7 +420,7 @@ describe("workOrdersApi.searchBoard", () => { }); }); -/** A WO# lookup spans every week and undated rows (SH-391). */ +/** A WO# lookup spans every week and undated rows. */ const ALL_WEEKS_SEARCH = { datePreset: "Custom", dateFrom: "2000-01-01", 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 3daaf798..866de6f6 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 @@ -135,7 +135,7 @@ describe("toBoardSearchQueryParams", () => { expect(thisWeek.dateTo).toBeUndefined(); }); - it("narrows strictly for every selected range, This week included (SH-391)", () => { + it("narrows strictly for every selected range, This week included", () => { for (const dateRange of ["this-week", "last-week", "last-3-months", "custom"] as const) { const params = toBoardSearchQueryParams({ filters: { @@ -151,7 +151,7 @@ describe("toBoardSearchQueryParams", () => { } }); - it("searches every week, undated rows included, when no range is selected (SH-391)", () => { + it("searches every week, undated rows included, when no range is selected", () => { const params = toBoardSearchQueryParams({ filters: { ...EMPTY_ADVANCED_FILTERS, dispatchers: new Set(["__unassigned__"]) }, }); @@ -165,7 +165,7 @@ describe("toBoardSearchQueryParams", () => { }); }); - it("keeps undated rows in the pinned Unassigned queue (SH-391)", () => { + it("keeps undated rows in the pinned Unassigned queue", () => { expect(toUnassignedQueueQueryParams().includeDateless).toBe(true); }); 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 296c7b6d..cb7b10e5 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 @@ -455,7 +455,7 @@ describe("matchesAdvancedFilters", () => { expect(matchesAdvancedFilters(undatedAssigned, filters)).toBe(true); }); - it("matches a range the same way the board search does (SH-391)", () => { + it("matches a range the same way the board search does", () => { const filters = { ...EMPTY_ADVANCED_FILTERS, dateRange: "custom" as const,