Update license year, enhance user management with last booking date, and improve admin dashboard navigation

This commit is contained in:
mikicvi
2025-10-06 11:18:55 +01:00
parent 7fdd7285a4
commit 69b456f3f8
4 changed files with 202 additions and 53 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ This project is licensed under the MIT License:
```
MIT License
Copyright (c) 2024 Table Tennis Booking System
Copyright (c) 2025 Vilim Mikic
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+7 -3
View File
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { users, bookings } from '@/lib/db/schema';
import { eq, desc, max, sql, and } from 'drizzle-orm';
import { getSession } from '@/lib/session';
import bcrypt from 'bcryptjs';
@@ -12,6 +12,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Get all users with their last booking date using LEFT JOIN and GROUP BY
const allUsers = await db
.select({
id: users.id,
@@ -20,8 +21,11 @@ export async function GET(request: NextRequest) {
email: users.email,
role: users.role,
createdAt: users.createdAt,
lastBookingDate: max(bookings.date),
})
.from(users);
.from(users)
.leftJoin(bookings, and(eq(bookings.userId, users.id), eq(bookings.status, 'active')))
.groupBy(users.id, users.name, users.surname, users.email, users.role, users.createdAt);
return NextResponse.json({ users: allUsers });
} catch (error) {
+141 -47
View File
@@ -30,6 +30,7 @@ interface User {
email: string;
role: 'user' | 'admin';
createdAt: string;
lastBookingDate: string | null;
}
interface UserFormData {
@@ -387,58 +388,151 @@ export function AdminUserManagement() {
<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}
{/* 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>
</TableCell>
<TableCell>
<Badge variant={user.role === 'admin' ? 'default' : 'secondary'}>
<Badge
variant={user.role === 'admin' ? 'default' : 'secondary'}
className='text-xs'
>
{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>
<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>
</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>
<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>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</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
+53 -2
View File
@@ -5,6 +5,12 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Users,
Calendar,
@@ -17,6 +23,7 @@ import {
Activity,
LogOut,
ArrowLeft,
ChevronDown,
} from 'lucide-react';
import { useRouter } from 'next/navigation';
import { AdminUserManagement } from './AdminUserManagement';
@@ -47,6 +54,7 @@ interface RecentBooking {
export function AdminDashboard() {
const router = useRouter();
const [activeTab, setActiveTab] = useState('bookings');
const [stats, setStats] = useState<AdminStats>({
totalUsers: 0,
activeCourts: 0,
@@ -206,8 +214,9 @@ export function AdminDashboard() {
</div>
{/* Admin Tabs */}
<Tabs defaultValue='bookings' className='space-y-6'>
<TabsList className='grid w-full grid-cols-6'>
<Tabs value={activeTab} onValueChange={setActiveTab} className='space-y-6'>
{/* Desktop Tabs */}
<TabsList className='hidden md:grid w-full grid-cols-6'>
<TabsTrigger value='bookings'>Bookings</TabsTrigger>
<TabsTrigger value='users'>Users</TabsTrigger>
<TabsTrigger value='courts'>Courts</TabsTrigger>
@@ -215,6 +224,48 @@ export function AdminDashboard() {
<TabsTrigger value='announcements'>Announcements</TabsTrigger>
<TabsTrigger value='logs'>Logs</TabsTrigger>
</TabsList>
{/* Mobile Dropdown */}
<div className='md:hidden'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='outline' className='w-full justify-between'>
{activeTab === 'bookings' && 'Bookings'}
{activeTab === 'users' && 'Users'}
{activeTab === 'courts' && 'Courts'}
{activeTab === 'settings' && 'Settings'}
{activeTab === 'announcements' && 'Announcements'}
{activeTab === 'logs' && 'Logs'}
<ChevronDown className='h-4 w-4' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className='w-full'>
<DropdownMenuItem onClick={() => setActiveTab('bookings')}>
<Calendar className='h-4 w-4 mr-2' />
Bookings
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setActiveTab('users')}>
<Users className='h-4 w-4 mr-2' />
Users
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setActiveTab('courts')}>
<MapPin className='h-4 w-4 mr-2' />
Courts
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setActiveTab('settings')}>
<Settings className='h-4 w-4 mr-2' />
Settings
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setActiveTab('announcements')}>
<Bell className='h-4 w-4 mr-2' />
Announcements
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setActiveTab('logs')}>
<Activity className='h-4 w-4 mr-2' />
Logs
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<TabsContent value='bookings'>
<AdminRecentBookings />
</TabsContent>