fixes, theming, branding

This commit is contained in:
mikicvi
2025-09-26 22:16:34 +01:00
parent 22c462c61c
commit 220f999f19
24 changed files with 787 additions and 260 deletions
+6 -4
View File
@@ -4,7 +4,7 @@ import { announcements } from '@/lib/db/schema';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { getSession } from '@/lib/session'; 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 { try {
const session = await getSession(); const session = await getSession();
if (!session || session.role !== 'admin') { 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 { title, content, priority, expiresAt, isActive } = await request.json();
const announcementId = params.id; const { id } = await context.params;
const announcementId = id;
if (!title || !content) { if (!title || !content) {
return NextResponse.json({ error: 'Title and content are required' }, { status: 400 }); 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 { try {
const session = await getSession(); const session = await getSession();
if (!session || session.role !== 'admin') { if (!session || session.role !== 'admin') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
} }
const announcementId = params.id; const { id } = await context.params;
const announcementId = id;
// Check if announcement exists // Check if announcement exists
const existing = await db.select().from(announcements).where(eq(announcements.id, announcementId)).limit(1); const existing = await db.select().from(announcements).where(eq(announcements.id, announcementId)).limit(1);
+6 -4
View File
@@ -4,7 +4,7 @@ import { courts } from '@/lib/db/schema';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { getSession } from '@/lib/session'; 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 { try {
const session = await getSession(); const session = await getSession();
if (!session || session.role !== 'admin') { 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 { name, isActive } = await request.json();
const courtId = params.id; const { id } = await context.params;
const courtId = id;
if (!name) { if (!name) {
return NextResponse.json({ error: 'Court name is required' }, { status: 400 }); 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 { try {
const session = await getSession(); const session = await getSession();
if (!session || session.role !== 'admin') { if (!session || session.role !== 'admin') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
} }
const courtId = params.id; const { id } = await context.params;
const courtId = id;
// Check if court exists // Check if court exists
const existing = await db.select().from(courts).where(eq(courts.id, courtId)).limit(1); const existing = await db.select().from(courts).where(eq(courts.id, courtId)).limit(1);
+4
View File
@@ -3,6 +3,7 @@ import { db } from '@/lib/db';
import { settings } from '@/lib/db/schema'; import { settings } from '@/lib/db/schema';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { getSession } from '@/lib/session'; import { getSession } from '@/lib/session';
import { invalidateAppConfigCache } from '@/lib/app-config';
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
@@ -70,6 +71,9 @@ export async function PUT(request: NextRequest) {
await Promise.all(updatePromises); await Promise.all(updatePromises);
// Invalidate app config cache since settings changed
invalidateAppConfigCache();
return NextResponse.json({ message: 'Settings updated successfully' }); return NextResponse.json({ message: 'Settings updated successfully' });
} catch (error) { } catch (error) {
console.error('Error updating settings:', error); console.error('Error updating settings:', error);
+6 -4
View File
@@ -5,7 +5,7 @@ import { eq } from 'drizzle-orm';
import { getSession } from '@/lib/session'; import { getSession } from '@/lib/session';
import bcrypt from 'bcryptjs'; 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 { try {
const session = await getSession(); const session = await getSession();
if (!session || session.role !== 'admin') { 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 { name, surname, email, role, password } = await request.json();
const userId = params.id; const { id } = await context.params;
const userId = id;
if (!name || !surname || !email) { if (!name || !surname || !email) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); 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 { try {
const session = await getSession(); const session = await getSession();
if (!session || session.role !== 'admin') { if (!session || session.role !== 'admin') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
} }
const userId = params.id; const { id } = await context.params;
const userId = id;
// Prevent admin from deleting themselves // Prevent admin from deleting themselves
if (session.userId === userId) { if (session.userId === userId) {
+6 -4
View File
@@ -5,7 +5,7 @@ import { bookings, settings } from '@/lib/db/schema';
import { eq, and } from 'drizzle-orm'; import { eq, and } from 'drizzle-orm';
import { logActivity, ACTIONS, ENTITY_TYPES } from '@/lib/activity-logger'; 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 { try {
const session = await getSession(); const session = await getSession();
if (!session) { if (!session) {
@@ -13,7 +13,8 @@ export async function PATCH(request: NextRequest, { params }: { params: { id: st
} }
const { notes } = await request.json(); 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 // Check if booking exists and belongs to user
const existingBooking = await db 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 { try {
const session = await getSession(); const session = await getSession();
if (!session) { if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 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 // Check if booking exists and belongs to user
const existingBooking = await db const existingBooking = await db
+6 -2
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 { bookings, courts, timeSlots, settings, metrics } from '@/lib/db/schema'; 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 { getSession } from '@/lib/session';
import { logActivity, ACTIONS, ENTITY_TYPES } from '@/lib/activity-logger'; 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 }); 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 const userBookings = await db
.select({ .select({
id: bookings.id, id: bookings.id,
@@ -29,7 +32,8 @@ export async function GET(request: NextRequest) {
}) })
.from(bookings) .from(bookings)
.innerJoin(courts, eq(bookings.courtId, courts.id)) .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 }); return NextResponse.json({ bookings: userBookings });
} catch (error) { } catch (error) {
+12
View File
@@ -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 });
}
}
+6 -2
View File
@@ -6,8 +6,10 @@ import { eq } from 'drizzle-orm';
import { DashboardHeader } from '@/components/dashboard/dashboard-header'; import { DashboardHeader } from '@/components/dashboard/dashboard-header';
import { EnhancedBookingCalendar } from '@/components/booking/enhanced-booking-calendar'; import { EnhancedBookingCalendar } from '@/components/booking/enhanced-booking-calendar';
import { UserBookingManagement } from '@/components/booking/user-booking-management'; import { UserBookingManagement } from '@/components/booking/user-booking-management';
import { getAppConfig } from '@/lib/app-config';
export default async function DashboardPage() { export default async function DashboardPage() {
const config = await getAppConfig();
const session = await getSession(); const session = await getSession();
if (!session) { if (!session) {
@@ -38,7 +40,7 @@ export default async function DashboardPage() {
}; };
return ( return (
<div className='min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800'> <div className='min-h-screen bg-background'>
<DashboardHeader user={userWithSession} /> <DashboardHeader user={userWithSession} />
<main className='container mx-auto px-4 py-8'> <main className='container mx-auto px-4 py-8'>
@@ -51,7 +53,9 @@ export default async function DashboardPage() {
{user.name && user.surname ? `${user.name} ${user.surname}` : user.email.split('@')[0]}! {user.name && user.surname ? `${user.name} ${user.surname}` : user.email.split('@')[0]}!
🏓 🏓
</h1> </h1>
<p className='text-muted-foreground'>Book your table tennis court and enjoy your game</p> <p className='text-muted-foreground'>
Book your {config.sportName.toLowerCase()} court and enjoy your game
</p>
</div> </div>
<EnhancedBookingCalendar /> <EnhancedBookingCalendar />
+90 -55
View File
@@ -4,65 +4,99 @@
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 98.8235%;
--foreground: 222.2 84% 4.9%; --foreground: 0 0% 9.0196%;
--card: 0 0% 98.8235%;
--card: 0 0% 100%; --card-foreground: 0 0% 9.0196%;
--card-foreground: 222.2 84% 4.9%; --popover: 0 0% 98.8235%;
--popover-foreground: 0 0% 32.1569%;
--popover: 0 0% 100%; --primary: 151.3274 66.8639% 66.8627%;
--popover-foreground: 222.2 84% 4.9%; --primary-foreground: 153.3333 13.0435% 13.5294%;
--secondary: 0 0% 99.2157%;
--primary: 222.2 47.4% 11.2%; --secondary-foreground: 0 0% 9.0196%;
--primary-foreground: 210 40% 98%; --muted: 0 0% 92.9412%;
--muted-foreground: 0 0% 12.549%;
--secondary: 210 40% 96%; --accent: 0 0% 92.9412%;
--secondary-foreground: 222.2 84% 4.9%; --accent-foreground: 0 0% 12.549%;
--destructive: 9.8901 81.982% 43.5294%;
--muted: 210 40% 96%; --destructive-foreground: 0 100% 99.4118%;
--muted-foreground: 215.4 16.3% 46.9%; --border: 0 0% 87.451%;
--input: 0 0% 96.4706%;
--accent: 210 40% 96%; --ring: 151.3274 66.8639% 66.8627%;
--accent-foreground: 222.2 84% 4.9%; --chart-1: 151.3274 66.8639% 66.8627%;
--chart-2: 217.2193 91.2195% 59.8039%;
--destructive: 0 84.2% 60.2%; --chart-3: 258.3117 89.5349% 66.2745%;
--destructive-foreground: 210 40% 98%; --chart-4: 37.6923 92.126% 50.1961%;
--chart-5: 160.1183 84.0796% 39.4118%;
--border: 214.3 31.8% 91.4%; --sidebar: 0 0% 98.8235%;
--input: 214.3 31.8% 91.4%; --sidebar-foreground: 0 0% 43.9216%;
--ring: 222.2 84% 4.9%; --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; --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 { .dark {
--background: 222.2 84% 4.9%; --background: 0 0% 7.0588%;
--foreground: 210 40% 98%; --foreground: 214.2857 31.8182% 91.3725%;
--card: 0 0% 9.0196%;
--card: 222.2 84% 4.9%; --card-foreground: 214.2857 31.8182% 91.3725%;
--card-foreground: 210 40% 98%; --popover: 0 0% 14.1176%;
--popover-foreground: 0 0% 66.2745%;
--popover: 222.2 84% 4.9%; --primary: 154.898 100% 19.2157%;
--popover-foreground: 210 40% 98%; --primary-foreground: 152.7273 19.2982% 88.8235%;
--secondary: 0 0% 14.1176%;
--primary: 210 40% 98%; --secondary-foreground: 0 0% 98.0392%;
--primary-foreground: 222.2 47.4% 11.2%; --muted: 0 0% 12.1569%;
--muted-foreground: 0 0% 63.5294%;
--secondary: 217.2 32.6% 17.5%; --accent: 0 0% 19.2157%;
--secondary-foreground: 210 40% 98%; --accent-foreground: 0 0% 98.0392%;
--destructive: 6.6667 60% 20.5882%;
--muted: 217.2 32.6% 17.5%; --destructive-foreground: 12 12.1951% 91.9608%;
--muted-foreground: 215 20.2% 65.1%; --border: 0 0% 16.0784%;
--input: 0 0% 14.1176%;
--accent: 217.2 32.6% 17.5%; --ring: 141.8919 69.1589% 58.0392%;
--accent-foreground: 210 40% 98%; --chart-1: 141.8919 69.1589% 58.0392%;
--chart-2: 213.1169 93.9024% 67.8431%;
--destructive: 0 62.8% 30.6%; --chart-3: 255.1351 91.7355% 76.2745%;
--destructive-foreground: 210 40% 98%; --chart-4: 43.2558 96.4126% 56.2745%;
--chart-5: 172.4551 66.0079% 50.3922%;
--border: 217.2 32.6% 17.5%; --sidebar: 0 0% 7.0588%;
--input: 217.2 32.6% 17.5%; --sidebar-foreground: 0 0% 53.7255%;
--ring: 212.7 26.8% 83.9%; --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 { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
letter-spacing: var(--tracking-normal);
} }
} }
+8 -3
View File
@@ -3,13 +3,18 @@ import type { Metadata } from 'next';
import { Inter } from 'next/font/google'; import { Inter } from 'next/font/google';
import { ThemeProvider } from '@/components/theme-provider'; import { ThemeProvider } from '@/components/theme-provider';
import { Toaster } from '@/components/ui/toaster'; import { Toaster } from '@/components/ui/toaster';
import { getAppConfig } from '@/lib/app-config';
const inter = Inter({ subsets: ['latin'] }); const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = { export async function generateMetadata(): Promise<Metadata> {
title: 'Table Tennis Booking System', const config = await getAppConfig();
description: 'Book your table tennis court slots with ease',
return {
title: config.appTitle,
description: config.appDescription,
}; };
}
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
+6 -3
View File
@@ -1,13 +1,16 @@
import Link from 'next/link'; import Link from 'next/link';
import { LoginForm } from '@/components/auth/LoginForm'; 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 ( return (
<div className='min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center px-4'> <div className='min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center px-4'>
<div className='w-full max-w-md space-y-6'> <div className='w-full max-w-md space-y-6'>
<div className='text-center'> <div className='text-center'>
<h1 className='text-3xl font-bold text-foreground mb-2'>🏓 TT Booking</h1> <h1 className='text-3xl font-bold text-foreground mb-2'>🏓 {config.clubName}</h1>
<p className='text-muted-foreground'>Professional table tennis court booking system</p> <p className='text-muted-foreground'>{config.appDescription}</p>
</div> </div>
<LoginForm /> <LoginForm />
+6 -3
View File
@@ -1,13 +1,16 @@
import Link from 'next/link'; import Link from 'next/link';
import { RegisterForm } from '@/components/auth/RegisterForm'; 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 ( return (
<div className='min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center px-4'> <div className='min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center px-4'>
<div className='w-full max-w-md space-y-6'> <div className='w-full max-w-md space-y-6'>
<div className='text-center'> <div className='text-center'>
<h1 className='text-3xl font-bold text-foreground mb-2'>🏓 TT Booking</h1> <h1 className='text-3xl font-bold text-foreground mb-2'>🏓 {config.clubName}</h1>
<p className='text-muted-foreground'>Join our table tennis community</p> <p className='text-muted-foreground'>Join our {config.sportName.toLowerCase()} community</p>
</div> </div>
<RegisterForm /> <RegisterForm />
@@ -8,6 +8,17 @@ import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
@@ -37,7 +48,9 @@ export function AdminAnnouncementManagement() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [editingAnnouncement, setEditingAnnouncement] = useState<Announcement | null>(null); const [editingAnnouncement, setEditingAnnouncement] = useState<Announcement | null>(null);
const [announcementToDelete, setAnnouncementToDelete] = useState<Announcement | null>(null);
const [formData, setFormData] = useState<AnnouncementFormData>({ const [formData, setFormData] = useState<AnnouncementFormData>({
title: '', title: '',
content: '', 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) => { const handleDeleteAnnouncement = async (announcementId: string) => {
try { try {
if (!confirm('Are you sure you want to delete this announcement?')) {
return;
}
const response = await fetch(`/api/admin/announcements/${announcementId}`, { const response = await fetch(`/api/admin/announcements/${announcementId}`, {
method: 'DELETE', method: 'DELETE',
}); });
@@ -444,7 +466,7 @@ export function AdminAnnouncementManagement() {
<Button <Button
variant='outline' variant='outline'
size='sm' size='sm'
onClick={() => handleDeleteAnnouncement(announcement.id)} onClick={() => openDeleteDialog(announcement)}
className='text-destructive hover:text-destructive/90' className='text-destructive hover:text-destructive/90'
> >
<Trash2 className='h-4 w-4' /> <Trash2 className='h-4 w-4' />
@@ -539,6 +561,29 @@ export function AdminAnnouncementManagement() {
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Delete Confirmation Dialog */}
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete{' '}
{announcementToDelete ? `"${announcementToDelete.title}"` : 'this announcement'}? This
action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDeleteAnnouncement}
className='bg-destructive hover:bg-destructive/90'
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div> </div>
); );
} }
+49 -5
View File
@@ -6,6 +6,17 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; 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 { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
@@ -32,7 +43,9 @@ export function AdminCourtManagement() {
const [editing, setEditing] = useState<string | null>(null); const [editing, setEditing] = useState<string | null>(null);
const [deleting, setDeleting] = useState<string | null>(null); const [deleting, setDeleting] = useState<string | null>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [editingCourt, setEditingCourt] = useState<Court | null>(null); const [editingCourt, setEditingCourt] = useState<Court | null>(null);
const [courtToDelete, setCourtToDelete] = useState<Court | null>(null);
const [formData, setFormData] = useState<CourtFormData>({ const [formData, setFormData] = useState<CourtFormData>({
name: '', name: '',
isActive: true, isActive: true,
@@ -150,11 +163,20 @@ export function AdminCourtManagement() {
setIsDialogOpen(true); setIsDialogOpen(true);
}; };
const handleDelete = async (courtId: string) => { const openDeleteDialog = (court: Court) => {
if (!confirm('Are you sure you want to delete this court? This action cannot be undone.')) { setCourtToDelete(court);
return; setIsDeleteDialogOpen(true);
} };
const confirmDeleteCourt = async () => {
if (courtToDelete) {
await handleDelete(courtToDelete.id);
setIsDeleteDialogOpen(false);
setCourtToDelete(null);
}
};
const handleDelete = async (courtId: string) => {
try { try {
setDeleting(courtId); setDeleting(courtId);
const response = await fetch(`/api/admin/courts/${courtId}`, { const response = await fetch(`/api/admin/courts/${courtId}`, {
@@ -319,7 +341,7 @@ export function AdminCourtManagement() {
<Button <Button
size='sm' size='sm'
variant='outline' variant='outline'
onClick={() => handleDelete(court.id)} onClick={() => openDeleteDialog(court)}
disabled={deleting === court.id} disabled={deleting === court.id}
className='text-red-600 hover:text-red-700' className='text-red-600 hover:text-red-700'
> >
@@ -337,6 +359,28 @@ export function AdminCourtManagement() {
</div> </div>
)} )}
</CardContent> </CardContent>
{/* Delete Confirmation Dialog */}
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete {courtToDelete ? `"${courtToDelete.name}"` : 'this court'}?
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDeleteCourt}
className='bg-destructive hover:bg-destructive/90'
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Card> </Card>
); );
} }
+89 -7
View File
@@ -17,6 +17,12 @@ interface Setting {
} }
interface SettingsData { interface SettingsData {
// Club/Brand Settings
club_name: string;
sport_name: string;
app_title: string;
app_description: string;
// Booking Settings
booking_window_days: string; booking_window_days: string;
max_booking_duration_hours: string; max_booking_duration_hours: string;
min_booking_duration_minutes: string; min_booking_duration_minutes: string;
@@ -30,6 +36,12 @@ interface SettingsData {
export function AdminSettingsManagement() { export function AdminSettingsManagement() {
const [settings, setSettings] = useState<SettingsData>({ const [settings, setSettings] = useState<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', booking_window_days: '7',
max_booking_duration_hours: '2', max_booking_duration_hours: '2',
min_booking_duration_minutes: '30', min_booking_duration_minutes: '30',
@@ -38,7 +50,7 @@ export function AdminSettingsManagement() {
allow_weekend_bookings: 'true', allow_weekend_bookings: 'true',
max_bookings_per_user_per_hour_per_day: '1', max_bookings_per_user_per_hour_per_day: '1',
allow_booking_modifications: 'true', allow_booking_modifications: 'true',
booking_modification_hours_before: '2', booking_modification_hours_before: '1',
}); });
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@@ -54,6 +66,12 @@ export function AdminSettingsManagement() {
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
const settingsMap: SettingsData = { 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', booking_window_days: '7',
max_booking_duration_hours: '2', max_booking_duration_hours: '2',
min_booking_duration_minutes: '30', min_booking_duration_minutes: '30',
@@ -62,10 +80,8 @@ export function AdminSettingsManagement() {
allow_weekend_bookings: 'true', allow_weekend_bookings: 'true',
max_bookings_per_user_per_hour_per_day: '1', max_bookings_per_user_per_hour_per_day: '1',
allow_booking_modifications: 'true', allow_booking_modifications: 'true',
booking_modification_hours_before: '2', booking_modification_hours_before: '1',
}; }; // Map the settings array to our object
// Map the settings array to our object
data.settings?.forEach((setting: Setting) => { data.settings?.forEach((setting: Setting) => {
if (setting.key in settingsMap) { if (setting.key in settingsMap) {
settingsMap[setting.key as keyof SettingsData] = setting.value; settingsMap[setting.key as keyof SettingsData] = setting.value;
@@ -189,6 +205,67 @@ export function AdminSettingsManagement() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className='space-y-6'> <CardContent className='space-y-6'>
{/* Club/Brand Configuration Section */}
<div>
<h3 className='text-lg font-medium mb-4'>Club & Branding</h3>
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
{/* Club Name */}
<div className='space-y-2'>
<Label htmlFor='club_name'>Club Name</Label>
<Input
id='club_name'
type='text'
placeholder='e.g., Downtown TT Club'
value={settings.club_name}
onChange={(e) => updateSetting('club_name', e.target.value)}
/>
<p className='text-sm text-gray-500'>The name of your club or organization</p>
</div>
{/* Sport Name */}
<div className='space-y-2'>
<Label htmlFor='sport_name'>Sport Name</Label>
<Input
id='sport_name'
type='text'
placeholder='e.g., Table Tennis, Ping Pong, Badminton'
value={settings.sport_name}
onChange={(e) => updateSetting('sport_name', e.target.value)}
/>
<p className='text-sm text-gray-500'>The sport played at your facility</p>
</div>
{/* App Title */}
<div className='space-y-2'>
<Label htmlFor='app_title'>Application Title</Label>
<Input
id='app_title'
type='text'
placeholder='e.g., Downtown TT Booking'
value={settings.app_title}
onChange={(e) => updateSetting('app_title', e.target.value)}
/>
<p className='text-sm text-gray-500'>Main title shown in browser and app header</p>
</div>
{/* App Description */}
<div className='space-y-2'>
<Label htmlFor='app_description'>Application Description</Label>
<Input
id='app_description'
type='text'
placeholder='e.g., Book your court slots with ease'
value={settings.app_description}
onChange={(e) => updateSetting('app_description', e.target.value)}
/>
<p className='text-sm text-gray-500'>Short description for login/register pages</p>
</div>
</div>
</div>
{/* Booking Configuration Section */}
<div>
<h3 className='text-lg font-medium mb-4'>Booking Configuration</h3>
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'> <div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
{/* Booking Window */} {/* Booking Window */}
<div className='space-y-2'> <div className='space-y-2'>
@@ -260,14 +337,18 @@ export function AdminSettingsManagement() {
{/* Booking Restrictions */} {/* Booking Restrictions */}
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='max_bookings_per_user_per_hour_per_day'>Max Bookings per User per Hour</Label> <Label htmlFor='max_bookings_per_user_per_hour_per_day'>
Max Bookings per User per Hour
</Label>
<Input <Input
id='max_bookings_per_user_per_hour_per_day' id='max_bookings_per_user_per_hour_per_day'
type='number' type='number'
min='1' min='1'
max='5' max='5'
value={settings.max_bookings_per_user_per_hour_per_day} value={settings.max_bookings_per_user_per_hour_per_day}
onChange={(e) => updateSetting('max_bookings_per_user_per_hour_per_day', e.target.value)} onChange={(e) =>
updateSetting('max_bookings_per_user_per_hour_per_day', e.target.value)
}
/> />
<p className='text-sm text-gray-500'>Maximum bookings per user per hour on the same day</p> <p className='text-sm text-gray-500'>Maximum bookings per user per hour on the same day</p>
</div> </div>
@@ -324,6 +405,7 @@ export function AdminSettingsManagement() {
<p className='text-sm text-gray-500'>Whether users can book courts on weekends</p> <p className='text-sm text-gray-500'>Whether users can book courts on weekends</p>
</div> </div>
</div> </div>
</div>
<div className='border-t pt-6'> <div className='border-t pt-6'>
<h3 className='text-lg font-medium mb-4'>Current Configuration Summary</h3> <h3 className='text-lg font-medium mb-4'>Current Configuration Summary</h3>
+84 -11
View File
@@ -7,6 +7,17 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; 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 { Switch } from '@/components/ui/switch';
import { Plus, Edit, Trash2, Clock } from 'lucide-react'; import { Plus, Edit, Trash2, Clock } from 'lucide-react';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
@@ -30,7 +41,11 @@ export function AdminTimeSlotManagement() {
const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]); const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showWipeDayDialog, setShowWipeDayDialog] = useState(false);
const [editingSlot, setEditingSlot] = useState<TimeSlot | null>(null); const [editingSlot, setEditingSlot] = useState<TimeSlot | null>(null);
const [slotToDelete, setSlotToDelete] = useState<TimeSlot | null>(null);
const [dayToWipe, setDayToWipe] = useState<number | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
dayOfWeek: 1, // Default to Monday (Irish standard) dayOfWeek: 1, // Default to Monday (Irish standard)
startTime: '', startTime: '',
@@ -115,11 +130,20 @@ export function AdminTimeSlotManagement() {
} }
}; };
const handleDelete = async (id: string) => { const openDeleteDialog = (slot: TimeSlot) => {
if (!confirm('Are you sure you want to delete this time slot?')) { setSlotToDelete(slot);
return; setShowDeleteDialog(true);
} };
const confirmDeleteSlot = async () => {
if (slotToDelete) {
await handleDelete(slotToDelete.id);
setShowDeleteDialog(false);
setSlotToDelete(null);
}
};
const handleDelete = async (id: string) => {
try { try {
setLoading(true); setLoading(true);
const response = await fetch(`/api/admin/time-slots/${id}`, { const response = await fetch(`/api/admin/time-slots/${id}`, {
@@ -152,14 +176,23 @@ export function AdminTimeSlotManagement() {
} }
}; };
const handleWipeDay = async (dayOfWeek: number) => { const openWipeDayDialog = (dayOfWeek: number) => {
const dayName = DAYS[dayOfWeek]; setDayToWipe(dayOfWeek);
if (!confirm(`Are you sure you want to delete ALL time slots for ${dayName}? This action cannot be undone.`)) { setShowWipeDayDialog(true);
return; };
}
const confirmWipeDay = async () => {
if (dayToWipe !== null) {
await handleWipeDay(dayToWipe);
setShowWipeDayDialog(false);
setDayToWipe(null);
}
};
const handleWipeDay = async (dayOfWeek: number) => {
try { try {
setLoading(true); setLoading(true);
const dayName = DAYS[dayOfWeek];
const slotsToDelete = timeSlots.filter((slot) => slot.dayOfWeek === dayOfWeek); const slotsToDelete = timeSlots.filter((slot) => slot.dayOfWeek === dayOfWeek);
// Delete all slots for this day // Delete all slots for this day
@@ -322,7 +355,7 @@ export function AdminTimeSlotManagement() {
<Button <Button
size='sm' size='sm'
variant='outline' variant='outline'
onClick={() => handleWipeDay(jsDayOfWeek)} onClick={() => openWipeDayDialog(jsDayOfWeek)}
className='text-destructive hover:text-destructive/80 hover:bg-destructive/10' className='text-destructive hover:text-destructive/80 hover:bg-destructive/10'
disabled={loading} disabled={loading}
> >
@@ -369,7 +402,7 @@ export function AdminTimeSlotManagement() {
<Button <Button
size='sm' size='sm'
variant='outline' variant='outline'
onClick={() => handleDelete(slot.id)} onClick={() => openDeleteDialog(slot)}
className='text-destructive hover:text-destructive/80' className='text-destructive hover:text-destructive/80'
> >
<Trash2 className='h-4 w-4' /> <Trash2 className='h-4 w-4' />
@@ -389,6 +422,46 @@ export function AdminTimeSlotManagement() {
</div> </div>
)} )}
</CardContent> </CardContent>
{/* Delete Time Slot Dialog */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this time slot? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDeleteSlot}
className='bg-destructive hover:bg-destructive/90'
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Wipe Day Dialog */}
<AlertDialog open={showWipeDayDialog} onOpenChange={setShowWipeDayDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete ALL time slots for{' '}
{dayToWipe !== null ? DAYS[dayToWipe] : 'this day'}? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={confirmWipeDay} className='bg-destructive hover:bg-destructive/90'>
Delete All
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Card> </Card>
); );
} }
+50 -5
View File
@@ -7,6 +7,17 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
@@ -35,7 +46,9 @@ export function AdminUserManagement() {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null); const [editingUser, setEditingUser] = useState<User | null>(null);
const [userToDelete, setUserToDelete] = useState<User | null>(null);
const [formData, setFormData] = useState<UserFormData>({ const [formData, setFormData] = useState<UserFormData>({
name: '', name: '',
surname: '', 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) => { const handleDeleteUser = async (userId: string) => {
try { 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}`, { const response = await fetch(`/api/admin/users/${userId}`, {
method: 'DELETE', method: 'DELETE',
}); });
@@ -406,7 +428,7 @@ export function AdminUserManagement() {
<Button <Button
variant='outline' variant='outline'
size='sm' size='sm'
onClick={() => handleDeleteUser(user.id)} onClick={() => openDeleteDialog(user)}
className='text-red-600 hover:text-red-700' className='text-red-600 hover:text-red-700'
> >
<Trash2 className='h-4 w-4' /> <Trash2 className='h-4 w-4' />
@@ -498,6 +520,29 @@ export function AdminUserManagement() {
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </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> </div>
); );
} }
@@ -473,11 +473,11 @@ export function EnhancedBookingCalendar() {
: '' : ''
} ${ } ${
isTodayDate && !isSelectedDate 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 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() {
<div <div
className={`text-center p-4 rounded-lg ${ className={`text-center p-4 rounded-lg ${
isToday(selectedDate) isToday(selectedDate)
? 'bg-gradient-to-r from-blue-500 to-indigo-600 text-white dark:from-blue-600 dark:to-indigo-700' ? 'bg-gradient-to-r from-primary to-primary/80 text-primary-foreground'
: 'bg-blue-50 dark:bg-blue-950' : 'bg-accent/50'
}`} }`}
> >
<h3 <h3
@@ -520,7 +520,7 @@ export function EnhancedBookingCalendar() {
{isToday(selectedDate) && ( {isToday(selectedDate) && (
<div className='flex items-center justify-center gap-2 mt-2'> <div className='flex items-center justify-center gap-2 mt-2'>
<div className='w-2 h-2 bg-orange-300 dark:bg-orange-400 rounded-full animate-pulse' /> <div className='w-2 h-2 bg-orange-300 dark:bg-orange-400 rounded-full animate-pulse' />
<span className='text-sm font-medium text-blue-100 dark:text-blue-200'>Today</span> <span className='text-sm font-medium text-primary-foreground/90'>Today</span>
<div className='w-2 h-2 bg-orange-300 dark:bg-orange-400 rounded-full animate-pulse' /> <div className='w-2 h-2 bg-orange-300 dark:bg-orange-400 rounded-full animate-pulse' />
</div> </div>
)} )}
+7 -15
View File
@@ -61,16 +61,8 @@ export function UserBookingManagement() {
const response = await fetch('/api/bookings'); const response = await fetch('/api/bookings');
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
// Filter to show only future and today's bookings // API already filters to show only active future bookings (today onwards)
const now = new Date(); setBookings(data.bookings || []);
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);
} }
} catch (error) { } catch (error) {
console.error('Error fetching bookings:', error); console.error('Error fetching bookings:', error);
@@ -282,7 +274,7 @@ export function UserBookingManagement() {
<CardHeader className='pb-3 flex flex-row items-center justify-between'> <CardHeader className='pb-3 flex flex-row items-center justify-between'>
<CardTitle className='text-base flex items-center gap-2'> <CardTitle className='text-base flex items-center gap-2'>
<Calendar className='h-4 w-4' /> <Calendar className='h-4 w-4' />
Your Bookings Your Upcoming Bookings
</CardTitle> </CardTitle>
<Button size='sm' variant='outline' onClick={fetchBookings}> <Button size='sm' variant='outline' onClick={fetchBookings}>
<RefreshCw className='h-4 w-4' /> <RefreshCw className='h-4 w-4' />
@@ -390,16 +382,16 @@ export function UserBookingManagement() {
</DialogHeader> </DialogHeader>
<div className='space-y-4'> <div className='space-y-4'>
{selectedBooking && ( {selectedBooking && (
<div className='bg-blue-50 p-4 rounded-lg space-y-2 dark:bg-blue-950'> <div className='bg-accent/50 p-4 rounded-lg space-y-2'>
<div className='flex items-center gap-2 text-sm'> <div className='flex items-center gap-2 text-sm text-foreground'>
<Calendar className='h-4 w-4' /> <Calendar className='h-4 w-4' />
{formatDate(selectedBooking.date)} {formatDate(selectedBooking.date)}
</div> </div>
<div className='flex items-center gap-2 text-sm'> <div className='flex items-center gap-2 text-sm text-foreground'>
<Clock className='h-4 w-4' /> <Clock className='h-4 w-4' />
{selectedBooking.startTime} - {selectedBooking.endTime} {selectedBooking.startTime} - {selectedBooking.endTime}
</div> </div>
<div className='flex items-center gap-2 text-sm'> <div className='flex items-center gap-2 text-sm text-foreground'>
<MapPin className='h-4 w-4' /> <MapPin className='h-4 w-4' />
{selectedBooking.court.name} {selectedBooking.court.name}
</div> </div>
+16 -3
View File
@@ -6,6 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { CalendarIcon, Clock, MapPin } from 'lucide-react'; import { CalendarIcon, Clock, MapPin } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
const timeSlots = [ const timeSlots = [
'09:00', '09:00',
@@ -31,6 +32,7 @@ export function BookingCalendar() {
const [selectedDate, setSelectedDate] = useState<Date>(new Date()); const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const [selectedSlot, setSelectedSlot] = useState<string | null>(null); const [selectedSlot, setSelectedSlot] = useState<string | null>(null);
const [selectedCourt, setSelectedCourt] = useState<string | null>(null); const [selectedCourt, setSelectedCourt] = useState<string | null>(null);
const { toast } = useToast();
const formatDate = (date: Date) => { const formatDate = (date: Date) => {
return date.toLocaleDateString('en-IE', { return date.toLocaleDateString('en-IE', {
@@ -66,13 +68,24 @@ export function BookingCalendar() {
setSelectedSlot(null); setSelectedSlot(null);
setSelectedCourt(null); setSelectedCourt(null);
// Show success message // Show success message
alert('Booking created successfully!'); toast({
title: 'Success',
description: 'Booking created successfully!',
});
} else { } else {
alert(result.error || 'Booking failed'); toast({
title: 'Error',
description: result.error || 'Booking failed',
variant: 'destructive',
});
} }
} catch (error) { } catch (error) {
console.error('Booking error:', error); console.error('Booking error:', error);
alert('An error occurred while creating the booking'); toast({
title: 'Error',
description: 'An error occurred while creating the booking',
variant: 'destructive',
});
} }
}; };
+19 -1
View File
@@ -10,6 +10,7 @@ import { useToast } from '@/hooks/use-toast';
import { NotificationBell, AnnouncementsModal } from '@/components/notifications/announcements'; import { NotificationBell, AnnouncementsModal } from '@/components/notifications/announcements';
import { UserProfile } from '@/components/user/user-profile'; import { UserProfile } from '@/components/user/user-profile';
import { ModeToggle } from '@/components/ui/mode-toggle'; import { ModeToggle } from '@/components/ui/mode-toggle';
import type { AppConfig } from '@/lib/app-config';
interface DashboardHeaderProps { interface DashboardHeaderProps {
user: { user: {
@@ -28,6 +29,23 @@ export function DashboardHeader({ user }: DashboardHeaderProps) {
const [showAnnouncements, setShowAnnouncements] = useState(false); const [showAnnouncements, setShowAnnouncements] = useState(false);
const [showUserProfile, setShowUserProfile] = useState(false); const [showUserProfile, setShowUserProfile] = useState(false);
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
const [appConfig, setAppConfig] = useState<AppConfig | null>(null);
useEffect(() => {
const fetchAppConfig = async () => {
try {
const response = await fetch('/api/config');
if (response.ok) {
const config = await response.json();
setAppConfig(config);
}
} catch (error) {
console.error('Error fetching app config:', error);
}
};
fetchAppConfig();
}, []);
// Fetch unread announcements count on component mount // Fetch unread announcements count on component mount
useEffect(() => { useEffect(() => {
@@ -78,7 +96,7 @@ export function DashboardHeader({ user }: DashboardHeaderProps) {
<div className='flex items-center space-x-4'> <div className='flex items-center space-x-4'>
<div className='flex items-center space-x-2'> <div className='flex items-center space-x-2'>
<Calendar className='h-6 w-6 text-primary' /> <Calendar className='h-6 w-6 text-primary' />
<h1 className='text-xl font-bold text-foreground'>TT Booking</h1> <h1 className='text-xl font-bold text-foreground'>{appConfig?.clubName || 'TT Booking'}</h1>
</div> </div>
{user.role === 'admin' && <Badge variant='secondary'>Admin</Badge>} {user.role === 'admin' && <Badge variant='secondary'>Admin</Badge>}
</div> </div>
+91
View File
@@ -0,0 +1,91 @@
import { db } from '@/lib/db';
import { settings } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
export interface AppConfig {
clubName: string;
sportName: string;
appTitle: string;
appDescription: string;
}
const defaultConfig: AppConfig = {
clubName: 'TT Club',
sportName: 'Table Tennis',
appTitle: 'Table Tennis Booking System',
appDescription: 'Book your table tennis court slots with ease',
};
let cachedConfig: AppConfig | null = null;
let cacheTime: number = 0;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
/**
* Get app configuration with caching
* This function fetches the club/brand settings from the database
*/
export async function getAppConfig(): Promise<AppConfig> {
const now = Date.now();
// Return cached config if still valid
if (cachedConfig && now - cacheTime < CACHE_DURATION) {
return cachedConfig;
}
try {
// Fetch all brand/club settings
const configSettings = await db
.select()
.from(settings)
.where(eq(settings.key, 'club_name'))
.union(db.select().from(settings).where(eq(settings.key, 'sport_name')))
.union(db.select().from(settings).where(eq(settings.key, 'app_title')))
.union(db.select().from(settings).where(eq(settings.key, 'app_description')));
// Build config object
const config: AppConfig = { ...defaultConfig };
configSettings.forEach((setting) => {
switch (setting.key) {
case 'club_name':
config.clubName = setting.value;
break;
case 'sport_name':
config.sportName = setting.value;
break;
case 'app_title':
config.appTitle = setting.value;
break;
case 'app_description':
config.appDescription = setting.value;
break;
}
});
// Cache the result
cachedConfig = config;
cacheTime = now;
return config;
} catch (error) {
console.error('Error fetching app config:', error);
// Return default config on error
return defaultConfig;
}
}
/**
* Invalidate the cache (call when settings are updated)
*/
export function invalidateAppConfigCache() {
cachedConfig = null;
cacheTime = 0;
}
/**
* Get app config for client components (no database access)
* This should be used with server-side rendered props
*/
export function getDefaultAppConfig(): AppConfig {
return { ...defaultConfig };
}
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env tsx
import { db } from '../lib/db';
import { settings } from '../lib/db/schema';
import { eq } from 'drizzle-orm';
async function seedBrandSettings() {
console.log('🌱 Seeding brand settings...');
const brandSettings = [
{ key: 'club_name', value: 'TT Club' },
{ key: 'sport_name', value: 'Table Tennis' },
{ key: 'app_title', value: 'Table Tennis Booking System' },
{ key: 'app_description', value: 'Book your table tennis court slots with ease' },
];
for (const setting of brandSettings) {
try {
// Check if setting exists
const existing = await db.select().from(settings).where(eq(settings.key, setting.key)).limit(1);
if (existing.length === 0) {
// Insert new setting
await db.insert(settings).values({
id: crypto.randomUUID(),
key: setting.key,
value: setting.value,
updatedAt: new Date(),
});
console.log(`✅ Added setting: ${setting.key} = ${setting.value}`);
} else {
console.log(`⚠️ Setting already exists: ${setting.key}`);
}
} catch (error) {
console.error(`❌ Error adding setting ${setting.key}:`, error);
}
}
console.log('✅ Brand settings seeded successfully!');
process.exit(0);
}
seedBrandSettings().catch((error) => {
console.error('❌ Error seeding brand settings:', error);
process.exit(1);
});
+1 -1
View File
@@ -27,5 +27,5 @@
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules", "scripts/old-seeds/**/*"]
} }