'use client'; import { useState, useEffect } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Calendar, Clock, MapPin, ChevronLeft, ChevronRight, Users, User } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; interface Court { id: string; name: string; isActive: boolean; } interface Booking { id: string; courtId: string; date: string; startTime: string; endTime: string; status: string; userId: string; notes?: string; user?: { id: string; name: string; surname: string; }; } interface BookingSlot { time: string; courtId: string; courtName: string; available: boolean; bookingId?: string; bookedBy?: string; partner?: string; } interface TimeSlot { id: string; dayOfWeek: number; startTime: string; endTime: string; isActive: boolean; } interface Settings { booking_window_days: string; booking_start_time: string; booking_end_time: string; allow_weekend_bookings: string; } export function EnhancedBookingCalendar() { const [selectedDate, setSelectedDate] = useState(new Date()); const [courts, setCourts] = useState([]); const [bookings, setBookings] = useState([]); const [bookingSlots, setBookingSlots] = useState([]); const [timeSlots, setTimeSlots] = useState([]); const [settings, setSettings] = useState(null); const [loading, setLoading] = useState(false); const [partnerName, setPartnerName] = useState(''); const [notes, setNotes] = useState(''); const [showBookingDialog, setShowBookingDialog] = useState(false); const [selectedSlot, setSelectedSlot] = useState(null); const { toast } = useToast(); useEffect(() => { fetchSettings(); fetchCourts(); fetchTimeSlots(); }, []); useEffect(() => { if (courts.length > 0 && timeSlots.length > 0) { fetchBookings(); } }, [selectedDate, courts, timeSlots]); // Fetch settings from public endpoint (not admin) const fetchSettings = async () => { try { const response = await fetch('/api/settings'); if (response.ok) { const data = await response.json(); const settingsMap: Settings = { booking_window_days: '7', booking_start_time: '08:00', booking_end_time: '22:00', allow_weekend_bookings: 'true', }; data.settings.forEach((setting: any) => { if (setting.key in settingsMap) { settingsMap[setting.key as keyof Settings] = setting.value; } }); setSettings(settingsMap); } else { // If settings fetch fails, use defaults setSettings({ booking_window_days: '7', booking_start_time: '08:00', booking_end_time: '22:00', allow_weekend_bookings: 'true', }); } } catch (error) { console.error('Error fetching settings:', error); // Set default settings setSettings({ booking_window_days: '7', booking_start_time: '08:00', booking_end_time: '22:00', allow_weekend_bookings: 'true', }); } }; // Fetch courts from public endpoint (not admin) const fetchCourts = async () => { try { const response = await fetch('/api/courts'); if (response.ok) { const data = await response.json(); setCourts(data.courts.filter((court: Court) => court.isActive)); } } catch (error) { console.error('Error fetching courts:', error); toast({ title: 'Error', description: 'Failed to fetch courts', variant: 'destructive', }); } }; // Fetch time slots for day-specific booking times const fetchTimeSlots = async () => { try { const response = await fetch('/api/time-slots'); if (response.ok) { const data = await response.json(); setTimeSlots(data.timeSlots); } } catch (error) { console.error('Error fetching time slots:', error); // If time slots fetch fails, we'll use fallback settings } }; const fetchBookings = async () => { try { const dateStr = selectedDate.toISOString().split('T')[0]; const response = await fetch(`/api/bookings/all?date=${dateStr}`); if (response.ok) { const data = await response.json(); setBookings(data.bookings); generateBookingSlots(data.bookings); } } catch (error) { console.error('Error fetching bookings:', error); toast({ title: 'Error', description: 'Failed to fetch bookings', variant: 'destructive', }); } }; const generateTimeSlots = (): string[] => { const dayOfWeek = selectedDate.getDay(); // Get time slots for the selected day 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) => { 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(); } // NO FALLBACK - If no day-specific time slots, return empty array // This prevents booking on days where no play is scheduled return []; }; const isDayBookable = (): boolean => { const dayOfWeek = selectedDate.getDay(); const dayTimeSlots = timeSlots.filter((slot) => slot.dayOfWeek === dayOfWeek && slot.isActive); return dayTimeSlots.length > 0; }; const getDayName = (dayOfWeek: number): string => { const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; 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(); const slots: BookingSlot[] = []; courts.forEach((court) => { timeSlots.forEach((time) => { const existingBooking = existingBookings.find( (booking) => booking.courtId === court.id && booking.date === dateStr && booking.startTime === time && booking.status === 'active' ); const bookedBy = existingBooking?.user ? `${existingBooking.user.name} ${existingBooking.user.surname}` : undefined; const { partner } = parseBookingNotes(existingBooking?.notes); slots.push({ time, courtId: court.id, courtName: court.name, available: !existingBooking, bookingId: existingBooking?.id, bookedBy, partner, }); }); }); setBookingSlots(slots); }; const isDateSelectable = (date: Date): boolean => { if (!settings) return false; const today = new Date(); today.setHours(0, 0, 0, 0); const selectedDateOnly = new Date(date); selectedDateOnly.setHours(0, 0, 0, 0); // Check if date is in the past if (selectedDateOnly < today) return false; // Check booking window const maxDate = new Date(today); maxDate.setDate(today.getDate() + parseInt(settings.booking_window_days)); if (selectedDateOnly > maxDate) return false; // 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); // If no time slots are configured for this day, it's not selectable if (dayTimeSlots.length === 0) return false; // Legacy weekend restriction check (now superseded by time slot configuration) // Keep for backward compatibility if global settings still matter if (settings.allow_weekend_bookings === 'false') { const dayOfWeek = selectedDateOnly.getDay(); if (dayOfWeek === 0 || dayOfWeek === 6) return false; // Sunday or Saturday } return true; }; const handleSlotClick = (slot: BookingSlot) => { if (!slot.available) return; // Double-check that this day is actually bookable if (!isDayBookable()) { toast({ title: 'Booking Not Available', description: `Courts are closed on ${getDayName(selectedDate.getDay())}s`, variant: 'destructive', }); return; } setSelectedSlot(slot); setPartnerName(''); setNotes(''); setShowBookingDialog(true); }; const handleBookingConfirm = async () => { if (!selectedSlot) return; // Final validation before API call if (!isDayBookable()) { toast({ title: 'Booking Not Available', description: `Courts are closed on ${getDayName(selectedDate.getDay())}s`, variant: 'destructive', }); setShowBookingDialog(false); return; } setLoading(true); try { const dateStr = selectedDate.toISOString().split('T')[0]; const bookingNotes = []; if (partnerName.trim()) { bookingNotes.push(`Partner: ${partnerName.trim()}`); } if (notes.trim()) { bookingNotes.push(notes.trim()); } const response = await fetch('/api/bookings', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ courtId: selectedSlot.courtId, date: dateStr, timeSlot: selectedSlot.time, notes: bookingNotes.join(' | '), }), }); const data = await response.json(); if (response.ok) { toast({ title: 'Success', description: 'Booking created successfully!', }); setShowBookingDialog(false); fetchBookings(); // Refresh bookings } else { toast({ title: 'Error', description: data.error || 'Failed to create booking', variant: 'destructive', }); } } catch (error) { console.error('Error booking slot:', error); toast({ title: 'Error', description: 'Failed to create booking', variant: 'destructive', }); } finally { setLoading(false); } }; const navigateDate = (direction: 'prev' | 'next') => { const newDate = new Date(selectedDate); newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1)); if (isDateSelectable(newDate)) { setSelectedDate(newDate); } }; const getAvailableDates = (): Date[] => { if (!settings) return []; const dates: Date[] = []; const today = new Date(); const maxDays = parseInt(settings.booking_window_days); for (let i = 0; i <= maxDays; i++) { const date = new Date(today); date.setDate(today.getDate() + i); if (isDateSelectable(date)) { dates.push(date); } } return dates; }; const isPastDate = (date: Date) => { const today = new Date(); today.setHours(0, 0, 0, 0); return date < today; }; const isToday = (date: Date) => { const today = new Date(); return date.toDateString() === today.toDateString(); }; if (!settings) { return (

Loading booking system...

); } return (
Book Your Court {/* Mobile-friendly date navigation */}
{/* Quick Date Selection */}

Select Date

{getAvailableDates() .slice(0, 8) .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', month: 'long', day: 'numeric', })}

{isToday(selectedDate) && (
Today
)}
{/* Loading State */} {loading && (

Loading booking slots...

)} {/* No Courts Available */} {!loading && courts.length === 0 && (

No courts available for booking

)} {/* Time Slots Grid - Organized by Time */} {!loading && courts.length > 0 && (

Available Time Slots

{/* 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 (
{time} -{' '} {String(parseInt(time.split(':')[0]) + 1).padStart(2, '0')}:00
{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
)}
))}
); })}
)} {/* No Slots Message */} {!loading && courts.length > 0 && bookingSlots.length === 0 && (
{!isDayBookable() ? (
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.

) : (

No booking slots available for this date

)}
)}
{/* Booking Dialog */} Confirm Your Booking
{selectedSlot && (
{selectedDate.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', })}
{selectedSlot.time} -{' '} {String(parseInt(selectedSlot.time.split(':')[0]) + 1).padStart(2, '0')}:00
{selectedSlot.courtName}
)}
setPartnerName(e.target.value)} className='pl-10' />

Enter the name of the person you'll be playing with