From 2281c7c82d2105ef2da2004f413a819709f16b5e Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 25 Sep 2026 17:10:51 -0300 Subject: [PATCH] fix(sites): open the site's board when open work orders exceed the id cap The delete dialog states the server's full open work-order count, but the server returns at most 200 ids. "View open work orders" navigated with that capped list, so a site with 240 open work orders showed 200, and an empty list opened the unfiltered board. The exact-id link is now used only when the ids cover the whole count. Otherwise the link opens Work Orders filtered to the site and every open status across all weeks, the ticket's "board filtered to that site". The board drilldown now reads a `sites` param for this; `ids` still wins. --- .../_components/site-delete-dialog.tsx | 12 ++++-- .../utils/dashboard-drilldown-params.ts | 17 ++++++++ .../locations/site-delete.test.tsx | 33 ++++++++++++++++ .../dashboard-drilldown-params.test.ts | 39 ++++++++++++++++++- 4 files changed, 96 insertions(+), 5 deletions(-) diff --git a/src/app/(protected)/locations/_components/site-delete-dialog.tsx b/src/app/(protected)/locations/_components/site-delete-dialog.tsx index 56e3c232..78f55716 100644 --- a/src/app/(protected)/locations/_components/site-delete-dialog.tsx +++ b/src/app/(protected)/locations/_components/site-delete-dialog.tsx @@ -11,6 +11,7 @@ import { import { DIALOG_BLUR_BACKDROP_SX } from "@/components/ui/dialog-blur-backdrop"; import { useDeleteLocation } from "@/domain/locations/use-cases/use-delete-location"; import { useSiteOpenWorkOrders } from "@/domain/locations/use-cases/use-site-open-work-orders"; +import { buildSiteOpenWorkOrdersHref } from "@/domain/work-orders/utils/dashboard-drilldown-params"; import { buildWorkOrderIdsHref } from "@/domain/work-orders/utils/work-order-ids-filter"; interface SiteDeleteDialogProps { @@ -52,6 +53,12 @@ export function SiteDeleteDialog({ const openCount = openWorkOrders?.count ?? 0; // Without the count the warning cannot be shown, so Delete waits for a successful check. const countUnknown = isLoading || isError; + // The server caps the id list; when it doesn't cover the stated count, open the site's whole board. + const openIds = openWorkOrders?.workOrderIds ?? []; + const openWorkOrdersHref = + openIds.length > 0 && openIds.length >= openCount + ? buildWorkOrderIdsHref(openIds) + : buildSiteOpenWorkOrdersHref(siteId); return ( )} {openCount > 0 && ( - )} diff --git a/src/domain/work-orders/utils/dashboard-drilldown-params.ts b/src/domain/work-orders/utils/dashboard-drilldown-params.ts index 2334b9b6..ebc73bee 100644 --- a/src/domain/work-orders/utils/dashboard-drilldown-params.ts +++ b/src/domain/work-orders/utils/dashboard-drilldown-params.ts @@ -61,6 +61,18 @@ function parseUpliftStatuses(raw: string | null): UpliftFilterStatus[] { ); } +/** Open = every lifecycle status except Completed (Canceled is not a board status). */ +const OPEN_STATUSES = ALL_WIZARD_STATUSES.filter((status) => status !== "Completed"); + +/** Work Orders filtered to one site's open work orders; `sites` takes the location id. */ +export function buildSiteOpenWorkOrdersHref(siteId: string | number): string { + const params = new URLSearchParams({ + sites: String(siteId), + statuses: OPEN_STATUSES.join(","), + }); + return `/workorders?${params.toString()}`; +} + export function parseDashboardDrilldownParams( params: URLSearchParams, ): WorkOrderAdvancedFilters | null { @@ -79,6 +91,7 @@ export function parseDashboardDrilldownParams( const types = parseTypes(params.get("types")); const dispatchers = parseList(params.get("dispatchers")); const regions = parseRegions(params.get("regions")); + const sites = [...new Set(parseList(params.get("sites")))]; const upliftStatuses = parseUpliftStatuses(params.get("upliftStatuses")); const hasUplift = params.get("hasUplift") === "1"; @@ -90,6 +103,7 @@ export function parseDashboardDrilldownParams( types.length, dispatchers.length, regions.length, + sites.length, upliftStatuses.length, hasUplift, ].some(Boolean); @@ -122,6 +136,9 @@ export function parseDashboardDrilldownParams( if (regions.length > 0) { filters.regions = new Set(regions); } + if (sites.length > 0) { + filters.sites = new Set(sites); + } if (upliftStatuses.length > 0) { filters.upliftStatuses = new Set(upliftStatuses); } diff --git a/src/test/app/(protected)/locations/site-delete.test.tsx b/src/test/app/(protected)/locations/site-delete.test.tsx index 24e9d981..a5395bb0 100644 --- a/src/test/app/(protected)/locations/site-delete.test.tsx +++ b/src/test/app/(protected)/locations/site-delete.test.tsx @@ -147,6 +147,39 @@ describe("Delete a site from the detail panel", () => { expect(deleteSite).not.toHaveBeenCalled(); }); + it.each([ + ["more open work orders than the capped id list", { count: 240, workOrderIds: [101, 205] }], + ["an open count with no ids", { count: 4, workOrderIds: [] }], + ])("links to the site's open work-order board when there are %s", async (_label, result) => { + getOpenWorkOrders.mockResolvedValue(result); + renderAs("Admin"); + const panel = await openPanel(); + + fireEvent.click(within(panel).getByRole("button", { name: "Delete" })); + const dialog = await screen.findByRole("dialog", { name: "Delete this site?" }); + await within(dialog).findByText(new RegExp(`It still has ${result.count} open work orders`)); + + fireEvent.click(within(dialog).getByRole("button", { name: "View open work orders" })); + + const url = new URL( + (await screen.findByTestId("work-orders-url")).textContent ?? "", + "https://app.test", + ); + expect(url.pathname).toBe("/workorders"); + expect(url.searchParams.get("ids")).toBeNull(); + expect(url.searchParams.get("sites")).toBe("1"); + expect(url.searchParams.get("statuses")?.split(",")).toEqual([ + "Incomplete", + "Pending", + "Scheduled", + "En Route", + "On Site", + "In Progress", + "Rescheduled", + "Pending Quote", + ]); + }); + it("confirms without a warning when nothing is open, deletes and closes the panel", async () => { getOpenWorkOrders.mockResolvedValue({ count: 0, workOrderIds: [] }); deleteSite.mockResolvedValue(undefined); 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 7faab8b0..eea7afe8 100644 --- a/src/test/domain/work-orders/dashboard-drilldown-params.test.ts +++ b/src/test/domain/work-orders/dashboard-drilldown-params.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { parseDashboardDrilldownParams } from "@/domain/work-orders/utils/dashboard-drilldown-params"; +import { buildResolvedBoardSearchParams } from "@/app/(protected)/workorders/_hooks/build-resolved-board-search-params"; +import { + buildSiteOpenWorkOrdersHref, + parseDashboardDrilldownParams, +} from "@/domain/work-orders/utils/dashboard-drilldown-params"; import { avetaPendingDrilldownSearch, unassignedDrilldownSearch, @@ -102,6 +106,39 @@ describe("parseDashboardDrilldownParams", () => { expect(cleared.dispatchers.size).toBe(0); }); + it("turns the site open work-order link into that site's open, all-weeks board search", () => { + const href = buildSiteOpenWorkOrdersHref(12); + const filters = parseDashboardDrilldownParams(new URL(href, "https://app.test").searchParams); + + expect(filters?.sites).toEqual(new Set(["12"])); + expect(filters?.workOrderIds.size).toBe(0); + const params = buildResolvedBoardSearchParams({ + advApplied: filters, + search: "", + page: 0, + pageSize: 200, + sortBy: "", + sortDir: "", + locations: [{ id: 12, name: "DAL1", siteCode: "DAL1" }], + vendors: [], + }); + expect(params).toMatchObject({ + sites: ["DAL1"], + statuses: [...NON_TERMINAL_LIFECYCLE_STATUSES], + datePreset: "Custom", + dateFrom: "2000-01-01", + dateTo: "2099-12-31", + includeDateless: true, + }); + }); + + it("keeps an exact id set ahead of a site filter", () => { + const filters = parseDashboardDrilldownParams(new URLSearchParams({ ids: "4,9", sites: "12" })); + + expect(filters?.workOrderIds).toEqual(new Set([4, 9])); + expect(filters?.sites.size).toBe(0); + }); + it("rejects partial or malformed date ranges", () => { expect( parseDashboardDrilldownParams(