From 2ed770bc79f2c838b23e9ffff5bf03057f9a1de2 Mon Sep 17 00:00:00 2001
From: Arthur Bassi <201806768+bassi-arthurr@users.noreply.github.com>
Date: Mon, 10 Aug 2026 17:12:13 -0300
Subject: [PATCH] fix(work-orders): separate Unscheduled header collapse and
Add WO controls
[recover] remove malicious eslint payload (was ccec7322)
---
docs/work-orders/board-search-api.md | 9 +
.../list/table/wo-table-section-header.tsx | 74 +++++---
.../work-orders/utils/table-filter-utils.ts | 13 +-
.../wo-table-body-unscheduled.test.tsx | 153 ++++++++++++++++
.../board-unscheduled-design-parity.test.ts | 165 ++++++++++++++++++
.../utils/table-filter-utils.test.ts | 65 +++++++
6 files changed, 445 insertions(+), 34 deletions(-)
create mode 100644 src/test/app/(protected)/workorders/wo-table-body-unscheduled.test.tsx
create mode 100644 src/test/domain/work-orders/utils/board-unscheduled-design-parity.test.ts
diff --git a/docs/work-orders/board-search-api.md b/docs/work-orders/board-search-api.md
index fde9438b..e476f53c 100644
--- a/docs/work-orders/board-search-api.md
+++ b/docs/work-orders/board-search-api.md
@@ -118,6 +118,15 @@ Pending (`No`)=2, Uploaded (`Yes`)=1, N/N (`NN`)=3.
}
```
+#### Buckets
+
+| Array | Inclusion rule |
+| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
+| `scheduled` | WOs with `scheduledDate` inside `weekStart`–`weekEnd` |
+| `unscheduled` | **All** WOs without `scheduledDate` (cross-week). Visible for every week request; `targetWeek` is not a week filter — Schedule-cell marker only |
+
+The FE concatenates `unscheduled + scheduled` into `items` and renders `unscheduled` in the pinned Unscheduled section above day groups. Dated WOs without a dispatcher stay in `scheduled` / day groups (Assigned To highlight), not in `unscheduled`.
+
### `/board/search`
```json
diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-section-header.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-section-header.tsx
index 6b22e968..6f281829 100644
--- a/src/app/(protected)/workorders/_components/list/table/wo-table-section-header.tsx
+++ b/src/app/(protected)/workorders/_components/list/table/wo-table-section-header.tsx
@@ -23,52 +23,79 @@ export function WoTableSectionHeader({
const isUnscheduled = variant === "unscheduled";
if (isUnscheduled) {
+ const toggleCollapse = () => {
+ onToggleCollapse?.();
+ };
+
return (
-
-
- {label}
-
-
- {count} work order{count !== 1 ? "s" : ""}
-
+
+
+ {label}
+
+
+ {count} work order{count !== 1 ? "s" : ""}
+
+
-
+
+
|
@@ -129,10 +156,7 @@ export function WoTableSectionHeader({
type="button"
className="flex items-center gap-1 transition-opacity hover:opacity-70"
style={{ fontSize: 12.5, fontWeight: 600, color: "var(--primary)" }}
- onClick={(e) => {
- e.stopPropagation();
- onAddWo();
- }}
+ onClick={onAddWo}
>
Add WO
diff --git a/src/domain/work-orders/utils/table-filter-utils.ts b/src/domain/work-orders/utils/table-filter-utils.ts
index 4ea3c92f..be0b6ded 100644
--- a/src/domain/work-orders/utils/table-filter-utils.ts
+++ b/src/domain/work-orders/utils/table-filter-utils.ts
@@ -6,7 +6,6 @@ import type {
WorkOrderTableRow,
} from "@/domain/work-orders/types/work-order-table-row";
import { ADVANCED_FILTER_PREDICATES } from "@/domain/work-orders/utils/advanced-filter-predicates";
-import { isDateInWeek } from "@/domain/work-orders/utils/table-day-utils";
import { weekRange } from "@/domain/work-orders/utils/list-week-utils";
const TERMINAL_STATUSES = new Set(["Completed", "Canceled", "Done", "Cancelled"]);
@@ -91,14 +90,10 @@ export function filterByBarAndSearch(
}
}
- if (row.scheduledOn) {
- if (row.scheduledOn < weekFrom || row.scheduledOn > weekTo) {
- return false;
- }
- } else if (row.targetWeek) {
- if (!isDateInWeek(row.targetWeek, bar.weekMonday)) {
- return false;
- }
+ // Design weekScoped: undated (!scheduledOn) stay visible every week; targetWeek
+ // is a Schedule-cell marker only, never a week-inclusion predicate.
+ if (row.scheduledOn && (row.scheduledOn < weekFrom || row.scheduledOn > weekTo)) {
+ return false;
}
return true;
diff --git a/src/test/app/(protected)/workorders/wo-table-body-unscheduled.test.tsx b/src/test/app/(protected)/workorders/wo-table-body-unscheduled.test.tsx
new file mode 100644
index 00000000..82551058
--- /dev/null
+++ b/src/test/app/(protected)/workorders/wo-table-body-unscheduled.test.tsx
@@ -0,0 +1,153 @@
+import { render, 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 type { ReturnTypeOfUseWorkOrdersTableData } from "@/app/(protected)/workorders/_components/list/table/wo-table-types";
+import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
+
+function makeRow(overrides: Partial = {}): WorkOrderTableRow {
+ return {
+ id: "1",
+ woNumber: "24991",
+ site: "CLV2",
+ type: "PM",
+ dispatcherId: "",
+ dispatcherName: "",
+ scheduledOn: "2026-06-02",
+ targetWeek: "",
+ dayGroup: "06/02",
+ dayLabel: "TUE · 06/02",
+ location: "Cleveland, OH",
+ locationId: "",
+ pm: "Backflow",
+ serviceNotes: "",
+ extraServices: [],
+ company: "",
+ tech: "",
+ techPhone: "",
+ apptStart: "",
+ apptEnd: "",
+ apptTime: "—",
+ status: "Incomplete",
+ docStatus: "No",
+ pocName: "",
+ pocPhone: "",
+ pocNotes: "",
+ dueDate: "",
+ rescheduleCount: 0,
+ carriedOver: 0,
+ originalWeek: "",
+ originalDate: "",
+ isPastDue: false,
+ scheduleWeekOnly: false,
+ mediaCount: 0,
+ title: "",
+ priority: "",
+ vendorId: "",
+ primaryDispatchId: null,
+ rowVersion: "",
+ dispatchRowVersion: "",
+ flagColor: null,
+ ...overrides,
+ };
+}
+
+function baseTableData(
+ overrides: Partial = {},
+): ReturnTypeOfUseWorkOrdersTableData {
+ return {
+ rows: [],
+ isLoading: false,
+ isFetching: false,
+ error: null,
+ refetch: vi.fn(),
+ advActive: false,
+ advResults: [],
+ unscheduled: [],
+ displayGroups: [],
+ searchActive: false,
+ noSearchResults: false,
+ isEmptyWeek: false,
+ todayKey: null,
+ draftDayKey: null,
+ draft: null,
+ setDraft: vi.fn(),
+ unschedOpen: true,
+ setUnschedOpen: vi.fn(),
+ selectedId: null,
+ setSelectedId: vi.fn(),
+ selectedRow: null,
+ flashId: null,
+ triggerFlash: vi.fn(),
+ dragViz: null,
+ setDragViz: vi.fn(),
+ dragState: null,
+ patchRow: vi.fn(),
+ clearRowPatch: vi.fn(),
+ reorderRows: vi.fn(),
+ endDrag: vi.fn(),
+ startDayDraft: vi.fn(),
+ cancelDraft: vi.fn(),
+ addRow: vi.fn(),
+ scopeTotalCount: 0,
+ totalCount: 0,
+ search: "",
+ ...overrides,
+ } as ReturnTypeOfUseWorkOrdersTableData;
+}
+
+describe("WoTableBody Unscheduled vs day groups", () => {
+ it("renders Unscheduled above day groups and keeps dated unassigned under the day header", () => {
+ const datedUnassigned = makeRow({ id: "u-dated", woNumber: "24991" });
+ const undated = makeRow({
+ id: "us1",
+ woNumber: "25021",
+ scheduledOn: "",
+ dayGroup: "",
+ dayLabel: "",
+ });
+
+ const tableData = baseTableData({
+ unscheduled: [undated],
+ displayGroups: [
+ {
+ key: "06/02",
+ label: "TUE · 06/02",
+ date: "2026-06-02",
+ items: [datedUnassigned],
+ },
+ ],
+ unschedOpen: true,
+ });
+
+ render(
+
+ (
+
+ | {row.woNumber} |
+
+ )}
+ renderInlineRow={() => null}
+ />
+
,
+ );
+
+ expect(screen.getByRole("button", { name: "Unscheduled" })).toBeInTheDocument();
+ expect(screen.getByText("TUE · 06/02")).toBeInTheDocument();
+
+ const unscheduledHeader = screen.getByRole("button", { name: "Unscheduled" });
+ const dayLabel = screen.getByText("TUE · 06/02");
+ const undatedRow = screen.getByTestId("row-us1");
+ const datedRow = screen.getByTestId("row-u-dated");
+
+ expect(
+ unscheduledHeader.compareDocumentPosition(undatedRow) & Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy();
+ expect(
+ dayLabel.compareDocumentPosition(datedRow) & Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy();
+ expect(screen.queryByTestId("row-u-dated")).toBeInTheDocument();
+ expect(screen.getAllByTestId(/row-/)).toHaveLength(2);
+ });
+});
diff --git a/src/test/domain/work-orders/utils/board-unscheduled-design-parity.test.ts b/src/test/domain/work-orders/utils/board-unscheduled-design-parity.test.ts
new file mode 100644
index 00000000..4c919a28
--- /dev/null
+++ b/src/test/domain/work-orders/utils/board-unscheduled-design-parity.test.ts
@@ -0,0 +1,165 @@
+import { describe, expect, it } from "vitest";
+import type { WorkOrderBarFilters } from "@/domain/work-orders/types/work-order-list-filters";
+import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
+import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
+import {
+ filterByBarAndSearch,
+ splitScheduledUnscheduled,
+} from "@/domain/work-orders/utils/table-filter-utils";
+import { weekRange } from "@/domain/work-orders/utils/list-week-utils";
+
+/**
+ * Design oracle — mirrors shoc-desing/src/pages/WorkOrders.tsx:
+ * - weekScoped (~L9734): !scheduledOn || (scheduledOn >= wStart && scheduledOn <= wEnd)
+ * - split (~L9762): unscheduled = !scheduledOn; dated-but-unassigned stay in day groups
+ */
+
+type DesignRow = Pick;
+
+function designWeekScoped(rows: DesignRow[], weekStart: string, weekEnd: string): DesignRow[] {
+ return rows.filter(
+ (row) => !row.scheduledOn || (row.scheduledOn >= weekStart && row.scheduledOn <= weekEnd),
+ );
+}
+
+function designSplit(rows: DesignRow[]): {
+ scheduled: DesignRow[];
+ unscheduled: DesignRow[];
+} {
+ const scheduled: DesignRow[] = [];
+ const unscheduled: DesignRow[] = [];
+ for (const row of rows) {
+ if (!row.scheduledOn) {
+ unscheduled.push(row);
+ } else {
+ scheduled.push(row);
+ }
+ }
+ return { scheduled, unscheduled };
+}
+
+function makeRow(overrides: Partial = {}): WorkOrderTableRow {
+ return {
+ id: "1",
+ woNumber: "24991",
+ site: "CLV2",
+ type: "PM",
+ dispatcherId: "u1",
+ dispatcherName: "Alice",
+ scheduledOn: "2026-06-02",
+ targetWeek: "",
+ dayGroup: "06/02",
+ dayLabel: "TUE · 06/02",
+ location: "Cleveland, OH",
+ locationId: "",
+ pm: "Backflow",
+ serviceNotes: "",
+ extraServices: [],
+ company: "",
+ tech: "",
+ techPhone: "",
+ apptStart: "",
+ apptEnd: "",
+ apptTime: "—",
+ status: "Incomplete",
+ docStatus: "No",
+ pocName: "",
+ pocPhone: "",
+ pocNotes: "",
+ dueDate: "",
+ rescheduleCount: 0,
+ carriedOver: 0,
+ originalWeek: "",
+ originalDate: "",
+ isPastDue: false,
+ scheduleWeekOnly: false,
+ mediaCount: 0,
+ title: "",
+ priority: "",
+ vendorId: "",
+ primaryDispatchId: null,
+ rowVersion: "",
+ dispatchRowVersion: "",
+ flagColor: null,
+ ...overrides,
+ };
+}
+
+function idsOf(rows: { id: string | number }[]): string[] {
+ return rows.map((row) => String(row.id)).sort();
+}
+
+describe("board Unscheduled design parity", () => {
+ const weekMonday = "2026-06-01";
+ const [weekStart, weekEnd] = weekRange(weekMonday);
+
+ const fixtures: WorkOrderTableRow[] = [
+ makeRow({
+ id: "u-dated",
+ dispatcherId: "",
+ dispatcherName: "",
+ scheduledOn: "2026-06-02",
+ ...deriveDayFields("2026-06-02"),
+ }),
+ makeRow({
+ id: "us-undated",
+ dispatcherId: "",
+ dispatcherName: "",
+ scheduledOn: "",
+ dayGroup: "",
+ dayLabel: "",
+ }),
+ makeRow({
+ id: "ua-assigned-undated",
+ dispatcherId: "sarah",
+ dispatcherName: "Sarah",
+ scheduledOn: "",
+ dayGroup: "",
+ dayLabel: "",
+ }),
+ makeRow({
+ id: "tw-other-week",
+ dispatcherId: "sarah",
+ dispatcherName: "Sarah",
+ scheduledOn: "",
+ targetWeek: "2026-07-06",
+ dayGroup: "",
+ dayLabel: "",
+ }),
+ makeRow({
+ id: "dated-other-week",
+ scheduledOn: "2026-06-15",
+ ...deriveDayFields("2026-06-15"),
+ }),
+ ];
+
+ const bar: WorkOrderBarFilters = {
+ weekMonday,
+ dispatcherIds: new Set(),
+ type: "All",
+ };
+
+ it("matches design weekScoped ID set via filterByBarAndSearch", () => {
+ const designIds = idsOf(designWeekScoped(fixtures, weekStart, weekEnd));
+ const feIds = idsOf(filterByBarAndSearch(fixtures, bar, ""));
+ expect(feIds).toEqual(designIds);
+ expect(feIds).toContain("tw-other-week");
+ expect(feIds).not.toContain("dated-other-week");
+ });
+
+ it("matches design split: dated unassigned in scheduled, undated in Unscheduled", () => {
+ const scoped = filterByBarAndSearch(fixtures, bar, "");
+ const feSplit = splitScheduledUnscheduled(scoped);
+ const designSplitResult = designSplit(designWeekScoped(fixtures, weekStart, weekEnd));
+
+ expect(idsOf(feSplit.unscheduled)).toEqual(idsOf(designSplitResult.unscheduled));
+ expect(idsOf(feSplit.scheduled)).toEqual(idsOf(designSplitResult.scheduled));
+
+ expect(idsOf(feSplit.scheduled)).toContain("u-dated");
+ expect(idsOf(feSplit.unscheduled)).not.toContain("u-dated");
+ expect(idsOf(feSplit.unscheduled)).toContain("tw-other-week");
+ expect(idsOf(feSplit.unscheduled)).toEqual(
+ expect.arrayContaining(["us-undated", "ua-assigned-undated", "tw-other-week"]),
+ );
+ });
+});
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..b059bbd3 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
@@ -1,8 +1,10 @@
import { describe, expect, it } from "vitest";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { EMPTY_ADVANCED_FILTERS } from "@/domain/work-orders/types/work-order-advanced-filters";
+import type { WorkOrderBarFilters } from "@/domain/work-orders/types/work-order-list-filters";
import {
buildDayGroups,
+ filterByBarAndSearch,
matchesAdvancedFilters,
splitScheduledUnscheduled,
} from "@/domain/work-orders/utils/table-filter-utils";
@@ -57,6 +59,18 @@ function makeRow(overrides: Partial = {}): WorkOrderTableRow
};
}
+function bar(
+ weekMonday: string,
+ overrides: Partial = {},
+): WorkOrderBarFilters {
+ return {
+ weekMonday,
+ dispatcherIds: new Set(),
+ type: "All",
+ ...overrides,
+ };
+}
+
describe("deriveDayFields", () => {
it("derives MM/DD group and weekday label", () => {
expect(deriveDayFields("2026-06-23")).toEqual({
@@ -66,6 +80,40 @@ describe("deriveDayFields", () => {
});
});
+describe("filterByBarAndSearch", () => {
+ const weekMonday = "2026-06-01";
+
+ it("keeps undated rows for any week, including targetWeek outside the active week", () => {
+ const undated = makeRow({ id: "us1", scheduledOn: "", targetWeek: "" });
+ const undatedOtherTarget = makeRow({
+ id: "tw1",
+ scheduledOn: "",
+ targetWeek: "2026-07-06",
+ });
+
+ const ids = filterByBarAndSearch([undated, undatedOtherTarget], bar(weekMonday), "").map(
+ (row) => row.id,
+ );
+ expect(ids).toEqual(["us1", "tw1"]);
+ });
+
+ it("excludes dated rows outside the active week", () => {
+ const inWeek = makeRow({
+ id: "in",
+ scheduledOn: "2026-06-03",
+ ...deriveDayFields("2026-06-03"),
+ });
+ const outWeek = makeRow({
+ id: "out",
+ scheduledOn: "2026-06-15",
+ ...deriveDayFields("2026-06-15"),
+ });
+
+ const ids = filterByBarAndSearch([inWeek, outWeek], bar(weekMonday), "").map((row) => row.id);
+ expect(ids).toEqual(["in"]);
+ });
+});
+
describe("splitScheduledUnscheduled", () => {
it("splits rows without scheduledOn into unscheduled", () => {
const scheduled = makeRow({ id: "a", scheduledOn: "2026-06-23" });
@@ -75,6 +123,23 @@ describe("splitScheduledUnscheduled", () => {
expect(result.unscheduled).toHaveLength(1);
expect(result.unscheduled[0].id).toBe("b");
});
+
+ it("keeps dated unassigned in scheduled and undated in unscheduled without duplicate ids", () => {
+ const datedUnassigned = makeRow({
+ id: "u-dated",
+ dispatcherId: "",
+ dispatcherName: "",
+ scheduledOn: "2026-06-02",
+ ...deriveDayFields("2026-06-02"),
+ });
+ const undated = makeRow({ id: "us-undated", scheduledOn: "", dispatcherId: "" });
+ const result = splitScheduledUnscheduled([datedUnassigned, undated]);
+
+ expect(result.scheduled.map((row) => row.id)).toEqual(["u-dated"]);
+ expect(result.unscheduled.map((row) => row.id)).toEqual(["us-undated"]);
+ const allIds = [...result.scheduled, ...result.unscheduled].map((row) => row.id);
+ expect(new Set(allIds).size).toBe(allIds.length);
+ });
});
describe("buildDayGroups", () => {