diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-body.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-body.tsx
index 3ffd7172..5b2d5b38 100644
--- a/src/app/(protected)/workorders/_components/list/table/wo-table-body.tsx
+++ b/src/app/(protected)/workorders/_components/list/table/wo-table-body.tsx
@@ -1,10 +1,12 @@
import { Fragment, type ReactNode } from "react";
-import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
+import { Text } from "@/components/ui/text";
import {
UNASSIGNED_DAY_KEY,
UNSCHEDULED_DAY_KEY,
+ type WorkOrderTableRow,
} from "@/domain/work-orders/types/work-order-table-row";
import type { ReturnTypeOfUseWorkOrdersTableData } from "./wo-table-types";
+import { WO_TABLE_COLS } from "./wo-table-cols";
import { WoTableSectionHeader } from "./wo-table-section-header";
type WoTableBodyProps = {
@@ -19,9 +21,11 @@ export function WoTableBody({ tableData, renderRow, renderInlineRow }: WoTableBo
}
const showUnassigned =
- tableData.unassigned.length > 0 || tableData.draftDayKey === UNASSIGNED_DAY_KEY;
+ (tableData.unassigned?.length ?? 0) > 0 ||
+ tableData.draftDayKey === UNASSIGNED_DAY_KEY ||
+ Boolean(tableData.unassignedError);
const showUnscheduled =
- tableData.unscheduled.length > 0 || tableData.draftDayKey === UNSCHEDULED_DAY_KEY;
+ (tableData.unscheduled?.length ?? 0) > 0 || tableData.draftDayKey === UNSCHEDULED_DAY_KEY;
return (
@@ -37,7 +41,17 @@ export function WoTableBody({ tableData, renderRow, renderInlineRow }: WoTableBo
{tableData.unassignedOpen && (
<>
{renderInlineRow(UNASSIGNED_DAY_KEY)}
- {tableData.unassigned.map((row) => renderRow(row))}
+ {Boolean(tableData.unassignedError) && (
+
+ |
+
+ {tableData.unassignedError?.message ??
+ "Unable to load unassigned work orders."}
+
+ |
+
+ )}
+ {(tableData.unassigned ?? []).map((row) => renderRow(row))}
>
)}
>
diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-board-source.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-board-source.ts
index 7522bb47..11771ca7 100644
--- a/src/app/(protected)/workorders/_hooks/use-work-orders-board-source.ts
+++ b/src/app/(protected)/workorders/_hooks/use-work-orders-board-source.ts
@@ -1,22 +1,28 @@
-import { useMemo } from "react";
+import { useCallback, useMemo } from "react";
import type { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters";
import { buildResolvedBoardSearchParams } from "@/app/(protected)/workorders/_hooks/build-resolved-board-search-params";
import {
useWorkOrdersBoard,
useWorkOrdersBoardSearch,
+ useWorkOrdersUnassignedQueue,
} from "@/domain/work-orders/use-cases/use-work-orders-list";
import {
useWorkOrderLocations,
useWorkOrderVendors,
} from "@/domain/work-orders/use-cases/use-work-order-detail";
import type { WorkOrderBoardListResult } from "@/domain/work-orders/types/work-order-board";
+import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import type { UseQueryResult } from "@tanstack/react-query";
type FiltersReturn = ReturnType;
+const EMPTY_UNASSIGNED_QUEUE: WorkOrderTableRow[] = [];
+
export type WorkOrdersBoardSource = {
advActive: boolean;
data: WorkOrderBoardListResult | undefined;
+ unassignedQueue: WorkOrderTableRow[];
+ unassignedError: Error | null;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
@@ -67,13 +73,30 @@ export function useWorkOrdersBoardSource(filters: FiltersReturn): WorkOrdersBoar
error: searchError,
refetch: refetchSearch,
} = useWorkOrdersBoardSearch(resolvedSearchParams ?? { datePreset: "ThisWeek" }, advActive);
+ const {
+ data: queueData,
+ isFetching: queueFetching,
+ error: queueError,
+ refetch: refetchQueue,
+ } = useWorkOrdersUnassignedQueue(!advActive);
+
+ const refetchBoardWithQueue: UseQueryResult["refetch"] =
+ useCallback(
+ async (options) => {
+ const [boardResult] = await Promise.all([refetchBoard(options), refetchQueue()]);
+ return boardResult;
+ },
+ [refetchBoard, refetchQueue],
+ );
return {
advActive,
data: advActive ? searchData : boardData,
+ unassignedQueue: advActive ? EMPTY_UNASSIGNED_QUEUE : (queueData ?? EMPTY_UNASSIGNED_QUEUE),
+ unassignedError: advActive ? null : (queueError ?? null),
isLoading: advActive ? searchLoading : boardLoading,
- isFetching: advActive ? searchFetching : boardFetching,
+ isFetching: advActive ? searchFetching : boardFetching || queueFetching,
error: advActive ? searchError : boardError,
- refetch: advActive ? refetchSearch : refetchBoard,
+ refetch: advActive ? refetchSearch : refetchBoardWithQueue,
};
}
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 c52a99bf..469fd465 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
@@ -3,11 +3,14 @@ import type { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hoo
import { useWorkOrdersBoardSource } from "@/app/(protected)/workorders/_hooks/use-work-orders-board-source";
import { useWorkOrdersTableLocalState } from "@/app/(protected)/workorders/_hooks/use-work-orders-table-local-state";
import { currentWeekMonday } from "@/domain/work-orders/utils/list-week-utils";
+import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { buildWeekSkeleton, deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
import {
buildDayGroups,
+ composeBoardSections,
filterByBarAndSearch,
- partitionBoardRows,
+ mergeWeeklyAndUnassignedQueue,
+ rowIdSet,
splitScheduledUnscheduled,
} from "@/domain/work-orders/utils/table-filter-utils";
@@ -15,12 +18,30 @@ export type { DragVizState } from "@/app/(protected)/workorders/_hooks/use-work-
type FiltersReturn = ReturnType;
+const EMPTY_WEEKLY_ITEMS: WorkOrderTableRow[] = [];
+
export function useWorkOrdersTableData(filters: FiltersReturn) {
- const { advActive, data, isLoading, isFetching, error, refetch } =
- useWorkOrdersBoardSource(filters);
+ const {
+ advActive,
+ data,
+ unassignedQueue,
+ unassignedError,
+ isLoading,
+ isFetching,
+ error,
+ refetch,
+ } = useWorkOrdersBoardSource(filters);
+
+ const weeklyItems = data?.items ?? EMPTY_WEEKLY_ITEMS;
+ const items = useMemo(
+ () => mergeWeeklyAndUnassignedQueue(weeklyItems, unassignedQueue),
+ [weeklyItems, unassignedQueue],
+ );
+ const weeklyIds = useMemo(() => rowIdSet(weeklyItems), [weeklyItems]);
+ const queueIds = useMemo(() => rowIdSet(unassignedQueue), [unassignedQueue]);
const local = useWorkOrdersTableLocalState({
- items: data?.items ?? [],
+ items,
weekMonday: filters.weekMonday,
advActive,
isFetching,
@@ -44,8 +65,13 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
const split = splitScheduledUnscheduled(advResults);
return { unassigned: [], ...split };
}
- return partitionBoardRows(filtered);
- }, [advActive, advResults, filtered]);
+ return composeBoardSections({
+ displayRows,
+ weekRows: filtered,
+ weeklyIds,
+ queueIds,
+ });
+ }, [advActive, advResults, displayRows, filtered, weeklyIds, queueIds]);
const dayGroups = useMemo(() => buildDayGroups(scheduled), [scheduled]);
const weekSkeleton = useMemo(() => buildWeekSkeleton(filters.weekMonday), [filters.weekMonday]);
@@ -80,6 +106,7 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
advActive,
advResults,
unassigned,
+ unassignedError,
unscheduled,
displayGroups,
searchActive,
@@ -111,7 +138,7 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
scopeTotalCount: data?.totalCount ?? 0,
totalCount: advActive
? (data?.totalCount ?? advResults.length)
- : (data?.returnedCount ?? visibleRows.length),
+ : (data?.returnedCount ?? weeklyItems.length),
search: filters.search,
};
}
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 981e0f0f..196bfe69 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
@@ -52,7 +52,7 @@ export function useWorkOrdersTableUiState(weekMonday: string) {
(key: string, scheduledIso?: string) => {
setDraftDayKey(key);
// Unscheduled "+ Add WO" starts with no schedule (SH-109); day groups prefill the day.
- if (key === UNSCHEDULED_DAY_KEY) {
+ if (key === UNSCHEDULED_DAY_KEY || key === UNASSIGNED_DAY_KEY) {
setDraft({ ...EMPTY_TABLE_DRAFT });
return;
}
diff --git a/src/domain/work-orders/api/fetch-unassigned-board-rows.ts b/src/domain/work-orders/api/fetch-unassigned-board-rows.ts
new file mode 100644
index 00000000..4887496a
--- /dev/null
+++ b/src/domain/work-orders/api/fetch-unassigned-board-rows.ts
@@ -0,0 +1,29 @@
+import { workOrdersApi } from "@/domain/work-orders/api/work-orders-api";
+import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
+import { toUnassignedQueueQueryParams } from "@/domain/work-orders/utils/board-query-params";
+
+const UNASSIGNED_QUEUE_MAX_PAGES = 50;
+
+/** Exhausts GET /board/search pages for the global Unassigned pin (SH-165). */
+export async function fetchUnassignedBoardRows(): Promise {
+ const rows: WorkOrderTableRow[] = [];
+ let page = 0;
+ let totalCount = Number.POSITIVE_INFINITY;
+
+ while (rows.length < totalCount) {
+ if (page >= UNASSIGNED_QUEUE_MAX_PAGES) {
+ throw new Error(
+ "Unable to load the unassigned queue: search result set exceeded the safety page limit.",
+ );
+ }
+ const result = await workOrdersApi.searchBoard(toUnassignedQueueQueryParams(page));
+ rows.push(...result.items);
+ totalCount = result.totalCount;
+ if (result.items.length === 0) {
+ break;
+ }
+ page += 1;
+ }
+
+ return rows;
+}
diff --git a/src/domain/work-orders/types/work-order-board.ts b/src/domain/work-orders/types/work-order-board.ts
index 51bbdc0b..04c885f9 100644
--- a/src/domain/work-orders/types/work-order-board.ts
+++ b/src/domain/work-orders/types/work-order-board.ts
@@ -41,6 +41,18 @@ export const LIFECYCLE_STATUS_API = {
export type LifecycleStatusApi = (typeof LIFECYCLE_STATUS_API)[keyof typeof LIFECYCLE_STATUS_API];
+/** Non-terminal lifecycle statuses for the global Unassigned queue (SH-165). */
+export const NON_TERMINAL_LIFECYCLE_STATUSES: LifecycleStatusApi[] = [
+ LIFECYCLE_STATUS_API.Incomplete,
+ LIFECYCLE_STATUS_API.Pending,
+ LIFECYCLE_STATUS_API.Scheduled,
+ LIFECYCLE_STATUS_API["En Route"],
+ LIFECYCLE_STATUS_API["On Site"],
+ LIFECYCLE_STATUS_API["In Progress"],
+ LIFECYCLE_STATUS_API.Rescheduled,
+ LIFECYCLE_STATUS_API["Pending Quote"],
+];
+
export const WORK_ORDER_TYPE_API_TO_FE: Record = {
[WORK_ORDER_TYPE_API.PM]: "PM",
[WORK_ORDER_TYPE_API.Reactive]: "Reactive",
diff --git a/src/domain/work-orders/use-cases/use-work-orders-list.ts b/src/domain/work-orders/use-cases/use-work-orders-list.ts
index 46fd94ef..59998f5a 100644
--- a/src/domain/work-orders/use-cases/use-work-orders-list.ts
+++ b/src/domain/work-orders/use-cases/use-work-orders-list.ts
@@ -1,4 +1,5 @@
import { keepPreviousData, useQuery, type UseQueryResult } from "@tanstack/react-query";
+import { fetchUnassignedBoardRows } from "@/domain/work-orders/api/fetch-unassigned-board-rows";
import { workOrdersApi } from "@/domain/work-orders/api/work-orders-api";
import type {
WorkOrdersListParams,
@@ -10,6 +11,7 @@ import type {
WorkOrderBoardQueryParams,
WorkOrderBoardSearchQueryParams,
} from "@/domain/work-orders/types/work-order-board";
+import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { queryKeys } from "@/infra/query-key/query-key";
export function useWorkOrdersList(
@@ -46,6 +48,17 @@ export function useWorkOrdersBoardSearch(
});
}
+export function useWorkOrdersUnassignedQueue(
+ enabled = true,
+): UseQueryResult {
+ return useQuery({
+ queryKey: queryKeys.workOrders.unassignedQueue(),
+ queryFn: fetchUnassignedBoardRows,
+ enabled,
+ placeholderData: keepPreviousData,
+ });
+}
+
export function useWorkOrderUsers(): UseQueryResult {
return useQuery({
queryKey: queryKeys.workOrders.dispatchers(),
diff --git a/src/domain/work-orders/utils/board-query-params.ts b/src/domain/work-orders/utils/board-query-params.ts
index 6f4824fe..293aa597 100644
--- a/src/domain/work-orders/utils/board-query-params.ts
+++ b/src/domain/work-orders/utils/board-query-params.ts
@@ -6,6 +6,7 @@ import {
DATE_RANGE_TO_PRESET,
DOC_STATUS_API,
LIFECYCLE_STATUS_API,
+ NON_TERMINAL_LIFECYCLE_STATUSES,
type WorkOrderBoardQueryParams,
type WorkOrderBoardSearchQueryParams,
} from "@/domain/work-orders/types/work-order-board";
@@ -16,6 +17,14 @@ import type { WizardWOStatus, WOType } from "@/domain/work-orders/types/work-ord
export const BOARD_SEARCH_MAX_PAGE_SIZE = 100;
+/**
+ * Wide Custom window for SH-165. GET /board/search defaults omitted
+ * `datePreset` to ThisWeek (`WorkOrderSearchDateRangeResolver`), which would
+ * hide scheduled-unassigned WOs outside the current week.
+ */
+export const UNASSIGNED_QUEUE_DATE_FROM = "1970-01-01";
+export const UNASSIGNED_QUEUE_DATE_TO = "2099-12-31";
+
function toBoardDispatcherId(id: string): string {
if (id === ASSIGNEE_FILTER_UNASSIGNED || id === BOARD_DISPATCHER_UNASSIGNED) {
return BOARD_DISPATCHER_UNASSIGNED;
@@ -175,3 +184,16 @@ export function buildBoardSearchParamsRecord(
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): WorkOrderBoardSearchQueryParams {
+ return {
+ page,
+ pageSize: BOARD_SEARCH_MAX_PAGE_SIZE,
+ dispatchers: [BOARD_DISPATCHER_UNASSIGNED],
+ statuses: [...NON_TERMINAL_LIFECYCLE_STATUSES],
+ datePreset: "Custom",
+ dateFrom: UNASSIGNED_QUEUE_DATE_FROM,
+ dateTo: UNASSIGNED_QUEUE_DATE_TO,
+ };
+}
diff --git a/src/domain/work-orders/utils/table-filter-utils.ts b/src/domain/work-orders/utils/table-filter-utils.ts
index 6c9df9ec..9c13d5bf 100644
--- a/src/domain/work-orders/utils/table-filter-utils.ts
+++ b/src/domain/work-orders/utils/table-filter-utils.ts
@@ -129,6 +129,51 @@ export function partitionBoardRows(rows: WorkOrderTableRow[]): {
return { unassigned, unscheduled, scheduled };
}
+export function rowIdSet(rows: WorkOrderTableRow[]): Set {
+ return new Set(rows.map((row) => String(row.id)));
+}
+
+/** Weekly board rows win on id overlap so PATCH state stays authoritative. */
+export function mergeWeeklyAndUnassignedQueue(
+ weekly: WorkOrderTableRow[],
+ queue: WorkOrderTableRow[],
+): WorkOrderTableRow[] {
+ if (queue.length === 0) {
+ return weekly;
+ }
+ const seen = rowIdSet(weekly);
+ const extras = queue.filter((row) => !seen.has(String(row.id)));
+ return extras.length === 0 ? weekly : [...weekly, ...extras];
+}
+
+type ComposeBoardSectionsInput = {
+ displayRows: WorkOrderTableRow[];
+ weekRows: WorkOrderTableRow[];
+ weeklyIds: ReadonlySet;
+ queueIds: ReadonlySet;
+};
+
+/**
+ * Pin is every non-terminal unassigned row (including out-of-week).
+ * Day/Unscheduled groups stay week-scoped and never repeat pin ids.
+ */
+export function composeBoardSections(input: ComposeBoardSectionsInput): {
+ unassigned: WorkOrderTableRow[];
+ unscheduled: WorkOrderTableRow[];
+ scheduled: WorkOrderTableRow[];
+} {
+ const unassigned = input.displayRows.filter(isUnassignedBoardRow);
+ const pinIds = rowIdSet(unassigned);
+ const weekAssigned = input.weekRows.filter((row) => {
+ const id = String(row.id);
+ if (pinIds.has(id) || isUnassignedBoardRow(row)) {
+ return false;
+ }
+ return input.weeklyIds.has(id) || !input.queueIds.has(id);
+ });
+ return { unassigned, ...splitScheduledUnscheduled(weekAssigned) };
+}
+
export function splitScheduledUnscheduled(rows: WorkOrderTableRow[]): {
scheduled: WorkOrderTableRow[];
unscheduled: WorkOrderTableRow[];
diff --git a/src/infra/query-key/query-key.ts b/src/infra/query-key/query-key.ts
index c775e00e..87eeda4e 100644
--- a/src/infra/query-key/query-key.ts
+++ b/src/infra/query-key/query-key.ts
@@ -17,6 +17,7 @@ export const queryKeys = {
[...queryKeys.workOrders.all, "board", params ?? {}] as const,
boardSearch: (params?: WorkOrderBoardSearchQueryParams) =>
[...queryKeys.workOrders.all, "boardSearch", params ?? {}] as const,
+ unassignedQueue: () => [...queryKeys.workOrders.all, "unassignedQueue"] as const,
byLocation: (locationId: string | number, search = "") =>
[...queryKeys.workOrders.all, "byLocation", locationId, search] as const,
detail: (id: string | number) => [...queryKeys.workOrders.all, "detail", id] as const,
diff --git a/src/test/app/(protected)/workorders/wo-table-unassigned-queue.test.tsx b/src/test/app/(protected)/workorders/wo-table-unassigned-queue.test.tsx
index d510af3f..a5a371b0 100644
--- a/src/test/app/(protected)/workorders/wo-table-unassigned-queue.test.tsx
+++ b/src/test/app/(protected)/workorders/wo-table-unassigned-queue.test.tsx
@@ -11,9 +11,11 @@ function makeRow(overrides: Partial = {}): WorkOrderTableRow
woNumber: "24991",
site: "CLV2",
type: "PM",
+ severity: null,
dispatcherId: "",
dispatcherName: "",
scheduledOn: "2026-06-02",
+ completedDate: "",
targetWeek: "",
dayGroup: "06/02",
dayLabel: "TUE · 06/02",
@@ -64,6 +66,7 @@ function baseTableData(
advActive: false,
advResults: [],
unassigned: [],
+ unassignedError: null,
unscheduled: [],
displayGroups: [],
searchActive: false,
@@ -202,4 +205,51 @@ describe("WoTableBody Unassigned queue (SH-165)", () => {
await user.click(screen.getByRole("button", { name: "Unassigned, 1 work order, expanded" }));
expect(setUnassignedOpen).toHaveBeenCalledWith(expect.any(Function));
});
+
+ it("renders a scheduled unassigned WO from outside the selected week in the pin", () => {
+ const outOfWeek = makeRow({
+ id: "out-week",
+ woNumber: "26001",
+ scheduledOn: "2026-07-15",
+ dayGroup: "07/15",
+ dayLabel: "WED · 07/15",
+ });
+
+ render(
+
+ (
+
+ | {row.woNumber} |
+
+ )}
+ renderInlineRow={() => null}
+ />
+
,
+ );
+
+ expect(screen.getByTestId("row-out-week")).toBeInTheDocument();
+ expect(screen.queryByText("WED · 07/15")).not.toBeInTheDocument();
+ });
+
+ it("keeps the Unassigned pin visible when the queue query fails", () => {
+ render(
+
+ (
+
+ | {row.woNumber} |
+
+ )}
+ renderInlineRow={() => null}
+ />
+
,
+ );
+
+ expect(screen.getByText("queue unavailable")).toBeInTheDocument();
+ });
});
diff --git a/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx b/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx
index f8edb9ff..71bb2206 100644
--- a/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx
+++ b/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx
@@ -75,6 +75,10 @@ const handlers: WoTableRowHandlers = {
function renderUnscheduledHarness() {
const tableData = {
advActive: false,
+ unassigned: [],
+ unassignedError: null,
+ unassignedOpen: true,
+ setUnassignedOpen: vi.fn(),
unscheduled: [unscheduledRow],
unschedOpen: true,
draftDayKey: null,
diff --git a/src/test/domain/work-orders/api/fetch-unassigned-board-rows.test.ts b/src/test/domain/work-orders/api/fetch-unassigned-board-rows.test.ts
new file mode 100644
index 00000000..bf8031e9
--- /dev/null
+++ b/src/test/domain/work-orders/api/fetch-unassigned-board-rows.test.ts
@@ -0,0 +1,52 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
+import { BOARD_SEARCH_MAX_PAGE_SIZE } from "@/domain/work-orders/utils/board-query-params";
+
+const searchBoard = vi.fn();
+
+vi.mock("@/domain/work-orders/api/work-orders-api", () => ({
+ workOrdersApi: {
+ searchBoard: (...args: unknown[]) => searchBoard(...args),
+ },
+}));
+
+import { fetchUnassignedBoardRows } from "@/domain/work-orders/api/fetch-unassigned-board-rows";
+
+function pageResult(items: Pick[], totalCount: number) {
+ return {
+ items,
+ scheduled: [],
+ unscheduled: [],
+ totalCount,
+ returnedCount: items.length,
+ page: 0,
+ pageSize: BOARD_SEARCH_MAX_PAGE_SIZE,
+ };
+}
+
+describe("fetchUnassignedBoardRows", () => {
+ beforeEach(() => {
+ searchBoard.mockReset();
+ });
+
+ it("pages until totalCount is exhausted", async () => {
+ const first = Array.from({ length: BOARD_SEARCH_MAX_PAGE_SIZE }, (_, index) => ({
+ id: `a-${index}`,
+ }));
+ const second = [{ id: "b-0" }];
+ searchBoard
+ .mockResolvedValueOnce(pageResult(first, 101))
+ .mockResolvedValueOnce(pageResult(second, 101));
+
+ const rows = await fetchUnassignedBoardRows();
+
+ expect(searchBoard).toHaveBeenCalledTimes(2);
+ expect(searchBoard.mock.calls[0]?.[0]).toMatchObject({
+ page: 0,
+ dispatchers: ["__unassigned__"],
+ datePreset: "Custom",
+ });
+ expect(searchBoard.mock.calls[1]?.[0]).toMatchObject({ page: 1 });
+ expect(rows).toHaveLength(101);
+ });
+});
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 3fd01ec9..ba5b25ea 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
@@ -7,6 +7,9 @@ import {
buildBoardSearchParamsRecord,
toBoardQueryParams,
toBoardSearchQueryParams,
+ toUnassignedQueueQueryParams,
+ UNASSIGNED_QUEUE_DATE_FROM,
+ UNASSIGNED_QUEUE_DATE_TO,
} from "@/domain/work-orders/utils/board-query-params";
describe("toBoardQueryParams", () => {
@@ -174,3 +177,21 @@ describe("buildBoardSearchParamsRecord", () => {
expect(Object.keys(record)).not.toContain("types[]");
});
});
+
+describe("toUnassignedQueueQueryParams", () => {
+ it("requests unassigned dispatchers with a Custom range, not the weekly board window", () => {
+ const params = toUnassignedQueueQueryParams(2);
+ const record = buildBoardSearchParamsRecord(params);
+
+ expect(params.page).toBe(2);
+ expect(params.pageSize).toBe(BOARD_SEARCH_MAX_PAGE_SIZE);
+ expect(params.dispatchers).toEqual(["__unassigned__"]);
+ expect(params.statuses).not.toContain(7);
+ expect(params.statuses).not.toContain(9);
+ expect(params.datePreset).toBe("Custom");
+ expect(params.dateFrom).toBe(UNASSIGNED_QUEUE_DATE_FROM);
+ expect(params.dateTo).toBe(UNASSIGNED_QUEUE_DATE_TO);
+ expect(record).not.toHaveProperty("weekStart");
+ expect(record).not.toHaveProperty("weekEnd");
+ });
+});
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 7985c879..146aa7ac 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
@@ -4,11 +4,13 @@ import { EMPTY_ADVANCED_FILTERS } from "@/domain/work-orders/types/work-order-ad
import type { WorkOrderBarFilters } from "@/domain/work-orders/types/work-order-list-filters";
import {
buildDayGroups,
+ composeBoardSections,
computeIsPastDue,
enrichTableRow,
filterByBarAndSearch,
isUnassignedBoardRow,
matchesAdvancedFilters,
+ mergeWeeklyAndUnassignedQueue,
partitionBoardRows,
splitScheduledUnscheduled,
} from "@/domain/work-orders/utils/table-filter-utils";
@@ -184,6 +186,69 @@ describe("partitionBoardRows", () => {
});
});
+describe("mergeWeeklyAndUnassignedQueue", () => {
+ it("keeps the weekly row when the same id is also in the queue", () => {
+ const weekly = makeRow({ id: "dup", dispatcherName: "Weekly" });
+ const queue = makeRow({ id: "dup", dispatcherName: "Queue" });
+ const extra = makeRow({ id: "out", dispatcherId: "", dispatcherName: "" });
+
+ const merged = mergeWeeklyAndUnassignedQueue([weekly], [queue, extra]);
+ expect(merged.map((row) => row.id)).toEqual(["dup", "out"]);
+ expect(merged[0]?.dispatcherName).toBe("Weekly");
+ });
+});
+
+describe("composeBoardSections", () => {
+ it("pins a scheduled unassigned WO that is outside the selected week", () => {
+ const inWeekAssigned = makeRow({
+ id: "in-week",
+ dispatcherId: "sarah",
+ dispatcherName: "Sarah",
+ scheduledOn: "2026-06-03",
+ ...deriveDayFields("2026-06-03"),
+ });
+ const outOfWeekUnassigned = makeRow({
+ id: "out-week",
+ dispatcherId: "",
+ dispatcherName: "",
+ scheduledOn: "2026-07-15",
+ ...deriveDayFields("2026-07-15"),
+ });
+
+ const result = composeBoardSections({
+ displayRows: [inWeekAssigned, outOfWeekUnassigned],
+ weekRows: [inWeekAssigned],
+ weeklyIds: new Set(["in-week"]),
+ queueIds: new Set(["out-week"]),
+ });
+
+ expect(result.unassigned.map((row) => row.id)).toEqual(["out-week"]);
+ expect(result.scheduled.map((row) => row.id)).toEqual(["in-week"]);
+ expect(result.unscheduled).toEqual([]);
+ });
+
+ it("does not place an assigned leftover queue row into the current week groups", () => {
+ const assignedOutOfWeek = makeRow({
+ id: "was-queue",
+ dispatcherId: "sarah",
+ dispatcherName: "Sarah",
+ scheduledOn: "2026-07-15",
+ ...deriveDayFields("2026-07-15"),
+ });
+
+ const result = composeBoardSections({
+ displayRows: [assignedOutOfWeek],
+ weekRows: [assignedOutOfWeek],
+ weeklyIds: new Set(),
+ queueIds: new Set(["was-queue"]),
+ });
+
+ expect(result.unassigned).toEqual([]);
+ expect(result.scheduled).toEqual([]);
+ expect(result.unscheduled).toEqual([]);
+ });
+});
+
describe("splitScheduledUnscheduled", () => {
it("splits rows without scheduledOn into unscheduled", () => {
const scheduled = makeRow({ id: "a", scheduledOn: "2026-06-23" });