Merge branch 'dev' into feat/sh-101-uplift-lifecycle

This commit is contained in:
Alexandre Brandizzi 2026-08-11 13:52:26 -03:00 • committed by GitHub
commit b177987081
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 554 additions and 21 deletions

View file

@ -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 (
<Dialog
open={open}
onClose={() => onOpenChange(false)}
maxWidth={false}
slotProps={{
...dialogBlurBackdropSlotProps,
paper: {
className: "w-full",
style: { borderRadius: 12, maxWidth: 320 },
onClick: (e: MouseEvent<HTMLDivElement>) => e.stopPropagation(),
},
}}
>
<DialogContent className="p-5" onClick={(e) => e.stopPropagation()}>
<DialogTitle
className="mb-3 p-0"
style={{ fontSize: 16, color: "var(--color-text-primary)" }}
>
Due Date
</DialogTitle>
<div className="flex justify-center">
<MiniCalendar
value={value}
onChange={(iso) => {
onSave(iso);
onOpenChange(false);
}}
/>
</div>
</DialogContent>
</Dialog>
);
}

View file

@ -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 <DateCell value={value} q={q} highlight={highlight} onSave={onSave} />;
}
return (
<>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setOpen(true);
}}
className="group/cell flex w-full items-center whitespace-nowrap rounded transition-opacity hover:opacity-80"
style={
highlight
? {
boxShadow: "0 0 0 2px var(--color-accent)",
background: "var(--color-primary-tint)",
padding: "2px 8px",
margin: "0 -8px",
}
: undefined
}
aria-haspopup="dialog"
>
<span className="flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden">
<CalendarIcon
size={11}
className="shrink-0"
style={{ color: highlight ? "var(--primary)" : "var(--color-text-muted)" }}
/>
{value ? (
<span className="truncate text-[13px] text-[var(--color-text-secondary)]">
<HighlightText text={value.replace(/^\d{4}-/, "")} q={q} />
</span>
) : (
<span className="text-[13px] text-[var(--color-text-muted)]">—</span>
)}
</span>
<ChevronDown
size={14}
className="ml-2 shrink-0 opacity-0 transition-opacity group-hover/cell:opacity-100"
style={{ color: "var(--muted-foreground)" }}
/>
</button>
<DueDateCalendarDialog open={open} onOpenChange={setOpen} value={value} onSave={onSave} />
</>
);
}

View file

@ -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.
</Text>
<button
@ -98,7 +97,7 @@ export function StatusCell({
}}
className="mt-2 block text-[11px] font-medium text-[var(--color-primary)]"
>
Reschedule →
Update Due Date →
</button>
</div>
</div>

View file

