feat(work-orders): add consistent save/delete toast feedback

Cancel WO and vendor assignment toast after a successful board PATCH.
This commit is contained in:
Arthur Bassi 2026-08-26 18:42:56 -03:00
parent d908b7e1da
commit 5a0e907067
14 changed files with 512 additions and 30 deletions

View file

@ -7,6 +7,7 @@ import {
buildSlideOverPatch,
isSlideOverDraftDirty,
} from "@/domain/work-orders/utils/slide-over-draft";
import { notifyVendorAssignedIfPatched } from "@/domain/work-orders/utils/work-order-feedback-toasts";
import {
isWorkOrderCoreLocked,
isWorkOrderFullyLocked,
@ -116,6 +117,7 @@ export function useSlideOverEditState({
}
onSave(row.id, patch, {
onSuccess: () => {
notifyVendorAssignedIfPatched(patch);
setBaseline(activeDraft);
setEditing(false);
setShowUnsaved(false);

View file

@ -1,9 +1,9 @@
import { useMemo } from "react";
import type {
WorkOrderTablePatch,
WorkOrderTableRow,
} from "@/domain/work-orders/types/work-order-table-row";
import { applySchedulePatch } from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import {
applySchedulePatch,
type WorkOrderTablePatchFn,
} from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
import { shouldNotifyCrossWeek } from "@/domain/work-orders/utils/should-notify-cross-week";
import type { ReturnTypeOfUseWorkOrdersTableData } from "./wo-table-types";
import { rescheduleNotify, weekMoveNotify } from "./reschedule-notify";
@ -19,7 +19,7 @@ type UseWoTableRowHandlersOptions = {
onOpenCompDoc?: (row: WorkOrderTableRow) => void;
onOpenUplifts?: (row: WorkOrderTableRow) => void;
onOpenMedia: (row: WorkOrderTableRow) => void;
onPatch: (id: string | number, patch: WorkOrderTablePatch) => void;
onPatch: WorkOrderTablePatchFn;
onScheduleHighlight: (id: string | number | null) => void;
onDuplicateFound?: (existing: WorkOrderTableRow) => void;
};
@ -76,8 +76,8 @@ export function useWoTableRowHandlers({
tableData.setSelectedId(row.id);
onOpenMedia(row);
},
onPatch: (id: string | number, patch: Partial<WorkOrderTableRow>) => {
onPatch(id, patch);
onPatch: (id, patch, options) => {
onPatch(id, patch, options);
},
onScheduleDate: (id: string | number, date: string) => {
onPatch(id, applySchedulePatch(date, false));

View file

@ -10,7 +10,7 @@ import { UpliftCell } from "./cells/uplift-cell";
import { VendorCell } from "./cells/vendor-cell";
import { EMPTY_UPLIFT_SUMMARY } from "@/domain/work-orders/types/work-order-uplift";
import { canOpenUpliftsDialog } from "@/domain/work-orders/utils/uplift-display-utils";
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
import { applyVendorTableSave } from "@/domain/work-orders/utils/work-order-feedback-toasts";
import type { WoTableRowHandlers } from "./wo-table-row";
type WoTableRowServiceCellsProps = {
@ -77,7 +77,7 @@ export function WoTableRowServiceCells({
vendors={vendors}
q={search}
viewOnly={viewOnly}
onSave={(p) => onPatchRow(toVendorTablePatch(p))}
onSave={(p) => applyVendorTableSave(handlers.onPatch, row.id, p)}
/>
</td>
<td

View file

@ -5,6 +5,7 @@ import type {
WorkOrderUser,
} from "@/domain/work-orders/types/work-order";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import type { WorkOrderTablePatchOptions } from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
import type { DragVizState } from "@/app/(protected)/workorders/_hooks/use-work-orders-table-data";
import {
canApplyWorkOrderTablePatch,
@ -29,7 +30,11 @@ export type WoTableRowHandlers = {
onOpenCompDoc?: (row: WorkOrderTableRow) => void;
onOpenUplifts?: (row: WorkOrderTableRow) => void;
onOpenMedia: (row: WorkOrderTableRow) => void;
onPatch: (id: string | number, patch: Partial<WorkOrderTableRow>) => void;
onPatch: (
id: string | number,
patch: Partial<WorkOrderTableRow>,
options?: WorkOrderTablePatchOptions,
) => void;
onScheduleDate: (id: string | number, date: string) => void;
onScheduleWeek: (id: string | number, week: string) => void;
onDuplicateFound?: (existing: WorkOrderTableRow) => void;
@ -84,9 +89,9 @@ export function WoTableRow({
const guardedHandlers: WoTableRowHandlers = {
...handlers,
onPatch: (id, patch) => {
onPatch: (id, patch, options) => {
if (!canApplyWorkOrderTablePatch(row.status, patch)) return;
handlers.onPatch(id, patch);
handlers.onPatch(id, patch, options);
},
onScheduleDate: (id, date) => {
if (!canApplyWorkOrderTablePatch(row.status, { scheduledOn: date })) return;

View file

@ -5,10 +5,8 @@ import type {
VendorDropdownItem,
WorkOrderUser,
} from "@/domain/work-orders/types/work-order";
import type {
WorkOrderTablePatch,
WorkOrderTableRow,
} from "@/domain/work-orders/types/work-order-table-row";
import type { WorkOrderTablePatchFn } from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { DuplicateWoNumberDialog } from "@/app/(protected)/workorders/_components/list/duplicate-wo-number-dialog";
import { WoTableSkeleton } from "@/app/(protected)/workorders/_components/loading/wo-table-skeleton";
import { WoDayPaginationFooter } from "./wo-day-pagination-footer";
@ -39,7 +37,7 @@ type WoTableProps = {
onOpenMedia: (row: WorkOrderTableRow) => void;
onClearSearch?: () => void;
onOpenAdvancedFilters?: () => void;
onPatch: (id: string | number, patch: WorkOrderTablePatch) => void;
onPatch: WorkOrderTablePatchFn;
};
type WoTableEmptyShellProps = {

View file

@ -8,6 +8,10 @@ import {
} from "@/domain/work-orders/use-cases/use-work-order-detail";
import { useWorkOrderUsers } from "@/domain/work-orders/use-cases/use-work-orders-list";
import { useWorkOrderTableMutations } from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
import {
patchWorkOrderAsCanceled,
patchWorkOrderAsCompleted,
} from "@/domain/work-orders/utils/work-order-feedback-toasts";
import { guardDialogClose } from "@/lib/dialog-guard";
import { useWorkOrdersListFilters } from "./use-work-orders-list-filters";
import { useWorkOrdersTableData } from "./use-work-orders-table-data";
@ -82,14 +86,14 @@ export function useWorkOrdersListPage() {
const handleConfirmCancel = () => {
if (!confirmCancel) return;
patchField(confirmCancel.id, { status: "Canceled" });
patchWorkOrderAsCanceled(patchField, confirmCancel);
setConfirmCancel(null);
handleCloseSlideOver();
};
const handleConfirmComplete = () => {
if (!confirmComplete) return;
patchField(confirmComplete.id, { status: "Completed" });
patchWorkOrderAsCompleted(patchField, confirmComplete.id);
setConfirmComplete(null);
};

View file

@ -24,6 +24,17 @@ import { queryKeys } from "@/infra/query-key/query-key";
export type { WorkOrderTablePatch };
export type WorkOrderTablePatchOptions = {
onSuccess?: () => void;
onError?: (error: Error) => void;
};
export type WorkOrderTablePatchFn = (
id: string | number,
patch: WorkOrderTablePatch,
options?: WorkOrderTablePatchOptions,
) => void;
export type WorkOrderTablePatchHandlers = {
onPatch: (id: string | number, patch: WorkOrderTablePatch) => void;
clearPatch: (id: string | number) => void;
@ -112,6 +123,7 @@ export function useWorkOrderTableMutations(
};
const patchMutation = useMutation({
meta: { suppressErrorToast: true },
mutationFn: ({ id, patch }: { id: string | number; patch: WorkOrderTablePatch }) =>
enqueuePatch(id, () => runWorkOrderTablePatch(id, patch, getRow)),
onMutate: async ({ id, patch }) => {
@ -146,14 +158,7 @@ export function useWorkOrderTableMutations(
},
});
const patchField = (
id: string | number,
patch: WorkOrderTablePatch,
options?: {
onSuccess?: () => void;
onError?: (error: Error) => void;
},
) => {
const patchField: WorkOrderTablePatchFn = (id, patch, options) => {
patchMutation.mutate(
{ id, patch },
{

View file

@ -0,0 +1,67 @@
import { toast } from "react-toastify";
import type { WorkOrderTablePatch } from "@/domain/work-orders/types/work-order-table-row";
import type { VendorAssignmentFields } from "@/domain/work-orders/utils/vendor-assignment-patch";
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
type PersistPatchFn = (
id: string | number,
patch: WorkOrderTablePatch,
options?: {
onSuccess?: () => void;
onError?: (error: Error) => void;
},
) => void;
export function workOrderCanceledMessage(woNumber: string) {
return `Work order #${woNumber} canceled.`;
}
/**
* AAP Work Orders board has no vendor-assign toast (bundle audit 2026-08-26).
* SH-119 names that silence as the gap to close; this copy is WO-specific.
*/
export function vendorAssignedMessage() {
return "Vendor assigned";
}
export function notifyWorkOrderCanceled(woNumber: string) {
toast.warning(workOrderCanceledMessage(woNumber));
}
export function notifyVendorAssigned() {
toast.success(vendorAssignedMessage());
}
export function isVendorAssignmentPatch(patch: WorkOrderTablePatch): boolean {
return Object.prototype.hasOwnProperty.call(patch, "vendorId");
}
export function notifyVendorAssignedIfPatched(patch: WorkOrderTablePatch) {
if (isVendorAssignmentPatch(patch)) notifyVendorAssigned();
}
export function patchWorkOrderAsCanceled(
patchField: PersistPatchFn,
row: { id: string | number; woNumber: string },
) {
patchField(
row.id,
{ status: "Canceled" },
{
onSuccess: () => notifyWorkOrderCanceled(row.woNumber),
},
);
}
/** AAP completes via document generate toast, not a dedicated Complete WO toast. */
export function patchWorkOrderAsCompleted(patchField: PersistPatchFn, id: string | number) {
patchField(id, { status: "Completed" });
}
export function applyVendorTableSave(
onPatch: PersistPatchFn,
id: string | number,
fields: VendorAssignmentFields,
) {
onPatch(id, toVendorTablePatch(fields), { onSuccess: notifyVendorAssigned });
}

View file

@ -1,7 +1,17 @@
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { toast } from "react-toastify";
import { useSlideOverEditState } from "@/app/(protected)/workorders/_components/detail/use-slide-over-edit-state";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { vendorAssignedMessage } from "@/domain/work-orders/utils/work-order-feedback-toasts";
vi.mock("react-toastify", () => ({
toast: {
warning: vi.fn(),
success: vi.fn(),
error: vi.fn(),
},
}));
function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow {
return {
@ -56,6 +66,10 @@ function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow
}
describe("useSlideOverEditState completed lock", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("leaves edit mode and refuses commit when the same work order becomes completed", () => {
const onSave = vi.fn();
const onClose = vi.fn();
@ -93,4 +107,66 @@ describe("useSlideOverEditState completed lock", () => {
expect(onSave).not.toHaveBeenCalled();
});
it("toasts vendor assigned after a successful save that includes vendorId", () => {
const onSave = vi.fn();
const open = baseRow();
const { result } = renderHook(() =>
useSlideOverEditState({
row: open,
editMode: true,
infoSource: open,
onSave,
onClose: vi.fn(),
closeDisabled: false,
setTab: vi.fn(),
}),
);
act(() => {
result.current.handleDraftChange({ vendorId: "v2", company: "New Co", tech: "Lee" });
});
act(() => {
result.current.commit();
});
expect(onSave).toHaveBeenCalled();
expect(toast.success).not.toHaveBeenCalled();
const options = onSave.mock.calls[0][2] as { onSuccess: () => void };
act(() => {
options.onSuccess();
});
expect(toast.success).toHaveBeenCalledWith(vendorAssignedMessage());
});
it("does not toast on slide-over save when vendorId did not change", () => {
const onSave = vi.fn();
const open = baseRow();
const { result } = renderHook(() =>
useSlideOverEditState({
row: open,
editMode: true,
infoSource: open,
onSave,
onClose: vi.fn(),
closeDisabled: false,
setTab: vi.fn(),
}),
);
act(() => {
result.current.handleDraftChange({ dispatcherName: "Blair" });
});
act(() => {
result.current.commit();
});
const options = onSave.mock.calls[0][2] as { onSuccess: () => void };
act(() => {
options.onSuccess();
});
expect(toast.success).not.toHaveBeenCalled();
});
});

View file

@ -41,4 +41,25 @@ describe("useWoTableRowHandlers onDuplicateFound", () => {
expect(onDuplicateFound).toHaveBeenCalledWith(existingRow);
expect(onOpenSlideOver).not.toHaveBeenCalled();
});
it("forwards patch options so vendor persist can toast on success", () => {
const onPatch = vi.fn();
const onSuccess = vi.fn();
const { result } = renderHook(() =>
useWoTableRowHandlers({
tableData: buildTableData(),
weekMonday: "2026-07-20",
onNavigateToDay: vi.fn(),
onOpenSlideOver: vi.fn(),
onOpenMedia: vi.fn(),
onPatch,
onScheduleHighlight: vi.fn(),
}),
);
result.current.onPatch("wo-1", { vendorId: "9" }, { onSuccess });
expect(onPatch).toHaveBeenCalledWith("wo-1", { vendorId: "9" }, { onSuccess });
});
});

View file

@ -0,0 +1,122 @@
import { act, renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { toast } from "react-toastify";
import { EMPTY_ADVANCED_FILTERS } from "@/domain/work-orders/types/work-order-advanced-filters";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { workOrderCanceledMessage } from "@/domain/work-orders/utils/work-order-feedback-toasts";
const patchField = vi.hoisted(() => vi.fn());
vi.mock("react-toastify", () => ({
toast: {
warning: vi.fn(),
success: vi.fn(),
error: vi.fn(),
},
}));
vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () => ({
useWorkOrdersListFilters: () => ({
search: "",
handleSearchChange: vi.fn(),
advOpen: false,
setAdvOpen: vi.fn(),
advApplied: null,
advInitial: EMPTY_ADVANCED_FILTERS,
applyAdvancedFilters: vi.fn(),
clearAdvancedFilters: vi.fn(),
clearAdvancedFilterChip: vi.fn(),
activeFilterCount: 0,
weekMonday: "2026-07-20",
selectedDayIso: "2026-07-20",
setWeekMonday: vi.fn(),
setSelectedDayIso: vi.fn(),
navigateToDay: vi.fn(),
dispatcherIds: new Set<string>(),
setDispatcherIds: vi.fn(),
types: new Set(),
setTypes: vi.fn(),
}),
}));
vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-table-data", () => ({
useWorkOrdersTableData: () => ({
rows: [],
totalCount: 0,
scopeTotalCount: 0,
isFetching: false,
isLoading: false,
setSelectedId: vi.fn(),
patchRow: vi.fn(),
clearRowPatch: vi.fn(),
}),
}));
vi.mock("@/domain/work-orders/use-cases/use-work-order-table-mutations", () => ({
useWorkOrderTableMutations: () => ({
patchField,
patchFieldAsync: vi.fn(),
isPending: false,
}),
}));
vi.mock("@/domain/work-orders/use-cases/use-work-orders-list", () => ({
useWorkOrderUsers: () => ({ data: [], isLoading: false }),
}));
vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({
useWorkOrderLocations: () => ({ data: [] }),
useWorkOrderVendors: () => ({ data: [] }),
}));
vi.mock("@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category", () => ({
useDropdownOptionsByCategory: () => ({ data: [] }),
}));
import { useWorkOrdersListPage } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-page";
const row = { id: 77, woNumber: "WO-77" } as WorkOrderTableRow;
describe("useWorkOrdersListPage SH-119 toasts", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("wires Cancel WO to warning toast only after patch onSuccess", () => {
const { result } = renderHook(() => useWorkOrdersListPage());
act(() => {
result.current.setConfirmCancel(row);
});
act(() => {
result.current.handleConfirmCancel();
});
expect(patchField).toHaveBeenCalledWith(
77,
{ status: "Canceled" },
expect.objectContaining({ onSuccess: expect.any(Function) }),
);
expect(toast.warning).not.toHaveBeenCalled();
const { onSuccess } = patchField.mock.calls[0][2] as { onSuccess: () => void };
onSuccess();
expect(toast.warning).toHaveBeenCalledWith(workOrderCanceledMessage("WO-77"));
expect(toast.success).not.toHaveBeenCalled();
});
it("completes a work order without a confirmation toast", () => {
const { result } = renderHook(() => useWorkOrdersListPage());
act(() => {
result.current.setConfirmComplete(row);
});
act(() => {
result.current.handleConfirmComplete();
});
expect(patchField).toHaveBeenCalledWith(77, { status: "Completed" });
expect(toast.success).not.toHaveBeenCalled();
expect(toast.warning).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,82 @@
import { fireEvent, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { WoTableRowServiceCells } from "@/app/(protected)/workorders/_components/list/table/wo-table-row-service-cells";
import type { WoTableRowHandlers } from "@/app/(protected)/workorders/_components/list/table/wo-table-row";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
import { notifyVendorAssigned } from "@/domain/work-orders/utils/work-order-feedback-toasts";
import { renderWithProviders } from "@/test/test-utils";
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/pm-type-cell", () => ({
PMTypeCell: () => null,
}));
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/appt-time-cell", () => ({
ApptTimeCell: () => null,
}));
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/status-cell", () => ({
StatusCell: () => null,
}));
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/comp-doc-cell", () => ({
CompDocCell: () => null,
}));
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/uplift-cell", () => ({
UpliftCell: () => null,
}));
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/date-cell", () => ({
DateCell: () => null,
}));
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/vendor-cell", () => ({
VendorCell: ({
onSave,
}: {
onSave: (patch: { vendorId: string; company: string; tech: string; techPhone: string }) => void;
}) => (
<button
type="button"
onClick={() => onSave({ vendorId: "9", company: "Gateway", tech: "Pat", techPhone: "555" })}
>
persist-vendor
</button>
),
}));
const row = { id: 7, upliftSummary: { hasUplift: false, pendingCount: 0 } } as WorkOrderTableRow;
describe("WoTableRowServiceCells vendor persist (SH-119)", () => {
it("forwards vendor save through patch onSuccess instead of onPatchRow", () => {
const onPatch = vi.fn();
const onPatchRow = vi.fn();
const handlers = { onPatch } as unknown as WoTableRowHandlers;
renderWithProviders(
<table>
<tbody>
<tr>
<WoTableRowServiceCells
row={row}
vendors={[]}
pmTypes={[]}
handlers={handlers}
onPatchRow={onPatchRow}
/>
</tr>
</tbody>
</table>,
{ withAuth: false },
);
fireEvent.click(screen.getByRole("button", { name: "persist-vendor" }));
expect(onPatchRow).not.toHaveBeenCalled();
expect(onPatch).toHaveBeenCalledWith(
7,
toVendorTablePatch({
vendorId: "9",
company: "Gateway",
tech: "Pat",
techPhone: "555",
}),
{ onSuccess: notifyVendorAssigned },
);
});
});

View file

@ -19,7 +19,9 @@ vi.mock("@/domain/work-orders/api/work-orders-api", () => ({
},
}));
vi.mock("react-toastify", () => ({ toast: { error: vi.fn() } }));
vi.mock("react-toastify", () => ({
toast: { error: vi.fn(), success: vi.fn(), warning: vi.fn() },
}));
import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-table-row-mapper";
import {

View file

@ -0,0 +1,98 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { toast } from "react-toastify";
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
import {
applyVendorTableSave,
isVendorAssignmentPatch,
notifyVendorAssigned,
notifyVendorAssignedIfPatched,
patchWorkOrderAsCanceled,
patchWorkOrderAsCompleted,
vendorAssignedMessage,
workOrderCanceledMessage,
} from "@/domain/work-orders/utils/work-order-feedback-toasts";
vi.mock("react-toastify", () => ({
toast: {
warning: vi.fn(),
success: vi.fn(),
error: vi.fn(),
},
}));
describe("work-order-feedback-toasts (SH-119)", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("builds the AAP cancel copy with the work order number", () => {
expect(workOrderCanceledMessage("20260819001")).toBe("Work order #20260819001 canceled.");
});
it("warns on cancel only after patch onSuccess", () => {
const patchField = vi.fn();
patchWorkOrderAsCanceled(patchField, { id: 42, woNumber: "20260819001" });
expect(patchField).toHaveBeenCalledWith(
42,
{ status: "Canceled" },
expect.objectContaining({ onSuccess: expect.any(Function) }),
);
expect(toast.warning).not.toHaveBeenCalled();
const { onSuccess } = patchField.mock.calls[0][2] as { onSuccess: () => void };
onSuccess();
expect(toast.warning).toHaveBeenCalledWith("Work order #20260819001 canceled.");
expect(toast.success).not.toHaveBeenCalled();
});
it("does not warn when the cancel patch reports onError", () => {
const patchField = vi.fn();
patchWorkOrderAsCanceled(patchField, { id: 42, woNumber: "20260819001" });
const options = patchField.mock.calls[0][2] as { onSuccess?: () => void; onError?: () => void };
options.onError?.(new Error("conflict"));
expect(toast.warning).not.toHaveBeenCalled();
});
it("toasts vendor assignment only after persist onSuccess", () => {
const onPatch = vi.fn();
const fields = {
vendorId: "9",
company: "Gateway",
tech: "Pat",
techPhone: "555",
};
applyVendorTableSave(onPatch, 7, fields);
expect(onPatch).toHaveBeenCalledWith(
7,
toVendorTablePatch(fields),
expect.objectContaining({ onSuccess: notifyVendorAssigned }),
);
expect(toast.success).not.toHaveBeenCalled();
const { onSuccess } = onPatch.mock.calls[0][2] as { onSuccess: () => void };
onSuccess();
expect(toast.success).toHaveBeenCalledWith(vendorAssignedMessage());
expect(toast.warning).not.toHaveBeenCalled();
});
it("toasts vendor assignment when a slide-over patch includes vendorId", () => {
notifyVendorAssignedIfPatched({ vendorId: "9", dispatcherId: "d1" });
expect(toast.success).toHaveBeenCalledWith(vendorAssignedMessage());
});
it("does not toast vendor assignment for non-vendor slide-over patches", () => {
expect(isVendorAssignmentPatch({ dispatcherId: "d1" })).toBe(false);
notifyVendorAssignedIfPatched({ dispatcherId: "d1" });
expect(toast.success).not.toHaveBeenCalled();
});
it("completes a work order without a success toast (AAP has no Complete WO toast)", () => {
const patchField = vi.fn();
patchWorkOrderAsCompleted(patchField, 42);
expect(patchField).toHaveBeenCalledWith(42, { status: "Completed" });
expect(toast.success).not.toHaveBeenCalled();
expect(toast.warning).not.toHaveBeenCalled();
});
});