From b89d91ade289be73e0c92ccf02619ca09dd7166c Mon Sep 17 00:00:00 2001 From: mikicvi <88291034+mikicvi@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:23:18 +0100 Subject: [PATCH] additional features, refinement and more control over the app from admin side, better bookings UX --- app/api/admin/stats/route.ts | 68 +++ app/api/admin/time-slots/[id]/route.ts | 10 +- app/api/admin/time-slots/route.ts | 10 +- app/api/bookings/all/route.ts | 2 +- app/api/bookings/route.ts | 89 ++- app/dashboard/page.tsx | 32 +- components/admin/AdminSettingsManagement.tsx | 21 + components/admin/AdminTimeSlotManagement.tsx | 197 ++++--- components/admin/AdminUserManagement.tsx | 31 +- components/admin/admin-dashboard.tsx | 88 ++- .../booking/enhanced-booking-calendar.tsx | 239 +++++--- .../booking/user-booking-management.tsx | 7 +- components/dashboard/dashboard-header.tsx | 6 +- docs/DAY_SPECIFIC_FEATURES.md | 82 +-- docs/ROBUST_VALIDATION_COMPLETE.md | 148 +++-- lib/db/migrations/0001_slimy_starjammers.sql | 8 + lib/db/migrations/meta/0001_snapshot.json | 549 ++++++++++++++++++ lib/db/migrations/meta/_journal.json | 7 + lib/db/schema.ts | 14 + scripts/init-admin-data.ts | 78 +++ 20 files changed, 1358 insertions(+), 328 deletions(-) create mode 100644 app/api/admin/stats/route.ts create mode 100644 lib/db/migrations/0001_slimy_starjammers.sql create mode 100644 lib/db/migrations/meta/0001_snapshot.json create mode 100644 scripts/init-admin-data.ts diff --git a/app/api/admin/stats/route.ts b/app/api/admin/stats/route.ts new file mode 100644 index 0000000..90f66df --- /dev/null +++ b/app/api/admin/stats/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getSession } from '@/lib/session'; +import { db } from '@/lib/db'; +import { users, courts, bookings, metrics } from '@/lib/db/schema'; +import { eq, and, gte, lte, desc, count } from 'drizzle-orm'; + +export async function GET(request: NextRequest) { + try { + const session = await getSession(); + if (!session || session.role !== 'admin') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // Get real stats from database + const totalUsers = await db.select({ count: count() }).from(users); + const activeCourts = await db.select({ count: count() }).from(courts).where(eq(courts.isActive, true)); + + // Get today's bookings count + const today = new Date().toISOString().split('T')[0]; + const todaysBookings = await db + .select({ count: count() }) + .from(bookings) + .where(and(eq(bookings.date, today), eq(bookings.status, 'active'))); + + // Get current month's bookings from metrics table + const currentMonth = new Date().toISOString().substring(0, 7); // "2025-09" + const monthlyBookings = await db + .select() + .from(metrics) + .where(and(eq(metrics.metricType, 'monthly_bookings'), eq(metrics.period, currentMonth))) + .limit(1); + + // Get recent bookings with user names + const recentBookings = await db + .select({ + id: bookings.id, + date: bookings.date, + startTime: bookings.startTime, + endTime: bookings.endTime, + courtName: courts.name, + userName: users.name, + userSurname: users.surname, + status: bookings.status, + createdAt: bookings.createdAt, + }) + .from(bookings) + .leftJoin(users, eq(bookings.userId, users.id)) + .leftJoin(courts, eq(bookings.courtId, courts.id)) + .orderBy(desc(bookings.createdAt)) + .limit(10); + + return NextResponse.json({ + stats: { + totalUsers: totalUsers[0]?.count || 0, + activeCourts: activeCourts[0]?.count || 0, + todaysBookings: todaysBookings[0]?.count || 0, + monthlyBookings: monthlyBookings[0]?.value || 0, + }, + recentBookings: recentBookings.map((booking) => ({ + ...booking, + userName: `${booking.userName} ${booking.userSurname}`, + })), + }); + } catch (error) { + console.error('Error fetching admin stats:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/app/api/admin/time-slots/[id]/route.ts b/app/api/admin/time-slots/[id]/route.ts index de39a3e..d4128bf 100644 --- a/app/api/admin/time-slots/[id]/route.ts +++ b/app/api/admin/time-slots/[id]/route.ts @@ -31,17 +31,11 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/; if (startTime && !timeRegex.test(startTime)) { - return NextResponse.json( - { error: 'Invalid startTime format. Use HH:MM format' }, - { status: 400 } - ); + return NextResponse.json({ error: 'Invalid startTime format. Use HH:MM format' }, { status: 400 }); } if (endTime && !timeRegex.test(endTime)) { - return NextResponse.json( - { error: 'Invalid endTime format. Use HH:MM format' }, - { status: 400 } - ); + return NextResponse.json({ error: 'Invalid endTime format. Use HH:MM format' }, { status: 400 }); } const updatedTimeSlot = await db diff --git a/app/api/admin/time-slots/route.ts b/app/api/admin/time-slots/route.ts index 7b6f9ca..3863958 100644 --- a/app/api/admin/time-slots/route.ts +++ b/app/api/admin/time-slots/route.ts @@ -12,10 +12,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const allTimeSlots = await db - .select() - .from(timeSlots) - .orderBy(timeSlots.dayOfWeek, timeSlots.startTime); + const allTimeSlots = await db.select().from(timeSlots).orderBy(timeSlots.dayOfWeek, timeSlots.startTime); return NextResponse.json({ timeSlots: allTimeSlots, @@ -53,10 +50,7 @@ export async function POST(request: NextRequest) { // Validate time format (HH:MM) const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/; if (!timeRegex.test(startTime) || !timeRegex.test(endTime)) { - return NextResponse.json( - { error: 'Invalid time format. Use HH:MM format' }, - { status: 400 } - ); + return NextResponse.json({ error: 'Invalid time format. Use HH:MM format' }, { status: 400 }); } const newTimeSlot = await db diff --git a/app/api/bookings/all/route.ts b/app/api/bookings/all/route.ts index d8e0f4c..878e307 100644 --- a/app/api/bookings/all/route.ts +++ b/app/api/bookings/all/route.ts @@ -17,7 +17,7 @@ export async function GET(request: NextRequest) { // Build query conditions const whereConditions = []; whereConditions.push(eq(bookings.status, 'active')); - + if (date) { whereConditions.push(eq(bookings.date, date)); } diff --git a/app/api/bookings/route.ts b/app/api/bookings/route.ts index 725fb0f..bd30e75 100644 --- a/app/api/bookings/route.ts +++ b/app/api/bookings/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; -import { bookings, courts, timeSlots } from '@/lib/db/schema'; +import { bookings, courts, timeSlots, settings, metrics } from '@/lib/db/schema'; import { eq, and } from 'drizzle-orm'; import { getSession } from '@/lib/session'; import { logActivity, ACTIONS, ENTITY_TYPES } from '@/lib/activity-logger'; @@ -45,7 +45,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const { courtId, date, timeSlot } = await request.json(); + const { courtId, date, timeSlot, notes } = await request.json(); if (!courtId || !date || !timeSlot) { return NextResponse.json( @@ -91,18 +91,15 @@ export async function POST(request: NextRequest) { const availableTimeSlots = await db .select() .from(timeSlots) - .where( - and( - eq(timeSlots.dayOfWeek, dayOfWeek), - eq(timeSlots.isActive, true) - ) - ); + .where(and(eq(timeSlots.dayOfWeek, dayOfWeek), eq(timeSlots.isActive, true))); // Check if any time slots are configured for this day if (availableTimeSlots.length === 0) { return NextResponse.json( { - error: `No bookings are allowed on ${['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][dayOfWeek]}s. The facility is closed on this day.`, + error: `No bookings are allowed on ${ + ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][dayOfWeek] + }s. The facility is closed on this day.`, }, { status: 400 } ); @@ -110,17 +107,50 @@ export async function POST(request: NextRequest) { // Check if the requested time slot is within any of the allowed time ranges const requestedHour = parseInt(startTime.split(':')[0]); - const isTimeSlotValid = availableTimeSlots.some(slot => { + const isTimeSlotValid = availableTimeSlots.some((slot) => { const slotStartHour = parseInt(slot.startTime.split(':')[0]); const slotEndHour = parseInt(slot.endTime.split(':')[0]); return requestedHour >= slotStartHour && requestedHour < slotEndHour; }); if (!isTimeSlotValid) { - const allowedRanges = availableTimeSlots.map(slot => `${slot.startTime}-${slot.endTime}`).join(', '); + const allowedRanges = availableTimeSlots.map((slot) => `${slot.startTime}-${slot.endTime}`).join(', '); return NextResponse.json( { - error: `Time slot ${startTime} is not available on ${['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][dayOfWeek]}s. Available times: ${allowedRanges}`, + error: `Time slot ${startTime} is not available on ${ + ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][dayOfWeek] + }s. Available times: ${allowedRanges}`, + }, + { status: 400 } + ); + } + + // Check booking restrictions per user per hour per day + const maxBookingsSetting = await db + .select() + .from(settings) + .where(eq(settings.key, 'max_bookings_per_user_per_hour_per_day')) + .limit(1); + + const maxBookingsPerHour = maxBookingsSetting.length > 0 ? parseInt(maxBookingsSetting[0].value) : 1; // Default to 1 if setting not found + + // Count user's existing bookings for this hour on this day + const userBookingsThisHour = await db + .select() + .from(bookings) + .where( + and( + eq(bookings.userId, session.userId), + eq(bookings.date, date), + eq(bookings.startTime, startTime), + eq(bookings.status, 'active') + ) + ); + + if (userBookingsThisHour.length >= maxBookingsPerHour) { + return NextResponse.json( + { + error: `You have reached the maximum limit of ${maxBookingsPerHour} booking(s) per hour. You already have ${userBookingsThisHour.length} booking(s) at ${startTime} on this date.`, }, { status: 400 } ); @@ -160,6 +190,7 @@ export async function POST(request: NextRequest) { startTime, endTime, status: 'active', + notes: notes || null, // Include notes from the request createdAt: new Date(), updatedAt: new Date(), }) @@ -181,6 +212,40 @@ export async function POST(request: NextRequest) { request, }); + // Update monthly metrics + const currentMonth = new Date().toISOString().substring(0, 7); // "2025-09" + try { + const existingMetric = await db + .select() + .from(metrics) + .where(and(eq(metrics.metricType, 'monthly_bookings'), eq(metrics.period, currentMonth))) + .limit(1); + + if (existingMetric.length > 0) { + // Increment existing metric + await db + .update(metrics) + .set({ + value: existingMetric[0].value + 1, + updatedAt: new Date(), + }) + .where(eq(metrics.id, existingMetric[0].id)); + } else { + // Create new metric for this month + await db.insert(metrics).values({ + id: crypto.randomUUID(), + metricType: 'monthly_bookings', + period: currentMonth, + value: 1, + createdAt: new Date(), + updatedAt: new Date(), + }); + } + } catch (error) { + console.error('Error updating monthly metrics:', error); + // Don't fail the booking if metrics update fails + } + return NextResponse.json({ booking: newBooking, message: 'Booking created successfully', diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 883afea..a43b9f1 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,5 +1,8 @@ import { redirect } from 'next/navigation'; import { getSession } from '@/lib/session'; +import { db } from '@/lib/db'; +import { users } from '@/lib/db/schema'; +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'; @@ -11,9 +14,32 @@ export default async function DashboardPage() { redirect('/login'); } + // Get full user information + const [user] = await db + .select({ + id: users.id, + email: users.email, + name: users.name, + surname: users.surname, + role: users.role, + }) + .from(users) + .where(eq(users.id, session.userId)) + .limit(1); + + if (!user) { + redirect('/login'); + } + + const userWithSession = { + ...session, + name: user.name, + surname: user.surname, + }; + return (
- +
@@ -21,7 +47,9 @@ export default async function DashboardPage() {

- Welcome back, {session.email.split('@')[0]}! ๐Ÿ“ + Welcome back,{' '} + {user.name && user.surname ? `${user.name} ${user.surname}` : user.email.split('@')[0]}! + ๐Ÿ“

Book your table tennis court and enjoy your game

diff --git a/components/admin/AdminSettingsManagement.tsx b/components/admin/AdminSettingsManagement.tsx index 00d38fc..d466279 100644 --- a/components/admin/AdminSettingsManagement.tsx +++ b/components/admin/AdminSettingsManagement.tsx @@ -23,6 +23,7 @@ interface SettingsData { booking_start_time: string; booking_end_time: string; allow_weekend_bookings: string; + max_bookings_per_user_per_hour_per_day: string; } export function AdminSettingsManagement() { @@ -33,6 +34,7 @@ export function AdminSettingsManagement() { booking_start_time: '08:00', booking_end_time: '22:00', allow_weekend_bookings: 'true', + max_bookings_per_user_per_hour_per_day: '1', }); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); @@ -54,6 +56,7 @@ export function AdminSettingsManagement() { booking_start_time: '08:00', booking_end_time: '22:00', allow_weekend_bookings: 'true', + max_bookings_per_user_per_hour_per_day: '1', }; // Map the settings array to our object @@ -249,6 +252,20 @@ export function AdminSettingsManagement() {

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

+
+ {/* Weekend Bookings */}
@@ -286,6 +303,10 @@ export function AdminSettingsManagement() { Weekend Bookings:{' '} {settings.allow_weekend_bookings === 'true' ? 'Enabled' : 'Disabled'}

+

+ Booking Limit: {settings.max_bookings_per_user_per_hour_per_day} per + hour +

diff --git a/components/admin/AdminTimeSlotManagement.tsx b/components/admin/AdminTimeSlotManagement.tsx index 522207a..3a93eec 100644 --- a/components/admin/AdminTimeSlotManagement.tsx +++ b/components/admin/AdminTimeSlotManagement.tsx @@ -21,15 +21,7 @@ interface TimeSlot { updatedAt: string; } -const DAYS = [ - 'Sunday', - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - 'Saturday' -]; +const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; export function AdminTimeSlotManagement() { const [timeSlots, setTimeSlots] = useState([]); @@ -76,16 +68,14 @@ export function AdminTimeSlotManagement() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - + try { setLoading(true); - - const url = editingSlot - ? `/api/admin/time-slots/${editingSlot.id}` - : '/api/admin/time-slots'; - + + const url = editingSlot ? `/api/admin/time-slots/${editingSlot.id}` : '/api/admin/time-slots'; + const method = editingSlot ? 'PUT' : 'POST'; - + const response = await fetch(url, { method, headers: { @@ -97,9 +87,7 @@ export function AdminTimeSlotManagement() { if (response.ok) { toast({ title: 'Success', - description: editingSlot - ? 'Time slot updated successfully' - : 'Time slot created successfully', + description: editingSlot ? 'Time slot updated successfully' : 'Time slot created successfully', }); fetchTimeSlots(); setShowDialog(false); @@ -161,6 +149,51 @@ 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; + } + + try { + setLoading(true); + const slotsToDelete = timeSlots.filter((slot) => slot.dayOfWeek === dayOfWeek); + + // Delete all slots for this day + const deletePromises = slotsToDelete.map((slot) => + fetch(`/api/admin/time-slots/${slot.id}`, { + method: 'DELETE', + }) + ); + + const responses = await Promise.all(deletePromises); + const successCount = responses.filter((response) => response.ok).length; + + if (successCount === slotsToDelete.length) { + toast({ + title: 'Success', + description: `All ${dayName} time slots deleted successfully`, + }); + fetchTimeSlots(); + } else { + toast({ + title: 'Partial Success', + description: `${successCount} of ${slotsToDelete.length} slots deleted`, + variant: 'destructive', + }); + } + } catch (error) { + console.error('Error wiping day slots:', error); + toast({ + title: 'Error', + description: 'Failed to delete day slots', + variant: 'destructive', + }); + } finally { + setLoading(false); + } + }; + const handleEdit = (slot: TimeSlot) => { setEditingSlot(slot); setFormData({ @@ -193,35 +226,33 @@ export function AdminTimeSlotManagement() { return ( -
- - +
+ + Time Slot Management - - {editingSlot ? 'Edit Time Slot' : 'Add New Time Slot'} - + {editingSlot ? 'Edit Time Slot' : 'Add New Time Slot'} -
+
- - setFormData({ ...formData, dayOfWeek: parseInt(value) }) } > - + {DAYS.map((day, index) => ( @@ -233,48 +264,38 @@ export function AdminTimeSlotManagement() {
- + - setFormData({ ...formData, startTime: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, startTime: e.target.value })} required />
- + - setFormData({ ...formData, endTime: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, endTime: e.target.value })} required />
-
+
- setFormData({ ...formData, isActive: checked }) - } + onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked })} /> - +
-
- -
@@ -285,57 +306,75 @@ export function AdminTimeSlotManagement() { {loading && timeSlots.length === 0 ? ( -
Loading time slots...
+
Loading time slots...
) : ( -
+
{DAYS.map((day, dayIndex) => ( -
-

{day}

+
+
+

{day}

+ {groupedTimeSlots[dayIndex]?.length > 0 && ( + + )} +
{groupedTimeSlots[dayIndex]?.length > 0 ? ( -
+
{groupedTimeSlots[dayIndex] .sort((a, b) => a.startTime.localeCompare(b.startTime)) .map((slot) => (
-
-
+
+
{slot.startTime} - {slot.endTime}
-
+
{slot.isActive ? 'Active' : 'Inactive'}
-
+
))}
) : ( -

No time slots configured for {day}

+

No time slots configured for {day}

)}
))} diff --git a/components/admin/AdminUserManagement.tsx b/components/admin/AdminUserManagement.tsx index d5f864f..71d75f7 100644 --- a/components/admin/AdminUserManagement.tsx +++ b/components/admin/AdminUserManagement.tsx @@ -71,8 +71,12 @@ export function AdminUserManagement() { } }; - const handleCreateUser = async () => { + const handleCreateUser = async (e?: React.FormEvent) => { try { + // Prevent form submission and double submissions + if (e) e.preventDefault(); + if (loading) return; + if (!formData.name || !formData.surname || !formData.email || !formData.password) { toast({ title: 'Error', @@ -82,6 +86,8 @@ export function AdminUserManagement() { return; } + setLoading(true); + const response = await fetch('/api/admin/users', { method: 'POST', headers: { @@ -114,11 +120,16 @@ export function AdminUserManagement() { description: 'Failed to create user', variant: 'destructive', }); + } finally { + setLoading(false); } }; const handleEditUser = async () => { try { + // Prevent double submissions + if (loading) return; + if (!editingUser || !formData.name || !formData.surname || !formData.email) { toast({ title: 'Error', @@ -128,6 +139,8 @@ export function AdminUserManagement() { return; } + setLoading(true); + const updateData = { ...formData }; if (!updateData.password) { delete updateData.password; // Don't update password if not provided @@ -166,6 +179,8 @@ export function AdminUserManagement() { description: 'Failed to update user', variant: 'destructive', }); + } finally { + setLoading(false); } }; @@ -262,7 +277,7 @@ export function AdminUserManagement() { Create New User -
+
@@ -321,12 +336,14 @@ export function AdminUserManagement() {
- - +
-
+
@@ -474,7 +491,9 @@ export function AdminUserManagement() { - +
diff --git a/components/admin/admin-dashboard.tsx b/components/admin/admin-dashboard.tsx index 4d82536..eca63e6 100644 --- a/components/admin/admin-dashboard.tsx +++ b/components/admin/admin-dashboard.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; @@ -15,14 +15,52 @@ import { AdminCourtManagement } from './AdminCourtManagement'; import { AdminSettingsManagement } from './AdminSettingsManagement'; import { AdminTimeSlotManagement } from './AdminTimeSlotManagement'; +interface AdminStats { + totalUsers: number; + activeCourts: number; + todaysBookings: number; + monthlyBookings: number; +} + +interface RecentBooking { + id: string; + date: string; + startTime: string; + endTime: string; + courtName: string; + userName: string; + status: string; +} + export function AdminDashboard() { const router = useRouter(); - const [stats] = useState({ - totalUsers: 125, - todayBookings: 18, - totalCourts: 2, - weeklyRevenue: 850, + const [stats, setStats] = useState({ + totalUsers: 0, + activeCourts: 0, + todaysBookings: 0, + monthlyBookings: 0, }); + const [recentBookings, setRecentBookings] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchStats(); + }, []); + + const fetchStats = async () => { + try { + const response = await fetch('/api/admin/stats'); + if (response.ok) { + const data = await response.json(); + setStats(data.stats); + setRecentBookings(data.recentBookings); + } + } catch (error) { + console.error('Error fetching admin stats:', error); + } finally { + setLoading(false); + } + }; const handleLogout = async () => { try { @@ -68,19 +106,8 @@ export function AdminDashboard() { -
{stats.totalUsers}
-

+12% from last month

-
- - - - - Today's Bookings - - - -
{stats.todayBookings}
-

+5% from yesterday

+
{loading ? '...' : stats.totalUsers}
+

Registered users

@@ -90,19 +117,30 @@ export function AdminDashboard() { -
{stats.totalCourts}
-

All courts operational

+
{loading ? '...' : stats.activeCourts}
+

Available for booking

- Weekly Revenue + Today's Bookings + + + +
{loading ? '...' : stats.todaysBookings}
+

Bookings for today

+
+
+ + + + Monthly Bookings -
${stats.weeklyRevenue}
-

+8% from last week

+
{loading ? '...' : stats.monthlyBookings}
+

This month's total

@@ -127,7 +165,7 @@ export function AdminDashboard() { {' '} -
+
diff --git a/components/booking/enhanced-booking-calendar.tsx b/components/booking/enhanced-booking-calendar.tsx index e9a2339..bb46300 100644 --- a/components/booking/enhanced-booking-calendar.tsx +++ b/components/booking/enhanced-booking-calendar.tsx @@ -39,6 +39,7 @@ interface BookingSlot { available: boolean; bookingId?: string; bookedBy?: string; + partner?: string; } interface TimeSlot { @@ -176,24 +177,22 @@ export function EnhancedBookingCalendar() { const generateTimeSlots = (): string[] => { const dayOfWeek = selectedDate.getDay(); - + // Get time slots for the selected day - const dayTimeSlots = timeSlots.filter( - slot => slot.dayOfWeek === dayOfWeek && slot.isActive - ); + const dayTimeSlots = timeSlots.filter((slot) => slot.dayOfWeek === dayOfWeek && slot.isActive); if (dayTimeSlots.length > 0) { // Use day-specific time slots const slots: string[] = []; - dayTimeSlots.forEach(timeSlot => { + dayTimeSlots.forEach((timeSlot) => { const start = parseInt(timeSlot.startTime.split(':')[0]); const end = parseInt(timeSlot.endTime.split(':')[0]); - + for (let hour = start; hour < end; hour++) { slots.push(`${hour.toString().padStart(2, '0')}:00`); } }); - + // Remove duplicates and sort return [...new Set(slots)].sort(); } @@ -205,9 +204,7 @@ export function EnhancedBookingCalendar() { const isDayBookable = (): boolean => { const dayOfWeek = selectedDate.getDay(); - const dayTimeSlots = timeSlots.filter( - slot => slot.dayOfWeek === dayOfWeek && slot.isActive - ); + const dayTimeSlots = timeSlots.filter((slot) => slot.dayOfWeek === dayOfWeek && slot.isActive); return dayTimeSlots.length > 0; }; @@ -216,6 +213,24 @@ export function EnhancedBookingCalendar() { return days[dayOfWeek]; }; + const parseBookingNotes = (notes?: string) => { + if (!notes) return { partner: '', additionalNotes: '' }; + + const parts = notes.split(' | '); + let partner = ''; + let additionalNotes = ''; + + parts.forEach((part) => { + if (part.startsWith('Partner: ')) { + partner = part.replace('Partner: ', ''); + } else { + additionalNotes = additionalNotes ? `${additionalNotes} | ${part}` : part; + } + }); + + return { partner, additionalNotes }; + }; + const generateBookingSlots = (existingBookings: Booking[]) => { const dateStr = selectedDate.toISOString().split('T')[0]; const timeSlots = generateTimeSlots(); @@ -231,10 +246,12 @@ export function EnhancedBookingCalendar() { booking.status === 'active' ); - const bookedBy = existingBooking?.user + const bookedBy = existingBooking?.user ? `${existingBooking.user.name} ${existingBooking.user.surname}` : undefined; + const { partner } = parseBookingNotes(existingBooking?.notes); + slots.push({ time, courtId: court.id, @@ -242,6 +259,7 @@ export function EnhancedBookingCalendar() { available: !existingBooking, bookingId: existingBooking?.id, bookedBy, + partner, }); }); }); @@ -268,10 +286,8 @@ export function EnhancedBookingCalendar() { // CRITICAL: Check if there are any active time slots for this day const dayOfWeek = selectedDateOnly.getDay(); - const dayTimeSlots = timeSlots.filter( - slot => slot.dayOfWeek === dayOfWeek && slot.isActive - ); - + const dayTimeSlots = timeSlots.filter((slot) => slot.dayOfWeek === dayOfWeek && slot.isActive); + // If no time slots are configured for this day, it's not selectable if (dayTimeSlots.length === 0) return false; @@ -441,34 +457,55 @@ export function EnhancedBookingCalendar() {
{getAvailableDates() .slice(0, 8) - .map((date, index) => ( - - ))} + .map((date, index) => { + const isSelectedDate = date.toDateString() === selectedDate.toDateString(); + const isTodayDate = isToday(date); + + return ( + + ); + })}
{/* Selected Date Display */} -
-

+
+

{selectedDate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', @@ -476,7 +513,13 @@ export function EnhancedBookingCalendar() { day: 'numeric', })}

- {isToday(selectedDate) && Today} + {isToday(selectedDate) && ( +
+
+ Today +
+
+ )}
{/* Loading State */} @@ -494,53 +537,85 @@ export function EnhancedBookingCalendar() {
)} - {/* Time Slots Grid */} + {/* Time Slots Grid - Organized by Time */} {!loading && courts.length > 0 && (

Available Time Slots

-
- {bookingSlots.map((slot, index) => ( -
handleSlotClick(slot)} - > -
-
-
+
+ {/* Group slots by time */} + {Array.from(new Set(bookingSlots.map((slot) => slot.time))) + .sort() + .map((time) => { + const slotsForTime = bookingSlots.filter((slot) => slot.time === time); + + return ( +
+
- {slot.time} -{' '} - {String(parseInt(slot.time.split(':')[0]) + 1).padStart(2, '0')} - :00 + {time} -{' '} + {String(parseInt(time.split(':')[0]) + 1).padStart(2, '0')}:00
-
- - {slot.courtName} +
+ {slotsForTime.map((slot) => ( +
handleSlotClick(slot)} + > +
+
+
+ + {slot.courtName} +
+ {!slot.available && slot.bookedBy && ( +
+
+ + Booked by {slot.bookedBy} +
+ {slot.partner && ( +
+ + Playing with: {slot.partner} +
+ )} +
+ )} + {!slot.available && !slot.bookedBy && ( +
+ Already booked +
+ )} +
+ +
+
+ ))}
- {!slot.available && slot.bookedBy && ( -
- - Booked by {slot.bookedBy} -
- )} - {!slot.available && !slot.bookedBy && ( -
Already booked
- )}
- -
-
- ))} + ); + })}
)} @@ -554,8 +629,8 @@ export function EnhancedBookingCalendar() { No courts available on {getDayName(selectedDate.getDay())}s

- This facility is closed on {getDayName(selectedDate.getDay())}s. - Please select a different day to make a booking. + This facility is closed on {getDayName(selectedDate.getDay())}s. Please + select a different day to make a booking.

) : ( diff --git a/components/booking/user-booking-management.tsx b/components/booking/user-booking-management.tsx index 9c9f4c8..c6576a7 100644 --- a/components/booking/user-booking-management.tsx +++ b/components/booking/user-booking-management.tsx @@ -268,8 +268,11 @@ export function UserBookingManagement() { {booking.court.name} {isToday(booking.date) && ( - - Today + + ๐ŸŽฏ Today )}
diff --git a/components/dashboard/dashboard-header.tsx b/components/dashboard/dashboard-header.tsx index 84eca2e..bcbe510 100644 --- a/components/dashboard/dashboard-header.tsx +++ b/components/dashboard/dashboard-header.tsx @@ -15,6 +15,8 @@ interface DashboardHeaderProps { userId: string; email: string; role: 'user' | 'admin'; + name?: string; + surname?: string; }; } @@ -101,7 +103,9 @@ export function DashboardHeader({ user }: DashboardHeaderProps) { className='flex items-center space-x-2' > - {user.email.split('@')[0]} + + {user.name && user.surname ? `${user.name} ${user.surname}` : user.email.split('@')[0]} +