feat(work-orders): show counter pills from the first occurrence

Dispatchers need reschedule and carried-over counts after one event.
Advanced filters and the detail header stay at two or more.
This commit is contained in:
Arthur Bassi 2026-10-05 14:13:52 -03:00 • committed by Cursor Agent
parent bb9f4b88c1
commit 96beacd6d6
No known key found for this signature in database
9 changed files with 130 additions and 12 deletions

View file

@ -36,7 +36,10 @@ export function SlideOverHeader({
>
#{row.woNumber || "—"}
</Text>
<RescheduleCounterPill count={row.rescheduleCount} originalDate={row.originalDate} />
{/* Column pills start at 1. This header keeps the previous threshold of 2. */}
{row.rescheduleCount >= 2 && (
<RescheduleCounterPill count={row.rescheduleCount} originalDate={row.originalDate} />
)}
{Boolean(row.avetaRequired) && (
<AvetaBadge size={13} attached={Boolean(row.hasAvetaDocument)} />
)}

View file

@ -31,7 +31,7 @@ export function RescheduleCounterPill({
count: number;
originalDate?: string;
}) {
if (count < 2) {
if (count < 1) {
return null;
}

View file

@ -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 {

View file

@ -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}.`;

View file

@ -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(<CarriedOverCounterPill count={1} />);
it("hides the pill when the count is 0", () => {
const { container } = renderWithProviders(<CarriedOverCounterPill count={0} />);
expect(container).toBeEmptyDOMElement();
});
it("shows the pill from the first carry-over", () => {
renderWithProviders(<CarriedOverCounterPill count={1} status="Scheduled" />);
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(<CarriedOverCounterPill count={3} />);
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(
<CarriedOverCounterPill count={3} originalWeek="2026-05-04" status="Completed" />,
);
expect(container).toBeEmptyDOMElement();
expect(completed.container).toBeEmptyDOMElement();
const canceled = renderWithProviders(<CarriedOverCounterPill count={3} status="Canceled" />);
expect(canceled.container).toBeEmptyDOMElement();
const completedOnce = renderWithProviders(
<CarriedOverCounterPill count={1} status="Completed" />,
);
expect(completedOnce.container).toBeEmptyDOMElement();
const canceledOnce = renderWithProviders(
<CarriedOverCounterPill count={1} status="Canceled" />,
);
expect(canceledOnce.container).toBeEmptyDOMElement();
});
it("is distinguishable from the reschedule pill beside it", () => {

View file

@ -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(<RescheduleCounterPill count={1} />);
it("hides the pill when the count is 0", () => {
const { container } = renderWithProviders(<RescheduleCounterPill count={0} />);
expect(container).toBeEmptyDOMElement();
});
it("shows the pill from the first reschedule", () => {
renderWithProviders(<RescheduleCounterPill count={1} />);
const pill = screen.getByLabelText(/^Rescheduled 1 time/i);
expect(pill).toHaveTextContent("1");
});
it("renders a purple count pill with history tooltip from originalDate", () => {
renderWithProviders(<RescheduleCounterPill count={4} originalDate="2026-05-08" />);
const pill = screen.getByLabelText(/Rescheduled 4 times.*Original date: May 8, 2026/i);

View file

@ -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(
<SlideOverHeader row={rowWithReschedules(1)} isCancelled={false} onClose={() => 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(
<SlideOverHeader row={rowWithReschedules(2)} isCancelled={false} onClose={() => undefined} />,
);
expect(screen.getByLabelText(/^Rescheduled 2 times/i)).toHaveTextContent("2");
});
});

View file

@ -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);
});

View file

@ -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", () => {