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 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal 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 { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
import { users } from '@/lib/db/schema'; import { users, bookings } from '@/lib/db/schema';
import { eq } from 'drizzle-orm'; import { eq, desc, max, sql, and } from 'drizzle-orm';
import { getSession } from '@/lib/session'; import { getSession } from '@/lib/session';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
@@ -12,6 +12,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 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 const allUsers = await db
.select({ .select({
id: users.id, id: users.id,
@@ -20,8 +21,11 @@ export async function GET(request: NextRequest) {
email: users.email, email: users.email,
role: users.role, role: users.role,
createdAt: users.createdAt, 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 }); return NextResponse.json({ users: allUsers });
} catch (error) { } catch (error) {
+95 -1
View File
@@ -30,6 +30,7 @@ interface User {
email: string; email: string;
role: 'user' | 'admin'; role: 'user' | 'admin';
createdAt: string; createdAt: string;
lastBookingDate: string | null;
} }
interface UserFormData { interface UserFormData {
@@ -387,6 +388,8 @@ export function AdminUserManagement() {
<CardTitle>All Users ({filteredUsers.length})</CardTitle> <CardTitle>All Users ({filteredUsers.length})</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{/* Desktop Table */}
<div className='hidden md:block'>
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
@@ -394,6 +397,7 @@ export function AdminUserManagement() {
<TableHead>Email</TableHead> <TableHead>Email</TableHead>
<TableHead>Role</TableHead> <TableHead>Role</TableHead>
<TableHead>Created</TableHead> <TableHead>Created</TableHead>
<TableHead>Last Played</TableHead>
<TableHead>Actions</TableHead> <TableHead>Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -420,9 +424,26 @@ export function AdminUserManagement() {
{new Date(user.createdAt).toLocaleDateString('en-IE')} {new Date(user.createdAt).toLocaleDateString('en-IE')}
</div> </div>
</TableCell> </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> <TableCell>
<div className='flex gap-2'> <div className='flex gap-2'>
<Button variant='outline' size='sm' onClick={() => openEditDialog(user)}> <Button
variant='outline'
size='sm'
onClick={() => openEditDialog(user)}
>
<Edit className='h-4 w-4' /> <Edit className='h-4 w-4' />
</Button> </Button>
<Button <Button
@@ -439,6 +460,79 @@ export function AdminUserManagement() {
))} ))}
</TableBody> </TableBody>
</Table> </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 && ( {filteredUsers.length === 0 && (
<div className='text-center py-8 text-gray-500'> <div className='text-center py-8 text-gray-500'>
No users found matching your search criteria 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 { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { import {
Users, Users,
Calendar, Calendar,
@@ -17,6 +23,7 @@ import {
Activity, Activity,
LogOut, LogOut,
ArrowLeft, ArrowLeft,
ChevronDown,
} from 'lucide-react'; } from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { AdminUserManagement } from './AdminUserManagement'; import { AdminUserManagement } from './AdminUserManagement';
@@ -47,6 +54,7 @@ interface RecentBooking {
export function AdminDashboard() { export function AdminDashboard() {
const router = useRouter(); const router = useRouter();
const [activeTab, setActiveTab] = useState('bookings');
const [stats, setStats] = useState<AdminStats>({ const [stats, setStats] = useState<AdminStats>({
totalUsers: 0, totalUsers: 0,
activeCourts: 0, activeCourts: 0,
@@ -206,8 +214,9 @@ export function AdminDashboard() {
</div> </div>
{/* Admin Tabs */} {/* Admin Tabs */}
<Tabs defaultValue='bookings' className='space-y-6'> <Tabs value={activeTab} onValueChange={setActiveTab} className='space-y-6'>
<TabsList className='grid w-full grid-cols-6'> {/* Desktop Tabs */}
<TabsList className='hidden md:grid w-full grid-cols-6'>
<TabsTrigger value='bookings'>Bookings</TabsTrigger> <TabsTrigger value='bookings'>Bookings</TabsTrigger>
<TabsTrigger value='users'>Users</TabsTrigger> <TabsTrigger value='users'>Users</TabsTrigger>
<TabsTrigger value='courts'>Courts</TabsTrigger> <TabsTrigger value='courts'>Courts</TabsTrigger>
@@ -215,6 +224,48 @@ export function AdminDashboard() {
<TabsTrigger value='announcements'>Announcements</TabsTrigger> <TabsTrigger value='announcements'>Announcements</TabsTrigger>
<TabsTrigger value='logs'>Logs</TabsTrigger> <TabsTrigger value='logs'>Logs</TabsTrigger>
</TabsList> </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'> <TabsContent value='bookings'>
<AdminRecentBookings /> <AdminRecentBookings />
</TabsContent> </TabsContent>