MIF_E31221222/sigap-website/components/admin/users/sheet.tsx

323 lines
11 KiB
TypeScript

"use client"
import type React from "react"
import { useState } from "react"
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter } from "@/components/ui/sheet"
import { Button } from "@/components/ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Switch } from "@/components/ui/switch"
import { Separator } from "@/components/ui/separator"
import { Badge } from "@/components/ui/badge"
import { useMutation } from "@tanstack/react-query"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { updateUser, deleteUser, sendPasswordRecovery, sendMagicLink, banUser, unbanUser } from "@/app/protected/(admin)/dashboard/user-management/action"
import { User } from "@/src/models/users/users.model"
import { toast } from "sonner"
interface UserSheetProps {
user: User
open: boolean
onOpenChange: (open: boolean) => void
onUserUpdate: () => void
}
export function UserSheet({ user, open, onOpenChange, onUserUpdate }: UserSheetProps) {
const [formData, setFormData] = useState({
email: user.email || "",
phone: user.phone || "",
metadata: JSON.stringify(user.raw_user_meta_data || {}, null, 2),
})
const updateUserMutation = useMutation({
mutationFn: async () => {
let metadata = {}
try {
metadata = JSON.parse(formData.metadata)
} catch (error) {
toast.error("Invalid JSON. Please check your metadata format.")
throw new Error("Invalid JSON")
}
return updateUser(user.id, {
email: formData.email,
phone: formData.phone,
user_metadata: metadata,
})
},
onSuccess: () => {
toast.success("User updated successfully")
onUserUpdate()
},
onError: () => {
toast.error("Failed to update user")
},
})
const deleteUserMutation = useMutation({
mutationFn: () => deleteUser(user.id),
onSuccess: () => {
toast.success("User deleted successfully")
onUserUpdate()
},
onError: () => {
toast.error("Failed to delete user")
},
})
const sendPasswordRecoveryMutation = useMutation({
mutationFn: () => {
if (!user.email) {
throw new Error("User does not have an email address")
}
return sendPasswordRecovery(user.email)
},
onSuccess: () => {
toast.success("Password recovery email sent")
},
onError: () => {
toast.error("Failed to send password recovery email")
},
})
const sendMagicLinkMutation = useMutation({
mutationFn: () => {
if (!user.email) {
throw new Error("User does not have an email address")
}
return sendMagicLink(user.email)
},
onSuccess: () => {
toast.success("Magic link sent successfully")
},
onError: () => {
toast.error("Failed to send magic link")
},
})
const toggleBanMutation = useMutation({
mutationFn: () => {
if (user.banned_until) {
return unbanUser(user.id)
} else {
return banUser(user.id)
}
},
onSuccess: () => {
toast.success("User ban status updated")
onUserUpdate()
},
onError: () => {
toast.error("Failed to update user ban status")
},
})
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setFormData((prev) => ({ ...prev, [name]: value }))
}
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="sm:max-w-md md:max-w-lg overflow-y-auto">
<SheetHeader className="space-y-1">
<SheetTitle className="text-xl flex items-center gap-2">
User Details
{user.banned_until && <Badge variant="destructive">Banned</Badge>}
{!user.email_confirmed_at && <Badge variant="outline">Unconfirmed</Badge>}
{!user.banned_until && user.email_confirmed_at && <Badge variant="default">Active</Badge>}
</SheetTitle>
<SheetDescription>ID: {user.id}</SheetDescription>
</SheetHeader>
<Tabs defaultValue="details" className="mt-6">
<TabsList className="grid grid-cols-3 mb-4">
<TabsTrigger value="details">Details</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger>
<TabsTrigger value="actions">Actions</TabsTrigger>
</TabsList>
<TabsContent value="details" className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" value={formData.email} onChange={handleInputChange} />
</div>
<div className="space-y-2">
<Label htmlFor="phone">Phone</Label>
<Input id="phone" name="phone" value={formData.phone} onChange={handleInputChange} />
</div>
<div className="space-y-2">
<Label htmlFor="metadata">Metadata (JSON)</Label>
<Textarea
id="metadata"
name="metadata"
value={formData.metadata}
onChange={handleInputChange}
className="font-mono text-sm h-40"
/>
</div>
<div className="space-y-2">
<Label>Created At</Label>
<div className="text-sm text-muted-foreground">{new Date(user.created_at).toLocaleString()}</div>
</div>
<div className="space-y-2">
<Label>Last Sign In</Label>
<div className="text-sm text-muted-foreground">
{user.last_sign_in_at ? new Date(user.last_sign_in_at).toLocaleString() : "Never"}
</div>
</div>
<Button
onClick={() => updateUserMutation.mutate()}
disabled={updateUserMutation.isPending}
className="w-full"
>
{updateUserMutation.isPending ? "Saving..." : "Save Changes"}
</Button>
</TabsContent>
<TabsContent value="security" className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="email-confirmed">Email Confirmed</Label>
<Switch id="email-confirmed" checked={!!user.email_confirmed_at} disabled />
</div>
{user.email_confirmed_at && (
<div className="text-xs text-muted-foreground">
Confirmed at: {new Date(user.email_confirmed_at).toLocaleString()}
</div>
)}
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="phone-confirmed">Phone Confirmed</Label>
<Switch id="phone-confirmed" checked={!!user.phone_confirmed_at} disabled />
</div>
{user.phone_confirmed_at && (
<div className="text-xs text-muted-foreground">
Confirmed at: {new Date(user.phone_confirmed_at).toLocaleString()}
</div>
)}
</div>
<Separator />
<div className="space-y-2">
<Label>Authentication Factors</Label>
<div className="text-sm text-muted-foreground">
{user.factors?.length
? user.factors.map((factor, i) => (
<div key={i} className="flex items-center gap-2">
<Badge variant="outline">{factor.factor_type}</Badge>
<span>{new Date(factor.created_at).toLocaleString()}</span>
</div>
))
: "No authentication factors"}
</div>
</div>
<Separator />
<div className="space-y-2">
<Label>Password Reset</Label>
<Button
variant="outline"
onClick={() => sendPasswordRecoveryMutation.mutate()}
disabled={sendPasswordRecoveryMutation.isPending || !user.email}
className="w-full"
>
Send Password Recovery Email
</Button>
</div>
<div className="space-y-2">
<Label>Magic Link</Label>
<Button
variant="outline"
onClick={() => sendMagicLinkMutation.mutate()}
disabled={sendMagicLinkMutation.isPending || !user.email}
className="w-full"
>
Send Magic Link
</Button>
</div>
</TabsContent>
<TabsContent value="actions" className="space-y-4">
<div className="space-y-2">
<Label>Ban User</Label>
<Button
variant={user.banned_until ? "default" : "destructive"}
onClick={() => toggleBanMutation.mutate()}
disabled={toggleBanMutation.isPending}
className="w-full"
>
{user.banned_until ? "Unban User" : "Ban User"}
</Button>
{user.banned_until && (
<div className="text-xs text-muted-foreground">
Banned until: {new Date(user.banned_until).toLocaleString()}
</div>
)}
</div>
<Separator />
<div className="space-y-2">
<Label>Delete User</Label>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" className="w-full">
Delete User
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the user account and remove their data
from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteUserMutation.mutate()}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</TabsContent>
</Tabs>
<SheetFooter className="mt-4">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Close
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
)
}