Files
tt-booking/components/admin/AdminUserManagement.tsx

643 lines
18 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;
lastBookingDate: string | null;
}
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>
{/* Desktop Table */}
<div className='hidden md:block'>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Created</TableHead>
<TableHead>Last Played</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>
{user.lastBookingDate ? (
<div className='flex items-center gap-2'>
<Calendar className='h-4 w-4 text-green-600' />
{new Date(user.lastBookingDate).toLocaleDateString('en-IE')}
</div>
) : (
<div className='flex items-center gap-2 text-gray-500'>
<Calendar className='h-4 w-4' />
Never played
</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>
</div>
{/* Mobile Card Layout */}
<div className='md:hidden space-y-4'>
{filteredUsers.map((user) => (
<Card key={user.id} className='p-4'>
<div className='space-y-3'>
<div className='flex items-start justify-between'>
<div>
<h3 className='font-medium text-sm'>
{user.name} {user.surname}
</h3>
<p className='text-xs text-muted-foreground flex items-center gap-1 mt-1'>
<Mail className='h-3 w-3' />
{user.email}
</p>
</div>
<Badge
variant={user.role === 'admin' ? 'default' : 'secondary'}
className='text-xs'
>
{user.role}
</Badge>
</div>
<div className='grid grid-cols-2 gap-3 text-xs'>
<div>
<span className='text-muted-foreground block'>Created</span>
<span className='flex items-center gap-1 mt-1'>
<Calendar className='h-3 w-3 text-gray-500' />
{new Date(user.createdAt).toLocaleDateString('en-IE')}
</span>
</div>
<div>
<span className='text-muted-foreground block'>Last Played</span>
{user.lastBookingDate ? (
<span className='flex items-center gap-1 mt-1'>
<Calendar className='h-3 w-3 text-green-600' />
{new Date(user.lastBookingDate).toLocaleDateString('en-IE')}
</span>
) : (
<span className='flex items-center gap-1 mt-1 text-gray-500'>
<Calendar className='h-3 w-3' />
Never played
</span>
)}
</div>
</div>
<div className='flex gap-2 pt-2'>
<Button
variant='outline'
size='sm'
onClick={() => openEditDialog(user)}
className='flex-1'
>
<Edit className='h-3 w-3 mr-1' />
Edit
</Button>
<Button
variant='outline'
size='sm'
onClick={() => openDeleteDialog(user)}
className='flex-1 text-red-600 hover:text-red-700'
>
<Trash2 className='h-3 w-3 mr-1' />
Delete
</Button>
</div>
</div>
</Card>
))}
</div>
{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>
);
}