diff --git a/client/src/shared/lib/__tests__/use-mutation-with-toast.test.tsx b/client/src/shared/lib/__tests__/use-mutation-with-toast.test.tsx
new file mode 100644
index 0000000..7b07073
--- /dev/null
+++ b/client/src/shared/lib/__tests__/use-mutation-with-toast.test.tsx
@@ -0,0 +1,68 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { renderHook, waitFor } from '@testing-library/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { useMutationWithToast } from '../use-mutation-with-toast'
+import { addNotification } from '../../model/notification'
+
+vi.mock('../../model/notification', () => ({
+ addNotification: vi.fn(),
+}))
+
+function createWrapper() {
+ const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
+ return ({ children }: { children: React.ReactNode }) => (
+ {children}
+ )
+}
+
+describe('useMutationWithToast', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('shows success notification on success with successMessage', async () => {
+ const mutationFn = vi.fn().mockResolvedValue({ ok: true })
+ const { result } = renderHook(() => useMutationWithToast({ mutationFn, successMessage: 'Done!' }), {
+ wrapper: createWrapper(),
+ })
+ result.current.mutate()
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(addNotification).toHaveBeenCalledWith({ type: 'success', message: 'Done!' })
+ })
+
+ it('does NOT show success notification without successMessage', async () => {
+ const mutationFn = vi.fn().mockResolvedValue({ ok: true })
+ const { result } = renderHook(() => useMutationWithToast({ mutationFn }), { wrapper: createWrapper() })
+ result.current.mutate()
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(addNotification).not.toHaveBeenCalled()
+ })
+
+ it('shows error notification on mutation error', async () => {
+ const mutationFn = vi.fn().mockRejectedValue(new Error('Boom'))
+ const { result } = renderHook(() => useMutationWithToast({ mutationFn }), { wrapper: createWrapper() })
+ result.current.mutate()
+ await waitFor(() => expect(result.current.isError).toBe(true))
+ expect(addNotification).toHaveBeenCalledWith({ type: 'error', message: 'Boom' })
+ })
+
+ it('calls user-provided onSuccess callback', async () => {
+ const onSuccess = vi.fn()
+ const mutationFn = vi.fn().mockResolvedValue({ ok: true })
+ const { result } = renderHook(() => useMutationWithToast({ mutationFn, onSuccess, successMessage: 'OK' }), {
+ wrapper: createWrapper(),
+ })
+ result.current.mutate()
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(onSuccess).toHaveBeenCalled()
+ })
+
+ it('calls user-provided onError callback', async () => {
+ const onError = vi.fn()
+ const mutationFn = vi.fn().mockRejectedValue(new Error('fail'))
+ const { result } = renderHook(() => useMutationWithToast({ mutationFn, onError }), { wrapper: createWrapper() })
+ result.current.mutate()
+ await waitFor(() => expect(result.current.isError).toBe(true))
+ expect(onError).toHaveBeenCalled()
+ })
+})
diff --git a/client/src/shared/lib/use-mutation-with-toast.ts b/client/src/shared/lib/use-mutation-with-toast.ts
new file mode 100644
index 0000000..e2a3e04
--- /dev/null
+++ b/client/src/shared/lib/use-mutation-with-toast.ts
@@ -0,0 +1,32 @@
+import { useMutation, type UseMutationOptions } from '@tanstack/react-query'
+import { addNotification } from '../model/notification'
+import { getApiErrorMessage } from './get-api-error-message'
+
+type MutationWithToastOptions = UseMutationOptions<
+ TData,
+ TError,
+ TVariables,
+ TContext
+> & {
+ successMessage?: string
+}
+
+export function useMutationWithToast(
+ options: MutationWithToastOptions,
+) {
+ const { successMessage, onSuccess, onError, ...mutationOptions } = options
+
+ return useMutation({
+ ...mutationOptions,
+ onSuccess: (data, variables, context) => {
+ if (successMessage) {
+ addNotification({ type: 'success', message: successMessage })
+ }
+ onSuccess?.(data, variables, context)
+ },
+ onError: (error, variables, context) => {
+ addNotification({ type: 'error', message: getApiErrorMessage(error) })
+ onError?.(error, variables, context)
+ },
+ })
+}