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(