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()))}
- />
-
+
);
}
+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/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/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(