import { useEffect, useState } from 'react'; import { useBanUserMutation, useCreateUserMutation, useDeleteUserMutation, useInviteUserMutation, useUnbanUserMutation, useUpdateUserMutation } from './queries'; import { IUserSchema, IUserFilterOptionsSchema } from '@/src/entities/models/users/users.model'; import { toast } from 'sonner'; import { set } from 'date-fns'; import { CreateUserSchema, defaulICreateUserSchemaValues, ICreateUserSchema } from '@/src/entities/models/users/create-user.model'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { defaulIInviteUserSchemaValues, IInviteUserSchema, InviteUserSchema } from '@/src/entities/models/users/invite-user.model'; import { useQueryClient } from '@tanstack/react-query'; import { useSendMagicLinkMutation, useSendPasswordRecoveryMutation } from '@/app/(pages)/(auth)/queries'; import { ValidBanDuration } from '@/app/_lib/types/ban-duration'; import { IUpdateUserSchema, UpdateUserSchema } from '@/src/entities/models/users/update-user.model'; export const useUsersHandlers = () => { const queryClient = useQueryClient(); // Core mutations const { updateUser, isPending: isUpdatePending, errors: isUpdateError } = useUpdateUserMutation(); const { deleteUser, isPending: isDeletePending } = useDeleteUserMutation(); const { sendPasswordRecovery, isPending: isSendPasswordRecoveryPending } = useSendPasswordRecoveryMutation(); const { sendMagicLink, isPending: isSendMagicLinkPending } = useSendMagicLinkMutation(); const { banUser, isPending: isBanPending } = useBanUserMutation(); const { unbanUser, isPending: isUnbanPending } = useUnbanUserMutation(); /** * update a user by ID */ const handleUpdateUser = async (userId: string, data: IUpdateUserSchema, options?: { onSuccess?: () => void, onError?: (error: unknown) => void }) => { await updateUser({ id: userId, data }, { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["users"] }); toast.success("User updated successfully"); options?.onSuccess?.(); }, onError: (error) => { toast.error("Failed to update user"); options?.onError?.(error); }, }); } /** * Deletes a user by ID */ const handleDeleteUser = async (userId: string, options?: { onSuccess?: () => void, onError?: (error: unknown) => void }) => { await deleteUser(userId, { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["users"] }); toast.success("User deleted successfully"); options?.onSuccess?.(); }, onError: (error) => { toast.error("Failed to delete user"); options?.onError?.(error); }, }); }; /** * Sends a password recovery email to the user */ const handleSendPasswordRecovery = async (email: string, options?: { onSuccess?: () => void, onError?: (error: unknown) => void }) => { if (!email) { toast.error("No email address provided"); options?.onError?.(new Error("No email address provided")); return; } await sendPasswordRecovery(email, { onSuccess: () => { toast.success("Recovery email sent"); options?.onSuccess?.(); }, onError: (error) => { toast.error("Failed to send recovery email"); options?.onError?.(error); }, }); }; /** * Sends a magic link to the user's email */ const handleSendMagicLink = async (email: string, options?: { onSuccess?: () => void, onError?: (error: unknown) => void }) => { if (!email) { toast.error("No email address provided"); options?.onError?.(new Error("No email address provided")); return; } await sendMagicLink(email, { onSuccess: () => { toast.success("Magic link sent"); options?.onSuccess?.(); }, onError: (error) => { toast.error("Failed to send magic link"); options?.onError?.(error); }, }); }; /** * Bans a user for the specified duration */ const handleBanUser = async (userId: string, banDuration: ValidBanDuration = "24h", options?: { onSuccess?: () => void, onError?: (error: unknown) => void }) => { await banUser({ credential: { id: userId }, data: { ban_duration: banDuration } }, { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["users"] }); toast.success("User banned successfully"); options?.onSuccess?.(); }, onError: (error) => { toast.error("Failed to ban user"); options?.onError?.(error); }, }); }; /** * Unbans a user */ const handleUnbanUser = async (userId: string, options?: { onSuccess?: () => void, onError?: (error: unknown) => void }) => { await unbanUser({ id: userId }, { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["users"] }); toast.success("User unbanned successfully"); options?.onSuccess?.(); }, onError: (error) => { toast.error("Failed to unban user"); options?.onError?.(error); }, }); }; /** * Toggles a user's ban status */ const handleToggleBan = async (user: { id: string, banned_until?: ValidBanDuration }, options?: { onSuccess?: () => void, onError?: (error: unknown) => void, banDuration?: ValidBanDuration }) => { if (user.banned_until) { await handleUnbanUser(user.id, options); } else { await handleBanUser(user.id, options?.banDuration, options); } }; /** * Copies text to clipboard */ const handleCopyItem = (item: string, options?: { onSuccess?: () => void, onError?: (error: unknown) => void }) => { if (!navigator.clipboard) { const error = new Error("Clipboard not supported"); toast.error("Clipboard not supported"); options?.onError?.(error); return; } if (!item) { const error = new Error("Nothing to copy"); toast.error("Nothing to copy"); options?.onError?.(error); return; } navigator.clipboard.writeText(item) .then(() => { toast.success("Copied to clipboard"); options?.onSuccess?.(); }) .catch((error) => { toast.error("Failed to copy to clipboard"); options?.onError?.(error); }); }; return { // Action handlers updateUser: handleUpdateUser, deleteUser: handleDeleteUser, sendPasswordRecovery: handleSendPasswordRecovery, sendMagicLink: handleSendMagicLink, banUser: handleBanUser, unbanUser: handleUnbanUser, toggleBan: handleToggleBan, copyToClipboard: handleCopyItem, // Loading states isUpdatePending, isDeletePending, isSendPasswordRecoveryPending, isSendMagicLinkPending, isBanPending, isUnbanPending, // Errors isUpdateError, }; }; // Specific handler for the component export const useAddUserDialogHandler = ({ onUserAdded, onOpenChange }: { onUserAdded: () => void; onOpenChange: (open: boolean) => void; }) => { const queryClient = useQueryClient(); const { createUser, isPending } = useCreateUserMutation(); const { register, handleSubmit, reset, formState: { errors: errors }, setError, getValues, clearErrors, watch, } = useForm({ resolver: zodResolver(CreateUserSchema), defaultValues: { email: "", password: "", email_confirm: true, } }); const emailConfirm = watch("email_confirm"); const onSubmit = handleSubmit(async (data) => { await createUser(data, { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["users"] }); toast.success("User created successfully."); onUserAdded(); onOpenChange(false); reset(); }, onError: (error) => { reset(); toast.error(error.message); }, }); }); const handleOpenChange = (open: boolean) => { if (!open) { reset(); } onOpenChange(open); }; return { register, handleSubmit: onSubmit, reset, errors, isPending, getValues, clearErrors, emailConfirm, handleOpenChange, }; } export const useInviteUserHandler = ({ onUserInvited, onOpenChange }: { onUserInvited: () => void; onOpenChange: (open: boolean) => void; }) => { const queryClient = useQueryClient(); const { inviteUser, isPending } = useInviteUserMutation(); const { register, handleSubmit, reset, formState: { errors: errors }, setError, getValues, clearErrors, watch, } = useForm({ resolver: zodResolver(InviteUserSchema), defaultValues: defaulIInviteUserSchemaValues }) const onSubmit = handleSubmit(async (data) => { await inviteUser(data, { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["users"] }); toast.success("Invitation sent"); onUserInvited(); onOpenChange(false); reset(); }, onError: () => { reset(); toast.error("Failed to send invitation"); }, }); }); const handleOpenChange = (open: boolean) => { if (!open) { reset(); } onOpenChange(open); }; return { register, handleSubmit: onSubmit, handleOpenChange, reset, getValues, clearErrors, watch, errors, isPending, }; } export const useUserDetailSheetHandlers = ({ open, user, onUserUpdated, onOpenChange }: { open: boolean; user: IUserSchema; onUserUpdated: () => void; onOpenChange: (open: boolean) => void; }) => { const { deleteUser, sendPasswordRecovery, sendMagicLink, banUser, unbanUser, toggleBan, copyToClipboard, isDeletePending, isSendPasswordRecoveryPending, isSendMagicLinkPending, isBanPending, isUnbanPending, } = useUsersHandlers(); const handleDeleteUser = async () => { await deleteUser(user.id, { onSuccess: () => { onOpenChange(false); } }); }; const handleSendPasswordRecovery = async () => { if (!user.email) { toast.error("User has no email address"); return; } await sendPasswordRecovery(user.email); }; const handleSendMagicLink = async () => { if (!user.email) { toast.error("User has no email address"); return; } await sendMagicLink(user.email); }; const handleBanUser = async () => { await banUser(user.id, "24h", { onSuccess: onUserUpdated }); }; const handleUnbanUser = async () => { await unbanUser(user.id, { onSuccess: onUserUpdated }); }; const handleToggleBan = async () => { await toggleBan({ id: user.id }, { onSuccess: onUserUpdated }); }; return { handleDeleteUser, handleSendPasswordRecovery, handleSendMagicLink, handleBanUser, handleUnbanUser, handleToggleBan, handleCopyItem: copyToClipboard, isDeletePending, isSendPasswordRecoveryPending, isSendMagicLinkPending, isBanPending, isUnbanPending, }; }; export const useUserProfileSheetHandlers = ({ open, onOpenChange, userData, onUserUpdated }: { open: boolean; userData: IUserSchema; onOpenChange: (open: boolean) => void; onUserUpdated: () => void; }) => { const { updateUser, isUpdatePending, isUpdateError } = useUsersHandlers(); // Initialize form with user data const form = useForm({ resolver: zodResolver(UpdateUserSchema), defaultValues: { email: userData?.email || undefined, encrypted_password: userData?.encrypted_password || undefined, role: (userData?.role as "user" | "staff" | "admin") || "user", phone: userData?.phone || undefined, invited_at: userData?.invited_at || undefined, confirmed_at: userData?.confirmed_at || undefined, // recovery_sent_at: userData?.recovery_sent_at || undefined, last_sign_in_at: userData?.last_sign_in_at || undefined, created_at: userData?.created_at || undefined, updated_at: userData?.updated_at || undefined, is_anonymous: userData?.is_anonymous || false, profile: { // id: userData?.profile?.id || undefined, // user_id: userData?.profile?.user_id || undefined, avatar: userData?.profile?.avatar || undefined, username: userData?.profile?.username || undefined, first_name: userData?.profile?.first_name || undefined, last_name: userData?.profile?.last_name || undefined, bio: userData?.profile?.bio || undefined, address: userData?.profile?.address || { street: "", city: "", state: "", country: "", postal_code: "", }, birth_date: userData?.profile?.birth_date ? new Date(userData.profile.birth_date) : undefined, }, }, }) const handleUpdateUser = async () => { await updateUser(userData.id, form.getValues(), { onSuccess: () => { onUserUpdated(); onOpenChange(false); }, onError: () => { onOpenChange(false); }, }); } return { handleUpdateUser, form, isUpdatePending, }; } export const useUserManagementHandlers = (refetch: () => void) => { const [searchQuery, setSearchQuery] = useState("") const [detailUser, setDetailUser] = useState(null) const [updateUser, setUpdateUser] = useState(null) const [isSheetOpen, setIsSheetOpen] = useState(false) const [isUpdateOpen, setIsUpdateOpen] = useState(false) const [isAddUserOpen, setIsAddUserOpen] = useState(false) const [isInviteUserOpen, setIsInviteUserOpen] = useState(false) // Filter states const [filters, setFilters] = useState({ email: "", phone: "", lastSignIn: "", createdAt: "", status: [], }) // Handle opening the detail sheet const handleUserClick = (user: IUserSchema) => { setDetailUser(user) setIsSheetOpen(true) } // Handle opening the update sheet const handleUserUpdate = (user: IUserSchema) => { setUpdateUser(user) setIsUpdateOpen(true) } // Close detail sheet when update sheet opens useEffect(() => { if (isUpdateOpen) { setIsSheetOpen(false) } }, [isUpdateOpen]) // Reset detail user when sheet closes useEffect(() => { if (!isSheetOpen) { // Use a small delay to prevent flickering if another sheet is opening const timer = setTimeout(() => { if (!isSheetOpen && !isUpdateOpen) { setDetailUser(null) } }, 300) return () => clearTimeout(timer) } }, [isSheetOpen, isUpdateOpen]) // Reset update user when update sheet closes useEffect(() => { if (!isUpdateOpen) { // Use a small delay to prevent flickering if another sheet is opening const timer = setTimeout(() => { if (!isUpdateOpen) { setUpdateUser(null) } }, 300) return () => clearTimeout(timer) } }, [isUpdateOpen]) const clearFilters = () => { setFilters({ email: "", phone: "", lastSignIn: "", createdAt: "", status: [], }) } const getActiveFilterCount = () => { return Object.values(filters).filter( (value) => (typeof value === "string" && value !== "") || (Array.isArray(value) && value.length > 0), ).length } return { searchQuery, setSearchQuery, detailUser, updateUser, isSheetOpen, setIsSheetOpen, isUpdateOpen, setIsUpdateOpen, isAddUserOpen, setIsAddUserOpen, isInviteUserOpen, setIsInviteUserOpen, filters, setFilters, handleUserClick, handleUserUpdate, clearFilters, getActiveFilterCount, } } export const filterUsers = (users: IUserSchema[], searchQuery: string, filters: IUserFilterOptionsSchema): IUserSchema[] => { return users.filter((user) => { // Global search if (searchQuery) { const query = searchQuery.toLowerCase() const matchesSearch = user.email?.toLowerCase().includes(query) || user.phone?.toLowerCase().includes(query) || user.id.toLowerCase().includes(query) if (!matchesSearch) return false } // Email filter if (filters.email && !user.email?.toLowerCase().includes(filters.email.toLowerCase())) { return false } // Phone filter if (filters.phone && !user.phone?.toLowerCase().includes(filters.phone.toLowerCase())) { return false } // Last sign in filter if (filters.lastSignIn) { if (filters.lastSignIn === "never" && user.last_sign_in_at) { return false } else if (filters.lastSignIn === "today") { const today = new Date() today.setHours(0, 0, 0, 0) const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null if (!signInDate || signInDate < today) return false } else if (filters.lastSignIn === "week") { const weekAgo = new Date() weekAgo.setDate(weekAgo.getDate() - 7) const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null if (!signInDate || signInDate < weekAgo) return false } else if (filters.lastSignIn === "month") { const monthAgo = new Date() monthAgo.setMonth(monthAgo.getMonth() - 1) const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null if (!signInDate || signInDate < monthAgo) return false } } // Created at filter if (filters.createdAt) { if (filters.createdAt === "today") { const today = new Date() today.setHours(0, 0, 0, 0) const createdAt = user.created_at ? (user.created_at ? new Date(user.created_at) : new Date()) : new Date() if (createdAt < today) return false } else if (filters.createdAt === "week") { const weekAgo = new Date() weekAgo.setDate(weekAgo.getDate() - 7) const createdAt = user.created_at ? new Date(user.created_at) : new Date() if (createdAt < weekAgo) return false } else if (filters.createdAt === "month") { const monthAgo = new Date() monthAgo.setMonth(monthAgo.getMonth() - 1) const createdAt = user.created_at ? new Date(user.created_at) : new Date() if (createdAt < monthAgo) return false } } // Status filter if (filters.status.length > 0) { const userStatus = user.banned_until ? "banned" : !user.email_confirmed_at ? "unconfirmed" : "active" if (!filters.status.includes(userStatus)) { return false } } return true }) }