import { useMutation, useQueryClient } from '@tanstack/react-query'; import { supabase } from '@/integrations/supabase/client'; import { queryKeys } from '@/lib/queryKeys'; import { toast } from 'sonner'; type ValidRole = 'admin' | 'moderator' | 'user'; interface GrantRoleParams { userId: string; role: ValidRole; } interface RevokeRoleParams { roleId: string; } interface UserWithRoles { id: string; user_id: string; username: string; email: string; display_name: string | null; avatar_url: string | null; banned: boolean; created_at: string; } /** * useRoleMutations Hook * * Provides TanStack Query mutations for granting and revoking user roles * with optimistic updates for instant UI feedback. * * Features: * - Optimistic updates for immediate UI response * - Automatic cache invalidation on success * - Error handling with rollback * - Toast notifications * * @example * ```tsx * const { grantRole, revokeRole } = useRoleMutations(); * * grantRole.mutate({ userId: 'user-id', role: 'moderator' }); * revokeRole.mutate({ roleId: 'role-id' }); * ``` */ export function useRoleMutations() { const queryClient = useQueryClient(); const grantRole = useMutation({ mutationFn: async ({ userId, role }: GrantRoleParams) => { const { data, error } = await supabase .from('user_roles') .insert([{ user_id: userId, role }]) .select() .single(); if (error) throw error; return data; }, onMutate: async ({ userId, role }) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: queryKeys.users.roles() }); // Snapshot previous value const previousUsers = queryClient.getQueryData(queryKeys.users.roles()); // Optimistically update cache - add role to user queryClient.setQueryData(queryKeys.users.roles(), (old) => { if (!old) return old; return old.map((user) => user.user_id === userId ? { ...user, role } // Optimistically assign role : user ); }); return { previousUsers }; }, onError: (error, variables, context) => { // Rollback on error if (context?.previousUsers) { queryClient.setQueryData(queryKeys.users.roles(), context.previousUsers); } toast.error(`Failed to grant role: ${error.message}`); }, onSuccess: (data, { role }) => { toast.success(`Role ${role} granted successfully`); }, onSettled: () => { // Refetch to ensure consistency queryClient.invalidateQueries({ queryKey: queryKeys.users.roles() }); queryClient.invalidateQueries({ queryKey: ['user-roles'] }); }, }); const revokeRole = useMutation({ mutationFn: async ({ roleId }: RevokeRoleParams) => { const { error } = await supabase .from('user_roles') .delete() .eq('id', roleId); if (error) throw error; }, onMutate: async ({ roleId }) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: queryKeys.users.roles() }); // Snapshot previous value const previousUsers = queryClient.getQueryData(queryKeys.users.roles()); // Optimistically remove role from cache queryClient.setQueryData(queryKeys.users.roles(), (old) => { if (!old) return old; // Remove the user from the list since they no longer have a role return old.filter((user) => user.id !== roleId); }); return { previousUsers }; }, onError: (error, variables, context) => { // Rollback on error if (context?.previousUsers) { queryClient.setQueryData(queryKeys.users.roles(), context.previousUsers); } toast.error(`Failed to revoke role: ${error.message}`); }, onSuccess: () => { toast.success('Role revoked successfully'); }, onSettled: () => { // Refetch to ensure consistency queryClient.invalidateQueries({ queryKey: queryKeys.users.roles() }); queryClient.invalidateQueries({ queryKey: ['user-roles'] }); }, }); return { grantRole, revokeRole, }; }