RFP_Space_V05 / components /UserManagementClient.tsx
Varun10000's picture
Upload 89 files
17cf14d verified
Raw
History Blame Contribute Delete
19.5 kB
'use client'
import { useCallback, useEffect, useState } from 'react'
import { useCurrentUser } from '@/lib/hooks/usePermissions'
import { broadcastPermissionsRefresh, usePermissions } from '@/lib/hooks/usePermissions'
interface Permission {
id: string
resource: string
action: string
description: string | null
}
interface Role {
id: string
name: string
description: string | null
}
interface User {
id: string
name: string
email: string
isActive: boolean
role: Role | null
roleId: string | null
createdAt: string
}
export default function UserManagementClient() {
const [users, setUsers] = useState<User[]>([])
const [roles, setRoles] = useState<Role[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [showCreateModal, setShowCreateModal] = useState(false)
const [newUserName, setNewUserName] = useState('')
const [newUserEmail, setNewUserEmail] = useState('')
const [newUserRoleId, setNewUserRoleId] = useState('')
const [newUserPassword, setNewUserPassword] = useState('')
const [newUserPasswordConfirm, setNewUserPasswordConfirm] = useState('')
// Bulk role assignment
const [selectedUsers, setSelectedUsers] = useState<string[]>([])
const [bulkRoleId, setBulkRoleId] = useState<string>('')
const { userId } = useCurrentUser()
const { hasPermission, loading: permsLoading } = usePermissions()
const fetchUsers = useCallback(async () => {
if (!userId) return
try {
const res = await fetch('/api/users', {
headers: {
'x-user-id': userId || '',
},
cache: 'no-store',
})
if (!res.ok) throw new Error('Failed to fetch users')
const data = await res.json()
setUsers(data)
setLoading(false)
} catch (err: any) {
setError(err.message)
setLoading(false)
}
}, [userId])
const fetchRoles = useCallback(async () => {
if (!userId) return
try {
const res = await fetch('/api/roles', {
headers: {
'x-user-id': userId || '',
},
cache: 'no-store',
})
if (!res.ok) throw new Error('Failed to fetch roles')
const data = await res.json()
setRoles(data)
} catch (err: any) {
console.error('Error fetching roles:', err)
}
}, [userId])
useEffect(() => {
if (!userId) return
fetchUsers()
fetchRoles()
}, [fetchRoles, fetchUsers, userId])
const handleToggleUserStatus = async (user: User) => {
try {
const res = await fetch(`/api/users/${user.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'x-user-id': userId || '',
},
cache: 'no-store',
body: JSON.stringify({
isActive: !user.isActive,
}),
})
if (!res.ok) throw new Error('Failed to update user status')
await fetchUsers()
broadcastPermissionsRefresh()
} catch (err: any) {
alert('Error updating user: ' + err.message)
}
}
const toggleUserSelection = (userId: string) => {
setSelectedUsers(prev =>
prev.includes(userId)
? prev.filter(id => id !== userId)
: [...prev, userId]
)
}
const toggleSelectAll = () => {
if (selectedUsers.length === users.length) {
setSelectedUsers([])
} else {
setSelectedUsers(users.map(u => u.id))
}
}
const handleBulkAssignRole = async () => {
if (selectedUsers.length === 0) {
alert('Please select at least one user')
return
}
if (!bulkRoleId) {
alert('Please select a role to assign')
return
}
try {
// Update all selected users
await Promise.all(
selectedUsers.map(selectedUserId =>
fetch(`/api/users/${selectedUserId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'x-user-id': userId || '',
},
cache: 'no-store',
body: JSON.stringify({ roleId: bulkRoleId }),
})
)
)
setSelectedUsers([])
setBulkRoleId('')
await fetchUsers()
broadcastPermissionsRefresh()
alert(`Successfully assigned role to ${selectedUsers.length} user(s)`)
} catch (err: any) {
alert('Error assigning roles: ' + err.message)
}
}
const handleCreateUser = async () => {
if (!newUserName || !newUserEmail) {
alert('Please enter name and email')
return
}
const wantsToSetPassword = Boolean(newUserPassword || newUserPasswordConfirm)
const canManagePasswords = !permsLoading && hasPermission('users', 'manage')
if (wantsToSetPassword && !canManagePasswords) {
alert('You do not have permission to set passwords. Ask an admin to set it after the user is created.')
return
}
if (wantsToSetPassword) {
if (newUserPassword.length < 8) {
alert('Password must be at least 8 characters')
return
}
if (newUserPassword !== newUserPasswordConfirm) {
alert('Passwords do not match')
return
}
}
try {
const res = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-user-id': userId || '',
},
cache: 'no-store',
body: JSON.stringify({
name: newUserName,
email: newUserEmail,
roleId: newUserRoleId || null,
}),
})
if (!res.ok) {
const data = await res.json()
throw new Error(data.error || 'Failed to create user')
}
const data = await res.json().catch(() => ({} as any))
const createdUser: User | undefined = data?.user
if (wantsToSetPassword && createdUser?.id) {
const pwRes = await fetch(`/api/users/${createdUser.id}/password`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'x-user-id': userId || '',
},
cache: 'no-store',
body: JSON.stringify({ password: newUserPassword }),
})
const pwData = await pwRes.json().catch(() => ({}))
if (!pwRes.ok) {
throw new Error(pwData?.error || 'User created, but failed to set password')
}
}
setShowCreateModal(false)
setNewUserName('')
setNewUserEmail('')
setNewUserRoleId('')
setNewUserPassword('')
setNewUserPasswordConfirm('')
await fetchUsers()
broadcastPermissionsRefresh()
alert(wantsToSetPassword ? 'User created and password set successfully!' : 'User created successfully!')
} catch (err: any) {
alert('Error creating user: ' + err.message)
}
}
const handleDeleteUser = async (user: User) => {
if (!confirm(`Are you sure you want to delete ${user.name}? This action cannot be undone.`)) {
return
}
try {
const res = await fetch(`/api/users/${user.id}`, {
method: 'DELETE',
headers: {
'x-user-id': userId || '',
},
cache: 'no-store',
})
if (!res.ok) {
const data = await res.json()
throw new Error(data.error || 'Failed to delete user')
}
await fetchUsers()
broadcastPermissionsRefresh()
alert('User deleted successfully!')
} catch (err: any) {
alert('Error deleting user: ' + err.message)
}
}
const handleSetUserPassword = async (user: User) => {
if (!userId) {
alert('Missing user session. Please sign in again.')
return
}
const pw1 = prompt(`Set a new password for ${user.email} (min 8 chars):`) || ''
if (!pw1) return
if (pw1.length < 8) {
alert('Password must be at least 8 characters')
return
}
const pw2 = prompt('Confirm new password:') || ''
if (pw1 !== pw2) {
alert('Passwords do not match')
return
}
try {
const res = await fetch(`/api/users/${user.id}/password`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'x-user-id': userId || '',
},
cache: 'no-store',
body: JSON.stringify({ password: pw1 }),
})
const data = await res.json().catch(() => ({}))
if (!res.ok) {
alert(data?.error || 'Failed to set password')
return
}
alert('Password set successfully')
} catch (err: any) {
alert('Error setting password: ' + (err?.message || String(err)))
}
}
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-lg">Loading users...</div>
</div>
)
}
if (error) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-red-600">Error: {error}</div>
</div>
)
}
return (
<div className="p-8">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">User Management</h1>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"
>
+ Add New User
</button>
</div>
{/* Bulk Role Assignment */}
{selectedUsers.length > 0 && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6 flex items-center gap-4">
<span className="font-medium text-blue-900">
{selectedUsers.length} user(s) selected
</span>
<select
value={bulkRoleId}
onChange={(e) => setBulkRoleId(e.target.value)}
className="border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select a role to assign</option>
{roles.map((role) => (
<option key={role.id} value={role.id}>
{role.name}
</option>
))}
</select>
<button
onClick={handleBulkAssignRole}
disabled={!bulkRoleId}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
Assign Role
</button>
<button
onClick={() => setSelectedUsers([])}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
>
Clear Selection
</button>
</div>
)}
<div className="bg-white shadow rounded-lg overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left">
<input
type="checkbox"
checked={selectedUsers.length === users.length && users.length > 0}
onChange={toggleSelectAll}
className="w-4 h-4 rounded border-gray-300 focus:ring-2 focus:ring-blue-500 cursor-pointer"
/>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
User
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Role
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{users.map((user) => (
<tr
key={user.id}
className={selectedUsers.includes(user.id) ? 'bg-blue-50' : 'hover:bg-gray-50'}
>
<td className="px-6 py-4 whitespace-nowrap">
<input
type="checkbox"
checked={selectedUsers.includes(user.id)}
onChange={() => toggleUserSelection(user.id)}
className="w-4 h-4 rounded border-gray-300 focus:ring-2 focus:ring-blue-500 cursor-pointer"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{user.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-500">{user.email}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
{user.role?.name || 'No Role'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
user.isActive
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}
>
{user.isActive ? 'Active' : 'Inactive'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
{!permsLoading && hasPermission('users', 'manage') && (
<button
onClick={() => handleSetUserPassword(user)}
className="text-blue-600 hover:text-blue-900 mr-4"
>
Set Password
</button>
)}
<button
onClick={() => handleToggleUserStatus(user)}
className={
user.isActive
? 'text-orange-600 hover:text-orange-900 mr-4'
: 'text-green-600 hover:text-green-900 mr-4'
}
>
{user.isActive ? 'Deactivate' : 'Activate'}
</button>
<button
onClick={() => handleDeleteUser(user)}
className="text-red-600 hover:text-red-900"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Create User Modal */}
{showCreateModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-8 max-w-md w-full">
<h2 className="text-2xl font-bold mb-6">Add New User</h2>
<div className="space-y-4 mb-6">
<div>
<label className="block text-sm font-medium mb-2">Full Name *</label>
<input
type="text"
value={newUserName}
onChange={(e) => setNewUserName(e.target.value)}
placeholder="John Doe"
className="w-full border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Email Address *</label>
<input
type="email"
value={newUserEmail}
onChange={(e) => setNewUserEmail(e.target.value)}
placeholder="user@company.com"
className="w-full border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Assign Role</label>
<select
value={newUserRoleId}
onChange={(e) => setNewUserRoleId(e.target.value)}
className="w-full border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">No Role (Assign Later)</option>
{roles.map((role) => (
<option key={role.id} value={role.id}>
{role.name}
</option>
))}
</select>
</div>
{!permsLoading && hasPermission('users', 'manage') && (
<>
<div>
<label className="block text-sm font-medium mb-2">Set Password (optional)</label>
<input
type="password"
value={newUserPassword}
onChange={(e) => setNewUserPassword(e.target.value)}
placeholder="Minimum 8 characters"
className="w-full border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Confirm Password</label>
<input
type="password"
value={newUserPasswordConfirm}
onChange={(e) => setNewUserPasswordConfirm(e.target.value)}
placeholder="Re-enter password"
className="w-full border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</>
)}
</div>
<div className="flex justify-end gap-3">
<button
onClick={() => {
setShowCreateModal(false)
setNewUserName('')
setNewUserEmail('')
setNewUserRoleId('')
setNewUserPassword('')
setNewUserPasswordConfirm('')
}}
className="px-4 py-2 border rounded hover:bg-gray-50"
>
Cancel
</button>
<button
onClick={handleCreateUser}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Create User
</button>
</div>
</div>
</div>
)}
</div>
)
}