diff --git a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx index fe420159..ce70990a 100644 --- a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx +++ b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx @@ -50,6 +50,9 @@ export function SlideOverInfoTabEditView({ rescheduleCount={draft.rescheduleCount} carriedOver={draft.carriedOver} originalDate={draft.originalDate} + originalWeek={draft.originalWeek} + scheduledOn={draft.scheduledOn} + status={draft.status} currentId={draft.id} allOrders={allOrders} onSave={(woNumber) => onDraftChange({ woNumber })} diff --git a/src/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell.tsx index b71f9c4f..369c1101 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell.tsx @@ -13,6 +13,9 @@ type EditableWONumberCellProps = { carriedOver?: number; isAddOn?: boolean; originalDate?: string; + originalWeek?: string; + scheduledOn?: string; + status?: string; q?: string; currentId: string | number; allOrders: WorkOrderTableRow[]; @@ -37,6 +40,9 @@ export function EditableWONumberCell({ carriedOver = 0, isAddOn = false, originalDate, + originalWeek, + scheduledOn, + status, q, currentId, allOrders, @@ -118,7 +124,13 @@ export function EditableWONumberCell({ - + ); } @@ -155,7 +167,13 @@ export function EditableWONumberCell({ )} - + ); } diff --git a/src/app/(protected)/workorders/_components/list/table/cells/wo-counter-pills.tsx b/src/app/(protected)/workorders/_components/list/table/cells/wo-counter-pills.tsx index a98552ae..32c13d49 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/wo-counter-pills.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/wo-counter-pills.tsx @@ -1,5 +1,10 @@ import { RefreshCw } from "lucide-react"; import { Tooltip } from "@mui/material"; +import { + formatCarriedOverHover, + resolveCarryOverSourceWeek, + shouldShowCarriedOverPill, +} from "@/domain/work-orders/utils/carry-over-display"; import { formatRescheduleHover } from "@/domain/work-orders/utils/reschedule-indicator"; const RESCHEDULE_PILL_STYLE = { @@ -46,17 +51,30 @@ export function RescheduleCounterPill({ ); } -export function CarriedOverCounterPill({ count }: { count: number }) { - if (count < 2) { +export function CarriedOverCounterPill({ + count, + originalWeek, + originalDate, + scheduledOn, + status, +}: { + count: number; + originalWeek?: string; + originalDate?: string; + scheduledOn?: string; + status?: string; +}) { + if (!shouldShowCarriedOverPill(count, status)) { return null; } - const label = `Carried over ${count} times`; + const sourceWeek = resolveCarryOverSourceWeek({ originalWeek, originalDate, scheduledOn }); + const hover = formatCarriedOverHover(count, sourceWeek); return ( - + diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx index c17deae3..c6fbde16 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx @@ -83,6 +83,9 @@ export function WoTableRowCells({ carriedOver={row.carriedOver} isAddOn={row.isAddOn} originalDate={row.originalDate} + originalWeek={row.originalWeek} + scheduledOn={row.scheduledOn} + status={row.status} q={search} currentId={row.id} allOrders={allRows} diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-table-local-state.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-table-local-state.ts index 86110721..9b3b853a 100644 --- a/src/app/(protected)/workorders/_hooks/use-work-orders-table-local-state.ts +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-table-local-state.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "react-toastify"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import { applyBoardRowOrderOnScheduleChange } from "@/domain/work-orders/utils/apply-board-row-order-on-schedule-change"; +import { applyCarryOverDisplayDayFields } from "@/domain/work-orders/utils/carry-over-display"; import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils"; import { enrichTableRow } from "@/domain/work-orders/utils/table-filter-utils"; import { isWorkOrderFullyLocked } from "@/domain/work-orders/utils/work-order-lock"; @@ -78,8 +79,11 @@ export function useWorkOrdersTableLocalState({ const displayRows = useMemo(() => { const apiIds = new Set(apiRows.map((r) => String(r.id))); const extra = localRows.filter((r) => !apiIds.has(String(r.id))); - return applyRowOrderByDay([...apiRows, ...extra.map(enrichTableRow)], rowOrderByDay); - }, [apiRows, localRows, rowOrderByDay]); + const merged = [...apiRows, ...extra.map(enrichTableRow)].map((row) => + applyCarryOverDisplayDayFields(row, weekMonday), + ); + return applyRowOrderByDay(merged, rowOrderByDay); + }, [apiRows, localRows, rowOrderByDay, weekMonday]); const clearRowPatch = useCallback((id: string | number) => { const key = String(id); diff --git a/src/domain/work-orders/utils/carry-over-display.ts b/src/domain/work-orders/utils/carry-over-display.ts new file mode 100644 index 00000000..dd92b2d3 --- /dev/null +++ b/src/domain/work-orders/utils/carry-over-display.ts @@ -0,0 +1,109 @@ +import { compactWeekLabel } from "@/domain/work-orders/utils/list-week-utils"; +import { formatRescheduleCountLabel } from "@/domain/work-orders/utils/reschedule-indicator"; +import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils"; +import { addDaysIso, mondayOf } from "@/domain/work-orders/utils/wizard-date-utils"; + +const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/; +const WEEKDAY_COUNT = 7; +const FRIDAY_OFFSET = 4; + +export type CarryOverRow = { + scheduledOn: string; + status?: string; + carriedOver?: number; +}; + +export function isTerminalCarryOverStatus(status?: string): boolean { + return status === "Completed" || status === "Canceled"; +} + +export function shouldShowCarriedOverPill(count: number, status?: string): boolean { + return count >= 2 && !isTerminalCarryOverStatus(status); +} + +export function toIsoDate(value?: string): string { + if (!value) { + return ""; + } + const iso = value.slice(0, 10); + return ISO_DATE.test(iso) ? iso : ""; +} + +export function resolveCarryOverSourceWeek(input: { + originalWeek?: string; + originalDate?: string; + scheduledOn?: string; +}): string { + const originalWeek = toIsoDate(input.originalWeek); + if (originalWeek) { + return mondayOf(originalWeek); + } + const originalDate = toIsoDate(input.originalDate); + if (originalDate) { + return mondayOf(originalDate); + } + const scheduledOn = toIsoDate(input.scheduledOn); + return scheduledOn ? mondayOf(scheduledOn) : ""; +} + +export function formatCarriedOverHover(count: number, sourceWeekIso?: string): string { + const times = formatRescheduleCountLabel(count); + const base = `Carried over ${times} — this WO was not completed and moved to the next period`; + const sourceLabel = sourceWeekIso ? compactWeekLabel(sourceWeekIso) : ""; + if (!sourceLabel) { + return base; + } + return `${base}. Source week: ${sourceLabel}.`; +} + +/** Non-terminal WOs scheduled before the viewed week are display carry-overs. */ +export function isCarryOverDisplayRow(row: CarryOverRow, weekMonday: string): boolean { + if (isTerminalCarryOverStatus(row.status)) { + return false; + } + const scheduled = toIsoDate(row.scheduledOn); + return Boolean(scheduled) && scheduled < weekMonday; +} + +export function mapCarryOverDisplayDay(scheduledOn: string, weekMonday: string): string { + const scheduled = toIsoDate(scheduledOn); + if (!scheduled || !ISO_DATE.test(weekMonday)) { + return ""; + } + const sourceMonday = mondayOf(scheduled); + const offsetMs = + new Date(`${scheduled}T12:00:00`).getTime() - new Date(`${sourceMonday}T12:00:00`).getTime(); + const offset = Math.round(offsetMs / 86_400_000); + if (offset < 0 || offset >= WEEKDAY_COUNT) { + return weekMonday; + } + if (offset > FRIDAY_OFFSET) { + return weekMonday; + } + return addDaysIso(weekMonday, offset); +} + +export function rowMatchesCarryOverDay( + row: CarryOverRow, + selectedDayIso: string, + weekMonday: string, +): boolean { + if (!isCarryOverDisplayRow(row, weekMonday)) { + return false; + } + return mapCarryOverDisplayDay(row.scheduledOn, weekMonday) === selectedDayIso; +} + +/** Aligns UI day keys with the mapped weekday so reorder stays on the displayed tab. */ +export function applyCarryOverDisplayDayFields< + T extends CarryOverRow & { dayGroup: string; dayLabel: string }, +>(row: T, weekMonday: string): T { + if (!isCarryOverDisplayRow(row, weekMonday)) { + return row; + } + const displayDay = mapCarryOverDisplayDay(row.scheduledOn, weekMonday); + if (!displayDay) { + return row; + } + return { ...row, ...deriveDayFields(displayDay) }; +} diff --git a/src/domain/work-orders/utils/table-filter-utils.ts b/src/domain/work-orders/utils/table-filter-utils.ts index dcda43c5..4f2a045b 100644 --- a/src/domain/work-orders/utils/table-filter-utils.ts +++ b/src/domain/work-orders/utils/table-filter-utils.ts @@ -7,6 +7,7 @@ import type { WorkOrderTableRow, } from "@/domain/work-orders/types/work-order-table-row"; import { ADVANCED_FILTER_PREDICATES } from "@/domain/work-orders/utils/advanced-filter-predicates"; +import { isCarryOverDisplayRow } from "@/domain/work-orders/utils/carry-over-display"; import { isDateInWeek } from "@/domain/work-orders/utils/table-day-utils"; import { weekRange } from "@/domain/work-orders/utils/list-week-utils"; @@ -96,7 +97,7 @@ export function filterByBarAndSearch( if (row.scheduledOn) { if (row.scheduledOn < weekFrom || row.scheduledOn > weekTo) { - return false; + return isCarryOverDisplayRow(row, bar.weekMonday); } } else if (row.targetWeek) { if (!isDateInWeek(row.targetWeek, bar.weekMonday)) { diff --git a/src/domain/work-orders/utils/week-day-nav-utils.ts b/src/domain/work-orders/utils/week-day-nav-utils.ts index 073cf231..f2b10778 100644 --- a/src/domain/work-orders/utils/week-day-nav-utils.ts +++ b/src/domain/work-orders/utils/week-day-nav-utils.ts @@ -2,6 +2,7 @@ import type { WorkOrderDayGroup, WorkOrderTableRow, } from "@/domain/work-orders/types/work-order-table-row"; +import { rowMatchesCarryOverDay } from "@/domain/work-orders/utils/carry-over-display"; import { isLiveWeek } from "@/domain/work-orders/utils/list-week-utils"; import { isDateInWeek } from "@/domain/work-orders/utils/table-day-utils"; import { addDaysIso, mondayOf, todayIso } from "@/domain/work-orders/utils/wizard-date-utils"; @@ -65,10 +66,17 @@ export function formatDayTabLabel(iso: string): string { } export function rowMatchesSelectedDay( - row: Pick, + row: Pick, selectedDayIso: string, + weekMonday?: string, ): boolean { - return Boolean(row.scheduledOn) && row.scheduledOn.slice(0, 10) === selectedDayIso; + if (Boolean(row.scheduledOn) && row.scheduledOn.slice(0, 10) === selectedDayIso) { + return true; + } + if (!weekMonday) { + return false; + } + return rowMatchesCarryOverDay(row, selectedDayIso, weekMonday); } export function buildSelectedDayGroup( @@ -83,6 +91,6 @@ export function buildSelectedDayGroup( key: `${month}/${day}`, label: formatDayTabLabel(date), date, - items: scheduled.filter((row) => rowMatchesSelectedDay(row, date)), + items: scheduled.filter((row) => rowMatchesSelectedDay(row, date, weekMonday)), }; } diff --git a/src/test/app/(protected)/workorders/carried-over-counter-pill.test.tsx b/src/test/app/(protected)/workorders/carried-over-counter-pill.test.tsx index 277f4a73..d0059873 100644 --- a/src/test/app/(protected)/workorders/carried-over-counter-pill.test.tsx +++ b/src/test/app/(protected)/workorders/carried-over-counter-pill.test.tsx @@ -18,6 +18,20 @@ describe("CarriedOverCounterPill", () => { expect(pill).toHaveTextContent("3"); }); + it("includes the source week in the accessible name", () => { + renderWithProviders( + , + ); + expect(screen.getByLabelText(/Source week: 5\/4–5\/8/i)).toBeInTheDocument(); + }); + + it("hides after resolution even when the counter remains", () => { + const { container } = renderWithProviders( + , + ); + expect(container).toBeEmptyDOMElement(); + }); + it("is distinguishable from the reschedule pill beside it", () => { renderWithProviders( <> diff --git a/src/test/app/(protected)/workorders/use-work-orders-list-filters-week.test.tsx b/src/test/app/(protected)/workorders/use-work-orders-list-filters-week.test.tsx new file mode 100644 index 00000000..223a9a96 --- /dev/null +++ b/src/test/app/(protected)/workorders/use-work-orders-list-filters-week.test.tsx @@ -0,0 +1,30 @@ +import { act, renderHook } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters"; +import { workOrdersApi } from "@/domain/work-orders/api/work-orders-api"; + +vi.mock("@/hooks/use-debounce", () => ({ + useDebounce: (value: T) => value, +})); + +vi.mock("@/domain/work-orders/api/work-orders-api", () => ({ + workOrdersApi: { + patchBoardField: vi.fn(), + getBoard: vi.fn(), + }, +})); + +describe("useWorkOrdersListFilters week navigation", () => { + it("changes the viewed week without patching schedule fields", () => { + const { result } = renderHook(() => useWorkOrdersListFilters()); + + act(() => { + result.current.setWeekMonday("2026-05-18"); + }); + + expect(result.current.weekMonday).toBe("2026-05-18"); + expect(result.current.selectedDayIso).toBe("2026-05-18"); + expect(result.current.boardParams.weekStart).toBe("2026-05-18"); + expect(workOrdersApi.patchBoardField).not.toHaveBeenCalled(); + }); +}); diff --git a/src/test/domain/work-orders/mappers/work-order-table-row-mapper.test.ts b/src/test/domain/work-orders/mappers/work-order-table-row-mapper.test.ts index b0208e84..81b8e3e8 100644 --- a/src/test/domain/work-orders/mappers/work-order-table-row-mapper.test.ts +++ b/src/test/domain/work-orders/mappers/work-order-table-row-mapper.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-table-row-mapper"; import { WORK_ORDER_TYPE_API } from "@/domain/work-orders/types/work-order-board"; +import { buildSelectedDayGroup } from "@/domain/work-orders/utils/week-day-nav-utils"; describe("mapWorkOrderTableRow isAddOn", () => { it("reads isAddOn from board DTO when present", () => { @@ -137,3 +138,32 @@ describe("mapWorkOrderTableRow primaryDispatchStatus", () => { expect(row.primaryDispatchId).toBe(11); }); }); + +describe("mapWorkOrderTableRow carry-over fields", () => { + it("maps carriedOver and originalWeek without rewriting scheduledOn", () => { + const row = mapWorkOrderTableRow({ + id: 41, + workOrderType: WORK_ORDER_TYPE_API.PM, + scheduledDate: "2026-05-13", + carriedOver: 2, + originalWeek: "2026-05-11", + }); + expect(row.carriedOver).toBe(2); + expect(row.originalWeek).toBe("2026-05-11"); + expect(row.scheduledOn).toBe("2026-05-13"); + }); + + it("shows an unresolved prior-week row on the next week's matching day tab", () => { + const row = mapWorkOrderTableRow({ + id: 42, + workOrderType: WORK_ORDER_TYPE_API.PM, + scheduledDate: "2026-05-13", + lifecycleStatus: 3, + carriedOver: 2, + originalWeek: "2026-05-11", + }); + const group = buildSelectedDayGroup("2026-05-18", "2026-05-20", [row]); + expect(row.status).toBe("Scheduled"); + expect(group.items.map((item) => item.id)).toEqual([42]); + }); +}); diff --git a/src/test/domain/work-orders/utils/carry-over-display.test.ts b/src/test/domain/work-orders/utils/carry-over-display.test.ts new file mode 100644 index 00000000..00b54dfe --- /dev/null +++ b/src/test/domain/work-orders/utils/carry-over-display.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from "vitest"; +import { + applyCarryOverDisplayDayFields, + formatCarriedOverHover, + isCarryOverDisplayRow, + mapCarryOverDisplayDay, + resolveCarryOverSourceWeek, + shouldShowCarriedOverPill, +} from "@/domain/work-orders/utils/carry-over-display"; + +describe("carry-over-display", () => { + it("hides the pill below 2 and on terminal statuses", () => { + expect(shouldShowCarriedOverPill(1, "Scheduled")).toBe(false); + expect(shouldShowCarriedOverPill(2, "Scheduled")).toBe(true); + expect(shouldShowCarriedOverPill(3, "Completed")).toBe(false); + expect(shouldShowCarriedOverPill(3, "Canceled")).toBe(false); + }); + + it("formats hover without a source week", () => { + expect(formatCarriedOverHover(2)).toBe( + "Carried over 2 times — this WO was not completed and moved to the next period", + ); + }); + + it("appends the source week label from originalWeek", () => { + expect(formatCarriedOverHover(3, "2026-05-04")).toBe( + "Carried over 3 times — this WO was not completed and moved to the next period. Source week: 5/4–5/8.", + ); + }); + + it("resolves source week from originalWeek, then originalDate, then scheduledOn", () => { + expect(resolveCarryOverSourceWeek({ originalWeek: "2026-05-04" })).toBe("2026-05-04"); + expect(resolveCarryOverSourceWeek({ originalDate: "2026-05-06" })).toBe("2026-05-04"); + expect(resolveCarryOverSourceWeek({ scheduledOn: "2026-05-13" })).toBe("2026-05-11"); + expect(resolveCarryOverSourceWeek({})).toBe(""); + }); + + it("treats non-terminal rows scheduled before the viewed week as carry-over", () => { + expect( + isCarryOverDisplayRow( + { scheduledOn: "2026-05-13", status: "Scheduled", carriedOver: 1 }, + "2026-05-18", + ), + ).toBe(true); + expect( + isCarryOverDisplayRow( + { scheduledOn: "2026-05-20", status: "Scheduled", carriedOver: 2 }, + "2026-05-18", + ), + ).toBe(false); + expect( + isCarryOverDisplayRow( + { scheduledOn: "2026-05-13", status: "Completed", carriedOver: 2 }, + "2026-05-18", + ), + ).toBe(false); + }); + + it("maps the original weekday onto the viewed week and weekends onto Monday", () => { + expect(mapCarryOverDisplayDay("2026-05-13", "2026-05-18")).toBe("2026-05-20"); + expect(mapCarryOverDisplayDay("2026-05-16", "2026-05-18")).toBe("2026-05-18"); + expect(mapCarryOverDisplayDay("2026-05-17", "2026-05-18")).toBe("2026-05-18"); + }); + + it("remaps dayGroup to the viewed weekday without changing scheduledOn", () => { + const row = { + scheduledOn: "2026-05-13", + status: "Scheduled", + dayGroup: "05/13", + dayLabel: "WED · 05/13", + }; + const next = applyCarryOverDisplayDayFields(row, "2026-05-18"); + expect(next.scheduledOn).toBe("2026-05-13"); + expect(next.dayGroup).toBe("05/20"); + expect(next.dayLabel).toBe("WED · 05/20"); + }); + + it("leaves in-week and terminal rows' dayGroup unchanged", () => { + const inWeek = { + scheduledOn: "2026-05-20", + status: "Scheduled", + dayGroup: "05/20", + dayLabel: "WED · 05/20", + }; + const completed = { + scheduledOn: "2026-05-13", + status: "Completed", + dayGroup: "05/13", + dayLabel: "WED · 05/13", + }; + expect(applyCarryOverDisplayDayFields(inWeek, "2026-05-18")).toEqual(inWeek); + expect(applyCarryOverDisplayDayFields(completed, "2026-05-18")).toEqual(completed); + }); +}); 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 fb68c6f4..85651d2c 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 @@ -137,6 +137,28 @@ describe("filterByBarAndSearch", () => { expect(ids).toEqual(["in"]); }); + it("keeps prior-week unresolved carry-over rows that match a one-character search", () => { + const carried = makeRow({ + id: "co", + woNumber: "WO-CARRY", + scheduledOn: "2026-05-13", + status: "Scheduled", + ...deriveDayFields("2026-05-13"), + }); + const completed = makeRow({ + id: "done", + woNumber: "WO-DONE", + scheduledOn: "2026-05-13", + status: "Completed", + ...deriveDayFields("2026-05-13"), + }); + + const ids = filterByBarAndSearch([carried, completed], bar("2026-05-18"), "W").map( + (row) => row.id, + ); + expect(ids).toEqual(["co"]); + }); + it("keeps the union of selected types (OR)", () => { const pm = makeRow({ id: "pm", type: "PM", scheduledOn: "2026-06-03" }); const emergency = makeRow({ id: "em", type: "Emergency", scheduledOn: "2026-06-03" }); diff --git a/src/test/domain/work-orders/utils/week-day-nav-utils.test.ts b/src/test/domain/work-orders/utils/week-day-nav-utils.test.ts index 0439dc61..ec6c9bc3 100644 --- a/src/test/domain/work-orders/utils/week-day-nav-utils.test.ts +++ b/src/test/domain/work-orders/utils/week-day-nav-utils.test.ts @@ -179,3 +179,44 @@ describe("week-day-nav-utils (SH-240)", () => { ).toBe(false); }); }); + +describe("week-day-nav-utils carry-over display", () => { + it("places a prior-week unresolved WO on the same weekday of the viewed week", () => { + const carried = row({ + id: "co", + scheduledOn: "2026-05-13", + dayGroup: "05/13", + status: "Scheduled", + carriedOver: 2, + originalWeek: "2026-05-11", + }); + const nativeWed = row({ + id: "native", + scheduledOn: "2026-05-20", + dayGroup: "05/20", + status: "Scheduled", + }); + + expect(rowMatchesSelectedDay(carried, "2026-05-20", "2026-05-18")).toBe(true); + expect(rowMatchesSelectedDay(carried, "2026-05-18", "2026-05-18")).toBe(false); + + const group = buildSelectedDayGroup("2026-05-18", "2026-05-20", [carried, nativeWed]); + expect(group.items.map((item) => item.id)).toEqual(["co", "native"]); + }); + + it("does not map completed carry-overs onto the viewed week", () => { + const completed = row({ + id: "done", + scheduledOn: "2026-05-13", + status: "Completed", + carriedOver: 2, + }); + expect(rowMatchesSelectedDay(completed, "2026-05-20", "2026-05-18")).toBe(false); + expect(buildSelectedDayGroup("2026-05-18", "2026-05-20", [completed]).items).toEqual([]); + }); + + it("keeps unscheduled rows out of day groups", () => { + const unscheduled = row({ id: "us", scheduledOn: "", scheduleWeekOnly: true, carriedOver: 2 }); + expect(rowMatchesSelectedDay(unscheduled, "2026-05-18", "2026-05-18")).toBe(false); + }); +});