549 lines
15 KiB
TypeScript
549 lines
15 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from '@/components/ui/alert-dialog';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
import { useToast } from '@/hooks/use-toast';
|
|
import { UserPlus, Edit, Trash2, Search, Users, Mail, Calendar } from 'lucide-react';
|
|
|
|
interface User {
|
|
id: string;
|
|
name: string;
|
|
surname: string;
|
|
email: string;
|
|
role: 'user' | 'admin';
|
|
createdAt: string;
|
|
}
|
|
|
|
interface UserFormData {
|
|
name: string;
|
|
surname: string;
|
|
email: string;
|
|
role: 'user' | 'admin';
|
|
password?: string;
|
|
}
|
|
|
|
export function AdminUserManagement() {
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
const [editingUser, setEditingUser] = useState<User | null>(null);
|
|
const [userToDelete, setUserToDelete] = useState<User | null>(null);
|
|
const [formData, setFormData] = useState<UserFormData>({
|
|
name: '',
|
|
surname: '',
|
|
email: '',
|
|
role: 'user',
|
|
password: '',
|
|
});
|
|
const { toast } = useToast();
|
|
|
|
useEffect(() => {
|
|
fetchUsers();
|
|
}, []);
|
|
|
|
const fetchUsers = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await fetch('/api/admin/users');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setUsers(data.users);
|
|
} else {
|
|
throw new Error('Failed to fetch users');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching users:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to fetch users',
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCreateUser = async (e?: React.FormEvent) => {
|
|
try {
|
|
// Prevent form submission and double submissions
|
|
if (e) e.preventDefault();
|
|
if (loading) return;
|
|
|
|
if (!formData.name || !formData.surname || !formData.email || !formData.password) {
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Please fill in all required fields',
|
|
variant: 'destructive',
|
|
});
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
const response = await fetch('/api/admin/users', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(formData),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
toast({
|
|
title: 'Success',
|
|
description: 'User created successfully',
|
|
});
|
|
setIsCreateDialogOpen(false);
|
|
resetForm();
|
|
fetchUsers();
|
|
} else {
|
|
toast({
|
|
title: 'Error',
|
|
description: data.error || 'Failed to create user',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating user:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to create user',
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleEditUser = async () => {
|
|
try {
|
|
// Prevent double submissions
|
|
if (loading) return;
|
|
|
|
if (!editingUser || !formData.name || !formData.surname || !formData.email) {
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Please fill in all required fields',
|
|
variant: 'destructive',
|
|
});
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
const updateData = { ...formData };
|
|
if (!updateData.password) {
|
|
delete updateData.password; // Don't update password if not provided
|
|
}
|
|
|
|
const response = await fetch(`/api/admin/users/${editingUser.id}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(updateData),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
toast({
|
|
title: 'Success',
|
|
description: 'User updated successfully',
|
|
});
|
|
setIsEditDialogOpen(false);
|
|
setEditingUser(null);
|
|
resetForm();
|
|
fetchUsers();
|
|
} else {
|
|
toast({
|
|
title: 'Error',
|
|
description: data.error || 'Failed to update user',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating user:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to update user',
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const openDeleteDialog = (user: User) => {
|
|
setUserToDelete(user);
|
|
setIsDeleteDialogOpen(true);
|
|
};
|
|
|
|
const confirmDeleteUser = async () => {
|
|
if (userToDelete) {
|
|
await handleDeleteUser(userToDelete.id);
|
|
setIsDeleteDialogOpen(false);
|
|
setUserToDelete(null);
|
|
}
|
|
};
|
|
|
|
const handleDeleteUser = async (userId: string) => {
|
|
try {
|
|
const response = await fetch(`/api/admin/users/${userId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
toast({
|
|
title: 'Success',
|
|
description: 'User deleted successfully',
|
|
});
|
|
fetchUsers();
|
|
} else {
|
|
toast({
|
|
title: 'Error',
|
|
description: data.error || 'Failed to delete user',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting user:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to delete user',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
const openEditDialog = (user: User) => {
|
|
setEditingUser(user);
|
|
setFormData({
|
|
name: user.name,
|
|
surname: user.surname,
|
|
email: user.email,
|
|
role: user.role,
|
|
password: '', // Don't pre-fill password
|
|
});
|
|
setIsEditDialogOpen(true);
|
|
};
|
|
|
|
const resetForm = () => {
|
|
setFormData({
|
|
name: '',
|
|
surname: '',
|
|
email: '',
|
|
role: 'user',
|
|
password: '',
|
|
});
|
|
};
|
|
|
|
const filteredUsers = users.filter(
|
|
(user) =>
|
|
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
user.surname.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
user.email.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card>
|
|
<CardContent className='flex justify-center items-center py-8'>
|
|
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900'></div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className='space-y-6'>
|
|
{/* Header */}
|
|
<div className='flex items-center justify-between'>
|
|
<div className='flex items-center gap-2'>
|
|
<Users className='h-6 w-6' />
|
|
<h2 className='text-2xl font-bold'>User Management</h2>
|
|
</div>
|
|
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button onClick={() => resetForm()}>
|
|
<UserPlus className='h-4 w-4 mr-2' />
|
|
Add User
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Create New User</DialogTitle>
|
|
</DialogHeader>
|
|
<form onSubmit={handleCreateUser} className='space-y-4'>
|
|
<div className='grid grid-cols-2 gap-4'>
|
|
<div>
|
|
<Label htmlFor='name'>First Name</Label>
|
|
<Input
|
|
id='name'
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
placeholder='John'
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor='surname'>Last Name</Label>
|
|
<Input
|
|
id='surname'
|
|
value={formData.surname}
|
|
onChange={(e) => setFormData({ ...formData, surname: e.target.value })}
|
|
placeholder='Doe'
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor='email'>Email</Label>
|
|
<Input
|
|
id='email'
|
|
type='email'
|
|
value={formData.email}
|
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
placeholder='john.doe@example.com'
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor='password'>Password</Label>
|
|
<Input
|
|
id='password'
|
|
type='password'
|
|
value={formData.password}
|
|
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
placeholder='Enter password'
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor='role'>Role</Label>
|
|
<Select
|
|
value={formData.role}
|
|
onValueChange={(value: 'user' | 'admin') =>
|
|
setFormData({ ...formData, role: value })
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder='Select role' />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value='user'>User</SelectItem>
|
|
<SelectItem value='admin'>Admin</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className='flex gap-2 justify-end'>
|
|
<Button type='button' variant='outline' onClick={() => setIsCreateDialogOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button type='submit' disabled={loading}>
|
|
{loading ? 'Creating...' : 'Create User'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
|
|
{/* Search */}
|
|
<div className='flex items-center gap-2'>
|
|
<Search className='h-4 w-4 text-gray-500' />
|
|
<Input
|
|
placeholder='Search users by name or email...'
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className='max-w-sm'
|
|
/>
|
|
</div>
|
|
|
|
{/* Users Table */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>All Users ({filteredUsers.length})</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Name</TableHead>
|
|
<TableHead>Email</TableHead>
|
|
<TableHead>Role</TableHead>
|
|
<TableHead>Created</TableHead>
|
|
<TableHead>Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredUsers.map((user) => (
|
|
<TableRow key={user.id}>
|
|
<TableCell className='font-medium'>
|
|
{user.name} {user.surname}
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className='flex items-center gap-2'>
|
|
<Mail className='h-4 w-4 text-gray-500' />
|
|
{user.email}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant={user.role === 'admin' ? 'default' : 'secondary'}>
|
|
{user.role}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className='flex items-center gap-2'>
|
|
<Calendar className='h-4 w-4 text-gray-500' />
|
|
{new Date(user.createdAt).toLocaleDateString('en-IE')}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className='flex gap-2'>
|
|
<Button variant='outline' size='sm' onClick={() => openEditDialog(user)}>
|
|
<Edit className='h-4 w-4' />
|
|
</Button>
|
|
<Button
|
|
variant='outline'
|
|
size='sm'
|
|
onClick={() => openDeleteDialog(user)}
|
|
className='text-red-600 hover:text-red-700'
|
|
>
|
|
<Trash2 className='h-4 w-4' />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
{filteredUsers.length === 0 && (
|
|
<div className='text-center py-8 text-gray-500'>
|
|
No users found matching your search criteria
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Edit User Dialog */}
|
|
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Edit User</DialogTitle>
|
|
</DialogHeader>
|
|
<div className='space-y-4'>
|
|
<div className='grid grid-cols-2 gap-4'>
|
|
<div>
|
|
<Label htmlFor='edit-name'>First Name</Label>
|
|
<Input
|
|
id='edit-name'
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
placeholder='John'
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor='edit-surname'>Last Name</Label>
|
|
<Input
|
|
id='edit-surname'
|
|
value={formData.surname}
|
|
onChange={(e) => setFormData({ ...formData, surname: e.target.value })}
|
|
placeholder='Doe'
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor='edit-email'>Email</Label>
|
|
<Input
|
|
id='edit-email'
|
|
type='email'
|
|
value={formData.email}
|
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
placeholder='john.doe@example.com'
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor='edit-password'>New Password (leave blank to keep current)</Label>
|
|
<Input
|
|
id='edit-password'
|
|
type='password'
|
|
value={formData.password}
|
|
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
placeholder='Enter new password'
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor='edit-role'>Role</Label>
|
|
<Select
|
|
value={formData.role}
|
|
onValueChange={(value: 'user' | 'admin') => setFormData({ ...formData, role: value })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder='Select role' />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value='user'>User</SelectItem>
|
|
<SelectItem value='admin'>Admin</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className='flex gap-2 justify-end'>
|
|
<Button variant='outline' onClick={() => setIsEditDialogOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleEditUser} disabled={loading}>
|
|
{loading ? 'Updating...' : 'Update User'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Are you sure you want to delete{' '}
|
|
{userToDelete ? `${userToDelete.name} ${userToDelete.surname}` : 'this user'}? This action
|
|
cannot be undone.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={confirmDeleteUser}
|
|
className='bg-destructive hover:bg-destructive/90'
|
|
>
|
|
Delete
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
}
|