From ed931ea2eb1054b2b4de833aa1d6c1e8a511c6af Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 24 Sep 2026 21:09:04 -0300 Subject: [PATCH] SH-392: Unassigned KPI tile on the Dashboard Shows open work orders with no dispatcher for the selected period and drills into the same list on the board (dispatcher Unassigned, open statuses, same dates; All time includes undated work orders). --- .../_components/dashboard-kpi-cards.tsx | 71 +++++++++++++++++++ src/app/(protected)/dashboard/index.tsx | 43 +---------- src/domain/dashboard/api/dashboard-api.ts | 1 + src/domain/dashboard/types/dashboard-stats.ts | 2 + .../utils/work-order-drilldown-links.ts | 13 ++++ .../dashboard/dashboard-page.test.tsx | 30 ++++++++ .../domain/dashboard/dashboard-api.test.ts | 2 + .../dashboard-drilldown-params.test.ts | 30 +++++++- 8 files changed, 150 insertions(+), 42 deletions(-) create mode 100644 src/app/(protected)/dashboard/_components/dashboard-kpi-cards.tsx 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..6557acd2 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,11 @@ 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 { toBoardSearchQueryParams } from "@/domain/work-orders/utils/board-query-params"; describe("parseDashboardDrilldownParams", () => { it("returns null when no recognized param is present", () => { @@ -60,6 +65,29 @@ 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("rejects partial or malformed date ranges", () => { expect( parseDashboardDrilldownParams(