fix(work-orders): separate Unscheduled header collapse and Add WO controls

[recover] remove malicious eslint payload (was ccec7322)
This commit is contained in:
Arthur Bassi 2026-08-10 17:12:13 -03:00
parent 78ed8571a0
commit 2ed770bc79
6 changed files with 445 additions and 34 deletions

View file

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

View file

@ -23,52 +23,79 @@ export function WoTableSectionHeader({
const isUnscheduled = variant === "unscheduled";
if (isUnscheduled) {
const toggleCollapse = () => {
onToggleCollapse?.();
};
return (
<tr style={{ background: "rgba(217, 119, 6, 0.08)" }}>
<td
colSpan={WO_TABLE_COLS.length}
onClick={onToggleCollapse}
style={{
height: 40,
padding: "0 14px",
cursor: "pointer",
boxShadow: "inset 3px 0 0 var(--warning)",
borderTop: "1px solid #FDE68A",
borderBottom: "1px solid #FDE68A",
}}
>
<div className="flex items-center gap-2.5">
<AlertTriangle size={14} className="shrink-0" style={{ color: "var(--warning)" }} />
<span
className="uppercase"
<button
type="button"
className="flex items-center gap-2.5"
aria-expanded={!collapsed}
aria-label={label}
onClick={toggleCollapse}
style={{
fontFamily: "var(--font-mono)",
fontSize: 12,
fontWeight: 700,
letterSpacing: "0.08em",
color: "var(--warning)",
background: "transparent",
border: "none",
padding: 0,
cursor: "pointer",
color: "inherit",
}}
>
{label}
</span>
<span style={{ fontSize: 12, color: "var(--color-text-muted)" }}>
{count} work order{count !== 1 ? "s" : ""}
</span>
<AlertTriangle size={14} className="shrink-0" style={{ color: "var(--warning)" }} />
<span
className="uppercase"
style={{
fontFamily: "var(--font-mono)",
fontSize: 12,
fontWeight: 700,
letterSpacing: "0.08em",
color: "var(--warning)",
}}
>
{label}
</span>
<span style={{ fontSize: 12, color: "var(--color-text-muted)" }}>
{count} work order{count !== 1 ? "s" : ""}
</span>
</button>
<button
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}
>
<Plus size={13} />
Add WO
</button>
<span className="ml-auto flex items-center" style={{ color: "var(--warning)" }}>
<button
type="button"
className="ml-auto flex items-center"
aria-expanded={!collapsed}
aria-label={collapsed ? `Expand ${label}` : `Collapse ${label}`}
onClick={toggleCollapse}
style={{
background: "transparent",
border: "none",
padding: 0,
cursor: "pointer",
color: "var(--warning)",
}}
>
{collapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
</span>
</button>
</div>
</td>
</tr>
@ -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}
>
<Plus size={13} />
Add WO

View file

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

View file

@ -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> = {}): 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> = {},
): 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(
<table>
<WoTableBody
tableData={tableData}
renderRow={(row) => (
<tr key={String(row.id)} data-testid={`row-${row.id}`}>
<td>{row.woNumber}</td>
</tr>
)}
renderInlineRow={() => null}
/>
</table>,
);
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);
});
});

View file

@ -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<WorkOrderTableRow, "id" | "scheduledOn" | "dispatcherId" | "targetWeek">;
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> = {}): 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"]),
);
});
});

View file

@ -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> = {}): WorkOrderTableRow
};
}
function bar(
weekMonday: string,
overrides: Partial<WorkOrderBarFilters> = {},
): 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", () => {