diff --git a/src/app/(protected)/workorders/_components/list/table/cells/due-date-calendar-dialog.tsx b/src/app/(protected)/workorders/_components/list/table/cells/due-date-calendar-dialog.tsx
new file mode 100644
index 00000000..a47b8e14
--- /dev/null
+++ b/src/app/(protected)/workorders/_components/list/table/cells/due-date-calendar-dialog.tsx
@@ -0,0 +1,56 @@
+import type { MouseEvent } from "react";
+import { Dialog, DialogContent, DialogTitle } from "@mui/material";
+import { MiniCalendar } from "@/app/(protected)/workorders/_components/wizard/mini-calendar";
+import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
+
+type DueDateCalendarDialogProps = {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ value: string;
+ onSave: (iso: string) => void;
+};
+
+/**
+ * Emergency Due Date picker — dedicated calendar Dialog (no time, no week/range).
+ * Reuses the shared MiniCalendar used by the standard Due Date popover.
+ */
+export function DueDateCalendarDialog({
+ open,
+ onOpenChange,
+ value,
+ onSave,
+}: DueDateCalendarDialogProps) {
+ return (
+
+ );
+}
diff --git a/src/app/(protected)/workorders/_components/list/table/cells/due-date-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/due-date-cell.tsx
new file mode 100644
index 00000000..d6cae8fb
--- /dev/null
+++ b/src/app/(protected)/workorders/_components/list/table/cells/due-date-cell.tsx
@@ -0,0 +1,71 @@
+import { useState } from "react";
+import { Calendar as CalendarIcon, ChevronDown } from "lucide-react";
+import { DateCell } from "@/app/(protected)/workorders/_components/list/table/cells/date-cell";
+import { DueDateCalendarDialog } from "@/app/(protected)/workorders/_components/list/table/cells/due-date-calendar-dialog";
+import { HighlightText } from "@/app/(protected)/workorders/_components/list/table/cells/highlight-text";
+import type { WOType } from "@/domain/work-orders/types/work-order-wizard";
+
+type DueDateCellProps = {
+ type: WOType | "";
+ value: string;
+ q?: string;
+ highlight?: boolean;
+ onSave: (date: string) => void;
+};
+
+/**
+ * Board / slide-over Due Date editor.
+ * Emergency → dedicated calendar Dialog; other types → standard DateCell popover.
+ */
+export function DueDateCell({ type, value, q, highlight, onSave }: DueDateCellProps) {
+ const [open, setOpen] = useState(false);
+
+ if (type !== "Emergency") {
+ return ;
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/app/(protected)/workorders/_components/list/table/cells/status-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/status-cell.tsx
index 733d17d0..d2e4c78a 100644
--- a/src/app/(protected)/workorders/_components/list/table/cells/status-cell.tsx
+++ b/src/app/(protected)/workorders/_components/list/table/cells/status-cell.tsx
@@ -49,7 +49,7 @@ export function StatusCell({
setAnchorEl(e.currentTarget);
}}
- title="This WO missed its service window. Reschedule to unlock status updates."
+ title="This WO missed its deadline. Update the Due Date 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,8 +83,7 @@ export function StatusCell({
color: "var(--status-pending-text)",
}}
>
- This work order is past due. A new date must be scheduled before the status can be
- updated.
+ This work order is past due. Update the Due Date before the status can be updated.
diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row-cells.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row-cells.tsx
index a9052764..a3056b2e 100644
--- a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row-cells.tsx
+++ b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row-cells.tsx
@@ -7,6 +7,7 @@ import type { WorkOrderTableDraft } from "@/domain/work-orders/types/work-order-
import { AssignedToCell } from "./cells/assigned-to-cell";
import { ApptTimeCell } from "./cells/appt-time-cell";
import { DateCell } from "./cells/date-cell";
+import { DueDateCell } from "./cells/due-date-cell";
import { EditableWONumberCell } from "./cells/editable-wo-number-cell";
import { PMTypeCell } from "./cells/pm-type-cell";
import { SiteCell } from "./cells/site-cell";
@@ -106,7 +107,11 @@ export function WoTableInlineRowCells({
- onPatch({ dueDate: date })} />
+ onPatch({ dueDate: date })}
+ />
|
diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx
index 59de1a9f..40f97933 100644
--- a/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx
+++ b/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx
@@ -3,6 +3,7 @@ import type { LocationOption, WorkOrderUser } from "@/domain/work-orders/types/w
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { AssignedToCell } from "./cells/assigned-to-cell";
import { DateCell } from "./cells/date-cell";
+import { DueDateCell } from "./cells/due-date-cell";
import { EditableWONumberCell } from "./cells/editable-wo-number-cell";
import { SiteCell } from "./cells/site-cell";
import { TypeCell } from "./cells/type-cell";
@@ -16,6 +17,7 @@ type WoTableRowCellsProps = {
sitePlace?: { city: string; state: string };
search?: string;
forceScheduleHighlight?: boolean;
+ forceDueDateHighlight?: boolean;
mutationCellStyle?: CSSProperties;
handlers: WoTableRowHandlers;
onPatchRow: (patch: Partial) => void;
@@ -29,6 +31,7 @@ export function WoTableRowCells({
sitePlace,
search,
forceScheduleHighlight,
+ forceDueDateHighlight,
mutationCellStyle,
handlers,
onPatchRow,
@@ -104,7 +107,7 @@ export function WoTableRowCells({
handlers.onScheduleDate(row.id, date)}
@@ -116,7 +119,13 @@ export function WoTableRowCells({
style={{ padding: "0 14px", ...mutationCellStyle }}
onClick={(e) => e.stopPropagation()}
>
- onPatchRow({ dueDate: date })} />
+ onPatchRow({ dueDate: date })}
+ />
|
>
);
diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-row.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-row.tsx
index b3ba685f..e6a22420 100644
--- a/src/app/(protected)/workorders/_components/list/table/wo-table-row.tsx
+++ b/src/app/(protected)/workorders/_components/list/table/wo-table-row.tsx
@@ -45,6 +45,7 @@ type WoTableRowProps = {
pmTypes: string[];
search?: string;
forceScheduleHighlight?: boolean;
+ forceDueDateHighlight?: boolean;
isSelected: boolean;
isFlashing: boolean;
dragViz: DragVizState;
@@ -61,6 +62,7 @@ export function WoTableRow({
pmTypes,
search,
forceScheduleHighlight,
+ forceDueDateHighlight,
isSelected,
isFlashing,
dragViz,
@@ -154,6 +156,7 @@ export function WoTableRow({
sitePlace={sitePlace}
search={search}
forceScheduleHighlight={forceScheduleHighlight}
+ forceDueDateHighlight={forceDueDateHighlight}
mutationCellStyle={mutationCellStyle}
handlers={handlers}
onPatchRow={patchRow}
diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table.tsx
index e94c3ad7..94e019a7 100644
--- a/src/app/(protected)/workorders/_components/list/table/wo-table.tsx
+++ b/src/app/(protected)/workorders/_components/list/table/wo-table.tsx
@@ -54,7 +54,7 @@ export function WoTable({
onOpenAdvancedFilters,
onPatch,
}: WoTableProps) {
- const [scheduleHighlightId, setScheduleHighlightId] = useState(null);
+ const [dueDateHighlightId, setDueDateHighlightId] = useState(null);
const { data: locationSites = [] } = useLocationSites();
const sitePlaceLookups = useMemo(() => buildSitePlaceLookups(locationSites), [locationSites]);
const createMutation = useCreateWorkOrderFromWizard(() => {
@@ -69,7 +69,7 @@ export function WoTable({
onOpenCompDoc,
onOpenMedia,
onPatch,
- onScheduleHighlight: setScheduleHighlightId,
+ onScheduleHighlight: setDueDateHighlightId,
});
const renderRow = (row: WorkOrderTableRow, forceScheduleHighlight = false) => (
@@ -83,7 +83,8 @@ export function WoTable({
vendors={vendors}
pmTypes={pmTypes}
search={tableData.search}
- forceScheduleHighlight={forceScheduleHighlight || scheduleHighlightId === row.id}
+ forceScheduleHighlight={forceScheduleHighlight}
+ forceDueDateHighlight={dueDateHighlightId === row.id}
isSelected={tableData.selectedId === row.id}
isFlashing={tableData.flashId === row.id}
dragViz={tableData.dragViz}
diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-due-date-field.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-due-date-field.tsx
new file mode 100644
index 00000000..7ec4888e
--- /dev/null
+++ b/src/app/(protected)/workorders/_components/wizard/wizard-due-date-field.tsx
@@ -0,0 +1,69 @@
+import { useState } from "react";
+import { CalendarIcon, ChevronDown } from "lucide-react";
+import { DueDateCalendarDialog } from "@/app/(protected)/workorders/_components/list/table/cells/due-date-calendar-dialog";
+import { WizardDateField } from "@/app/(protected)/workorders/_components/wizard/wizard-date-field";
+import type { WOType } from "@/domain/work-orders/types/work-order-wizard";
+import { fmtDate } from "@/domain/work-orders/utils/wizard-date-utils";
+
+type WizardDueDateFieldProps = {
+ type: WOType | "";
+ value: string;
+ placeholder: string;
+ onChange: (iso: string) => void;
+};
+
+/**
+ * Wizard Due Date editor.
+ * Emergency → dedicated calendar Dialog; other types → WizardDateField popover.
+ */
+export function WizardDueDateField({
+ type,
+ value,
+ placeholder,
+ onChange,
+}: WizardDueDateFieldProps) {
+ const [open, setOpen] = useState(false);
+
+ if (type !== "Emergency") {
+ return ;
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule.tsx
index e3d4de1e..51c6f0d3 100644
--- a/src/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule.tsx
+++ b/src/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule.tsx
@@ -3,6 +3,7 @@ import { Text } from "@/components/ui/text";
import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard";
import { WO_TYPE_META, WO_TYPES, type WOType } from "@/domain/work-orders/types/work-order-wizard";
import { WizardDateField } from "@/app/(protected)/workorders/_components/wizard/wizard-date-field";
+import { WizardDueDateField } from "@/app/(protected)/workorders/_components/wizard/wizard-due-date-field";
import { WizardFieldSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-field-select";
import {
DLG_INPUT_CLS,
@@ -197,7 +198,8 @@ export function WizardStepTypeSchedule({
>
Due Date
- onUpdate("dueDate", iso)}
diff --git a/src/domain/work-orders/utils/board-audit-action-rules.ts b/src/domain/work-orders/utils/board-audit-action-rules.ts
index eed1beaa..505275dd 100644
--- a/src/domain/work-orders/utils/board-audit-action-rules.ts
+++ b/src/domain/work-orders/utils/board-audit-action-rules.ts
@@ -87,7 +87,7 @@ function tryPastDueRule({ fieldKey, actionKey }: AuditActionRuleContext): string
fieldKey.includes("pastdue") || actionKey.includes("pastdue") || actionKey === "markedpastdue";
if (!matches) return null;
- return "WO marked as Past Due. Scheduled date has passed without completion.";
+ return "WO marked as Past Due. Due date has passed without completion.";
}
function tryCreatedRule({ fieldKey, actionKey }: AuditActionRuleContext): string | null {
diff --git a/src/domain/work-orders/utils/table-filter-utils.ts b/src/domain/work-orders/utils/table-filter-utils.ts
index 4ea3c92f..4a3046d3 100644
--- a/src/domain/work-orders/utils/table-filter-utils.ts
+++ b/src/domain/work-orders/utils/table-filter-utils.ts
@@ -12,22 +12,19 @@ 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 (TERMINAL_STATUSES.has(row.status)) {
+ if (!row.dueDate || TERMINAL_STATUSES.has(row.status)) {
return false;
}
const today = new Date().toISOString().slice(0, 10);
- const date = row.scheduledOn || row.dueDate;
- if (!date) {
- return false;
- }
- return date < today;
+ return row.dueDate < today;
}
export function enrichTableRow(row: WorkOrderTableRow): WorkOrderTableRow {
return {
...row,
- // Prefer API board flag when true; otherwise recompute from scheduledOn/dueDate.
- isPastDue: row.isPastDue || computeIsPastDue(row),
+ // Recompute from dueDate (same date-only rule as BE). Avoid sticky
+ // `apiTrue || compute` so a future Due Date clears Past Due immediately.
+ isPastDue: computeIsPastDue(row),
};
}
diff --git a/src/test/app/(protected)/workorders/due-date-calendar-dialog.test.tsx b/src/test/app/(protected)/workorders/due-date-calendar-dialog.test.tsx
new file mode 100644
index 00000000..abe7c429
--- /dev/null
+++ b/src/test/app/(protected)/workorders/due-date-calendar-dialog.test.tsx
@@ -0,0 +1,62 @@
+import { ThemeProvider } from "@mui/material";
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import { DueDateCalendarDialog } from "@/app/(protected)/workorders/_components/list/table/cells/due-date-calendar-dialog";
+import { DueDateCell } from "@/app/(protected)/workorders/_components/list/table/cells/due-date-cell";
+import { WizardDueDateField } from "@/app/(protected)/workorders/_components/wizard/wizard-due-date-field";
+import { muiTheme } from "@/lib/theme/mui-theme";
+
+function renderWithTheme(ui: React.ReactElement) {
+ return render({ui});
+}
+
+describe("DueDateCalendarDialog", () => {
+ it("saves the selected day as ISO and closes", () => {
+ const onSave = vi.fn();
+ const onOpenChange = vi.fn();
+
+ renderWithTheme(
+ ,
+ );
+
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
+ expect(screen.getByText("Due Date")).toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole("button", { name: "20" }));
+
+ expect(onSave).toHaveBeenCalledWith("2026-07-20");
+ expect(onOpenChange).toHaveBeenCalledWith(false);
+ });
+});
+
+describe("DueDateCell", () => {
+ it("opens a dialog for Emergency and keeps popover for PM", () => {
+ const onSave = vi.fn();
+
+ const { rerender } = renderWithTheme(
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: /07-15|7\/15|15/i }));
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
+
+ rerender(
+
+
+ ,
+ );
+
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ });
+});
+
+describe("WizardDueDateField", () => {
+ it("opens a dialog when type is Emergency", () => {
+ renderWithTheme(
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: /No due date/i }));
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
+ });
+});
diff --git a/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx b/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx
new file mode 100644
index 00000000..011c95a6
--- /dev/null
+++ b/src/test/app/(protected)/workorders/wo-table-unscheduled-highlight.test.tsx
@@ -0,0 +1,118 @@
+import { renderWithProviders } from "@/test/test-utils";
+import { screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import { WoTableBody } from "@/app/(protected)/workorders/_components/list/table/wo-table-body";
+import { WoTableRowCells } from "@/app/(protected)/workorders/_components/list/table/wo-table-row-cells";
+import type { WoTableRowHandlers } from "@/app/(protected)/workorders/_components/list/table/wo-table-row";
+import type { ReturnTypeOfUseWorkOrdersTableData } from "@/app/(protected)/workorders/_components/list/table/wo-table-types";
+import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
+
+const HIGHLIGHT_BOX_SHADOW = "0 0 0 2px var(--color-accent)";
+
+const unscheduledRow: WorkOrderTableRow = {
+ id: 42,
+ woNumber: "WO-42",
+ site: "CHI-1042",
+ type: "PM",
+ dispatcherId: "u1",
+ dispatcherName: "Test Dispatcher",
+ scheduledOn: "",
+ targetWeek: "",
+ dayGroup: "",
+ dayLabel: "",
+ location: "Chicago Loop",
+ locationId: "mock-loc-1",
+ pm: "HVAC — Quarterly PM",
+ serviceNotes: "",
+ extraServices: [],
+ vendorId: "",
+ company: "",
+ tech: "",
+ techPhone: "",
+ apptStart: "",
+ apptEnd: "",
+ apptTime: "",
+ status: "Pending",
+ docStatus: "No",
+ pocName: "",
+ pocPhone: "",
+ pocNotes: "",
+ dueDate: "2026-07-15",
+ rescheduleCount: 0,
+ carriedOver: 0,
+ originalWeek: "",
+ originalDate: "",
+ isPastDue: false,
+ scheduleWeekOnly: false,
+ mediaCount: 0,
+ title: "",
+ priority: "",
+ primaryDispatchId: null,
+ rowVersion: "",
+ dispatchRowVersion: "",
+ flagColor: null,
+};
+
+const handlers: WoTableRowHandlers = {
+ onRowClick: vi.fn(),
+ onView: vi.fn(),
+ onEdit: vi.fn(),
+ onOpenMedia: vi.fn(),
+ onPatch: vi.fn(),
+ onScheduleDate: vi.fn(),
+ onScheduleWeek: vi.fn(),
+ onOpenSlideOver: vi.fn(),
+ onReorder: vi.fn(),
+ onDragStart: vi.fn(),
+ onDragOver: vi.fn(),
+ onDrop: vi.fn(),
+ onDragEnd: vi.fn(),
+};
+
+/** Mirrors WoTable.renderRow: body `true` → Schedule On only. */
+function renderUnscheduledHarness() {
+ const tableData = {
+ advActive: false,
+ unscheduled: [unscheduledRow],
+ unschedOpen: true,
+ draftDayKey: null,
+ displayGroups: [],
+ todayKey: null,
+ startDayDraft: vi.fn(),
+ setUnschedOpen: vi.fn(),
+ } as unknown as ReturnTypeOfUseWorkOrdersTableData;
+
+ const renderRow = (row: WorkOrderTableRow, forceScheduleHighlight = false) => (
+
+
+
+ );
+
+ return renderWithProviders(
+ ,
+ { withAuth: false },
+ );
+}
+
+describe("WoTableBody unscheduled Schedule On highlight (SH-165)", () => {
+ it("force-highlights Schedule On for Unscheduled rows, not Due Date", () => {
+ renderUnscheduledHarness();
+
+ const scheduleOn = screen.getByRole("button", { name: /^—$/ });
+ const dueDate = screen.getByRole("button", { name: /07-15/ });
+
+ expect(scheduleOn).toHaveStyle({ boxShadow: HIGHLIGHT_BOX_SHADOW });
+ expect(dueDate).not.toHaveStyle({ boxShadow: HIGHLIGHT_BOX_SHADOW });
+ });
+});
diff --git a/src/test/domain/work-orders/mappers/work-order-board-patch-mapper.test.ts b/src/test/domain/work-orders/mappers/work-order-board-patch-mapper.test.ts
index 1e9e7ffa..8dfdf8f3 100644
--- a/src/test/domain/work-orders/mappers/work-order-board-patch-mapper.test.ts
+++ b/src/test/domain/work-orders/mappers/work-order-board-patch-mapper.test.ts
@@ -124,6 +124,24 @@ describe("expandTablePatchToBoardFieldOps", () => {
}),
).toEqual([{ field: "siteCode", value: "BK5" }]);
});
+
+ it("keeps dueDate and scheduledDate patches independent", () => {
+ expect(expandTablePatchToBoardFieldOps({ dueDate: "2026-08-01" })).toEqual([
+ { field: "dueDate", value: "2026-08-01" },
+ ]);
+ expect(
+ expandTablePatchToBoardFieldOps({
+ scheduledOn: "2026-07-15",
+ scheduleWeekOnly: false,
+ }),
+ ).toEqual([
+ { field: "scheduleWeekOnly", value: "false" },
+ { field: "scheduledDate", value: "2026-07-15" },
+ ]);
+ expect(expandTablePatchToBoardFieldOps({ dueDate: "2026-08-01", isPastDue: false })).toEqual([
+ { field: "dueDate", value: "2026-08-01" },
+ ]);
+ });
});
describe("buildBoardPatchPayload", () => {
diff --git a/src/test/domain/work-orders/utils/format-board-audit-action.test.ts b/src/test/domain/work-orders/utils/format-board-audit-action.test.ts
index c13f6dc0..9cfdef81 100644
--- a/src/test/domain/work-orders/utils/format-board-audit-action.test.ts
+++ b/src/test/domain/work-orders/utils/format-board-audit-action.test.ts
@@ -65,7 +65,7 @@ describe("formatBoardAuditAction", () => {
oldValue: "false",
newValue: "true",
}),
- ).toBe("WO marked as Past Due. Scheduled date has passed without completion.");
+ ).toBe("WO marked as Past Due. Due date has passed without completion.");
});
it("falls back to from → to for unknown fields", () => {
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 4667ee78..4ff5dc93 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
@@ -3,6 +3,8 @@ import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-ta
import { EMPTY_ADVANCED_FILTERS } from "@/domain/work-orders/types/work-order-advanced-filters";
import {
buildDayGroups,
+ computeIsPastDue,
+ enrichTableRow,
matchesAdvancedFilters,
splitScheduledUnscheduled,
} from "@/domain/work-orders/utils/table-filter-utils";
@@ -10,6 +12,11 @@ import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-mapper";
import { mondayOf, todayIso } from "@/domain/work-orders/utils/wizard-date-utils";
+function shiftIso(days: number): string {
+ const d = new Date(`${todayIso()}T12:00:00.000Z`);
+ d.setUTCDate(d.getUTCDate() + days);
+ return d.toISOString().slice(0, 10);
+}
function makeRow(overrides: Partial = {}): WorkOrderTableRow {
return {
id: "1",
@@ -123,6 +130,122 @@ describe("matchesAdvancedFilters", () => {
};
expect(matchesAdvancedFilters(row, noMatch)).toBe(false);
});
+
+ it("matches Overdue when isPastDue is true", () => {
+ const weekMonday = mondayOf(todayIso());
+ const pastDue = makeRow({
+ scheduledOn: weekMonday,
+ ...deriveDayFields(weekMonday),
+ isPastDue: true,
+ });
+ const filters = {
+ ...EMPTY_ADVANCED_FILTERS,
+ dateRange: "custom" as const,
+ customFrom: "0000-01-01",
+ customTo: "9999-12-31",
+ types: new Set(["Overdue" as const]),
+ };
+ expect(matchesAdvancedFilters(pastDue, filters)).toBe(true);
+ expect(
+ matchesAdvancedFilters(makeRow({ isPastDue: false, scheduledOn: weekMonday }), filters),
+ ).toBe(false);
+ });
+});
+
+describe("computeIsPastDue", () => {
+ it("is true when dueDate is before today even if scheduledOn is future", () => {
+ expect(
+ computeIsPastDue(
+ makeRow({
+ dueDate: shiftIso(-1),
+ scheduledOn: shiftIso(5),
+ status: "Scheduled",
+ }),
+ ),
+ ).toBe(true);
+ });
+
+ it("is false when dueDate is future even if scheduledOn is past", () => {
+ expect(
+ computeIsPastDue(
+ makeRow({
+ dueDate: shiftIso(5),
+ scheduledOn: shiftIso(-1),
+ status: "Incomplete",
+ }),
+ ),
+ ).toBe(false);
+ });
+
+ it("is false when dueDate is empty", () => {
+ expect(
+ computeIsPastDue(
+ makeRow({
+ dueDate: "",
+ scheduledOn: shiftIso(-3),
+ status: "Scheduled",
+ }),
+ ),
+ ).toBe(false);
+ });
+
+ it("is false for terminal statuses even with past dueDate", () => {
+ expect(
+ computeIsPastDue(
+ makeRow({
+ dueDate: shiftIso(-2),
+ scheduledOn: shiftIso(-2),
+ status: "Completed",
+ }),
+ ),
+ ).toBe(false);
+ expect(
+ computeIsPastDue(
+ makeRow({
+ dueDate: shiftIso(-2),
+ status: "Canceled",
+ }),
+ ),
+ ).toBe(false);
+ });
+
+ it("is false when dueDate is today", () => {
+ expect(
+ computeIsPastDue(
+ makeRow({
+ dueDate: todayIso(),
+ scheduledOn: shiftIso(-1),
+ status: "Scheduled",
+ }),
+ ),
+ ).toBe(false);
+ });
+});
+
+describe("enrichTableRow", () => {
+ it("recomputes isPastDue from dueDate and ignores sticky API true", () => {
+ const enriched = enrichTableRow(
+ makeRow({
+ dueDate: shiftIso(5),
+ scheduledOn: shiftIso(-1),
+ isPastDue: true,
+ status: "Scheduled",
+ }),
+ );
+ expect(enriched.isPastDue).toBe(false);
+ });
+
+ it("sets isPastDue true from past dueDate even when API flag is false", () => {
+ const enriched = enrichTableRow(
+ makeRow({
+ dueDate: shiftIso(-1),
+ scheduledOn: shiftIso(5),
+ isPastDue: false,
+ status: "Incomplete",
+ }),
+ );
+ expect(enriched.isPastDue).toBe(true);
+ });
});
describe("mapWorkOrderTableRow", () => {