mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 21:33:50 +00:00
Let dispatchers pick one week and one weekday so the board replaces rows instead of stacking the full week.
408 lines
12 KiB
TypeScript
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();
|
|
});
|
|
});
|