diff --git a/e2e/__screenshots__/dashboard/dashboard.visual.spec.ts/dashboard-admin.png b/e2e/__screenshots__/dashboard/dashboard.visual.spec.ts/dashboard-admin.png
index 8a314581..8ee41bac 100644
Binary files a/e2e/__screenshots__/dashboard/dashboard.visual.spec.ts/dashboard-admin.png and b/e2e/__screenshots__/dashboard/dashboard.visual.spec.ts/dashboard-admin.png differ
diff --git a/e2e/__screenshots__/dashboard/dashboard.visual.spec.ts/dashboard-dispatcher.png b/e2e/__screenshots__/dashboard/dashboard.visual.spec.ts/dashboard-dispatcher.png
index 8f3a6331..0bcbb17a 100644
Binary files a/e2e/__screenshots__/dashboard/dashboard.visual.spec.ts/dashboard-dispatcher.png and b/e2e/__screenshots__/dashboard/dashboard.visual.spec.ts/dashboard-dispatcher.png differ
diff --git a/e2e/dashboard/dashboard.visual.spec.ts b/e2e/dashboard/dashboard.visual.spec.ts
index ab51543b..2e60bf41 100644
--- a/e2e/dashboard/dashboard.visual.spec.ts
+++ b/e2e/dashboard/dashboard.visual.spec.ts
@@ -191,7 +191,11 @@ async function mockDashboardApi(page: Page, userRoles: string) {
return;
}
- if (pathname.includes("/Dashboard/Stats")) return fulfillJson(route, { data: STATS });
+ if (pathname.includes("/Dashboard/Stats")) {
+ // The server reports no unassigned work for a dispatcher-scoped view.
+ const unassigned = userRoles === "Dispatcher" ? 0 : 11;
+ return fulfillJson(route, { data: { ...STATS, unassigned } });
+ }
if (pathname.includes("/Dashboard/Workload")) return fulfillJson(route, { data: WORKLOAD });
if (pathname.includes("/Dashboard/Performance"))
return fulfillJson(route, { data: PERFORMANCE });
diff --git a/src/app/(protected)/dashboard/_components/dashboard-kpi-cards.tsx b/src/app/(protected)/dashboard/_components/dashboard-kpi-cards.tsx
new file mode 100644
index 00000000..5329b2d2
--- /dev/null
+++ b/src/app/(protected)/dashboard/_components/dashboard-kpi-cards.tsx
@@ -0,0 +1,71 @@
+import { useNavigate } from "react-router";
+import AssignmentLateOutlinedIcon from "@mui/icons-material/AssignmentLateOutlined";
+import EventOutlinedIcon from "@mui/icons-material/EventOutlined";
+import PendingActionsIcon from "@mui/icons-material/PendingActions";
+import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined";
+import { Box } from "@mui/material";
+import { DashboardKpiCard } from "@/app/(protected)/dashboard/_components/dashboard-kpi-card";
+import type { DashboardRangeKey } from "@/domain/dashboard/types/dashboard-range";
+import type { DashboardStats } from "@/domain/dashboard/types/dashboard-stats";
+import {
+ avetaPendingDrilldownSearch,
+ scheduledTomorrowDrilldownSearch,
+ unassignedDrilldownSearch,
+ UPLIFTS_ROUTE,
+ workOrderDrilldownUrl,
+} from "@/domain/dashboard/utils/work-order-drilldown-links";
+
+interface DashboardKpiCardsProps {
+ stats?: DashboardStats;
+ loading: boolean;
+ range: DashboardRangeKey;
+}
+
+export function DashboardKpiCards({ stats, loading, range }: DashboardKpiCardsProps) {
+ const navigate = useNavigate();
+ const scheduledTomorrow = stats?.scheduledTomorrow ?? 0;
+ const pendingUplifts = stats?.pendingUplifts ?? 0;
+ const avetaPending = stats?.avetaPending ?? 0;
+ const unassigned = stats?.unassigned ?? 0;
+
+ return (
+
+ }
+ tone="primary"
+ value={stats?.scheduledTomorrow}
+ loading={loading}
+ active={scheduledTomorrow > 0}
+ onSelect={() => navigate(workOrderDrilldownUrl(scheduledTomorrowDrilldownSearch()))}
+ />
+ }
+ tone="info"
+ value={stats?.pendingUplifts}
+ loading={loading}
+ active={pendingUplifts > 0}
+ onSelect={() => navigate(UPLIFTS_ROUTE)}
+ />
+ }
+ tone="warning"
+ value={stats?.avetaPending}
+ loading={loading}
+ active={avetaPending > 0}
+ onSelect={() => navigate(workOrderDrilldownUrl(avetaPendingDrilldownSearch()))}
+ />
+ }
+ tone="warning"
+ value={stats?.unassigned}
+ loading={loading}
+ active={unassigned > 0}
+ onSelect={() => navigate(workOrderDrilldownUrl(unassignedDrilldownSearch(range)))}
+ />
+
+ );
+}
diff --git a/src/app/(protected)/dashboard/index.tsx b/src/app/(protected)/dashboard/index.tsx
index 60795d8f..9dd4f6ff 100644
--- a/src/app/(protected)/dashboard/index.tsx
+++ b/src/app/(protected)/dashboard/index.tsx
@@ -1,11 +1,7 @@
-import { useNavigate } from "react-router";
-import EventOutlinedIcon from "@mui/icons-material/EventOutlined";
-import PendingActionsIcon from "@mui/icons-material/PendingActions";
-import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined";
import { Box } from "@mui/material";
import { PageHeader } from "@/components/ui/page-header";
import { DashboardHeaderActions } from "@/app/(protected)/dashboard/_components/dashboard-header-actions";
-import { DashboardKpiCard } from "@/app/(protected)/dashboard/_components/dashboard-kpi-card";
+import { DashboardKpiCards } from "@/app/(protected)/dashboard/_components/dashboard-kpi-cards";
import { DashboardTrendCard } from "@/app/(protected)/dashboard/_components/dashboard-trend-card";
import { DispatcherTablesSection } from "@/app/(protected)/dashboard/_components/dispatcher-tables-section";
import { StatusDistributionCard } from "@/app/(protected)/dashboard/_components/status-distribution-card";
@@ -19,15 +15,8 @@ import { useDashboardStats } from "@/domain/dashboard/use-cases/use-dashboard-st
import { useDashboardVendorInsights } from "@/domain/dashboard/use-cases/use-dashboard-vendor-insights";
import { useDashboardWorkload } from "@/domain/dashboard/use-cases/use-dashboard-workload";
import { useWorkOrderUsers } from "@/domain/work-orders/use-cases/use-work-orders-list";
-import {
- avetaPendingDrilldownSearch,
- scheduledTomorrowDrilldownSearch,
- UPLIFTS_ROUTE,
- workOrderDrilldownUrl,
-} from "@/domain/dashboard/utils/work-order-drilldown-links";
export default function DashboardPage() {
- const navigate = useNavigate();
const scope = useDashboardScope();
const { canViewAll, range, dispatcherId, workloadPage, performancePage } = scope;
@@ -62,35 +51,7 @@ export default function DashboardPage() {
/>
}
/>
-
- }
- tone="primary"
- value={stats.data?.scheduledTomorrow}
- loading={stats.isLoading}
- active={(stats.data?.scheduledTomorrow ?? 0) > 0}
- onSelect={() => navigate(workOrderDrilldownUrl(scheduledTomorrowDrilldownSearch()))}
- />
- }
- tone="info"
- value={stats.data?.pendingUplifts}
- loading={stats.isLoading}
- active={(stats.data?.pendingUplifts ?? 0) > 0}
- onSelect={() => navigate(UPLIFTS_ROUTE)}
- />
- }
- tone="warning"
- value={stats.data?.avetaPending}
- loading={stats.isLoading}
- active={(stats.data?.avetaPending ?? 0) > 0}
- onSelect={() => navigate(workOrderDrilldownUrl(avetaPendingDrilldownSearch()))}
- />
-
+
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.
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..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,8 @@ export type DateRangeKey =
export type CompletionDocFilter = "No" | "Yes" | "NN";
export interface WorkOrderAdvancedFilters {
- dateRange: DateRangeKey;
+ /** `null` = no date range selected: search every week. */
+ 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..317e7cad 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;
+ /**
+ * `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..45fb611e 100644
--- a/src/domain/work-orders/utils/advanced-filter-predicates.ts
+++ b/src/domain/work-orders/utils/advanced-filter-predicates.ts
@@ -2,18 +2,44 @@ 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 = (
row: WorkOrderTableRow,
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: 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 [from, to] = dateRangeFor(filters);
- return !row.scheduledOn || (row.scheduledOn >= from && row.scheduledOn <= to);
+ 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;
+ 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/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..998d0605 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) {
+ // 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)/dashboard/dashboard-page.test.tsx b/src/test/app/(protected)/dashboard/dashboard-page.test.tsx
index 37c3632e..b74fd594 100644
--- a/src/test/app/(protected)/dashboard/dashboard-page.test.tsx
+++ b/src/test/app/(protected)/dashboard/dashboard-page.test.tsx
@@ -63,6 +63,7 @@ const statsData = {
scheduledTomorrow: 3,
pendingUplifts: 5,
avetaPending: 0,
+ unassigned: 4,
breakdown: [
{ status: "PM", count: 7 },
{ status: "Completed", count: 0 },
@@ -88,6 +89,11 @@ function renderDashboard() {
return renderWithProviders();
}
+function lastNavigation(): string {
+ const calls = navigate.mock.calls;
+ return String(calls[calls.length - 1]?.[0]);
+}
+
describe("DashboardPage", () => {
beforeEach(() => {
authUser.current = { id: "99", userRoles: "Admin" };
@@ -120,6 +126,7 @@ describe("DashboardPage", () => {
"Scheduled Tomorrow",
"Pending Uplifts",
"Aveta Pending",
+ "Unassigned",
"Work Order Breakdown",
"Status Distribution",
"Dispatcher Workload",
@@ -257,6 +264,29 @@ describe("DashboardPage", () => {
expect(lastRequestedPage).toBe(1);
});
+ it("shows unassigned WOs for the selected period and drills into that list (SH-392)", () => {
+ renderDashboard();
+
+ const card = screen.getByRole("button", { name: /Unassigned/ });
+ expect(card).toHaveTextContent("4");
+ fireEvent.click(card);
+
+ const weekUrl = new URL(lastNavigation(), "http://x");
+ expect(weekUrl.pathname).toBe("/workorders");
+ expect(weekUrl.searchParams.get("dispatchers")).toBe("__unassigned");
+ expect(weekUrl.searchParams.get("statuses")).not.toContain("Completed");
+ expect(weekUrl.searchParams.get("dateFrom")).toMatch(/^\d{4}-\d{2}-\d{2}$/);
+ expect(weekUrl.searchParams.get("dateTo")).toMatch(/^\d{4}-\d{2}-\d{2}$/);
+
+ fireEvent.click(screen.getByRole("button", { name: "All time" }));
+ fireEvent.click(screen.getByRole("button", { name: /Unassigned/ }));
+
+ const allTimeUrl = new URL(lastNavigation(), "http://x");
+ expect(allTimeUrl.searchParams.get("dispatchers")).toBe("__unassigned");
+ expect(allTimeUrl.searchParams.has("dateFrom")).toBe(false);
+ expect(allTimeUrl.searchParams.has("dateTo")).toBe(false);
+ });
+
it("keeps drilldown dates deterministic on the business clock", () => {
const today = businessTodayIso();
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/dashboard/dashboard-api.test.ts b/src/test/domain/dashboard/dashboard-api.test.ts
index 95f8c258..27a87695 100644
--- a/src/test/domain/dashboard/dashboard-api.test.ts
+++ b/src/test/domain/dashboard/dashboard-api.test.ts
@@ -27,6 +27,7 @@ describe("dashboardApi", () => {
ScheduledTomorrow: "3",
PendingUplifts: 2,
AvetaPending: "1",
+ Unassigned: "6",
Breakdown: [
{ Status: "Scheduled", Count: "7" },
{ Status: "Completed", Count: 4 },
@@ -52,6 +53,7 @@ describe("dashboardApi", () => {
scheduledTomorrow: 3,
pendingUplifts: 2,
avetaPending: 1,
+ unassigned: 6,
breakdown: [
{ status: "Scheduled", count: 7 },
{ status: "Completed", count: 4 },
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/dashboard-drilldown-params.test.ts b/src/test/domain/work-orders/dashboard-drilldown-params.test.ts
index 8ff30f72..7faab8b0 100644
--- a/src/test/domain/work-orders/dashboard-drilldown-params.test.ts
+++ b/src/test/domain/work-orders/dashboard-drilldown-params.test.ts
@@ -1,6 +1,13 @@
import { describe, expect, it } from "vitest";
import { parseDashboardDrilldownParams } from "@/domain/work-orders/utils/dashboard-drilldown-params";
-import { avetaPendingDrilldownSearch } from "@/domain/dashboard/utils/work-order-drilldown-links";
+import {
+ avetaPendingDrilldownSearch,
+ unassignedDrilldownSearch,
+} from "@/domain/dashboard/utils/work-order-drilldown-links";
+import { NON_TERMINAL_LIFECYCLE_STATUSES } from "@/domain/work-orders/types/work-order-board";
+import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list";
+import { buildAdvancedFilterChips } from "@/domain/work-orders/utils/advanced-filters-utils";
+import { toBoardSearchQueryParams } from "@/domain/work-orders/utils/board-query-params";
describe("parseDashboardDrilldownParams", () => {
it("returns null when no recognized param is present", () => {
@@ -60,6 +67,41 @@ describe("parseDashboardDrilldownParams", () => {
expect(filters?.regions).toEqual(new Set(["California"]));
});
+ it("turns the Unassigned tile link into the board search the Dashboard counts (SH-392)", () => {
+ const allTime = parseDashboardDrilldownParams(unassignedDrilldownSearch("all-time"));
+ expect(allTime).not.toBeNull();
+ expect(toBoardSearchQueryParams({ filters: allTime! })).toMatchObject({
+ dispatchers: ["__unassigned__"],
+ statuses: [...NON_TERMINAL_LIFECYCLE_STATUSES],
+ datePreset: "Custom",
+ dateFrom: "2000-01-01",
+ dateTo: "2099-12-31",
+ includeDateless: true,
+ });
+
+ const week = parseDashboardDrilldownParams(unassignedDrilldownSearch("this-week"));
+ const weekParams = toBoardSearchQueryParams({ filters: week! });
+ expect(week?.dateRange).toBe("custom");
+ expect(weekParams).toMatchObject({
+ dispatchers: ["__unassigned__"],
+ statuses: [...NON_TERMINAL_LIFECYCLE_STATUSES],
+ datePreset: "Custom",
+ });
+ expect(weekParams.includeDateless).toBeUndefined();
+ });
+
+ it("opens the Unassigned tile link as the Unassigned option of Assigned to", () => {
+ const filters = parseDashboardDrilldownParams(unassignedDrilldownSearch("this-week"));
+ expect(filters?.dispatchers).toEqual(new Set([ASSIGNEE_FILTER_UNASSIGNED]));
+
+ const chips = buildAdvancedFilterChips(filters!, { dispatcherNameOf: (id) => id });
+ const assignedTo = chips.filter((chip) => chip.group === "Assigned to");
+ expect(assignedTo.map((chip) => chip.value)).toEqual(["Unassigned"]);
+
+ const cleared = assignedTo[0]!.clear(filters!);
+ expect(cleared.dispatchers.size).toBe(0);
+ });
+
it("rejects partial or malformed date ranges", () => {
expect(
parseDashboardDrilldownParams(
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)", () => {
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..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
@@ -454,6 +454,56 @@ 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);
+ // 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);
+ });
});
describe("computeIsPastDue", () => {