initial commit
This commit is contained in:
586
components/admin/UserManagement.tsx
Normal file
586
components/admin/UserManagement.tsx
Normal file
@@ -0,0 +1,586 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Search, Edit, Trash2, UserPlus, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
|
||||
interface User {
|
||||
_id: string
|
||||
name: string
|
||||
email: string
|
||||
siliconId: string
|
||||
role: 'user' | 'admin'
|
||||
isVerified: boolean
|
||||
balance: number
|
||||
lastLogin: string
|
||||
createdAt: string
|
||||
provider: string
|
||||
}
|
||||
|
||||
interface UserManagementProps {
|
||||
initialUsers?: User[]
|
||||
}
|
||||
|
||||
export default function UserManagement({ initialUsers = [] }: UserManagementProps) {
|
||||
const [users, setUsers] = useState<User[]>(initialUsers)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
const [roleFilter, setRoleFilter] = useState('all')
|
||||
const [verifiedFilter, setVerifiedFilter] = useState('all')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null)
|
||||
const [showAddDialog, setShowAddDialog] = useState(false)
|
||||
const [editForm, setEditForm] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
role: 'user' as 'user' | 'admin',
|
||||
isVerified: false,
|
||||
balance: 0,
|
||||
})
|
||||
const [addForm, setAddForm] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role: 'user' as 'user' | 'admin',
|
||||
isVerified: false,
|
||||
balance: 0,
|
||||
})
|
||||
|
||||
const fetchUsers = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const token =
|
||||
localStorage.getItem('accessToken') ||
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('accessToken='))
|
||||
?.split('=')[1]
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: currentPage.toString(),
|
||||
limit: '20',
|
||||
search,
|
||||
role: roleFilter,
|
||||
verified: verifiedFilter,
|
||||
})
|
||||
|
||||
const response = await fetch(`/api/admin/users?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch users')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setUsers(data.users)
|
||||
setTotalPages(data.pagination.totalPages)
|
||||
} catch (error) {
|
||||
console.error('Users fetch error:', error)
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to load users',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [search, roleFilter, verifiedFilter, currentPage])
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers()
|
||||
}, [fetchUsers])
|
||||
|
||||
const handleEditUser = (user: User) => {
|
||||
setEditingUser(user)
|
||||
setEditForm({
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
isVerified: user.isVerified,
|
||||
balance: user.balance,
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdateUser = async () => {
|
||||
if (!editingUser) return
|
||||
|
||||
try {
|
||||
const token =
|
||||
localStorage.getItem('accessToken') ||
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('accessToken='))
|
||||
?.split('=')[1]
|
||||
|
||||
const response = await fetch('/api/admin/users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'update',
|
||||
userId: editingUser._id,
|
||||
data: editForm,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update user')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setUsers(users.map((u) => (u._id === editingUser._id ? data.user : u)))
|
||||
setEditingUser(null)
|
||||
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'User updated successfully',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('User update error:', error)
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to update user',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteUser = async (userId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this user?')) return
|
||||
|
||||
try {
|
||||
const token =
|
||||
localStorage.getItem('accessToken') ||
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('accessToken='))
|
||||
?.split('=')[1]
|
||||
|
||||
const response = await fetch('/api/admin/users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'delete',
|
||||
userId,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete user')
|
||||
}
|
||||
|
||||
setUsers(users.filter((u) => u._id !== userId))
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'User deleted successfully',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error deleting user:', error)
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to delete user',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddUser = async () => {
|
||||
try {
|
||||
const token =
|
||||
localStorage.getItem('accessToken') ||
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('accessToken='))
|
||||
?.split('=')[1]
|
||||
|
||||
const response = await fetch('/api/admin/users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'create',
|
||||
data: addForm,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create user')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setUsers([data.user, ...users])
|
||||
setShowAddDialog(false)
|
||||
setAddForm({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role: 'user',
|
||||
isVerified: false,
|
||||
balance: 0,
|
||||
})
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'User created successfully',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error creating user:', error)
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to create user',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'INR',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>User Management</CardTitle>
|
||||
<Button onClick={() => setShowAddDialog(true)}>
|
||||
<UserPlus className="h-4 w-4 mr-2" />
|
||||
Add User
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* Filters */}
|
||||
<div className="flex flex-col md:flex-row gap-4 mb-6">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Search users..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Select value={roleFilter} onValueChange={setRoleFilter}>
|
||||
<SelectTrigger className="w-full md:w-40">
|
||||
<SelectValue placeholder="Role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Roles</SelectItem>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={verifiedFilter} onValueChange={setVerifiedFilter}>
|
||||
<SelectTrigger className="w-full md:w-40">
|
||||
<SelectValue placeholder="Verified" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Users</SelectItem>
|
||||
<SelectItem value="true">Verified</SelectItem>
|
||||
<SelectItem value="false">Unverified</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Users Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left py-3 px-4">User</th>
|
||||
<th className="text-left py-3 px-4">Role</th>
|
||||
<th className="text-left py-3 px-4">Status</th>
|
||||
<th className="text-left py-3 px-4">Balance</th>
|
||||
<th className="text-left py-3 px-4">Last Login</th>
|
||||
<th className="text-left py-3 px-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center py-8">
|
||||
Loading users...
|
||||
</td>
|
||||
</tr>
|
||||
) : users.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center py-8 text-gray-500">
|
||||
No users found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
users.map((user) => (
|
||||
<tr key={user._id} className="border-b hover:bg-gray-50">
|
||||
<td className="py-3 px-4">
|
||||
<div>
|
||||
<p className="font-medium">{user.name}</p>
|
||||
<p className="text-sm text-gray-500">{user.email}</p>
|
||||
<p className="text-xs text-gray-400">ID: {user.siliconId}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<Badge variant={user.role === 'admin' ? 'default' : 'secondary'}>
|
||||
{user.role}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant={user.isVerified ? 'default' : 'destructive'}>
|
||||
{user.isVerified ? 'Verified' : 'Unverified'}
|
||||
</Badge>
|
||||
<Badge variant="outline">{user.provider}</Badge>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="font-medium">{formatCurrency(user.balance)}</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="text-sm text-gray-600">
|
||||
{user.lastLogin
|
||||
? formatDistanceToNow(new Date(user.lastLogin), { addSuffix: true })
|
||||
: 'Never'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={() => handleEditUser(user)}>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteUser(user._id)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-6">
|
||||
<p className="text-sm text-gray-600">
|
||||
Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add User Dialog */}
|
||||
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New User</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="add-name">Name</Label>
|
||||
<Input
|
||||
id="add-name"
|
||||
value={addForm.name}
|
||||
onChange={(e) => setAddForm({ ...addForm, name: e.target.value })}
|
||||
placeholder="Enter user name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="add-email">Email</Label>
|
||||
<Input
|
||||
id="add-email"
|
||||
type="email"
|
||||
value={addForm.email}
|
||||
onChange={(e) => setAddForm({ ...addForm, email: e.target.value })}
|
||||
placeholder="Enter email address"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="add-password">Password</Label>
|
||||
<Input
|
||||
id="add-password"
|
||||
type="password"
|
||||
value={addForm.password}
|
||||
onChange={(e) => setAddForm({ ...addForm, password: e.target.value })}
|
||||
placeholder="Enter password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="add-role">Role</Label>
|
||||
<Select
|
||||
value={addForm.role}
|
||||
onValueChange={(value: 'user' | 'admin') =>
|
||||
setAddForm({ ...addForm, role: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="add-balance">Initial Balance (₹)</Label>
|
||||
<Input
|
||||
id="add-balance"
|
||||
type="number"
|
||||
value={addForm.balance}
|
||||
onChange={(e) =>
|
||||
setAddForm({ ...addForm, balance: parseInt(e.target.value) || 0 })
|
||||
}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="add-verified"
|
||||
checked={addForm.isVerified}
|
||||
onCheckedChange={(checked) => setAddForm({ ...addForm, isVerified: checked })}
|
||||
/>
|
||||
<Label htmlFor="add-verified">Verified</Label>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={() => setShowAddDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleAddUser}>Create User</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Edit User Dialog */}
|
||||
<Dialog open={!!editingUser} onOpenChange={() => setEditingUser(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit User</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={editForm.name}
|
||||
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={editForm.email}
|
||||
onChange={(e) => setEditForm({ ...editForm, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="role">Role</Label>
|
||||
<Select
|
||||
value={editForm.role}
|
||||
onValueChange={(value: 'user' | 'admin') =>
|
||||
setEditForm({ ...editForm, role: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="balance">Balance (₹)</Label>
|
||||
<Input
|
||||
id="balance"
|
||||
type="number"
|
||||
value={editForm.balance}
|
||||
onChange={(e) =>
|
||||
setEditForm({ ...editForm, balance: parseInt(e.target.value) || 0 })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="verified"
|
||||
checked={editForm.isVerified}
|
||||
onCheckedChange={(checked) => setEditForm({ ...editForm, isVerified: checked })}
|
||||
/>
|
||||
<Label htmlFor="verified">Verified</Label>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={() => setEditingUser(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleUpdateUser}>Update User</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user