mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 08:03:13 +00:00
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:
parent
d908b7e1da
commit
5a0e907067
14 changed files with 512 additions and 30 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
{
|
||||
|
|
|
|||
67
src/domain/work-orders/utils/work-order-feedback-toasts.ts
Normal file
67
src/domain/work-orders/utils/work-order-feedback-toasts.ts
Normal 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 });
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue