diff --git a/docs/work-orders/board-search-api.md b/docs/work-orders/board-search-api.md index e476f53c..fde9438b 100644 --- a/docs/work-orders/board-search-api.md +++ b/docs/work-orders/board-search-api.md @@ -118,15 +118,6 @@ Pending (`No`)=2, Uploaded (`Yes`)=1, N/N (`NN`)=3. } ``` -#### Buckets - -| Array | Inclusion rule | -| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| `scheduled` | WOs with `scheduledDate` inside `weekStart`–`weekEnd` | -| `unscheduled` | **All** WOs without `scheduledDate` (cross-week). Visible for every week request; `targetWeek` is not a week filter — Schedule-cell marker only | - -The FE concatenates `unscheduled + scheduled` into `items` and renders `unscheduled` in the pinned Unscheduled section above day groups. Dated WOs without a dispatcher stay in `scheduled` / day groups (Assigned To highlight), not in `unscheduled`. - ### `/board/search` ```json diff --git a/src/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell.tsx index 92557ea2..42a1bf93 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell.tsx @@ -75,6 +75,18 @@ export function AssignedToCell({ dispatcherId, users, highlight, onSave }: Assig />
+ {filtered.map((d) => ( + {Boolean(onAddWo) && ( + + )} +
+ + + ); +} diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx index 60bc03f4..76c5deb6 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx @@ -8,7 +8,10 @@ import type { import type { WorkOrderTableDraft } from "@/domain/work-orders/types/work-order-table-row"; -import { UNSCHEDULED_DAY_KEY } from "@/domain/work-orders/types/work-order-table-row"; +import { + UNASSIGNED_DAY_KEY, + UNSCHEDULED_DAY_KEY, +} from "@/domain/work-orders/types/work-order-table-row"; import { WO_TABLE_ROW_HEIGHT } from "./wo-table-cols"; import { WoTableInlineRowCells } from "./wo-table-inline-row-cells"; @@ -65,7 +68,7 @@ export function WoTableInlineRow({ onCancel, }: WoTableInlineRowProps) { - const weekDefault = draftDayKey === UNSCHEDULED_DAY_KEY; + const weekDefault = draftDayKey === UNSCHEDULED_DAY_KEY || draftDayKey === UNASSIGNED_DAY_KEY; const canSave = !saving && (weekDefault ? !!draft.targetWeek || !!draft.scheduledOn : !!draft.scheduledOn); diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-section-header.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-section-header.tsx index a27feca7..a730c7f5 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-section-header.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-section-header.tsx @@ -1,14 +1,15 @@ -import { AlertTriangle, ChevronDown, ChevronRight, Plus } from "lucide-react"; +import { AlertTriangle, Plus, Users } from "lucide-react"; import { WO_TABLE_COLS } from "./wo-table-cols"; +import { WoTableCollapsibleSectionHeader } from "./wo-table-collapsible-section-header"; type WoTableSectionHeaderProps = { - variant: "unscheduled" | "day"; + variant: "unassigned" | "unscheduled" | "day"; label: string; count: number; isToday?: boolean; collapsed?: boolean; onToggleCollapse?: () => void; - onAddWo: () => void; + onAddWo?: () => void; }; export function WoTableSectionHeader({ @@ -20,72 +21,30 @@ export function WoTableSectionHeader({ onToggleCollapse, onAddWo, }: WoTableSectionHeaderProps) { - const isUnscheduled = variant === "unscheduled"; - - if (isUnscheduled) { - const toggleCollapse = () => { - onToggleCollapse?.(); - }; - + if (variant === "unassigned") { return ( - - -
- - -
- - + onToggleCollapse?.()} + /> + ); + } + + if (variant === "unscheduled") { + return ( + onToggleCollapse?.()} + onAddWo={onAddWo} + /> ); } @@ -139,15 +98,17 @@ export function WoTableSectionHeader({ Today )} - + {Boolean(onAddWo) && ( + + )} diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-table-data.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-table-data.ts index 09f819b6..c52a99bf 100644 --- a/src/app/(protected)/workorders/_hooks/use-work-orders-table-data.ts +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-table-data.ts @@ -7,6 +7,7 @@ import { buildWeekSkeleton, deriveDayFields } from "@/domain/work-orders/utils/t import { buildDayGroups, filterByBarAndSearch, + partitionBoardRows, splitScheduledUnscheduled, } from "@/domain/work-orders/utils/table-filter-utils"; @@ -38,10 +39,13 @@ export function useWorkOrdersTableData(filters: FiltersReturn) { return displayRows; }, [displayRows, filters.barFilters, filters.search, advActive]); - const { scheduled, unscheduled } = useMemo( - () => (advActive ? splitScheduledUnscheduled(advResults) : splitScheduledUnscheduled(filtered)), - [advActive, advResults, filtered], - ); + const { unassigned, scheduled, unscheduled } = useMemo(() => { + if (advActive) { + const split = splitScheduledUnscheduled(advResults); + return { unassigned: [], ...split }; + } + return partitionBoardRows(filtered); + }, [advActive, advResults, filtered]); const dayGroups = useMemo(() => buildDayGroups(scheduled), [scheduled]); const weekSkeleton = useMemo(() => buildWeekSkeleton(filters.weekMonday), [filters.weekMonday]); @@ -49,7 +53,12 @@ export function useWorkOrdersTableData(filters: FiltersReturn) { const searchActive = filters.search.trim() !== ""; const visibleRows = advActive ? advResults : filtered; const noSearchResults = visibleRows.length === 0 && searchActive; - const isEmptyWeek = !advActive && scheduled.length === 0 && !searchActive; + const isEmptyWeek = + !advActive && + scheduled.length === 0 && + unassigned.length === 0 && + unscheduled.length === 0 && + !searchActive; const displayGroups = isEmptyWeek ? weekSkeleton : dayGroups; const todayKey = @@ -70,6 +79,7 @@ export function useWorkOrdersTableData(filters: FiltersReturn) { refetch, advActive, advResults, + unassigned, unscheduled, displayGroups, searchActive, @@ -79,6 +89,8 @@ export function useWorkOrdersTableData(filters: FiltersReturn) { draftDayKey: local.draftDayKey, draft: local.draft, setDraft: local.setDraft, + unassignedOpen: local.unassignedOpen, + setUnassignedOpen: local.setUnassignedOpen, unschedOpen: local.unschedOpen, setUnschedOpen: local.setUnschedOpen, selectedId: local.selectedId, 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 633db819..9401ba1f 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 @@ -158,6 +158,8 @@ export function useWorkOrdersTableLocalState({ setDraft: ui.setDraft, startDayDraft: ui.startDayDraft, cancelDraft: ui.cancelDraft, + unassignedOpen: ui.unassignedOpen, + setUnassignedOpen: ui.setUnassignedOpen, unschedOpen: ui.unschedOpen, setUnschedOpen: ui.setUnschedOpen, selectedId: ui.selectedId, diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-table-ui-state.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-table-ui-state.ts index ad0797e9..c1c8953f 100644 --- a/src/app/(protected)/workorders/_hooks/use-work-orders-table-ui-state.ts +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-table-ui-state.ts @@ -1,6 +1,7 @@ import { useCallback, useRef, useState } from "react"; import { EMPTY_TABLE_DRAFT, + UNASSIGNED_DAY_KEY, UNSCHEDULED_DAY_KEY, type WorkOrderTableDraft, } from "@/domain/work-orders/types/work-order-table-row"; @@ -11,7 +12,7 @@ export type DragVizState = { }; function resolveDraftScheduledOn(key: string, weekMonday: string, scheduledIso?: string): string { - if (key === UNSCHEDULED_DAY_KEY) { + if (key === UNSCHEDULED_DAY_KEY || key === UNASSIGNED_DAY_KEY) { return ""; } if (scheduledIso && /^\d{4}-\d{2}-\d{2}$/.test(scheduledIso)) { @@ -31,10 +32,11 @@ function resolveDraftScheduledOn(key: string, weekMonday: string, scheduledIso?: return ""; } -/** Draft, selection, unsched accordion, drag viz and flash scroll helpers. */ +/** Draft, selection, pinned-section accordions, drag viz and flash scroll helpers. */ export function useWorkOrdersTableUiState(weekMonday: string) { const [draftDayKey, setDraftDayKey] = useState(null); const [draft, setDraft] = useState(EMPTY_TABLE_DRAFT); + const [unassignedOpen, setUnassignedOpen] = useState(true); const [unschedOpen, setUnschedOpen] = useState(true); const [selectedId, setSelectedId] = useState(null); const [flashId, setFlashId] = useState(null); @@ -81,6 +83,8 @@ export function useWorkOrdersTableUiState(weekMonday: string) { setDraft, startDayDraft, cancelDraft, + unassignedOpen, + setUnassignedOpen, unschedOpen, setUnschedOpen, selectedId, diff --git a/src/domain/work-orders/types/work-order-table-row.ts b/src/domain/work-orders/types/work-order-table-row.ts index 4bcb4b60..ab7ea16d 100644 --- a/src/domain/work-orders/types/work-order-table-row.ts +++ b/src/domain/work-orders/types/work-order-table-row.ts @@ -115,3 +115,4 @@ export type WorkOrderDayGroup = { export type WorkOrderTablePatch = Partial; export const UNSCHEDULED_DAY_KEY = "__unscheduled__" as const; +export const UNASSIGNED_DAY_KEY = "__unassigned__" as const; diff --git a/src/domain/work-orders/types/work-order.ts b/src/domain/work-orders/types/work-order.ts index f266bc6a..265344da 100644 --- a/src/domain/work-orders/types/work-order.ts +++ b/src/domain/work-orders/types/work-order.ts @@ -57,6 +57,7 @@ export type { export { EMPTY_TABLE_DRAFT, + UNASSIGNED_DAY_KEY, UNSCHEDULED_DAY_KEY, WORK_ORDERS_TABLE_PAGE_SIZE, } from "./work-order-table-row"; diff --git a/src/domain/work-orders/utils/table-filter-utils.ts b/src/domain/work-orders/utils/table-filter-utils.ts index be0b6ded..b1245ee7 100644 --- a/src/domain/work-orders/utils/table-filter-utils.ts +++ b/src/domain/work-orders/utils/table-filter-utils.ts @@ -6,6 +6,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 { isDateInWeek } from "@/domain/work-orders/utils/table-day-utils"; import { weekRange } from "@/domain/work-orders/utils/list-week-utils"; const TERMINAL_STATUSES = new Set(["Completed", "Canceled", "Done", "Cancelled"]); @@ -90,16 +91,47 @@ export function filterByBarAndSearch( } } - // Design weekScoped: undated (!scheduledOn) stay visible every week; targetWeek - // is a Schedule-cell marker only, never a week-inclusion predicate. - if (row.scheduledOn && (row.scheduledOn < weekFrom || row.scheduledOn > weekTo)) { - return false; + if (row.scheduledOn) { + if (row.scheduledOn < weekFrom || row.scheduledOn > weekTo) { + return false; + } + } else if (row.targetWeek) { + if (!isDateInWeek(row.targetWeek, bar.weekMonday)) { + return false; + } } return true; }); } +export function isUnassignedBoardRow(row: WorkOrderTableRow): boolean { + if (TERMINAL_STATUSES.has(row.status)) { + return false; + } + return !row.dispatcherId; +} + +export function partitionBoardRows(rows: WorkOrderTableRow[]): { + unassigned: WorkOrderTableRow[]; + unscheduled: WorkOrderTableRow[]; + scheduled: WorkOrderTableRow[]; +} { + const unassigned: WorkOrderTableRow[] = []; + const remaining: WorkOrderTableRow[] = []; + + for (const row of rows) { + if (isUnassignedBoardRow(row)) { + unassigned.push(row); + } else { + remaining.push(row); + } + } + + const { scheduled, unscheduled } = splitScheduledUnscheduled(remaining); + return { unassigned, unscheduled, scheduled }; +} + export function splitScheduledUnscheduled(rows: WorkOrderTableRow[]): { scheduled: WorkOrderTableRow[]; unscheduled: WorkOrderTableRow[]; diff --git a/src/test/app/(protected)/workorders/wo-table-body-unscheduled.test.tsx b/src/test/app/(protected)/workorders/wo-table-unassigned-queue.test.tsx similarity index 55% rename from src/test/app/(protected)/workorders/wo-table-body-unscheduled.test.tsx rename to src/test/app/(protected)/workorders/wo-table-unassigned-queue.test.tsx index 82551058..d510af3f 100644 --- a/src/test/app/(protected)/workorders/wo-table-body-unscheduled.test.tsx +++ b/src/test/app/(protected)/workorders/wo-table-unassigned-queue.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import { WoTableBody } from "@/app/(protected)/workorders/_components/list/table/wo-table-body"; import type { ReturnTypeOfUseWorkOrdersTableData } from "@/app/(protected)/workorders/_components/list/table/wo-table-types"; @@ -62,6 +63,7 @@ function baseTableData( refetch: vi.fn(), advActive: false, advResults: [], + unassigned: [], unscheduled: [], displayGroups: [], searchActive: false, @@ -71,6 +73,8 @@ function baseTableData( draftDayKey: null, draft: null, setDraft: vi.fn(), + unassignedOpen: true, + setUnassignedOpen: vi.fn(), unschedOpen: true, setUnschedOpen: vi.fn(), selectedId: null, @@ -95,28 +99,43 @@ function baseTableData( } as ReturnTypeOfUseWorkOrdersTableData; } -describe("WoTableBody Unscheduled vs day groups", () => { - it("renders Unscheduled above day groups and keeps dated unassigned under the day header", () => { +describe("WoTableBody Unassigned queue (SH-165)", () => { + it("renders Unassigned above Unscheduled and day groups without duplicate rows", () => { const datedUnassigned = makeRow({ id: "u-dated", woNumber: "24991" }); - const undated = makeRow({ + const undatedUnassigned = makeRow({ id: "us1", woNumber: "25021", scheduledOn: "", dayGroup: "", dayLabel: "", }); + const undatedAssigned = makeRow({ + id: "us-assigned", + woNumber: "25022", + dispatcherId: "sarah", + dispatcherName: "Sarah", + scheduledOn: "", + dayGroup: "", + dayLabel: "", + }); + const datedAssigned = makeRow({ + id: "day-assigned", + woNumber: "25023", + dispatcherId: "sarah", + dispatcherName: "Sarah", + }); const tableData = baseTableData({ - unscheduled: [undated], + unassigned: [datedUnassigned, undatedUnassigned], + unscheduled: [undatedAssigned], displayGroups: [ { key: "06/02", label: "TUE · 06/02", date: "2026-06-02", - items: [datedUnassigned], + items: [datedAssigned], }, ], - unschedOpen: true, }); render( @@ -133,21 +152,54 @@ describe("WoTableBody Unscheduled vs day groups", () => { , ); - expect(screen.getByRole("button", { name: "Unscheduled" })).toBeInTheDocument(); - expect(screen.getByText("TUE · 06/02")).toBeInTheDocument(); - - const unscheduledHeader = screen.getByRole("button", { name: "Unscheduled" }); + const unassignedHeader = screen.getByRole("button", { + name: "Unassigned, 2 work orders, expanded", + }); + const unscheduledHeader = screen.getByRole("button", { + name: "Unscheduled, 1 work order, expanded", + }); const dayLabel = screen.getByText("TUE · 06/02"); - const undatedRow = screen.getByTestId("row-us1"); - const datedRow = screen.getByTestId("row-u-dated"); - expect( - unscheduledHeader.compareDocumentPosition(undatedRow) & Node.DOCUMENT_POSITION_FOLLOWING, - ).toBeTruthy(); - expect( - dayLabel.compareDocumentPosition(datedRow) & Node.DOCUMENT_POSITION_FOLLOWING, - ).toBeTruthy(); - expect(screen.queryByTestId("row-u-dated")).toBeInTheDocument(); - expect(screen.getAllByTestId(/row-/)).toHaveLength(2); + expect(unassignedHeader.compareDocumentPosition(screen.getByTestId("row-u-dated"))).toBe( + Node.DOCUMENT_POSITION_FOLLOWING, + ); + expect(unassignedHeader.compareDocumentPosition(unscheduledHeader)).toBe( + Node.DOCUMENT_POSITION_FOLLOWING, + ); + expect(unscheduledHeader.compareDocumentPosition(dayLabel)).toBe( + Node.DOCUMENT_POSITION_FOLLOWING, + ); + + expect(screen.getByTestId("row-u-dated")).toBeInTheDocument(); + expect(screen.getByTestId("row-us1")).toBeInTheDocument(); + expect(screen.getByTestId("row-us-assigned")).toBeInTheDocument(); + expect(screen.getByTestId("row-day-assigned")).toBeInTheDocument(); + expect(screen.getAllByTestId(/row-/)).toHaveLength(4); + }); + + it("collapses and expands the Unassigned section via the disclosure control", async () => { + const user = userEvent.setup(); + const setUnassignedOpen = vi.fn(); + const tableData = baseTableData({ + unassigned: [makeRow({ id: "u1", woNumber: "24991" })], + setUnassignedOpen, + }); + + render( + + ( + + + + )} + renderInlineRow={() => null} + /> +
{row.woNumber}
, + ); + + await user.click(screen.getByRole("button", { name: "Unassigned, 1 work order, expanded" })); + expect(setUnassignedOpen).toHaveBeenCalledWith(expect.any(Function)); }); }); diff --git a/src/test/domain/work-orders/utils/board-unscheduled-design-parity.test.ts b/src/test/domain/work-orders/utils/board-unscheduled-design-parity.test.ts deleted file mode 100644 index 4c919a28..00000000 --- a/src/test/domain/work-orders/utils/board-unscheduled-design-parity.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { describe, expect, it } from "vitest"; -import type { WorkOrderBarFilters } from "@/domain/work-orders/types/work-order-list-filters"; -import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; -import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils"; -import { - filterByBarAndSearch, - splitScheduledUnscheduled, -} from "@/domain/work-orders/utils/table-filter-utils"; -import { weekRange } from "@/domain/work-orders/utils/list-week-utils"; - -/** - * Design oracle — mirrors shoc-desing/src/pages/WorkOrders.tsx: - * - weekScoped (~L9734): !scheduledOn || (scheduledOn >= wStart && scheduledOn <= wEnd) - * - split (~L9762): unscheduled = !scheduledOn; dated-but-unassigned stay in day groups - */ - -type DesignRow = Pick; - -function designWeekScoped(rows: DesignRow[], weekStart: string, weekEnd: string): DesignRow[] { - return rows.filter( - (row) => !row.scheduledOn || (row.scheduledOn >= weekStart && row.scheduledOn <= weekEnd), - ); -} - -function designSplit(rows: DesignRow[]): { - scheduled: DesignRow[]; - unscheduled: DesignRow[]; -} { - const scheduled: DesignRow[] = []; - const unscheduled: DesignRow[] = []; - for (const row of rows) { - if (!row.scheduledOn) { - unscheduled.push(row); - } else { - scheduled.push(row); - } - } - return { scheduled, unscheduled }; -} - -function makeRow(overrides: Partial = {}): WorkOrderTableRow { - return { - id: "1", - woNumber: "24991", - site: "CLV2", - type: "PM", - dispatcherId: "u1", - dispatcherName: "Alice", - scheduledOn: "2026-06-02", - targetWeek: "", - dayGroup: "06/02", - dayLabel: "TUE · 06/02", - location: "Cleveland, OH", - locationId: "", - pm: "Backflow", - serviceNotes: "", - extraServices: [], - company: "", - tech: "", - techPhone: "", - apptStart: "", - apptEnd: "", - apptTime: "—", - status: "Incomplete", - docStatus: "No", - pocName: "", - pocPhone: "", - pocNotes: "", - dueDate: "", - rescheduleCount: 0, - carriedOver: 0, - originalWeek: "", - originalDate: "", - isPastDue: false, - scheduleWeekOnly: false, - mediaCount: 0, - title: "", - priority: "", - vendorId: "", - primaryDispatchId: null, - rowVersion: "", - dispatchRowVersion: "", - flagColor: null, - ...overrides, - }; -} - -function idsOf(rows: { id: string | number }[]): string[] { - return rows.map((row) => String(row.id)).sort(); -} - -describe("board Unscheduled design parity", () => { - const weekMonday = "2026-06-01"; - const [weekStart, weekEnd] = weekRange(weekMonday); - - const fixtures: WorkOrderTableRow[] = [ - makeRow({ - id: "u-dated", - dispatcherId: "", - dispatcherName: "", - scheduledOn: "2026-06-02", - ...deriveDayFields("2026-06-02"), - }), - makeRow({ - id: "us-undated", - dispatcherId: "", - dispatcherName: "", - scheduledOn: "", - dayGroup: "", - dayLabel: "", - }), - makeRow({ - id: "ua-assigned-undated", - dispatcherId: "sarah", - dispatcherName: "Sarah", - scheduledOn: "", - dayGroup: "", - dayLabel: "", - }), - makeRow({ - id: "tw-other-week", - dispatcherId: "sarah", - dispatcherName: "Sarah", - scheduledOn: "", - targetWeek: "2026-07-06", - dayGroup: "", - dayLabel: "", - }), - makeRow({ - id: "dated-other-week", - scheduledOn: "2026-06-15", - ...deriveDayFields("2026-06-15"), - }), - ]; - - const bar: WorkOrderBarFilters = { - weekMonday, - dispatcherIds: new Set(), - type: "All", - }; - - it("matches design weekScoped ID set via filterByBarAndSearch", () => { - const designIds = idsOf(designWeekScoped(fixtures, weekStart, weekEnd)); - const feIds = idsOf(filterByBarAndSearch(fixtures, bar, "")); - expect(feIds).toEqual(designIds); - expect(feIds).toContain("tw-other-week"); - expect(feIds).not.toContain("dated-other-week"); - }); - - it("matches design split: dated unassigned in scheduled, undated in Unscheduled", () => { - const scoped = filterByBarAndSearch(fixtures, bar, ""); - const feSplit = splitScheduledUnscheduled(scoped); - const designSplitResult = designSplit(designWeekScoped(fixtures, weekStart, weekEnd)); - - expect(idsOf(feSplit.unscheduled)).toEqual(idsOf(designSplitResult.unscheduled)); - expect(idsOf(feSplit.scheduled)).toEqual(idsOf(designSplitResult.scheduled)); - - expect(idsOf(feSplit.scheduled)).toContain("u-dated"); - expect(idsOf(feSplit.unscheduled)).not.toContain("u-dated"); - expect(idsOf(feSplit.unscheduled)).toContain("tw-other-week"); - expect(idsOf(feSplit.unscheduled)).toEqual( - expect.arrayContaining(["us-undated", "ua-assigned-undated", "tw-other-week"]), - ); - }); -}); 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 b059bbd3..7316b094 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 @@ -5,7 +5,9 @@ import type { WorkOrderBarFilters } from "@/domain/work-orders/types/work-order- import { buildDayGroups, filterByBarAndSearch, + isUnassignedBoardRow, matchesAdvancedFilters, + partitionBoardRows, splitScheduledUnscheduled, } from "@/domain/work-orders/utils/table-filter-utils"; import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils"; @@ -83,18 +85,24 @@ describe("deriveDayFields", () => { describe("filterByBarAndSearch", () => { const weekMonday = "2026-06-01"; - it("keeps undated rows for any week, including targetWeek outside the active week", () => { + it("keeps undated rows without targetWeek for any active week", () => { const undated = makeRow({ id: "us1", scheduledOn: "", targetWeek: "" }); + + const ids = filterByBarAndSearch([undated], bar(weekMonday), "").map((row) => row.id); + expect(ids).toEqual(["us1"]); + }); + + it("excludes undated rows whose targetWeek is outside the active week", () => { const undatedOtherTarget = makeRow({ id: "tw1", scheduledOn: "", targetWeek: "2026-07-06", }); - const ids = filterByBarAndSearch([undated, undatedOtherTarget], bar(weekMonday), "").map( + const ids = filterByBarAndSearch([undatedOtherTarget], bar(weekMonday), "").map( (row) => row.id, ); - expect(ids).toEqual(["us1", "tw1"]); + expect(ids).toEqual([]); }); it("excludes dated rows outside the active week", () => { @@ -114,6 +122,59 @@ describe("filterByBarAndSearch", () => { }); }); +describe("isUnassignedBoardRow", () => { + it("treats empty dispatcher as unassigned unless terminal", () => { + expect(isUnassignedBoardRow(makeRow({ dispatcherId: "" }))).toBe(true); + expect(isUnassignedBoardRow(makeRow({ dispatcherId: "u1" }))).toBe(false); + expect(isUnassignedBoardRow(makeRow({ dispatcherId: "", status: "Completed" }))).toBe(false); + }); +}); + +describe("partitionBoardRows", () => { + it("pins unassigned rows first and keeps assigned undated rows in Unscheduled", () => { + const datedUnassigned = makeRow({ + id: "u-dated", + dispatcherId: "", + dispatcherName: "", + scheduledOn: "2026-06-02", + ...deriveDayFields("2026-06-02"), + }); + const undatedUnassigned = makeRow({ + id: "us-undated", + dispatcherId: "", + dispatcherName: "", + scheduledOn: "", + dayGroup: "", + dayLabel: "", + }); + const undatedAssigned = makeRow({ + id: "us-assigned", + dispatcherId: "sarah", + dispatcherName: "Sarah", + scheduledOn: "", + dayGroup: "", + dayLabel: "", + }); + const datedAssigned = makeRow({ id: "day-assigned", dispatcherId: "sarah" }); + + const result = partitionBoardRows([ + datedUnassigned, + undatedUnassigned, + undatedAssigned, + datedAssigned, + ]); + + expect(result.unassigned.map((row) => row.id)).toEqual(["u-dated", "us-undated"]); + expect(result.unscheduled.map((row) => row.id)).toEqual(["us-assigned"]); + expect(result.scheduled.map((row) => row.id)).toEqual(["day-assigned"]); + + const allIds = [...result.unassigned, ...result.unscheduled, ...result.scheduled].map( + (row) => row.id, + ); + expect(new Set(allIds).size).toBe(allIds.length); + }); +}); + describe("splitScheduledUnscheduled", () => { it("splits rows without scheduledOn into unscheduled", () => { const scheduled = makeRow({ id: "a", scheduledOn: "2026-06-23" }); @@ -123,23 +184,6 @@ describe("splitScheduledUnscheduled", () => { expect(result.unscheduled).toHaveLength(1); expect(result.unscheduled[0].id).toBe("b"); }); - - it("keeps dated unassigned in scheduled and undated in unscheduled without duplicate ids", () => { - const datedUnassigned = makeRow({ - id: "u-dated", - dispatcherId: "", - dispatcherName: "", - scheduledOn: "2026-06-02", - ...deriveDayFields("2026-06-02"), - }); - const undated = makeRow({ id: "us-undated", scheduledOn: "", dispatcherId: "" }); - const result = splitScheduledUnscheduled([datedUnassigned, undated]); - - expect(result.scheduled.map((row) => row.id)).toEqual(["u-dated"]); - expect(result.unscheduled.map((row) => row.id)).toEqual(["us-undated"]); - const allIds = [...result.scheduled, ...result.unscheduled].map((row) => row.id); - expect(new Set(allIds).size).toBe(allIds.length); - }); }); describe("buildDayGroups", () => {