@ -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({
</td>
<td className="group/cell" style={{ padding: "0 14px" }}>
<DateCell value={draft.dueDate} onSave={(date) => onPatch({ dueDate: date })} />
<DueDateCell
type={draft.type}
value={draft.dueDate}
onSave={(date) => onPatch({ dueDate: date })}
/>
</td>
<td className="group/cell" style={{ padding: "0 14px" }}>

View file

@ -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<WorkOrderTableRow>) => void;
@ -29,6 +31,7 @@ export function WoTableRowCells({
sitePlace,
search,
forceScheduleHighlight,
forceDueDateHighlight,
mutationCellStyle,
handlers,
onPatchRow,
@ -104,7 +107,7 @@ export function WoTableRowCells({
<DateCell
value={row.scheduledOn}
q={search}
highlight={forceScheduleHighlight || row.isPastDue}
highlight={forceScheduleHighlight}
allowWeekMode
targetWeek={row.targetWeek}
onSave={(date) => handlers.onScheduleDate(row.id, date)}
@ -116,7 +119,13 @@ export function WoTableRowCells({
style={{ padding: "0 14px", ...mutationCellStyle }}
onClick={(e) => e.stopPropagation()}
>
<DateCell value={row.dueDate} q={search} onSave={(date) => onPatchRow({ dueDate: date })} />
<DueDateCell
type={row.type}
value={row.dueDate}
q={search}
highlight={forceDueDateHighlight || row.isPastDue}
onSave={(date) => onPatchRow({ dueDate: date })}
/>
</td>
</>
);

View file

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

View file

@ -54,7 +54,7 @@ export function WoTable({
onOpenAdvancedFilters,
onPatch,
}: WoTableProps) {
const [scheduleHighlightId, setScheduleHighlightId] = useState<string | number | null>(null);
const [dueDateHighlightId, setDueDateHighlightId] = useState<string | number | null>(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}

View file

@ -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 <WizardDateField value={value} placeholder={placeholder} onChange={onChange} />;
}
return (
<>
<button
type="button"
onClick={() => setOpen(true)}
className="flex w-full items-center gap-2 transition-colors"
style={{
height: 36,
border: "1px solid var(--color-border)",
borderRadius: 6,
background: "var(--color-bg-surface)",
padding: "0 10px",
outline: "none",
}}
onFocus={(e) => {
e.currentTarget.style.boxShadow = "0 0 0 2px var(--color-primary)";
}}
onBlur={(e) => {
e.currentTarget.style.boxShadow = "none";
}}
aria-haspopup="dialog"
>
<CalendarIcon size={16} className="shrink-0" style={{ color: "var(--muted-foreground)" }} />
<span
className="flex-1 truncate text-left"
style={{
fontFamily: "var(--font-sans)",
fontSize: 13,
color: value ? "var(--foreground)" : "var(--muted-foreground)",
}}
>
{value ? fmtDate(value) : placeholder}
</span>
<ChevronDown size={13} className="shrink-0" style={{ color: "var(--muted-foreground)" }} />
</button>
<DueDateCalendarDialog open={open} onOpenChange={setOpen} value={value} onSave={onChange} />
</>
);
}

View file

@ -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
</WizLabelRow>
<WizardDateField
<WizardDueDateField
type={draft.type}
value={draft.dueDate}
placeholder="No due date"
onChange={(iso) => onUpdate("dueDate", iso)}

View file

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

View file

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

View file

@ -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(<ThemeProvider theme={muiTheme}>{ui}</ThemeProvider>);
}
describe("DueDateCalendarDialog", () => {
it("saves the selected day as ISO and closes", () => {
const onSave = vi.fn();
const onOpenChange = vi.fn();
renderWithTheme(
<DueDateCalendarDialog open onOpenChange={onOpenChange} value="2026-07-15" onSave={onSave} />,
);
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(
<DueDateCell type="Emergency" value="2026-07-15" onSave={onSave} />,
);
fireEvent.click(screen.getByRole("button", { name: /07-15|7\/15|15/i }));
expect(screen.getByRole("dialog")).toBeInTheDocument();
rerender(
<ThemeProvider theme={muiTheme}>
<DueDateCell type="PM" value="2026-07-15" onSave={onSave} />
</ThemeProvider>,
);
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});
describe("WizardDueDateField", () => {
it("opens a dialog when type is Emergency", () => {
renderWithTheme(
<WizardDueDateField type="Emergency" value="" placeholder="No due date" onChange={vi.fn()} />,
);
fireEvent.click(screen.getByRole("button", { name: /No due date/i }));
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
});

View file

@ -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) => (
<tr key={String(row.id)}>
<WoTableRowCells
row={row}
allRows={[row]}
users={[]}
sites={[]}
forceScheduleHighlight={forceScheduleHighlight}
forceDueDateHighlight={false}
handlers={handlers}
onPatchRow={vi.fn()}
/>
</tr>
);
return renderWithProviders(
<table>
<WoTableBody tableData={tableData} renderRow={renderRow} renderInlineRow={() => null} />
</table>,
{ 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 });
});
});

View file

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

View file

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

View file

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