fix(work-orders): align Past Due overlay and copy to Schedule On (SH-185)

Dispatchers were told to change Due Date, which cannot clear Past Due.
Drive the table overlay from Schedule On and match the 422 contract.
This commit is contained in:
Arthur Bassi 2026-08-18 09:20:18 -03:00
parent 4a95da32d2
commit 8d1a3340a8
4 changed files with 65 additions and 27 deletions

View file

@ -49,7 +49,7 @@ export function StatusCell({
setAnchorEl(e.currentTarget);
}}
title="This WO missed its deadline. Update the Due Date to unlock status updates."
title="This WO missed its deadline. Update Schedule On to unlock status updates."
className="group/cell flex w-full cursor-pointer items-center gap-1.5 whitespace-nowrap leading-none transition-opacity hover:opacity-80"
style={{ fontSize: 12.5, fontWeight: 600, color: "var(--status-pastdue-text)" }}
>
@ -83,7 +83,7 @@ export function StatusCell({
color: "var(--status-pending-text)",
}}
>
This work order is past due. Update the Due Date before the status can be updated.
This work order is past due. Update Schedule On before the status can be updated.
</Text>
<button
@ -97,7 +97,7 @@ export function StatusCell({
}}
className="mt-2 block text-[11px] font-medium text-[var(--color-primary)]"
>
Update Due Date →
Update Schedule On →
</button>
</div>
</div>

View file

@ -12,18 +12,18 @@ import { weekRange } from "@/domain/work-orders/utils/list-week-utils";
const TERMINAL_STATUSES = new Set(["Completed", "Canceled", "Done", "Cancelled"]);
export function computeIsPastDue(row: WorkOrderTableRow): boolean {
if (!row.dueDate || TERMINAL_STATUSES.has(row.status)) {
if (!row.scheduledOn || TERMINAL_STATUSES.has(row.status)) {
return false;
}
const today = new Date().toISOString().slice(0, 10);
return row.dueDate < today;
return row.scheduledOn.slice(0, 10) < today;
}
export function enrichTableRow(row: WorkOrderTableRow): WorkOrderTableRow {
return {
...row,
// Recompute from dueDate (same date-only rule as BE). Avoid sticky
// `apiTrue || compute` so a future Due Date clears Past Due immediately.
// Recompute from Schedule On (SH-185). Avoid sticky `apiTrue || compute`
// so a future Schedule On clears Past Due immediately.
isPastDue: computeIsPastDue(row),
};
}

View file

@ -0,0 +1,38 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { StatusCell } from "@/app/(protected)/workorders/_components/list/table/cells/status-cell";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
const pastDueRow = {
id: "wo-1",
status: "Scheduled",
dispatcherId: "",
dispatcherName: "",
company: "",
tech: "",
completedDate: "",
woNumber: "",
pm: "",
docStatus: "",
mediaCount: 0,
type: "PM",
isPastDue: true,
scheduledOn: "2026-06-01",
dueDate: "2026-08-01",
} as WorkOrderTableRow;
describe("StatusCell Past Due copy (SH-185)", () => {
it("directs the dispatcher to update Schedule On, not Due Date", () => {
render(<StatusCell wo={pastDueRow} onChangeStatus={vi.fn()} onOpenSlideOver={vi.fn()} />);
const trigger = screen.getByRole("button", { name: /past due/i });
expect(trigger.getAttribute("title") ?? "").toContain("Schedule On");
expect(trigger.getAttribute("title") ?? "").not.toMatch(/due date/i);
fireEvent.click(trigger);
expect(screen.getByText(/update schedule on before the status can be updated/i)).toBeTruthy();
expect(screen.getByRole("button", { name: /update schedule on/i })).toBeTruthy();
expect(screen.queryByText(/due date/i)).toBeNull();
});
});

View file

@ -156,43 +156,43 @@ describe("matchesAdvancedFilters", () => {
});
describe("computeIsPastDue", () => {
it("is true when dueDate is before today even if scheduledOn is future", () => {
it("is true when scheduledOn is before today even if dueDate is future", () => {
expect(
computeIsPastDue(
makeRow({
dueDate: shiftIso(-1),
scheduledOn: shiftIso(5),
dueDate: shiftIso(5),
scheduledOn: shiftIso(-1),
status: "Scheduled",
}),
),
).toBe(true);
});
it("is false when dueDate is future even if scheduledOn is past", () => {
it("is false when scheduledOn is future even if dueDate is past", () => {
expect(
computeIsPastDue(
makeRow({
dueDate: shiftIso(5),
scheduledOn: shiftIso(-1),
dueDate: shiftIso(-1),
scheduledOn: shiftIso(5),
status: "Incomplete",
}),
),
).toBe(false);
});
it("is false when dueDate is empty", () => {
it("is false when scheduledOn is empty", () => {
expect(
computeIsPastDue(
makeRow({
dueDate: "",
scheduledOn: shiftIso(-3),
dueDate: shiftIso(-3),
scheduledOn: "",
status: "Scheduled",
}),
),
).toBe(false);
});
it("is false for terminal statuses even with past dueDate", () => {
it("is false for terminal statuses even with past scheduledOn", () => {
expect(
computeIsPastDue(
makeRow({
@ -205,19 +205,19 @@ describe("computeIsPastDue", () => {
expect(
computeIsPastDue(
makeRow({
dueDate: shiftIso(-2),
scheduledOn: shiftIso(-2),
status: "Canceled",
}),
),
).toBe(false);
});
it("is false when dueDate is today", () => {
it("is false when scheduledOn is today", () => {
expect(
computeIsPastDue(
makeRow({
dueDate: todayIso(),
scheduledOn: shiftIso(-1),
dueDate: shiftIso(-1),
scheduledOn: todayIso(),
status: "Scheduled",
}),
),
@ -226,11 +226,11 @@ describe("computeIsPastDue", () => {
});
describe("enrichTableRow", () => {
it("recomputes isPastDue from dueDate and ignores sticky API true", () => {
it("recomputes isPastDue from scheduledOn and ignores sticky API true", () => {
const enriched = enrichTableRow(
makeRow({
dueDate: shiftIso(5),
scheduledOn: shiftIso(-1),
dueDate: shiftIso(-1),
scheduledOn: shiftIso(5),
isPastDue: true,
status: "Scheduled",
}),
@ -238,11 +238,11 @@ describe("enrichTableRow", () => {
expect(enriched.isPastDue).toBe(false);
});
it("sets isPastDue true from past dueDate even when API flag is false", () => {
it("sets isPastDue true from past scheduledOn even when API flag is false", () => {
const enriched = enrichTableRow(
makeRow({
dueDate: shiftIso(-1),
scheduledOn: shiftIso(5),
dueDate: shiftIso(5),
scheduledOn: shiftIso(-1),
isPastDue: false,
status: "Incomplete",
}),