diff --git a/src/app/(protected)/workorders/_components/detail/slide-over-header.tsx b/src/app/(protected)/workorders/_components/detail/slide-over-header.tsx index 32facebb..ff9b80e1 100644 --- a/src/app/(protected)/workorders/_components/detail/slide-over-header.tsx +++ b/src/app/(protected)/workorders/_components/detail/slide-over-header.tsx @@ -36,7 +36,10 @@ export function SlideOverHeader({ > #{row.woNumber || "—"} - + {/* Column pills start at 1. This header keeps the previous threshold of 2. */} + {row.rescheduleCount >= 2 && ( + + )} {Boolean(row.avetaRequired) && ( )} diff --git a/src/app/(protected)/workorders/_components/list/table/cells/wo-counter-pills.tsx b/src/app/(protected)/workorders/_components/list/table/cells/wo-counter-pills.tsx index 32c13d49..4bf19890 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/wo-counter-pills.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/wo-counter-pills.tsx @@ -31,7 +31,7 @@ export function RescheduleCounterPill({ count: number; originalDate?: string; }) { - if (count < 2) { + if (count < 1) { return null; } diff --git a/src/domain/work-orders/utils/carry-over-display.ts b/src/domain/work-orders/utils/carry-over-display.ts index dd92b2d3..8b14ddfa 100644 --- a/src/domain/work-orders/utils/carry-over-display.ts +++ b/src/domain/work-orders/utils/carry-over-display.ts @@ -18,7 +18,7 @@ export function isTerminalCarryOverStatus(status?: string): boolean { } export function shouldShowCarriedOverPill(count: number, status?: string): boolean { - return count >= 2 && !isTerminalCarryOverStatus(status); + return count >= 1 && !isTerminalCarryOverStatus(status); } export function toIsoDate(value?: string): string { diff --git a/src/domain/work-orders/utils/reschedule-indicator.ts b/src/domain/work-orders/utils/reschedule-indicator.ts index 26bee35e..4ca4ac29 100644 --- a/src/domain/work-orders/utils/reschedule-indicator.ts +++ b/src/domain/work-orders/utils/reschedule-indicator.ts @@ -4,7 +4,7 @@ export function formatRescheduleCountLabel(count: number): string { return count === 1 ? "1 time" : `${count} times`; } -/** Hover copy for the Reschedule pill (count ≥ 2). Uses originalDate when the API sends it. */ +/** Hover copy for the Reschedule pill (count ≥ 1). Uses originalDate when the API sends it. */ export function formatRescheduleHover(count: number, originalDate?: string): string { const times = formatRescheduleCountLabel(count); const base = `Rescheduled ${times} — this WO has had its date changed ${times}.`; diff --git a/src/test/app/(protected)/workorders/carried-over-counter-pill.test.tsx b/src/test/app/(protected)/workorders/carried-over-counter-pill.test.tsx index d0059873..343ddc41 100644 --- a/src/test/app/(protected)/workorders/carried-over-counter-pill.test.tsx +++ b/src/test/app/(protected)/workorders/carried-over-counter-pill.test.tsx @@ -7,11 +7,17 @@ import { import { renderWithProviders } from "@/test/test-utils"; describe("CarriedOverCounterPill", () => { - it("hides the pill when the count is below 2", () => { - const { container } = renderWithProviders(); + it("hides the pill when the count is 0", () => { + const { container } = renderWithProviders(); expect(container).toBeEmptyDOMElement(); }); + it("shows the pill from the first carry-over", () => { + renderWithProviders(); + const pill = screen.getByLabelText(/^Carried over 1 time/i); + expect(pill).toHaveTextContent("1"); + }); + it("names the count so it is not a bare number", () => { renderWithProviders(); const pill = screen.getByLabelText(/Carried over 3 times/i); @@ -26,10 +32,23 @@ describe("CarriedOverCounterPill", () => { }); it("hides after resolution even when the counter remains", () => { - const { container } = renderWithProviders( + const completed = renderWithProviders( , ); - expect(container).toBeEmptyDOMElement(); + expect(completed.container).toBeEmptyDOMElement(); + + const canceled = renderWithProviders(); + expect(canceled.container).toBeEmptyDOMElement(); + + const completedOnce = renderWithProviders( + , + ); + expect(completedOnce.container).toBeEmptyDOMElement(); + + const canceledOnce = renderWithProviders( + , + ); + expect(canceledOnce.container).toBeEmptyDOMElement(); }); it("is distinguishable from the reschedule pill beside it", () => { diff --git a/src/test/app/(protected)/workorders/reschedule-counter-pill.test.tsx b/src/test/app/(protected)/workorders/reschedule-counter-pill.test.tsx index 8f81de8d..10742ffd 100644 --- a/src/test/app/(protected)/workorders/reschedule-counter-pill.test.tsx +++ b/src/test/app/(protected)/workorders/reschedule-counter-pill.test.tsx @@ -4,11 +4,17 @@ import { RescheduleCounterPill } from "@/app/(protected)/workorders/_components/ import { renderWithProviders } from "@/test/test-utils"; describe("RescheduleCounterPill", () => { - it("hides the pill when the count is below 2", () => { - const { container } = renderWithProviders(); + it("hides the pill when the count is 0", () => { + const { container } = renderWithProviders(); expect(container).toBeEmptyDOMElement(); }); + it("shows the pill from the first reschedule", () => { + renderWithProviders(); + const pill = screen.getByLabelText(/^Rescheduled 1 time/i); + expect(pill).toHaveTextContent("1"); + }); + it("renders a purple count pill with history tooltip from originalDate", () => { renderWithProviders(); const pill = screen.getByLabelText(/Rescheduled 4 times.*Original date: May 8, 2026/i); diff --git a/src/test/app/(protected)/workorders/slide-over-header-reschedule-pill.test.tsx b/src/test/app/(protected)/workorders/slide-over-header-reschedule-pill.test.tsx new file mode 100644 index 00000000..f06f5fe3 --- /dev/null +++ b/src/test/app/(protected)/workorders/slide-over-header-reschedule-pill.test.tsx @@ -0,0 +1,37 @@ +import { screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { SlideOverHeader } from "@/app/(protected)/workorders/_components/detail/slide-over-header"; +import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import { renderWithProviders } from "@/test/test-utils"; + +function rowWithReschedules(count: number): WorkOrderTableRow { + return { + woNumber: "24880", + rescheduleCount: count, + originalDate: "2026-05-19", + avetaRequired: false, + site: "", + isPastDue: false, + status: "", + type: "", + } as WorkOrderTableRow; +} + +describe("SlideOverHeader reschedule pill", () => { + it("keeps a single reschedule out of the detail header", () => { + renderWithProviders( + undefined} />, + ); + + expect(screen.getByRole("heading", { name: "#24880" })).toBeInTheDocument(); + expect(screen.queryByLabelText(/^Rescheduled 1 time/i)).not.toBeInTheDocument(); + }); + + it("still shows the pill in the detail header from the second reschedule", () => { + renderWithProviders( + undefined} />, + ); + + expect(screen.getByLabelText(/^Rescheduled 2 times/i)).toHaveTextContent("2"); + }); +}); diff --git a/src/test/domain/work-orders/utils/carry-over-display.test.ts b/src/test/domain/work-orders/utils/carry-over-display.test.ts index 00b54dfe..db449608 100644 --- a/src/test/domain/work-orders/utils/carry-over-display.test.ts +++ b/src/test/domain/work-orders/utils/carry-over-display.test.ts @@ -9,10 +9,13 @@ import { } from "@/domain/work-orders/utils/carry-over-display"; describe("carry-over-display", () => { - it("hides the pill below 2 and on terminal statuses", () => { - expect(shouldShowCarriedOverPill(1, "Scheduled")).toBe(false); + it("shows the pill from 1 and hides it on terminal statuses", () => { + expect(shouldShowCarriedOverPill(0, "Scheduled")).toBe(false); + expect(shouldShowCarriedOverPill(1, "Scheduled")).toBe(true); expect(shouldShowCarriedOverPill(2, "Scheduled")).toBe(true); + expect(shouldShowCarriedOverPill(1, "Completed")).toBe(false); expect(shouldShowCarriedOverPill(3, "Completed")).toBe(false); + expect(shouldShowCarriedOverPill(1, "Canceled")).toBe(false); expect(shouldShowCarriedOverPill(3, "Canceled")).toBe(false); }); 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 56097d24..51a9e7f9 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 @@ -565,6 +565,56 @@ describe("matchesAdvancedFilters", () => { expect(matchesAdvancedFilters(completedUndated, unscheduledOnly)).toBe(false); expect(matchesAdvancedFilters(canceledUndated, unscheduledOnly)).toBe(false); }); + + it("keeps Rescheduled and Carried Over at count 2 or more", () => { + const weekMonday = mondayOf(todayIso()); + const dated = { scheduledOn: weekMonday, ...deriveDayFields(weekMonday) }; + const openRange = { + ...EMPTY_ADVANCED_FILTERS, + dateRange: "custom" as const, + customFrom: "0000-01-01", + customTo: "9999-12-31", + }; + + expect( + matchesAdvancedFilters(makeRow({ ...dated, rescheduleCount: 1 }), { + ...openRange, + rescheduled: true, + }), + ).toBe(false); + expect( + matchesAdvancedFilters(makeRow({ ...dated, rescheduleCount: 2 }), { + ...openRange, + rescheduled: true, + }), + ).toBe(true); + expect( + matchesAdvancedFilters(makeRow({ ...dated, carriedOver: 1 }), { + ...openRange, + carriedOver: true, + }), + ).toBe(false); + expect( + matchesAdvancedFilters(makeRow({ ...dated, carriedOver: 2 }), { + ...openRange, + carriedOver: true, + }), + ).toBe(true); + expect( + matchesAdvancedFilters(makeRow({ ...dated, rescheduleCount: 2, carriedOver: 1 }), { + ...openRange, + rescheduled: true, + carriedOver: true, + }), + ).toBe(false); + expect( + matchesAdvancedFilters(makeRow({ ...dated, rescheduleCount: 2, carriedOver: 2 }), { + ...openRange, + rescheduled: true, + carriedOver: true, + }), + ).toBe(true); + }); }); describe("computeIsPastDue", () => {