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 ( + onOpenChange(false)} + maxWidth={false} + slotProps={{ + ...dialogBlurBackdropSlotProps, + paper: { + className: "w-full", + style: { borderRadius: 12, maxWidth: 320 }, + onClick: (e: MouseEvent) => e.stopPropagation(), + }, + }} + > + e.stopPropagation()}> + + Due Date + +
+ { + onSave(iso); + onOpenChange(false); + }} + /> +
+
+
+ ); +} 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( + + null} /> +
, + { 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", () => {