shoc-frontend-new/src/test/app/(protected)/workorders/wo-table-unassigned-queue.test.tsx
Arthur Bassi 2c14b08945 feat(work-orders): day-scoped week navigation (SH-240)
Let dispatchers pick one week and one weekday so the board
replaces rows instead of stacking the full week.
2026-08-19 14:45:59 -03:00

408 lines
12 KiB
TypeScript

import { useMemo, useState } from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { AssignedToCell } from "@/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell";
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 { expandTablePatchToBoardFieldOps } from "@/domain/work-orders/mappers/work-order-board-patch-mapper";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import {
buildDayGroups,
composeBoardSections,
rowIdSet,
} from "@/domain/work-orders/utils/table-filter-utils";
function makeRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow {
return {
id: "1",
woNumber: "24991",
site: "CLV2",
type: "PM",
severity: null,
dispatcherId: "",
dispatcherName: "",
scheduledOn: "2026-06-02",
completedDate: "",
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,
isAddOn: false,
originalWeek: "",
originalDate: "",
isPastDue: false,
scheduleWeekOnly: false,
mediaCount: 0,
title: "",
priority: "",
vendorId: "",
primaryDispatchId: null,
rowVersion: "",
dispatchRowVersion: "",
flagColor: null,
additionalContacts: [],
...overrides,
};
}
function baseTableData(
overrides: Partial<ReturnTypeOfUseWorkOrdersTableData> = {},
): ReturnTypeOfUseWorkOrdersTableData {
return {
rows: [],
isLoading: false,
isFetching: false,
error: null,
refetch: vi.fn(),
advActive: false,
advResults: [],
unassigned: [],
unassignedError: null,
unassignedLoading: false,
unscheduled: [],
displayGroups: [],
searchActive: false,
noSearchResults: false,
isEmptyWeek: false,
todayKey: null,
draftDayKey: null,
draft: null,
setDraft: vi.fn(),
unassignedOpen: true,
setUnassignedOpen: 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;
}
const DISPATCHER = { id: "sarah", name: "Sarah Chen", initials: "SC", color: "#3366cc" };
function UnassignedAssignHarness({
initialRow,
patchBoardField,
}: {
initialRow: WorkOrderTableRow;
patchBoardField: (
id: string | number,
payload: { field: string; value?: string | null },
) => Promise<WorkOrderTableRow>;
}) {
const [rows, setRows] = useState([initialRow]);
const weeklyIds = rowIdSet(rows);
const sections = useMemo(
() =>
composeBoardSections({
displayRows: rows,
weekRows: rows,
weeklyIds,
queueIds: weeklyIds,
}),
[rows, weeklyIds],
);
const tableData = baseTableData({
unassigned: sections.unassigned,
unscheduled: sections.unscheduled,
displayGroups: buildDayGroups(sections.scheduled),
});
return (
<table>
<WoTableBody
tableData={tableData}
renderRow={(row) => (
<tr key={String(row.id)} data-testid={`row-${row.id}`}>
<td>{row.woNumber}</td>
<td>
<AssignedToCell
dispatcherId={row.dispatcherId}
users={[DISPATCHER]}
highlight={!row.dispatcherId}
onSave={async (id, name) => {
const ops = expandTablePatchToBoardFieldOps({
dispatcherId: id,
dispatcherName: name,
});
const patched = await patchBoardField(row.id, ops[0]!);
setRows((current) =>
current.map((item) =>
String(item.id) === String(row.id)
? { ...patched, dispatcherId: id, dispatcherName: name }
: item,
),
);
}}
/>
</td>
</tr>
)}
renderInlineRow={() => null}
/>
</table>
);
}
describe("WoTableBody Unassigned queue (SH-165)", () => {
it("renders Unassigned above the selected day without Unscheduled (SH-240)", () => {
const datedUnassigned = makeRow({ id: "u-dated", woNumber: "24991" });
const undatedUnassigned = makeRow({
id: "us1",
woNumber: "25021",
scheduledOn: "",
dayGroup: "",
dayLabel: "",
});
const undatedAssigned = makeRow({
id: "us-assigned",
woNumber: "25022",
dispatcherId: "sarah",
dispatcherName: "Sarah",
scheduledOn: "",
dayGroup: "",
dayLabel: "",
});
const datedAssigned = makeRow({
id: "day-assigned",
woNumber: "25023",
dispatcherId: "sarah",
dispatcherName: "Sarah",
});
const tableData = baseTableData({
unassigned: [datedUnassigned, undatedUnassigned],
unscheduled: [undatedAssigned],
displayGroups: [
{
key: "06/02",
label: "TUE · 06/02",
date: "2026-06-02",
items: [datedAssigned],
},
],
});
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>,
);
const unassignedHeader = screen.getByRole("button", {
name: "Unassigned, 2 work orders, expanded",
});
const dayLabel = screen.getByText("TUE · 06/02");
expect(unassignedHeader.compareDocumentPosition(screen.getByTestId("row-u-dated"))).toBe(
Node.DOCUMENT_POSITION_FOLLOWING,
);
expect(unassignedHeader.compareDocumentPosition(dayLabel)).toBe(
Node.DOCUMENT_POSITION_FOLLOWING,
);
expect(screen.queryByRole("button", { name: /Unscheduled/ })).not.toBeInTheDocument();
expect(screen.getByTestId("row-u-dated")).toBeInTheDocument();
expect(screen.getByTestId("row-us1")).toBeInTheDocument();
expect(screen.queryByTestId("row-us-assigned")).not.toBeInTheDocument();
expect(screen.getByTestId("row-day-assigned")).toBeInTheDocument();
expect(screen.getAllByTestId(/row-/)).toHaveLength(3);
});
it("collapses and expands the Unassigned section via the disclosure control", async () => {
const user = userEvent.setup();
const setUnassignedOpen = vi.fn();
const tableData = baseTableData({
unassigned: [makeRow({ id: "u1", woNumber: "24991" })],
setUnassignedOpen,
});
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>,
);
await user.click(screen.getByRole("button", { name: "Unassigned, 1 work order, expanded" }));
expect(setUnassignedOpen).toHaveBeenCalledWith(expect.any(Function));
});
it("renders a scheduled unassigned WO from outside the selected week in the pin", () => {
const outOfWeek = makeRow({
id: "out-week",
woNumber: "26001",
scheduledOn: "2026-07-15",
dayGroup: "07/15",
dayLabel: "WED · 07/15",
});
render(
<table>
<WoTableBody
tableData={baseTableData({ unassigned: [outOfWeek] })}
renderRow={(row) => (
<tr key={String(row.id)} data-testid={`row-${row.id}`}>
<td>{row.woNumber}</td>
</tr>
)}
renderInlineRow={() => null}
/>
</table>,
);
expect(screen.getByTestId("row-out-week")).toBeInTheDocument();
expect(screen.queryByText("WED · 07/15")).not.toBeInTheDocument();
});
it("distinguishes a still-loading queue from an empty one", () => {
render(
<table>
<WoTableBody
tableData={baseTableData({ unassignedLoading: true })}
renderRow={() => null}
renderInlineRow={() => null}
/>
</table>,
);
expect(screen.getByText(/Loading unassigned work orders/)).toBeInTheDocument();
expect(screen.queryByText("No unassigned work orders.")).not.toBeInTheDocument();
});
it("announces an empty queue only once loading has settled", () => {
render(
<table>
<WoTableBody
tableData={baseTableData({ unassignedLoading: false })}
renderRow={() => null}
renderInlineRow={() => null}
/>
</table>,
);
expect(screen.getByText("No unassigned work orders.")).toBeInTheDocument();
expect(screen.queryByText(/Loading unassigned work orders/)).not.toBeInTheDocument();
});
it("keeps the Unassigned pin visible when the queue query fails", () => {
render(
<table>
<WoTableBody
tableData={baseTableData({
unassignedError: new Error("queue unavailable"),
})}
renderRow={(row) => (
<tr key={String(row.id)} data-testid={`row-${row.id}`}>
<td>{row.woNumber}</td>
</tr>
)}
renderInlineRow={() => null}
/>
</table>,
);
expect(screen.getByText("queue unavailable")).toBeInTheDocument();
});
it("keeps the Unassigned section visible with an empty state when the queue is empty", () => {
render(
<table>
<WoTableBody
tableData={baseTableData()}
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: "Unassigned, 0 work orders, expanded" }),
).toBeInTheDocument();
expect(screen.getByText("No unassigned work orders.")).toBeInTheDocument();
expect(screen.queryByTestId(/row-/)).not.toBeInTheDocument();
});
it("removes a pinned row immediately after assigning through the board PATCH path", async () => {
const user = userEvent.setup();
const pinned = makeRow({
id: "u-pin",
woNumber: "24991",
dispatcherId: "",
dispatcherName: "",
rowVersion: "AAAAAAA=",
});
const patchBoardField = vi.fn(async (_id: string | number, payload: { field: string }) => {
expect(payload.field).toBe("assignTo");
return { ...pinned, dispatcherId: DISPATCHER.id, dispatcherName: DISPATCHER.name };
});
render(<UnassignedAssignHarness initialRow={pinned} patchBoardField={patchBoardField} />);
expect(screen.getByTestId("row-u-pin")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /Select/ }));
await user.click(screen.getByRole("button", { name: /Sarah Chen/ }));
expect(patchBoardField).toHaveBeenCalled();
expect(
screen.getByRole("button", { name: "Unassigned, 0 work orders, expanded" }),
).toBeInTheDocument();
expect(screen.getByText("No unassigned work orders.")).toBeInTheDocument();
expect(screen.getByTestId("row-u-pin")).toBeInTheDocument();
expect(screen.getByText("TUE · 06/02")).toBeInTheDocument();
});
});