mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 18:12:05 +00:00
merge(dev): keep Linux CI New WO visual baseline
The only conflict was wo-new.png (Type and schedule). Appt Time is on a later step, so origin/dev's Assigned To screenshot is the correct combined UI.
This commit is contained in:
commit
55a6ff03b1
17 changed files with 385 additions and 10 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 185 KiB |
|
|
@ -1,11 +1,16 @@
|
||||||
|
import { useMemo } from "react";
|
||||||
import { SlideOverAuditList } from "@/app/(protected)/workorders/_components/detail/slide-over-audit-list";
|
import { SlideOverAuditList } from "@/app/(protected)/workorders/_components/detail/slide-over-audit-list";
|
||||||
|
import { useWorkOrderDirectory } from "@/domain/work-orders/use-cases/use-work-orders-list";
|
||||||
import type { WorkOrderAuditEntry, WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
import type { WorkOrderAuditEntry, WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
||||||
|
import { mergeWorkOrderUsers } from "@/domain/work-orders/utils/merge-work-order-users";
|
||||||
|
|
||||||
type SlideOverAuditTabProps = {
|
type SlideOverAuditTabProps = {
|
||||||
entries: WorkOrderAuditEntry[];
|
entries: WorkOrderAuditEntry[];
|
||||||
users?: WorkOrderUser[];
|
users?: WorkOrderUser[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SlideOverAuditTab({ entries, users }: SlideOverAuditTabProps) {
|
export function SlideOverAuditTab({ entries, users = [] }: SlideOverAuditTabProps) {
|
||||||
return <SlideOverAuditList entries={entries} users={users} />;
|
const { data: directory = [] } = useWorkOrderDirectory();
|
||||||
|
const actors = useMemo(() => mergeWorkOrderUsers(users, directory), [users, directory]);
|
||||||
|
return <SlideOverAuditList entries={entries} users={actors} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export function AssignedToCell({ dispatcherId, users, highlight, onSave }: Assig
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
style={{ color: highlight ? "var(--primary)" : "var(--color-text-muted)" }}
|
style={{ color: highlight ? "var(--primary)" : "var(--color-text-muted)" }}
|
||||||
/>
|
/>
|
||||||
Select …
|
Select dispatcher
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<HoverChevron />
|
<HoverChevron />
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,7 @@ export function WizardStepTypeSchedule({
|
||||||
<WizLabel optional>Assigned To</WizLabel>
|
<WizLabel optional>Assigned To</WizLabel>
|
||||||
<WizardFieldSelect
|
<WizardFieldSelect
|
||||||
value={draft.assigneeId}
|
value={draft.assigneeId}
|
||||||
placeholder="Select…"
|
placeholder="Select dispatcher"
|
||||||
icon={
|
icon={
|
||||||
selectedUser ? (
|
selectedUser ? (
|
||||||
<ColorDot color={getDispatcherAvatarColor(selectedUser)} />
|
<ColorDot color={getDispatcherAvatarColor(selectedUser)} />
|
||||||
|
|
|
||||||
|
|
@ -110,18 +110,34 @@ function toUrlSearchParams(
|
||||||
return sp;
|
return sp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readOptionalString(item: Record<string, unknown>, ...keys: string[]): string {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = item[key];
|
||||||
|
if (typeof value === "string" && value.trim()) {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET User serializes `Name` as FirstName + LastName; compose those fields if present. */
|
||||||
|
function mapUserDisplayName(item: Record<string, unknown>): string {
|
||||||
|
const firstName = readOptionalString(item, "firstName", "FirstName");
|
||||||
|
const lastName = readOptionalString(item, "lastName", "LastName");
|
||||||
|
const fallback = readOptionalString(item, "name", "Name", "userName", "UserName");
|
||||||
|
if (lastName) {
|
||||||
|
return `${firstName || fallback} ${lastName}`.trim();
|
||||||
|
}
|
||||||
|
return firstName || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
function mapUser(raw: unknown): WorkOrderUser {
|
function mapUser(raw: unknown): WorkOrderUser {
|
||||||
const item = (typeof raw === "object" && raw !== null ? raw : {}) as Record<string, unknown>;
|
const item = (typeof raw === "object" && raw !== null ? raw : {}) as Record<string, unknown>;
|
||||||
const id = item.id ?? item.Id;
|
const id = item.id ?? item.Id;
|
||||||
const name =
|
|
||||||
(typeof item.name === "string" ? item.name : undefined) ??
|
|
||||||
(typeof item.Name === "string" ? item.Name : undefined) ??
|
|
||||||
(typeof item.userName === "string" ? item.userName : "") ??
|
|
||||||
"";
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: typeof id === "string" || typeof id === "number" ? id : "",
|
id: typeof id === "string" || typeof id === "number" ? id : "",
|
||||||
name,
|
name: mapUserDisplayName(item),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,15 @@ export function useWorkOrderUsers(): UseQueryResult<WorkOrderUser[], Error> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Full user directory for audit actor names (Schedulers are not on the dispatcher lookup). */
|
||||||
|
export function useWorkOrderDirectory(): UseQueryResult<WorkOrderUser[], Error> {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: queryKeys.workOrders.directory(),
|
||||||
|
queryFn: () => workOrdersApi.fetchUsers(),
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useWorkOrdersByLocation(
|
export function useWorkOrdersByLocation(
|
||||||
locationId: string | number | undefined,
|
locationId: string | number | undefined,
|
||||||
search = "",
|
search = "",
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,24 @@ function tryCreatedRule({ fieldKey, actionKey }: AuditActionRuleContext): string
|
||||||
return "Work order created in the system.";
|
return "Work order created in the system.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tryAssignedToRule({
|
||||||
|
fieldKey,
|
||||||
|
actionKey,
|
||||||
|
newValue,
|
||||||
|
}: AuditActionRuleContext): string | null {
|
||||||
|
const matches =
|
||||||
|
fieldKey === "assignto" ||
|
||||||
|
fieldKey === "assignedto" ||
|
||||||
|
fieldKey === "assigneeid" ||
|
||||||
|
fieldKey === "dispatcherid" ||
|
||||||
|
actionKey === "assignmentchanged" ||
|
||||||
|
actionKey.includes("changeassignment");
|
||||||
|
if (!matches) return null;
|
||||||
|
|
||||||
|
const value = newValue.text ? wrapAuditValue(newValue) : '"—"';
|
||||||
|
return `Changed Assigned To to ${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Applied in order; the first non-null result wins. */
|
/** Applied in order; the first non-null result wins. */
|
||||||
export const BOARD_AUDIT_ACTION_RULES: AuditActionRule[] = [
|
export const BOARD_AUDIT_ACTION_RULES: AuditActionRule[] = [
|
||||||
tryStatusRule,
|
tryStatusRule,
|
||||||
|
|
@ -105,5 +123,6 @@ export const BOARD_AUDIT_ACTION_RULES: AuditActionRule[] = [
|
||||||
tryScheduleRule,
|
tryScheduleRule,
|
||||||
tryCarriedOverRule,
|
tryCarriedOverRule,
|
||||||
tryPastDueRule,
|
tryPastDueRule,
|
||||||
|
tryAssignedToRule,
|
||||||
tryCreatedRule,
|
tryCreatedRule,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
18
src/domain/work-orders/utils/merge-work-order-users.ts
Normal file
18
src/domain/work-orders/utils/merge-work-order-users.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
||||||
|
|
||||||
|
/** Primary list wins on id collision (dispatcher lookup before directory). */
|
||||||
|
export function mergeWorkOrderUsers(
|
||||||
|
primary: WorkOrderUser[],
|
||||||
|
extra: WorkOrderUser[],
|
||||||
|
): WorkOrderUser[] {
|
||||||
|
const byId = new Map<string, WorkOrderUser>();
|
||||||
|
for (const user of primary) {
|
||||||
|
const key = String(user.id);
|
||||||
|
if (key) byId.set(key, user);
|
||||||
|
}
|
||||||
|
for (const user of extra) {
|
||||||
|
const key = String(user.id);
|
||||||
|
if (key && !byId.has(key)) byId.set(key, user);
|
||||||
|
}
|
||||||
|
return [...byId.values()];
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ export const queryKeys = {
|
||||||
dispatch: (id: string | number) => [...queryKeys.workOrders.all, "dispatch", id] as const,
|
dispatch: (id: string | number) => [...queryKeys.workOrders.all, "dispatch", id] as const,
|
||||||
taskTemplates: () => [...queryKeys.workOrders.all, "taskTemplates"] as const,
|
taskTemplates: () => [...queryKeys.workOrders.all, "taskTemplates"] as const,
|
||||||
dispatchers: () => [...queryKeys.workOrders.all, "dispatchers"] as const,
|
dispatchers: () => [...queryKeys.workOrders.all, "dispatchers"] as const,
|
||||||
|
directory: () => [...queryKeys.workOrders.all, "directory"] as const,
|
||||||
uplifts: (workOrderId: string | number) =>
|
uplifts: (workOrderId: string | number) =>
|
||||||
[...queryKeys.workOrders.all, "uplifts", workOrderId] as const,
|
[...queryKeys.workOrders.all, "uplifts", workOrderId] as const,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
const USERS = [
|
||||||
|
{ id: "sarah", name: "Sarah Chen" },
|
||||||
|
{ id: "morgan", name: "Morgan Blake" },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("AssignedToCell", () => {
|
||||||
|
it("lets any board user pick a dispatcher without a role/disabled prop", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onSave = vi.fn();
|
||||||
|
|
||||||
|
render(<AssignedToCell dispatcherId="" users={USERS} highlight onSave={onSave} />);
|
||||||
|
|
||||||
|
const trigger = screen.getByRole("button", { name: /select dispatcher/i });
|
||||||
|
expect(trigger).toBeEnabled();
|
||||||
|
expect(trigger).not.toHaveAttribute("aria-disabled", "true");
|
||||||
|
|
||||||
|
await user.click(trigger);
|
||||||
|
await user.click(await screen.findByRole("button", { name: /sarah chen/i }));
|
||||||
|
|
||||||
|
expect(onSave).toHaveBeenCalledWith("sarah", "Sarah Chen");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can clear assignment to Unassigned", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onSave = vi.fn();
|
||||||
|
|
||||||
|
render(<AssignedToCell dispatcherId="sarah" users={USERS} onSave={onSave} />);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: /sarah chen/i }));
|
||||||
|
await user.click(await screen.findByRole("button", { name: "Unassigned" }));
|
||||||
|
|
||||||
|
expect(onSave).toHaveBeenCalledWith("", "");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still opens with an empty dispatcher list", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onSave = vi.fn();
|
||||||
|
|
||||||
|
render(<AssignedToCell dispatcherId="" users={[]} onSave={onSave} />);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: /select dispatcher/i }));
|
||||||
|
await user.click(await screen.findByRole("button", { name: "Unassigned" }));
|
||||||
|
|
||||||
|
expect(onSave).toHaveBeenCalledWith("", "");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -54,4 +54,42 @@ describe("buildInlineDraftCreateInput", () => {
|
||||||
expect(payload.targetWeek).toBe("2026-06-22");
|
expect(payload.targetWeek).toBe("2026-06-22");
|
||||||
expect(payload.scheduledDate).toBeUndefined();
|
expect(payload.scheduledDate).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sends assignTo when a dispatcher is selected at inline create", () => {
|
||||||
|
const draft = {
|
||||||
|
...EMPTY_TABLE_DRAFT,
|
||||||
|
site: "BK5",
|
||||||
|
locationId: "9",
|
||||||
|
woNumber: "25003",
|
||||||
|
pocName: "Jane",
|
||||||
|
pocPhone: "555-0100",
|
||||||
|
dispatcherId: "sarah",
|
||||||
|
};
|
||||||
|
const users = [{ id: "sarah", name: "Sarah Chen" }];
|
||||||
|
|
||||||
|
const input = buildInlineDraftCreateInput(draft, users, [{ id: 9, name: "BK5 · Dallas, TX" }]);
|
||||||
|
const payload = mapWizardDraftToBoardCreatePayload(input);
|
||||||
|
|
||||||
|
expect(input.assigneeId).toBe("sarah");
|
||||||
|
expect(input.assigneeName).toBe("Sarah Chen");
|
||||||
|
expect(payload.assignTo).toBe("sarah");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits assignTo when Assigned To is left unassigned", () => {
|
||||||
|
const draft = {
|
||||||
|
...EMPTY_TABLE_DRAFT,
|
||||||
|
site: "BK5",
|
||||||
|
locationId: "9",
|
||||||
|
woNumber: "25004",
|
||||||
|
pocName: "Jane",
|
||||||
|
pocPhone: "555-0100",
|
||||||
|
dispatcherId: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const input = buildInlineDraftCreateInput(draft, [], [{ id: 9, name: "BK5 · Dallas, TX" }]);
|
||||||
|
const payload = mapWizardDraftToBoardCreatePayload(input);
|
||||||
|
|
||||||
|
expect(input.assigneeId).toBe("");
|
||||||
|
expect(payload).not.toHaveProperty("assignTo");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { SlideOverAuditList } from "@/app/(protected)/workorders/_components/detail/slide-over-audit-list";
|
||||||
|
import type { WorkOrderAuditEntry } from "@/domain/work-orders/types/work-order";
|
||||||
|
|
||||||
|
const ACTOR = { id: "actor-1", name: "Alex Dispatcher" };
|
||||||
|
|
||||||
|
function assignmentEntry(overrides: Partial<WorkOrderAuditEntry> = {}): WorkOrderAuditEntry {
|
||||||
|
return {
|
||||||
|
id: "a1",
|
||||||
|
fieldName: "AssignTo",
|
||||||
|
oldValue: "Unassigned",
|
||||||
|
newValue: "Morgan Blake",
|
||||||
|
action: "AssignmentChanged",
|
||||||
|
createdAt: "2026-06-01T13:10:00.000Z",
|
||||||
|
userName: "",
|
||||||
|
type: "manual",
|
||||||
|
dispatcherId: ACTOR.id,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("SlideOverAuditList assignment actor", () => {
|
||||||
|
it("names who changed Assigned To from dispatcherId on the users list", () => {
|
||||||
|
render(<SlideOverAuditList entries={[assignmentEntry()]} users={[ACTOR]} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("Alex Dispatcher")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Changed Assigned To to "Morgan Blake"')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to userName when dispatcherId is not in the users list", () => {
|
||||||
|
render(
|
||||||
|
<SlideOverAuditList
|
||||||
|
entries={[assignmentEntry({ dispatcherId: "missing", userName: "Pat Scheduler" })]}
|
||||||
|
users={[ACTOR]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Pat Scheduler")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Changed Assigned To to "Morgan Blake"')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { screen, waitFor } from "@testing-library/react";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { SlideOverAuditTab } from "@/app/(protected)/workorders/_components/detail/tabs/slide-over-audit-tab";
|
||||||
|
import type { WorkOrderAuditEntry } from "@/domain/work-orders/types/work-order";
|
||||||
|
import { renderWithProviders } from "@/test/test-utils";
|
||||||
|
|
||||||
|
const fetchUsers = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("@/domain/work-orders/api/work-orders-api", () => ({
|
||||||
|
workOrdersApi: {
|
||||||
|
fetchUsers: (...args: unknown[]) => fetchUsers(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const DISPATCHER = { id: "d1", name: "Alex Dispatcher" };
|
||||||
|
const SCHEDULER = { id: "s1", name: "Pat Scheduler" };
|
||||||
|
|
||||||
|
const entry: WorkOrderAuditEntry = {
|
||||||
|
id: "a1",
|
||||||
|
fieldName: "AssignTo",
|
||||||
|
oldValue: "Unassigned",
|
||||||
|
newValue: "Morgan Blake",
|
||||||
|
action: "AssignmentChanged",
|
||||||
|
createdAt: "2026-06-01T13:10:00.000Z",
|
||||||
|
userName: "",
|
||||||
|
type: "manual",
|
||||||
|
dispatcherId: SCHEDULER.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("SlideOverAuditTab assignment actor", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetchUsers.mockReset();
|
||||||
|
fetchUsers.mockResolvedValue([SCHEDULER]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("names a scheduler actor who is not on the dispatcher lookup", async () => {
|
||||||
|
renderWithProviders(<SlideOverAuditTab entries={[entry]} users={[DISPATCHER]} />, {
|
||||||
|
withAuth: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Pat Scheduler")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Changed Assigned To to "Morgan Blake"')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("s1")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { render, screen, within } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { WizardStepTypeSchedule } from "@/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule";
|
||||||
|
import { EMPTY_WIZARD_DRAFT } from "@/domain/work-orders/types/work-order-wizard";
|
||||||
|
|
||||||
|
const USERS = [
|
||||||
|
{ id: "sarah", name: "Sarah Chen" },
|
||||||
|
{ id: "morgan", name: "Morgan Blake" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function assignedToBlock() {
|
||||||
|
return screen.getByText("Assigned To").closest("div");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("WizardStepTypeSchedule Assigned To", () => {
|
||||||
|
it("keeps Assigned To optional and selectable without a role gate", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onPatch = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<WizardStepTypeSchedule
|
||||||
|
draft={EMPTY_WIZARD_DRAFT}
|
||||||
|
users={USERS}
|
||||||
|
onUpdate={vi.fn()}
|
||||||
|
onPatch={onPatch}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const block = assignedToBlock();
|
||||||
|
expect(block).not.toBeNull();
|
||||||
|
expect(within(block!).getByText("optional")).toBeInTheDocument();
|
||||||
|
|
||||||
|
const trigger = within(block!).getByRole("button", { name: /select dispatcher/i });
|
||||||
|
expect(trigger).toBeEnabled();
|
||||||
|
|
||||||
|
await user.click(trigger);
|
||||||
|
await user.click(await screen.findByRole("button", { name: "Sarah Chen" }));
|
||||||
|
|
||||||
|
expect(onPatch).toHaveBeenCalledWith({
|
||||||
|
assigneeId: "sarah",
|
||||||
|
assigneeName: "Sarah Chen",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stays interactive when the dispatcher list is empty", () => {
|
||||||
|
render(
|
||||||
|
<WizardStepTypeSchedule
|
||||||
|
draft={EMPTY_WIZARD_DRAFT}
|
||||||
|
users={[]}
|
||||||
|
onUpdate={vi.fn()}
|
||||||
|
onPatch={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const block = assignedToBlock();
|
||||||
|
expect(block).not.toBeNull();
|
||||||
|
expect(within(block!).getByRole("button", { name: /select dispatcher/i })).toBeEnabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -200,6 +200,30 @@ describe("workOrdersApi.fetchDispatchers", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const GET_USER_SERIALIZED_DIRECTORY = {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
Id: "s1",
|
||||||
|
Name: "Alex Smith",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("workOrdersApi.fetchUsers", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
apiGet.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps the serialized GET User Name as the full display name", async () => {
|
||||||
|
apiGet.mockResolvedValue(GET_USER_SERIALIZED_DIRECTORY);
|
||||||
|
|
||||||
|
const users = await workOrdersApi.fetchUsers();
|
||||||
|
|
||||||
|
expect(apiGet).toHaveBeenCalledWith(API_PATHS.user.list);
|
||||||
|
expect(users).toEqual([{ id: "s1", name: "Alex Smith" }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("workOrdersApi.getBoard", () => {
|
describe("workOrdersApi.getBoard", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
apiGet.mockReset();
|
apiGet.mockReset();
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,28 @@ describe("formatBoardAuditAction", () => {
|
||||||
).toBe('Changed extra services to "Other", "Installation", "Preventive Maintenance"');
|
).toBe('Changed extra services to "Other", "Installation", "Preventive Maintenance"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("formats AssignmentChanged as AAP Assigned To copy", () => {
|
||||||
|
expect(
|
||||||
|
formatBoardAuditAction({
|
||||||
|
action: "AssignmentChanged",
|
||||||
|
fieldName: "AssignTo",
|
||||||
|
oldValue: "Unassigned",
|
||||||
|
newValue: "Morgan Blake",
|
||||||
|
}),
|
||||||
|
).toBe('Changed Assigned To to "Morgan Blake"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats empty AssignTo as an em dash", () => {
|
||||||
|
expect(
|
||||||
|
formatBoardAuditAction({
|
||||||
|
action: "AssignmentChanged",
|
||||||
|
fieldName: "AssignedTo",
|
||||||
|
oldValue: "Morgan Blake",
|
||||||
|
newValue: "",
|
||||||
|
}),
|
||||||
|
).toBe('Changed Assigned To to "—"');
|
||||||
|
});
|
||||||
|
|
||||||
it("falls back to Updated work order when empty", () => {
|
it("falls back to Updated work order when empty", () => {
|
||||||
expect(
|
expect(
|
||||||
formatBoardAuditAction({
|
formatBoardAuditAction({
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { mergeWorkOrderUsers } from "@/domain/work-orders/utils/merge-work-order-users";
|
||||||
|
|
||||||
|
describe("mergeWorkOrderUsers", () => {
|
||||||
|
it("keeps dispatcher lookup entries and adds directory-only actors", () => {
|
||||||
|
const dispatchers = [{ id: "d1", name: "Alex Dispatcher" }];
|
||||||
|
const directory = [
|
||||||
|
{ id: "d1", name: "Alex Dispatcher (stale)" },
|
||||||
|
{ id: "s1", name: "Pat Scheduler" },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(mergeWorkOrderUsers(dispatchers, directory)).toEqual([
|
||||||
|
{ id: "d1", name: "Alex Dispatcher" },
|
||||||
|
{ id: "s1", name: "Pat Scheduler" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips empty ids", () => {
|
||||||
|
expect(mergeWorkOrderUsers([{ id: "", name: "Bad" }], [{ id: "s1", name: "Pat" }])).toEqual([
|
||||||
|
{ id: "s1", name: "Pat" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue