diff --git a/app/api/admin/announcements/[id]/route.ts b/app/api/admin/announcements/[id]/route.ts index a3ef906..7525170 100644 --- a/app/api/admin/announcements/[id]/route.ts +++ b/app/api/admin/announcements/[id]/route.ts @@ -4,7 +4,7 @@ import { announcements } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; import { getSession } from '@/lib/session'; -export async function PUT(request: NextRequest, { params }: { params: { id: string } }) { +export async function PUT(request: NextRequest, context: { params: Promise<{ id: string }> }) { try { const session = await getSession(); if (!session || session.role !== 'admin') { @@ -12,7 +12,8 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri } const { title, content, priority, expiresAt, isActive } = await request.json(); - const announcementId = params.id; + const { id } = await context.params; + const announcementId = id; if (!title || !content) { return NextResponse.json({ error: 'Title and content are required' }, { status: 400 }); @@ -49,14 +50,15 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri } } -export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { +export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) { try { const session = await getSession(); if (!session || session.role !== 'admin') { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const announcementId = params.id; + const { id } = await context.params; + const announcementId = id; // Check if announcement exists const existing = await db.select().from(announcements).where(eq(announcements.id, announcementId)).limit(1); diff --git a/app/api/admin/courts/[id]/route.ts b/app/api/admin/courts/[id]/route.ts index 19b972d..e93b69f 100644 --- a/app/api/admin/courts/[id]/route.ts +++ b/app/api/admin/courts/[id]/route.ts @@ -4,7 +4,7 @@ import { courts } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; import { getSession } from '@/lib/session'; -export async function PUT(request: NextRequest, { params }: { params: { id: string } }) { +export async function PUT(request: NextRequest, context: { params: Promise<{ id: string }> }) { try { const session = await getSession(); if (!session || session.role !== 'admin') { @@ -12,7 +12,8 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri } const { name, isActive } = await request.json(); - const courtId = params.id; + const { id } = await context.params; + const courtId = id; if (!name) { return NextResponse.json({ error: 'Court name is required' }, { status: 400 }); @@ -46,14 +47,15 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri } } -export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { +export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) { try { const session = await getSession(); if (!session || session.role !== 'admin') { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const courtId = params.id; + const { id } = await context.params; + const courtId = id; // Check if court exists const existing = await db.select().from(courts).where(eq(courts.id, courtId)).limit(1); diff --git a/app/api/admin/settings/route.ts b/app/api/admin/settings/route.ts index afd084b..bf96585 100644 --- a/app/api/admin/settings/route.ts +++ b/app/api/admin/settings/route.ts @@ -3,6 +3,7 @@ import { db } from '@/lib/db'; import { settings } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; import { getSession } from '@/lib/session'; +import { invalidateAppConfigCache } from '@/lib/app-config'; export async function GET(request: NextRequest) { try { @@ -70,6 +71,9 @@ export async function PUT(request: NextRequest) { await Promise.all(updatePromises); + // Invalidate app config cache since settings changed + invalidateAppConfigCache(); + return NextResponse.json({ message: 'Settings updated successfully' }); } catch (error) { console.error('Error updating settings:', error); diff --git a/app/api/admin/users/[id]/route.ts b/app/api/admin/users/[id]/route.ts index 671435b..6662076 100644 --- a/app/api/admin/users/[id]/route.ts +++ b/app/api/admin/users/[id]/route.ts @@ -5,7 +5,7 @@ import { eq } from 'drizzle-orm'; import { getSession } from '@/lib/session'; import bcrypt from 'bcryptjs'; -export async function PUT(request: NextRequest, { params }: { params: { id: string } }) { +export async function PUT(request: NextRequest, context: { params: Promise<{ id: string }> }) { try { const session = await getSession(); if (!session || session.role !== 'admin') { @@ -13,7 +13,8 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri } const { name, surname, email, role, password } = await request.json(); - const userId = params.id; + const { id } = await context.params; + const userId = id; if (!name || !surname || !email) { return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); @@ -58,14 +59,15 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri } } -export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { +export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) { try { const session = await getSession(); if (!session || session.role !== 'admin') { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const userId = params.id; + const { id } = await context.params; + const userId = id; // Prevent admin from deleting themselves if (session.userId === userId) { diff --git a/app/api/bookings/[id]/route.ts b/app/api/bookings/[id]/route.ts index 8774655..df16617 100644 --- a/app/api/bookings/[id]/route.ts +++ b/app/api/bookings/[id]/route.ts @@ -5,7 +5,7 @@ import { bookings, settings } from '@/lib/db/schema'; import { eq, and } from 'drizzle-orm'; import { logActivity, ACTIONS, ENTITY_TYPES } from '@/lib/activity-logger'; -export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) { +export async function PATCH(request: NextRequest, context: { params: Promise<{ id: string }> }) { try { const session = await getSession(); if (!session) { @@ -13,7 +13,8 @@ export async function PATCH(request: NextRequest, { params }: { params: { id: st } const { notes } = await request.json(); - const bookingId = params.id; + const { id } = await context.params; + const bookingId = id; // Check if booking exists and belongs to user const existingBooking = await db @@ -98,14 +99,15 @@ export async function PATCH(request: NextRequest, { params }: { params: { id: st } } -export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { +export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) { try { const session = await getSession(); if (!session) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const bookingId = params.id; + const { id } = await context.params; + const bookingId = id; // Check if booking exists and belongs to user const existingBooking = await db diff --git a/app/api/bookings/route.ts b/app/api/bookings/route.ts index bd30e75..9dc0406 100644 --- a/app/api/bookings/route.ts +++ b/app/api/bookings/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { bookings, courts, timeSlots, settings, metrics } from '@/lib/db/schema'; -import { eq, and } from 'drizzle-orm'; +import { eq, and, gte, asc } from 'drizzle-orm'; import { getSession } from '@/lib/session'; import { logActivity, ACTIONS, ENTITY_TYPES } from '@/lib/activity-logger'; @@ -12,6 +12,9 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } + // Get today's date to filter out past bookings + const today = new Date().toISOString().split('T')[0]; + const userBookings = await db .select({ id: bookings.id, @@ -29,7 +32,8 @@ export async function GET(request: NextRequest) { }) .from(bookings) .innerJoin(courts, eq(bookings.courtId, courts.id)) - .where(eq(bookings.userId, session.userId)); + .where(and(eq(bookings.userId, session.userId), eq(bookings.status, 'active'), gte(bookings.date, today))) + .orderBy(asc(bookings.date), asc(bookings.startTime)); return NextResponse.json({ bookings: userBookings }); } catch (error) { diff --git a/app/api/config/route.ts b/app/api/config/route.ts new file mode 100644 index 0000000..daffa0a --- /dev/null +++ b/app/api/config/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from 'next/server'; +import { getAppConfig } from '@/lib/app-config'; + +export async function GET() { + try { + const config = await getAppConfig(); + return NextResponse.json(config); + } catch (error) { + console.error('Error fetching app config:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 293e7f4..9ed13c6 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -6,8 +6,10 @@ import { eq } from 'drizzle-orm'; import { DashboardHeader } from '@/components/dashboard/dashboard-header'; import { EnhancedBookingCalendar } from '@/components/booking/enhanced-booking-calendar'; import { UserBookingManagement } from '@/components/booking/user-booking-management'; +import { getAppConfig } from '@/lib/app-config'; export default async function DashboardPage() { + const config = await getAppConfig(); const session = await getSession(); if (!session) { @@ -38,7 +40,7 @@ export default async function DashboardPage() { }; return ( -
+
@@ -51,7 +53,9 @@ export default async function DashboardPage() { {user.name && user.surname ? `${user.name} ${user.surname}` : user.email.split('@')[0]}! 🏓 -

Book your table tennis court and enjoy your game

+

+ Book your {config.sportName.toLowerCase()} court and enjoy your game +

diff --git a/app/globals.css b/app/globals.css index d22c59d..f428892 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,65 +4,99 @@ @layer base { :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - - --secondary: 210 40% 96%; - --secondary-foreground: 222.2 84% 4.9%; - - --muted: 210 40% 96%; - --muted-foreground: 215.4 16.3% 46.9%; - - --accent: 210 40% 96%; - --accent-foreground: 222.2 84% 4.9%; - - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; - + --background: 0 0% 98.8235%; + --foreground: 0 0% 9.0196%; + --card: 0 0% 98.8235%; + --card-foreground: 0 0% 9.0196%; + --popover: 0 0% 98.8235%; + --popover-foreground: 0 0% 32.1569%; + --primary: 151.3274 66.8639% 66.8627%; + --primary-foreground: 153.3333 13.0435% 13.5294%; + --secondary: 0 0% 99.2157%; + --secondary-foreground: 0 0% 9.0196%; + --muted: 0 0% 92.9412%; + --muted-foreground: 0 0% 12.549%; + --accent: 0 0% 92.9412%; + --accent-foreground: 0 0% 12.549%; + --destructive: 9.8901 81.982% 43.5294%; + --destructive-foreground: 0 100% 99.4118%; + --border: 0 0% 87.451%; + --input: 0 0% 96.4706%; + --ring: 151.3274 66.8639% 66.8627%; + --chart-1: 151.3274 66.8639% 66.8627%; + --chart-2: 217.2193 91.2195% 59.8039%; + --chart-3: 258.3117 89.5349% 66.2745%; + --chart-4: 37.6923 92.126% 50.1961%; + --chart-5: 160.1183 84.0796% 39.4118%; + --sidebar: 0 0% 98.8235%; + --sidebar-foreground: 0 0% 43.9216%; + --sidebar-primary: 151.3274 66.8639% 66.8627%; + --sidebar-primary-foreground: 153.3333 13.0435% 13.5294%; + --sidebar-accent: 0 0% 92.9412%; + --sidebar-accent-foreground: 0 0% 12.549%; + --sidebar-border: 0 0% 87.451%; + --sidebar-ring: 151.3274 66.8639% 66.8627%; + --font-sans: Outfit, sans-serif; + --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; + --font-mono: monospace; --radius: 0.5rem; + --shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09); + --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09); + --shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17); + --shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17); + --shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17); + --shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17); + --shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17); + --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43); + --tracking-normal: 0.025em; + --spacing: 0.25rem; } .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; - - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; + --background: 0 0% 7.0588%; + --foreground: 214.2857 31.8182% 91.3725%; + --card: 0 0% 9.0196%; + --card-foreground: 214.2857 31.8182% 91.3725%; + --popover: 0 0% 14.1176%; + --popover-foreground: 0 0% 66.2745%; + --primary: 154.898 100% 19.2157%; + --primary-foreground: 152.7273 19.2982% 88.8235%; + --secondary: 0 0% 14.1176%; + --secondary-foreground: 0 0% 98.0392%; + --muted: 0 0% 12.1569%; + --muted-foreground: 0 0% 63.5294%; + --accent: 0 0% 19.2157%; + --accent-foreground: 0 0% 98.0392%; + --destructive: 6.6667 60% 20.5882%; + --destructive-foreground: 12 12.1951% 91.9608%; + --border: 0 0% 16.0784%; + --input: 0 0% 14.1176%; + --ring: 141.8919 69.1589% 58.0392%; + --chart-1: 141.8919 69.1589% 58.0392%; + --chart-2: 213.1169 93.9024% 67.8431%; + --chart-3: 255.1351 91.7355% 76.2745%; + --chart-4: 43.2558 96.4126% 56.2745%; + --chart-5: 172.4551 66.0079% 50.3922%; + --sidebar: 0 0% 7.0588%; + --sidebar-foreground: 0 0% 53.7255%; + --sidebar-primary: 154.898 100% 19.2157%; + --sidebar-primary-foreground: 152.7273 19.2982% 88.8235%; + --sidebar-accent: 0 0% 19.2157%; + --sidebar-accent-foreground: 0 0% 98.0392%; + --sidebar-border: 0 0% 16.0784%; + --sidebar-ring: 141.8919 69.1589% 58.0392%; + --font-sans: Outfit, sans-serif; + --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; + --font-mono: monospace; + --radius: 0.5rem; + --shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09); + --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09); + --shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17); + --shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17); + --shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17); + --shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17); + --shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17); + --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43); } } @@ -72,5 +106,6 @@ } body { @apply bg-background text-foreground; + letter-spacing: var(--tracking-normal); } } diff --git a/app/layout.tsx b/app/layout.tsx index 95e8333..9c1e461 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,13 +3,18 @@ import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import { ThemeProvider } from '@/components/theme-provider'; import { Toaster } from '@/components/ui/toaster'; +import { getAppConfig } from '@/lib/app-config'; const inter = Inter({ subsets: ['latin'] }); -export const metadata: Metadata = { - title: 'Table Tennis Booking System', - description: 'Book your table tennis court slots with ease', -}; +export async function generateMetadata(): Promise { + const config = await getAppConfig(); + + return { + title: config.appTitle, + description: config.appDescription, + }; +} export default function RootLayout({ children }: { children: React.ReactNode }) { return ( diff --git a/app/login/page.tsx b/app/login/page.tsx index eb76114..083f43f 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,13 +1,16 @@ import Link from 'next/link'; import { LoginForm } from '@/components/auth/LoginForm'; +import { getAppConfig } from '@/lib/app-config'; + +export default async function LoginPage() { + const config = await getAppConfig(); -export default function LoginPage() { return (
-

🏓 TT Booking

-

Professional table tennis court booking system

+

🏓 {config.clubName}

+

{config.appDescription}

diff --git a/app/register/page.tsx b/app/register/page.tsx index 8be7746..57c6e6d 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -1,13 +1,16 @@ import Link from 'next/link'; import { RegisterForm } from '@/components/auth/RegisterForm'; +import { getAppConfig } from '@/lib/app-config'; + +export default async function RegisterPage() { + const config = await getAppConfig(); -export default function RegisterPage() { return (
-

🏓 TT Booking

-

Join our table tennis community

+

🏓 {config.clubName}

+

Join our {config.sportName.toLowerCase()} community

diff --git a/components/admin/AdminAnnouncementManagement.tsx b/components/admin/AdminAnnouncementManagement.tsx index 1c7d321..253cd9f 100644 --- a/components/admin/AdminAnnouncementManagement.tsx +++ b/components/admin/AdminAnnouncementManagement.tsx @@ -8,6 +8,17 @@ import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Textarea } from '@/components/ui/textarea'; 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'; @@ -37,7 +48,9 @@ export function AdminAnnouncementManagement() { const [loading, setLoading] = useState(true); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [editingAnnouncement, setEditingAnnouncement] = useState(null); + const [announcementToDelete, setAnnouncementToDelete] = useState(null); const [formData, setFormData] = useState({ title: '', content: '', @@ -176,12 +189,21 @@ export function AdminAnnouncementManagement() { } }; + const openDeleteDialog = (announcement: Announcement) => { + setAnnouncementToDelete(announcement); + setIsDeleteDialogOpen(true); + }; + + const confirmDeleteAnnouncement = async () => { + if (announcementToDelete) { + await handleDeleteAnnouncement(announcementToDelete.id); + setIsDeleteDialogOpen(false); + setAnnouncementToDelete(null); + } + }; + const handleDeleteAnnouncement = async (announcementId: string) => { try { - if (!confirm('Are you sure you want to delete this announcement?')) { - return; - } - const response = await fetch(`/api/admin/announcements/${announcementId}`, { method: 'DELETE', }); @@ -444,7 +466,7 @@ export function AdminAnnouncementManagement() {
+ + {/* Delete Confirmation Dialog */} + + + + Are you sure? + + Are you sure you want to delete{' '} + {announcementToDelete ? `"${announcementToDelete.title}"` : 'this announcement'}? This + action cannot be undone. + + + + Cancel + + Delete + + + +
); } diff --git a/components/admin/AdminCourtManagement.tsx b/components/admin/AdminCourtManagement.tsx index 9a459db..0de8a2f 100644 --- a/components/admin/AdminCourtManagement.tsx +++ b/components/admin/AdminCourtManagement.tsx @@ -6,6 +6,17 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Switch } from '@/components/ui/switch'; 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 { Badge } from '@/components/ui/badge'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; @@ -32,7 +43,9 @@ export function AdminCourtManagement() { const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); const [isDialogOpen, setIsDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [editingCourt, setEditingCourt] = useState(null); + const [courtToDelete, setCourtToDelete] = useState(null); const [formData, setFormData] = useState({ name: '', isActive: true, @@ -150,11 +163,20 @@ export function AdminCourtManagement() { setIsDialogOpen(true); }; - const handleDelete = async (courtId: string) => { - if (!confirm('Are you sure you want to delete this court? This action cannot be undone.')) { - return; - } + const openDeleteDialog = (court: Court) => { + setCourtToDelete(court); + setIsDeleteDialogOpen(true); + }; + const confirmDeleteCourt = async () => { + if (courtToDelete) { + await handleDelete(courtToDelete.id); + setIsDeleteDialogOpen(false); + setCourtToDelete(null); + } + }; + + const handleDelete = async (courtId: string) => { try { setDeleting(courtId); const response = await fetch(`/api/admin/courts/${courtId}`, { @@ -319,7 +341,7 @@ export function AdminCourtManagement() {
)} + + {/* Delete Confirmation Dialog */} + + + + Are you sure? + + Are you sure you want to delete {courtToDelete ? `"${courtToDelete.name}"` : 'this court'}? + This action cannot be undone. + + + + Cancel + + Delete + + + + ); } diff --git a/components/admin/AdminSettingsManagement.tsx b/components/admin/AdminSettingsManagement.tsx index e1da9ce..b02a87f 100644 --- a/components/admin/AdminSettingsManagement.tsx +++ b/components/admin/AdminSettingsManagement.tsx @@ -17,6 +17,12 @@ interface Setting { } interface SettingsData { + // Club/Brand Settings + club_name: string; + sport_name: string; + app_title: string; + app_description: string; + // Booking Settings booking_window_days: string; max_booking_duration_hours: string; min_booking_duration_minutes: string; @@ -30,6 +36,12 @@ interface SettingsData { export function AdminSettingsManagement() { const [settings, setSettings] = useState({ + // Club/Brand Settings + club_name: 'TT Club', + sport_name: 'Table Tennis', + app_title: 'Table Tennis Booking System', + app_description: 'Book your table tennis court slots with ease', + // Booking Settings booking_window_days: '7', max_booking_duration_hours: '2', min_booking_duration_minutes: '30', @@ -38,7 +50,7 @@ export function AdminSettingsManagement() { allow_weekend_bookings: 'true', max_bookings_per_user_per_hour_per_day: '1', allow_booking_modifications: 'true', - booking_modification_hours_before: '2', + booking_modification_hours_before: '1', }); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); @@ -54,6 +66,12 @@ export function AdminSettingsManagement() { if (response.ok) { const data = await response.json(); const settingsMap: SettingsData = { + // Club/Brand Settings + club_name: 'TT Club', + sport_name: 'Table Tennis', + app_title: 'Table Tennis Booking System', + app_description: 'Book your table tennis court slots with ease', + // Booking Settings booking_window_days: '7', max_booking_duration_hours: '2', min_booking_duration_minutes: '30', @@ -62,10 +80,8 @@ export function AdminSettingsManagement() { allow_weekend_bookings: 'true', max_bookings_per_user_per_hour_per_day: '1', allow_booking_modifications: 'true', - booking_modification_hours_before: '2', - }; - - // Map the settings array to our object + booking_modification_hours_before: '1', + }; // Map the settings array to our object data.settings?.forEach((setting: Setting) => { if (setting.key in settingsMap) { settingsMap[setting.key as keyof SettingsData] = setting.value; @@ -189,139 +205,205 @@ export function AdminSettingsManagement() {
-
- {/* Booking Window */} -
- - updateSetting('booking_window_days', e.target.value)} - /> -

How many days in advance users can book

-
+ {/* Club/Brand Configuration Section */} +
+

Club & Branding

+
+ {/* Club Name */} +
+ + updateSetting('club_name', e.target.value)} + /> +

The name of your club or organization

+
- {/* Max Duration */} -
- - updateSetting('max_booking_duration_hours', e.target.value)} - /> -

Maximum hours per booking session

-
+ {/* Sport Name */} +
+ + updateSetting('sport_name', e.target.value)} + /> +

The sport played at your facility

+
- {/* Min Duration */} -
- - updateSetting('min_booking_duration_minutes', e.target.value)} - /> -

Minimum minutes per booking session

-
+ {/* App Title */} +
+ + updateSetting('app_title', e.target.value)} + /> +

Main title shown in browser and app header

+
- {/* Start Time */} -
- - updateSetting('booking_start_time', e.target.value)} - /> -

When courts open for booking each day

+ {/* App Description */} +
+ + updateSetting('app_description', e.target.value)} + /> +

Short description for login/register pages

+
+
- {/* End Time */} -
- - updateSetting('booking_end_time', e.target.value)} - /> -

When courts close for booking each day

-
+ {/* Booking Configuration Section */} +
+

Booking Configuration

+
+ {/* Booking Window */} +
+ + updateSetting('booking_window_days', e.target.value)} + /> +

How many days in advance users can book

+
- {/* Booking Restrictions */} -
- - updateSetting('max_bookings_per_user_per_hour_per_day', e.target.value)} - /> -

Maximum bookings per user per hour on the same day

-
+ {/* Max Duration */} +
+ + updateSetting('max_booking_duration_hours', e.target.value)} + /> +

Maximum hours per booking session

+
- {/* Booking Modification Settings */} -
-
- - updateSetting('allow_booking_modifications', checked.toString()) + {/* Min Duration */} +
+ + updateSetting('min_booking_duration_minutes', e.target.value)} + /> +

Minimum minutes per booking session

+
+ + {/* Start Time */} +
+ + updateSetting('booking_start_time', e.target.value)} + /> +

When courts open for booking each day

+
+ + {/* End Time */} +
+ + updateSetting('booking_end_time', e.target.value)} + /> +

When courts close for booking each day

+
+ + {/* Booking Restrictions */} +
+ + + updateSetting('max_bookings_per_user_per_hour_per_day', e.target.value) } /> - +

Maximum bookings per user per hour on the same day

-

Whether users can edit or cancel their bookings

-
- {/* Modification Time Restriction */} -
- - updateSetting('booking_modification_hours_before', e.target.value)} - disabled={settings.allow_booking_modifications !== 'true'} - /> -

- {settings.allow_booking_modifications === 'true' - ? 'How many hours before a session users can still modify bookings' - : 'Enable booking modifications to configure this setting'} -

-
+ {/* Booking Modification Settings */} +
+
+ + updateSetting('allow_booking_modifications', checked.toString()) + } + /> + +
+

Whether users can edit or cancel their bookings

+
- {/* Weekend Bookings */} -
-
- - updateSetting('allow_weekend_bookings', checked.toString()) - } + {/* Modification Time Restriction */} +
+ + updateSetting('booking_modification_hours_before', e.target.value)} + disabled={settings.allow_booking_modifications !== 'true'} /> - +

+ {settings.allow_booking_modifications === 'true' + ? 'How many hours before a session users can still modify bookings' + : 'Enable booking modifications to configure this setting'} +

+
+ + {/* Weekend Bookings */} +
+
+ + updateSetting('allow_weekend_bookings', checked.toString()) + } + /> + +
+

Whether users can book courts on weekends

-

Whether users can book courts on weekends

diff --git a/components/admin/AdminTimeSlotManagement.tsx b/components/admin/AdminTimeSlotManagement.tsx index 0ffb44e..ec606bf 100644 --- a/components/admin/AdminTimeSlotManagement.tsx +++ b/components/admin/AdminTimeSlotManagement.tsx @@ -7,6 +7,17 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; 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 { Switch } from '@/components/ui/switch'; import { Plus, Edit, Trash2, Clock } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; @@ -30,7 +41,11 @@ export function AdminTimeSlotManagement() { const [timeSlots, setTimeSlots] = useState([]); const [loading, setLoading] = useState(false); const [showDialog, setShowDialog] = useState(false); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [showWipeDayDialog, setShowWipeDayDialog] = useState(false); const [editingSlot, setEditingSlot] = useState(null); + const [slotToDelete, setSlotToDelete] = useState(null); + const [dayToWipe, setDayToWipe] = useState(null); const [formData, setFormData] = useState({ dayOfWeek: 1, // Default to Monday (Irish standard) startTime: '', @@ -115,11 +130,20 @@ export function AdminTimeSlotManagement() { } }; - const handleDelete = async (id: string) => { - if (!confirm('Are you sure you want to delete this time slot?')) { - return; - } + const openDeleteDialog = (slot: TimeSlot) => { + setSlotToDelete(slot); + setShowDeleteDialog(true); + }; + const confirmDeleteSlot = async () => { + if (slotToDelete) { + await handleDelete(slotToDelete.id); + setShowDeleteDialog(false); + setSlotToDelete(null); + } + }; + + const handleDelete = async (id: string) => { try { setLoading(true); const response = await fetch(`/api/admin/time-slots/${id}`, { @@ -152,14 +176,23 @@ export function AdminTimeSlotManagement() { } }; - const handleWipeDay = async (dayOfWeek: number) => { - const dayName = DAYS[dayOfWeek]; - if (!confirm(`Are you sure you want to delete ALL time slots for ${dayName}? This action cannot be undone.`)) { - return; - } + const openWipeDayDialog = (dayOfWeek: number) => { + setDayToWipe(dayOfWeek); + setShowWipeDayDialog(true); + }; + const confirmWipeDay = async () => { + if (dayToWipe !== null) { + await handleWipeDay(dayToWipe); + setShowWipeDayDialog(false); + setDayToWipe(null); + } + }; + + const handleWipeDay = async (dayOfWeek: number) => { try { setLoading(true); + const dayName = DAYS[dayOfWeek]; const slotsToDelete = timeSlots.filter((slot) => slot.dayOfWeek === dayOfWeek); // Delete all slots for this day @@ -322,7 +355,7 @@ export function AdminTimeSlotManagement() {
)} + + {/* Delete Time Slot Dialog */} + + + + Are you sure? + + Are you sure you want to delete this time slot? This action cannot be undone. + + + + Cancel + + Delete + + + + + + {/* Wipe Day Dialog */} + + + + Are you sure? + + Are you sure you want to delete ALL time slots for{' '} + {dayToWipe !== null ? DAYS[dayToWipe] : 'this day'}? This action cannot be undone. + + + + Cancel + + Delete All + + + + ); } diff --git a/components/admin/AdminUserManagement.tsx b/components/admin/AdminUserManagement.tsx index c62de37..35fd833 100644 --- a/components/admin/AdminUserManagement.tsx +++ b/components/admin/AdminUserManagement.tsx @@ -7,6 +7,17 @@ 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'; @@ -35,7 +46,9 @@ export function AdminUserManagement() { const [searchTerm, setSearchTerm] = useState(''); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [editingUser, setEditingUser] = useState(null); + const [userToDelete, setUserToDelete] = useState(null); const [formData, setFormData] = useState({ name: '', surname: '', @@ -184,12 +197,21 @@ export function AdminUserManagement() { } }; + 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 { - if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) { - return; - } - const response = await fetch(`/api/admin/users/${userId}`, { method: 'DELETE', }); @@ -406,7 +428,7 @@ export function AdminUserManagement() {
+ + {/* Delete Confirmation Dialog */} + + + + Are you sure? + + Are you sure you want to delete{' '} + {userToDelete ? `${userToDelete.name} ${userToDelete.surname}` : 'this user'}? This action + cannot be undone. + + + + Cancel + + Delete + + + +
); } diff --git a/components/booking/enhanced-booking-calendar.tsx b/components/booking/enhanced-booking-calendar.tsx index ed8d9cb..04bd233 100644 --- a/components/booking/enhanced-booking-calendar.tsx +++ b/components/booking/enhanced-booking-calendar.tsx @@ -473,11 +473,11 @@ export function EnhancedBookingCalendar() { : '' } ${ isTodayDate && !isSelectedDate - ? 'ring-2 ring-blue-400 ring-opacity-50 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900 text-foreground' + ? 'ring-2 ring-primary/20 bg-accent border-primary/20 hover:bg-accent/80 text-foreground' : '' } ${ isSelectedDate && isTodayDate - ? 'bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 dark:from-blue-700 dark:to-blue-800 dark:hover:from-blue-800 dark:hover:to-blue-900 text-white' + ? 'bg-gradient-to-r from-primary to-primary/80 hover:from-primary/90 hover:to-primary/70 text-primary-foreground' : '' }`} > @@ -501,8 +501,8 @@ export function EnhancedBookingCalendar() {

- Today + Today
)} diff --git a/components/booking/user-booking-management.tsx b/components/booking/user-booking-management.tsx index e3edf1a..4f2ea2b 100644 --- a/components/booking/user-booking-management.tsx +++ b/components/booking/user-booking-management.tsx @@ -61,16 +61,8 @@ export function UserBookingManagement() { const response = await fetch('/api/bookings'); if (response.ok) { const data = await response.json(); - // Filter to show only future and today's bookings - const now = new Date(); - const today = now.toISOString().split('T')[0]; - - const relevantBookings = data.bookings.filter((booking: Booking) => { - if (booking.status !== 'active') return false; - return booking.date >= today; - }); - - setBookings(relevantBookings); + // API already filters to show only active future bookings (today onwards) + setBookings(data.bookings || []); } } catch (error) { console.error('Error fetching bookings:', error); @@ -282,7 +274,7 @@ export function UserBookingManagement() { - Your Bookings + Your Upcoming Bookings