From e84a6f52c0a93a1f54598e8dc641a936bdc0b053 Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Sun, 23 Mar 2025 09:43:15 +0700 Subject: [PATCH] Refactor CRUD Users --- .../user-management/_components/sheet.tsx | 248 ++++++------ .../_components/update-user.tsx | 83 +--- .../_components/user-management.tsx | 2 +- .../_components/users-table.tsx | 18 +- .../dashboard/user-management/action.ts | 31 +- .../dashboard/user-management/handler.tsx | 366 +++++++++++++++++- .../dashboard/user-management/queries.ts | 102 ++++- sigap-website/app/(pages)/(auth)/action.ts | 6 +- sigap-website/app/(pages)/(auth)/queries.ts | 56 ++- sigap-website/app/(pages)/layout.tsx | 2 +- .../app/_components/form-wrapper.tsx | 48 ++- sigap-website/app/_components/ui/sidebar.tsx | 2 +- .../app/_lib/types/date-format.interface.ts | 74 ++++ .../types/error-server-action.interface.ts | 5 + sigap-website/app/_utils/common.ts | 49 ++- sigap-website/di/modules/users.module.ts | 16 +- .../auth/send-magic-link.use-case.ts | 4 +- .../auth/send-password-recovery.use-case.ts | 4 +- sigap-website/src/entities/errors/common.ts | 7 +- .../models/users/update-user.model.ts | 46 ++- .../users/delete-user.controller.ts | 2 +- 21 files changed, 891 insertions(+), 280 deletions(-) create mode 100644 sigap-website/app/_lib/types/date-format.interface.ts create mode 100644 sigap-website/app/_lib/types/error-server-action.interface.ts diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/sheet.tsx b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/sheet.tsx index feb8f38..149b8e8 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/sheet.tsx +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/sheet.tsx @@ -39,122 +39,134 @@ import { } from "@/app/(pages)/(admin)/dashboard/user-management/action"; import { format } from "date-fns"; import { sendMagicLink, sendPasswordRecovery } from "@/app/(pages)/(auth)/action"; +import { useUserDetailSheetHandlers } from "../handler"; +import { IUserSchema } from "@/src/entities/models/users/users.model"; +import { formatDate } from "@/app/_utils/common"; interface UserDetailSheetProps { open: boolean; + user: IUserSchema; onOpenChange: (open: boolean) => void; - user: any; - onUserUpdate: () => void; + onUserUpdated: () => void; } export function UserDetailSheet({ open, onOpenChange, user, - onUserUpdate, + onUserUpdated, }: UserDetailSheetProps) { - const [isDeleting, setIsDeleting] = useState(false); - const [isLoading, setIsLoading] = useState({ - deleteUser: false, - sendPasswordRecovery: false, - sendMagicLink: false, - toggleBan: false, - }); + // const [isDeleting, setIsDeleting] = useState(false); + // const [isLoading, setIsLoading] = useState({ + // deleteUser: false, + // sendPasswordRecovery: false, + // sendMagicLink: false, + // toggleBan: false, + // }); - const deleteUserMutation = useMutation({ - mutationFn: () => deleteUser(user.id), - onMutate: () => { - setIsLoading((prev) => ({ ...prev, deleteUser: true })); - setIsDeleting(true); - }, - onSuccess: () => { - toast.success("User deleted successfully"); - onUserUpdate(); - onOpenChange(false); - }, - onError: () => { - toast.error("Failed to delete user"); - }, - onSettled: () => { - setIsLoading((prev) => ({ ...prev, deleteUser: false })); - setIsDeleting(false); - }, - }); + // const deleteUserMutation = useMutation({ + // mutationFn: () => deleteUser(user.id), + // onMutate: () => { + // setIsLoading((prev) => ({ ...prev, deleteUser: true })); + // setIsDeleting(true); + // }, + // onSuccess: () => { + // toast.success("User deleted successfully"); + // onUserUpdate(); + // onOpenChange(false); + // }, + // onError: () => { + // toast.error("Failed to delete user"); + // }, + // onSettled: () => { + // setIsLoading((prev) => ({ ...prev, deleteUser: false })); + // setIsDeleting(false); + // }, + // }); - const sendPasswordRecoveryMutation = useMutation({ - mutationFn: () => { - if (!user.email) { - throw new Error("User does not have an email address"); - } - return sendPasswordRecovery(user.email); - }, - onMutate: () => { - setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: true })); - }, - onSuccess: () => { - toast.success("Password recovery email sent"); - }, - onError: () => { - toast.error("Failed to send password recovery email"); - }, - onSettled: () => { - setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: false })); - }, - }); + // const sendPasswordRecoveryMutation = useMutation({ + // mutationFn: () => { + // if (!user.email) { + // throw new Error("User does not have an email address"); + // } + // return sendPasswordRecovery(user.email); + // }, + // onMutate: () => { + // setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: true })); + // }, + // onSuccess: () => { + // toast.success("Password recovery email sent"); + // }, + // onError: () => { + // toast.error("Failed to send password recovery email"); + // }, + // onSettled: () => { + // setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: false })); + // }, + // }); - const sendMagicLinkMutation = useMutation({ - mutationFn: () => { - if (!user.email) { - throw new Error("User does not have an email address"); - } - return sendMagicLink(user.email); - }, - onMutate: () => { - setIsLoading((prev) => ({ ...prev, sendMagicLink: true })); - }, - onSuccess: () => { - toast.success("Magic link sent successfully"); - }, - onError: () => { - toast.error("Failed to send magic link"); - }, - onSettled: () => { - setIsLoading((prev) => ({ ...prev, sendMagicLink: false })); - }, - }); + // const sendMagicLinkMutation = useMutation({ + // mutationFn: () => { + // if (!user.email) { + // throw new Error("User does not have an email address"); + // } + // return sendMagicLink(user.email); + // }, + // onMutate: () => { + // setIsLoading((prev) => ({ ...prev, sendMagicLink: true })); + // }, + // onSuccess: () => { + // toast.success("Magic link sent successfully"); + // }, + // onError: () => { + // toast.error("Failed to send magic link"); + // }, + // onSettled: () => { + // setIsLoading((prev) => ({ ...prev, sendMagicLink: false })); + // }, + // }); - const toggleBanMutation = useMutation({ - mutationFn: () => { - if (user.banned_until) { - return unbanUser(user.id); - } else { - const ban_duration = "7h"; // Example: Ban duration set to 7 days - return banUser(user.id, ban_duration); - } - }, - onMutate: () => { - setIsLoading((prev) => ({ ...prev, toggleBan: true })); - }, - onSuccess: () => { - toast.success("User ban status updated"); - onUserUpdate(); - }, - onError: () => { - toast.error("Failed to update user ban status"); - }, - onSettled: () => { - setIsLoading((prev) => ({ ...prev, toggleBan: false })); - }, - }); + // const toggleBanMutation = useMutation({ + // mutationFn: () => { + // if (user.banned_until) { + // return unbanUser(user.id); + // } else { + // const ban_duration = "7h"; // Example: Ban duration set to 7 days + // return banUser({ id: user.id, ban_duration }); + // } + // }, + // onMutate: () => { + // setIsLoading((prev) => ({ ...prev, toggleBan: true })); + // }, + // onSuccess: () => { + // toast.success("User ban status updated"); + // onUserUpdate(); + // }, + // onError: () => { + // toast.error("Failed to update user ban status"); + // }, + // onSettled: () => { + // setIsLoading((prev) => ({ ...prev, toggleBan: false })); + // }, + // }); - const handleCopyItem = (item: string) => { - navigator.clipboard.writeText(item); - toast.success("Copied to clipboard"); - }; + // const handleCopyItem = (item: string) => { + // navigator.clipboard.writeText(item); + // toast.success("Copied to clipboard"); + // }; - const formatDate = (date: string | undefined | null) => { - return date ? format(new Date(date), "PPpp") : "-"; - }; + const { + handleDeleteUser, + handleSendPasswordRecovery, + handleSendMagicLink, + handleCopyItem, + handleToggleBan, + isBanPending, + isUnbanPending, + isDeletePending, + isSendPasswordRecoveryPending, + isSendMagicLinkPending, + } = useUserDetailSheetHandlers({ open, user, onUserUpdated, onOpenChange }); return ( @@ -166,7 +178,7 @@ export function UserDetailSheet({ variant="ghost" size="icon" className="h-4 w-4" - onClick={() => handleCopyItem(user.email)} + onClick={() => handleCopyItem(user.email ?? "")} > @@ -203,15 +215,13 @@ export function UserDetailSheet({
Created at - {new Date(user.created_at).toLocaleString()} + {formatDate(user.created_at)}
Updated at - {new Date( - user.updated_at || user.created_at - ).toLocaleString()} + {formatDate(user.updated_at)}
@@ -224,7 +234,7 @@ export function UserDetailSheet({ Confirmation sent at - {formatDate(user.email_confirmation_sent_at)} + {formatDate(user.email_confirmed_at)}
@@ -284,10 +294,10 @@ export function UserDetailSheet({ -
diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-management.tsx b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-management.tsx index 86df656..87a096b 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-management.tsx +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-management.tsx @@ -143,7 +143,7 @@ export default function UserManagement() { user={detailUser} open={isSheetOpen} onOpenChange={setIsSheetOpen} - onUserUpdate={() => { }} + onUserUpdated={() => { }} /> )} @@ -24,6 +25,13 @@ export const createUserColumns = ( setFilters: (filters: IUserFilterOptionsSchema) => void, handleUserUpdate: (user: IUserSchema) => void, ): UserTableColumn[] => { + + const { + deleteUser, + banUser, + unbanUser, + } = useUsersHandlers(); + return [ { id: "email", @@ -313,16 +321,18 @@ export const createUserColumns = ( Update { - /* handle delete */ - }} + onClick={() => deleteUser(row.original.id)} > Delete { - /* handle ban */ + if (row.original.banned_until != null) { + unbanUser(row.original.id) + } else { + banUser(row.original.id) + } }} > diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/action.ts b/sigap-website/app/(pages)/(admin)/dashboard/user-management/action.ts index 0c9bdeb..be05ac3 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/action.ts +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/action.ts @@ -14,24 +14,26 @@ import { IBanDuration, IBanUserSchema, ICredentialsBanUserSchema } from '@/src/e import { ICreateUserSchema } from '@/src/entities/models/users/create-user.model'; import { IUpdateUserSchema } from '@/src/entities/models/users/update-user.model'; import { ICredentialsInviteUserSchema } from '@/src/entities/models/users/invite-user.model'; -import { ICredentialGetUserByEmailSchema } from '@/src/entities/models/users/read-user.model'; +import { ICredentialGetUserByEmailSchema, ICredentialGetUserByIdSchema, ICredentialGetUserByUsernameSchema } from '@/src/entities/models/users/read-user.model'; +import { ICredentialsUnbanUserSchema } from '@/src/entities/models/users/unban-user.model'; -export async function banUser(id: string, ban_duration: IBanDuration) { +export async function banUser(credential: ICredentialsBanUserSchema, data: IBanUserSchema) { const instrumentationService = getInjection('IInstrumentationService'); return await instrumentationService.instrumentServerAction( - 'banUser', + 'unbanUser', { recordResponse: true }, async () => { try { const banUserController = getInjection('IBanUserController'); - await banUserController({ id }, { ban_duration }); + await banUserController({ id: credential.id }, { ban_duration: data.ban_duration }); return { success: true }; + } catch (err) { if (err instanceof InputParseError) { - // return { - // error: err.message, - // }; + // return { + // error: err.message, + // }; throw new InputParseError(err.message); } @@ -40,7 +42,7 @@ export async function banUser(id: string, ban_duration: IBanDuration) { // return { // error: 'Must be logged in to create a user.', // }; - throw new UnauthenticatedError('Must be logged in to ban a user.'); + throw new UnauthenticatedError('Must be logged in to unban a user.'); } if (err instanceof AuthenticationError) { @@ -63,7 +65,8 @@ export async function banUser(id: string, ban_duration: IBanDuration) { ); } -export async function unbanUser(id: string) { + +export async function unbanUser(credential: ICredentialsUnbanUserSchema) { const instrumentationService = getInjection('IInstrumentationService'); return await instrumentationService.instrumentServerAction( 'unbanUser', @@ -71,7 +74,7 @@ export async function unbanUser(id: string) { async () => { try { const unbanUserController = getInjection('IUnbanUserController'); - await unbanUserController({ id }); + await unbanUserController(credential); return { success: true }; } catch (err) { @@ -134,7 +137,7 @@ export async function getCurrentUser() { ); } -export async function getUserById(id: string) { +export async function getUserById(credential: ICredentialGetUserByIdSchema) { const instrumentationService = getInjection('IInstrumentationService'); return await instrumentationService.instrumentServerAction( 'getUserById', @@ -142,7 +145,7 @@ export async function getUserById(id: string) { async () => { try { const getUserByIdController = getInjection('IGetUserByIdController'); - return await getUserByIdController({ id }); + return await getUserByIdController(credential); } catch (err) { @@ -230,7 +233,7 @@ export async function getUserByEmail(credential: ICredentialGetUserByEmailSchema ); } -export async function getUserByUsername(username: string) { +export async function getUserByUsername(credential: ICredentialGetUserByUsernameSchema) { const instrumentationService = getInjection('IInstrumentationService'); return await instrumentationService.instrumentServerAction( 'getUserByUsername', @@ -240,7 +243,7 @@ export async function getUserByUsername(username: string) { const getUserByUsernameController = getInjection( 'IGetUserByUsernameController' ); - return await getUserByUsernameController({ username }); + return await getUserByUsernameController(credential); } catch (err) { diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/handler.tsx b/sigap-website/app/(pages)/(admin)/dashboard/user-management/handler.tsx index d34ab0a..2f7c645 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/handler.tsx +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/handler.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { useCreateUserMutation, useInviteUserMutation } from './queries'; +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'; @@ -8,6 +8,226 @@ 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; @@ -139,6 +359,150 @@ export const useInviteUserHandler = ({ onUserInvited, onOpenChange }: { }; } + +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) diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/queries.ts b/sigap-website/app/(pages)/(admin)/dashboard/user-management/queries.ts index c2de53f..a9b19b9 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/queries.ts +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/queries.ts @@ -13,11 +13,12 @@ import { getUserByUsername } from "./action"; import { IUserSchema } from "@/src/entities/models/users/users.model"; -import { IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model"; -import { IUnbanUserSchema } from "@/src/entities/models/users/unban-user.model"; +import { IBanDuration, IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model"; +import { ICredentialsUnbanUserSchema, IUnbanUserSchema } from "@/src/entities/models/users/unban-user.model"; import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model"; import { IUpdateUserSchema } from "@/src/entities/models/users/update-user.model"; import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model"; +import { ICredentialGetUserByEmailSchema, ICredentialGetUserByIdSchema, ICredentialGetUserByUsernameSchema, IGetUserByEmailSchema, IGetUserByIdSchema, IGetUserByUsernameSchema } from "@/src/entities/models/users/read-user.model"; const useUsersAction = () => { @@ -28,35 +29,35 @@ const useUsersAction = () => { }); // Current user query doesn't need parameters - const getCurrentUserQuery = useQuery({ + const getCurrentUserQuery = useQuery({ queryKey: ["user", "current"], queryFn: async () => await getCurrentUser() }); - const getUserByIdQuery = (id: string) => ({ - queryKey: ["user", id], - queryFn: async () => await getUserById(id) + const getUserByIdQuery = (credential: ICredentialGetUserByIdSchema) => useQuery({ + queryKey: ["user", "id", credential.id], + queryFn: async () => await getUserById(credential) }); - const getUserByEmailQuery = (email: string) => ({ - queryKey: ["user", "email", email], - queryFn: async () => await getUserByEmail({ email }) + const getUserByEmailQuery = (credential: IGetUserByEmailSchema) => useQuery({ + queryKey: ["user", "email", credential.email], + queryFn: async () => await getUserByEmail(credential) }); - const getUserByUsernameQuery = (username: string) => ({ - queryKey: ["user", "username", username], - queryFn: async () => await getUserByUsername(username) + const getUserByUsernameQuery = (credential: IGetUserByUsernameSchema) => useQuery({ + queryKey: ["user", "username", credential.username], + queryFn: async () => await getUserByUsername(credential) }); // Mutations that don't need dynamic parameters const banUserMutation = useMutation({ mutationKey: ["banUser"], - mutationFn: async ({ credential, params }: { credential: ICredentialsBanUserSchema; params: IBanUserSchema }) => await banUser(credential.id, params.ban_duration) + mutationFn: async ({ credential, data }: { credential: ICredentialsBanUserSchema; data: IBanUserSchema }) => await banUser(credential, data) }); const unbanUserMutation = useMutation({ mutationKey: ["unbanUser"], - mutationFn: async (params: IUnbanUserSchema) => await unbanUser(params.id) + mutationFn: async (credential: ICredentialsUnbanUserSchema) => await unbanUser(credential) }); // Create functions that return configured hooks @@ -117,6 +118,39 @@ export const useGetCurrentUserQuery = () => { }; } +export const useGetUserByIdQuery = (credential: ICredentialGetUserByIdSchema) => { + const { getUserById } = useUsersAction(); + + return { + data: getUserById(credential).data, + isPending: getUserById(credential).isPending, + error: getUserById(credential).error, + refetch: getUserById(credential).refetch, + }; +} + +export const useGetUserByEmailQuery = (credential: ICredentialGetUserByEmailSchema) => { + const { getUserByEmailQuery } = useUsersAction(); + + return { + data: getUserByEmailQuery(credential).data, + isPending: getUserByEmailQuery(credential).isPending, + error: getUserByEmailQuery(credential).error, + refetch: getUserByEmailQuery(credential).refetch, + }; +} + +export const useGetUserByUsernameQuery = (credential: ICredentialGetUserByUsernameSchema) => { + const { getUserByUsernameQuery } = useUsersAction(); + + return { + data: getUserByUsernameQuery(credential).data, + isPending: getUserByUsernameQuery(credential).isPending, + error: getUserByUsernameQuery(credential).error, + refetch: getUserByUsernameQuery(credential).refetch, + }; +} + export const useCreateUserMutation = () => { const { createUser } = useUsersAction(); @@ -136,3 +170,43 @@ export const useInviteUserMutation = () => { errors: inviteUser.error, } } + +export const useUpdateUserMutation = () => { + const { updateUser } = useUsersAction(); + + return { + updateUser: updateUser.mutateAsync, + isPending: updateUser.isPending, + errors: updateUser.error, + } +} + +export const useBanUserMutation = () => { + const { banUser } = useUsersAction(); + + return { + banUser: banUser.mutateAsync, + isPending: banUser.isPending, + errors: banUser.error, + } +} + +export const useUnbanUserMutation = () => { + const { unbanUser } = useUsersAction(); + + return { + unbanUser: unbanUser.mutateAsync, + isPending: unbanUser.isPending, + errors: unbanUser.error, + } +} + +export const useDeleteUserMutation = () => { + const { deleteUser } = useUsersAction(); + + return { + deleteUser: deleteUser.mutateAsync, + isPending: deleteUser.isPending, + errors: deleteUser.error, + } +} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(auth)/action.ts b/sigap-website/app/(pages)/(auth)/action.ts index 936d40b..00dfc79 100644 --- a/sigap-website/app/(pages)/(auth)/action.ts +++ b/sigap-website/app/(pages)/(auth)/action.ts @@ -134,13 +134,12 @@ export async function verifyOtp(formData: FormData) { }) } -export async function sendMagicLink(formData: FormData) { +export async function sendMagicLink(email: string) { const instrumentationService = getInjection("IInstrumentationService") return await instrumentationService.instrumentServerAction("sendMagicLink", { recordResponse: true }, async () => { try { - const email = formData.get("email")?.toString() const sendMagicLinkController = getInjection("ISendMagicLinkController") await sendMagicLinkController({ email }) @@ -161,13 +160,12 @@ export async function sendMagicLink(formData: FormData) { }) } -export async function sendPasswordRecovery(formData: FormData) { +export async function sendPasswordRecovery(email: string) { const instrumentationService = getInjection("IInstrumentationService") return await instrumentationService.instrumentServerAction("sendPasswordRecovery", { recordResponse: true }, async () => { try { - const email = formData.get("email")?.toString() const sendPasswordRecoveryController = getInjection("ISendPasswordRecoveryController") await sendPasswordRecoveryController({ email }) diff --git a/sigap-website/app/(pages)/(auth)/queries.ts b/sigap-website/app/(pages)/(auth)/queries.ts index 31f17d3..c80b775 100644 --- a/sigap-website/app/(pages)/(auth)/queries.ts +++ b/sigap-website/app/(pages)/(auth)/queries.ts @@ -21,12 +21,12 @@ export function useAuthActions() { const sendMagicLinkMutation = useMutation({ mutationKey: ["sendMagicLink"], - mutationFn: async (formData: FormData) => await sendMagicLink(formData) + mutationFn: async (email: string) => await sendMagicLink(email) }); const sendPasswordRecoveryMutation = useMutation({ mutationKey: ["sendPasswordRecovery"], - mutationFn: async (formData: FormData) => await sendPasswordRecovery(formData) + mutationFn: async (email: string) => await sendPasswordRecovery(email) }); return { @@ -36,4 +36,54 @@ export function useAuthActions() { sendMagicLink: sendMagicLinkMutation, sendPasswordRecovery: sendPasswordRecoveryMutation }; -} \ No newline at end of file +} + +export const useSignInMutation = () => { + const { signIn } = useAuthActions(); + + return { + signIn: signIn.mutateAsync, + isPending: signIn.isPending, + error: signIn.error, + }; +} + +export const useVerifyOtpMutation = () => { + const { verifyOtp } = useAuthActions(); + + return { + verifyOtp: verifyOtp.mutateAsync, + isPending: verifyOtp.isPending, + error: verifyOtp.error, + } +} + +export const useSignOutMutation = () => { + const { signOut } = useAuthActions(); + + return { + signOut: signOut.mutateAsync, + isPending: signOut.isPending + } +} + +export const useSendMagicLinkMutation = () => { + const { sendMagicLink } = useAuthActions(); + + return { + sendMagicLink: sendMagicLink.mutateAsync, + isPending: sendMagicLink.isPending, + error: sendMagicLink.error, + } +} + +export const useSendPasswordRecoveryMutation = () => { + const { sendPasswordRecovery } = useAuthActions(); + + return { + sendPasswordRecovery: sendPasswordRecovery.mutateAsync, + isPending: sendPasswordRecovery.isPending, + error: sendPasswordRecovery.error, + } +} + diff --git a/sigap-website/app/(pages)/layout.tsx b/sigap-website/app/(pages)/layout.tsx index f8a0d28..93a0fef 100644 --- a/sigap-website/app/(pages)/layout.tsx +++ b/sigap-website/app/(pages)/layout.tsx @@ -54,7 +54,7 @@ export default function RootLayout({ */}
{children} - +
{/*