-
Profile Information
-
Profile data will be loaded here.
+
+ Phone
+
+
+
+ Metadata (JSON)
+
+
+
+
Created At
+
{new Date(user.created_at).toLocaleString()}
+
+
+
Last Sign In
+
+ {user.last_sign_in_at ? new Date(user.last_sign_in_at).toLocaleString() : "Never"}
-
-
-
-
-
-
-
Reset password
-
Send a password recovery email to the user
-
-
-
- Send password recovery
-
-
-
+
+ updateUserMutation.mutate()}
+ disabled={updateUserMutation.isPending}
+ className="w-full"
+ >
+ {updateUserMutation.isPending ? "Saving..." : "Save Changes"}
+
+
-
-
-
-
Send magic link
-
Passwordless login via email for the user
-
-
-
- Send magic link
-
-
-
-
-
-
Danger zone
-
- Be wary of the following features as they cannot be undone.
-
-
-
-
-
-
-
Remove MFA factors
-
This will log the user out of all active sessions
-
-
-
- Remove MFA factors
-
-
-
-
-
-
-
-
Ban user
-
Revoke access to the project for a set duration
-
-
-
- Ban user
-
-
-
-
-
-
-
-
Delete user
-
User will no longer have access to the project
-
-
-
-
-
- Delete user
-
-
-
-
- Are you absolutely sure?
-
- This action cannot be undone. This will permanently delete the user account and remove
- their data from our servers.
-
-
-
-
- Cancel
-
-
- Delete
-
-
-
-
-
-
-
-
+
+
+
+ Email Confirmed
+
-
-
-
+ {user.email_confirmed_at && (
+
+ Confirmed at: {new Date(user.email_confirmed_at).toLocaleString()}
+
+ )}
+
+
+
+
+ Phone Confirmed
+
+
+ {user.phone_confirmed_at && (
+
+ Confirmed at: {new Date(user.phone_confirmed_at).toLocaleString()}
+
+ )}
+
+
+
+
+
+
Authentication Factors
+
+ {user.factors?.length
+ ? user.factors.map((factor, i) => (
+
+ {factor.factor_type}
+ {new Date(factor.created_at).toLocaleString()}
+
+ ))
+ : "No authentication factors"}
+
+
+
+
+
+
+ Password Reset
+ sendPasswordRecoveryMutation.mutate()}
+ disabled={sendPasswordRecoveryMutation.isPending || !user.email}
+ className="w-full"
+ >
+ Send Password Recovery Email
+
+
+
+
+ Magic Link
+ sendMagicLinkMutation.mutate()}
+ disabled={sendMagicLinkMutation.isPending || !user.email}
+ className="w-full"
+ >
+ Send Magic Link
+
+
+
+
+
+
+
Ban User
+
toggleBanMutation.mutate()}
+ disabled={toggleBanMutation.isPending}
+ className="w-full"
+ >
+ {user.banned_until ? "Unban User" : "Ban User"}
+
+ {user.banned_until && (
+
+ Banned until: {new Date(user.banned_until).toLocaleString()}
+
+ )}
+
+
+
+
+
+
Delete User
+
+
+
+ Delete User
+
+
+
+
+ Are you absolutely sure?
+
+ This action cannot be undone. This will permanently delete the user account and remove their data
+ from our servers.
+
+
+
+ Cancel
+ deleteUserMutation.mutate()}
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ >
+ Delete
+
+
+
+
+
+
+
+
+
+ onOpenChange(false)}>
+ Close
+
+
)
diff --git a/sigap-website/components/admin/users/user-form.tsx b/sigap-website/components/admin/users/user-form.tsx
index 0b87af7..0b11988 100644
--- a/sigap-website/components/admin/users/user-form.tsx
+++ b/sigap-website/components/admin/users/user-form.tsx
@@ -1,148 +1,148 @@
-"use client"
+// "use client"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { useForm } from "react-hook-form"
-import { z } from "zod"
+// import { zodResolver } from "@hookform/resolvers/zod"
+// import { useForm } from "react-hook-form"
+// import { z } from "zod"
-import { Button } from "@/components/ui/button"
-import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
-import { Input } from "@/components/ui/input"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
-import { useState } from "react"
-import { User } from "./column"
-import { updateUser } from "../../user-management/action"
-import { toast } from "@/app/_hooks/use-toast"
+// import { Button } from "@/components/ui/button"
+// import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
+// import { Input } from "@/components/ui/input"
+// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+// import { useState } from "react"
+// import { User } from "./column"
+// import { updateUser } from "../../user-management/action"
+// import { toast } from "@/app/_hooks/use-toast"
-const userFormSchema = z.object({
- email: z.string().email({ message: "Please enter a valid email address" }),
- first_name: z.string().nullable(),
- last_name: z.string().nullable(),
- role: z.enum(["user", "admin", "moderator"]),
-})
+// const userFormSchema = z.object({
+// email: z.string().email({ message: "Please enter a valid email address" }),
+// first_name: z.string().nullable(),
+// last_name: z.string().nullable(),
+// role: z.enum(["user", "admin", "moderator"]),
+// })
-type UserFormValues = z.infer
+// type UserFormValues = z.infer
-interface UserFormProps {
- user: User
-}
+// interface UserFormProps {
+// user: User
+// }
-export function UserForm({ user }: UserFormProps) {
- const [isSubmitting, setIsSubmitting] = useState(false)
+// export function UserForm({ user }: UserFormProps) {
+// const [isSubmitting, setIsSubmitting] = useState(false)
- const form = useForm({
- resolver: zodResolver(userFormSchema),
- defaultValues: {
- email: user.email,
- first_name: user.first_name,
- last_name: user.last_name,
- role: user.role as "user" | "admin" | "moderator",
- },
- })
+// const form = useForm({
+// resolver: zodResolver(userFormSchema),
+// defaultValues: {
+// email: user.email,
+// first_name: user.first_name,
+// last_name: user.last_name,
+// role: user.role as "user" | "admin" | "moderator",
+// },
+// })
- async function onSubmit(data: UserFormValues) {
- try {
- setIsSubmitting(true)
- await updateUser(user.id, data)
- toast({
- title: "User updated",
- description: "The user" + user.email + " has been updated.",
- })
- } catch (error) {
- toast({
- title: "Failed to update user",
- description: "An error occurred while updating the user.",
- variant: "destructive",
- })
- console.error(error)
- } finally {
- setIsSubmitting(false)
- }
- }
+// async function onSubmit(data: UserFormValues) {
+// try {
+// setIsSubmitting(true)
+// await updateUser(user.id, data)
+// toast({
+// title: "User updated",
+// description: "The user" + user.email + " has been updated.",
+// })
+// } catch (error) {
+// toast({
+// title: "Failed to update user",
+// description: "An error occurred while updating the user.",
+// variant: "destructive",
+// })
+// console.error(error)
+// } finally {
+// setIsSubmitting(false)
+// }
+// }
- return (
-
-
- )
-}
+// return (
+//
+//
+// )
+// }
diff --git a/sigap-website/components/admin/users/user-management.tsx b/sigap-website/components/admin/users/user-management.tsx
new file mode 100644
index 0000000..f50b896
--- /dev/null
+++ b/sigap-website/components/admin/users/user-management.tsx
@@ -0,0 +1,219 @@
+"use client"
+
+import { useState } from "react"
+import { PlusCircle, Search, Filter, MoreHorizontal, X, ChevronDown } from 'lucide-react'
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Badge } from "@/components/ui/badge"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { useQuery } from "@tanstack/react-query"
+import { fetchUsers } from "@/app/protected/(admin)/dashboard/user-management/action"
+import { User } from "@/src/models/users/users.model"
+import { toast } from "sonner"
+import { DataTable } from "./data-table"
+import { UserSheet } from "./sheet"
+import { InviteUserDialog } from "./invite-user"
+import { AddUserDialog } from "./add-user-dialog"
+
+
+export default function UserManagement() {
+ const [searchQuery, setSearchQuery] = useState("")
+ const [selectedUser, setSelectedUser] = useState(null)
+ const [isSheetOpen, setIsSheetOpen] = useState(false)
+ const [isAddUserOpen, setIsAddUserOpen] = useState(false)
+ const [isInviteUserOpen, setIsInviteUserOpen] = useState(false)
+
+
+ // Use React Query to fetch users
+ const { data: users = [], isLoading, refetch } = useQuery({
+ queryKey: ['users'],
+ queryFn: async () => {
+ try {
+ return await fetchUsers()
+ } catch (error) {
+ toast.error("Failed to fetch users")
+ return []
+ }
+ }
+ })
+
+ const handleUserClick = (user: User) => {
+ setSelectedUser(user)
+ setIsSheetOpen(true)
+ }
+
+ const handleUserUpdate = () => {
+ refetch()
+ setIsSheetOpen(false)
+ }
+
+ const filteredUsers = users.filter((user) => {
+ if (!searchQuery) return true
+
+ const query = searchQuery.toLowerCase()
+ return (
+ user.email?.toLowerCase().includes(query) ||
+ user.phone?.toLowerCase().includes(query) ||
+ user.id.toLowerCase().includes(query)
+ )
+ })
+
+ const columns = [
+ {
+ id: "email",
+ header: "Email",
+ cell: ({ row }: { row: { original: User } }) => (
+
+
+ {row.original.email?.[0]?.toUpperCase() || "?"}
+
+
+
{row.original.email || "No email"}
+
{row.original.id}
+
+
+ ),
+ filterFn: (row: any, id: string, value: string) => {
+ return row.original.email?.toLowerCase().includes(value.toLowerCase())
+ },
+ },
+ {
+ id: "phone",
+ header: "Phone",
+ cell: ({ row }: { row: { original: User } }) => row.original.phone || "-",
+ filterFn: (row: any, id: string, value: string) => {
+ return row.original.phone?.toLowerCase().includes(value.toLowerCase())
+ },
+ },
+ {
+ id: "lastSignIn",
+ header: "Last Sign In",
+ cell: ({ row }: { row: { original: User } }) => {
+ return row.original.last_sign_in_at
+ ? new Date(row.original.last_sign_in_at).toLocaleString()
+ : "Never"
+ },
+ },
+ {
+ id: "createdAt",
+ header: "Created At",
+ cell: ({ row }: { row: { original: User } }) => {
+ return new Date(row.original.created_at).toLocaleString()
+ },
+ },
+ {
+ id: "status",
+ header: "Status",
+ cell: ({ row }: { row: { original: User } }) => {
+ if (row.original.banned_until) {
+ return Banned
+ }
+ if (!row.original.email_confirmed_at) {
+ return Unconfirmed
+ }
+ return Active
+ },
+ filterFn: (row: any, id: string, value: string) => {
+ const status = row.original.banned_until
+ ? "banned"
+ : !row.original.email_confirmed_at
+ ? "unconfirmed"
+ : "active"
+ return status.includes(value.toLowerCase())
+ },
+ },
+ {
+ id: "actions",
+ header: "",
+ cell: ({ row }: { row: { original: User } }) => (
+ {
+ e.stopPropagation()
+ handleUserClick(row.original)
+ }}>
+
+
+ ),
+ },
+ ]
+
+ return (
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+ {searchQuery && (
+ setSearchQuery("")}
+ >
+
+
+ )}
+
+
+
+
+
+
+ Add User
+
+
+
+
+ setIsAddUserOpen(true)}>
+ Add User
+
+ setIsInviteUserOpen(true)}>
+ Invite User
+
+
+
+
+
+
+
+
+
+
handleUserClick(user)}
+ />
+
+ {selectedUser && (
+
+ )}
+
+ refetch()}
+ />
+
+ refetch()}
+ />
+
+ )
+}
diff --git a/sigap-website/components/auth/verify-otp-form.tsx b/sigap-website/components/auth/verify-otp-form.tsx
index 937fd28..5b5e511 100644
--- a/sigap-website/components/auth/verify-otp-form.tsx
+++ b/sigap-website/components/auth/verify-otp-form.tsx
@@ -48,7 +48,7 @@ export function VerifyOtpForm({ className, ...props }: VerifyOtpFormProps) {