import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { bookings, courts, timeSlots, settings, metrics, courtBlocks } from '@/lib/db/schema'; import { eq, and, gte, asc, or, isNull } from 'drizzle-orm'; import { getSession } from '@/lib/session'; import { logActivity, ACTIONS, ENTITY_TYPES } from '@/lib/activity-logger'; export async function GET(request: NextRequest) { try { const session = await getSession(); if (!session) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } // Get today's date to filter out past bookings const today = new Date().toISOString().split('T')[0]; const userBookings = await db .select({ id: bookings.id, courtId: bookings.courtId, date: bookings.date, startTime: bookings.startTime, endTime: bookings.endTime, status: bookings.status, notes: bookings.notes, createdAt: bookings.createdAt, court: { id: courts.id, name: courts.name, }, }) .from(bookings) .innerJoin(courts, eq(bookings.courtId, courts.id)) .where(and(eq(bookings.userId, session.userId), eq(bookings.status, 'active'), gte(bookings.date, today))) .orderBy(asc(bookings.date), asc(bookings.startTime)); return NextResponse.json({ bookings: userBookings }); } catch (error) { console.error('Error fetching bookings:', error); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } } export async function POST(request: NextRequest) { try { const session = await getSession(); if (!session) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { courtId, date, timeSlot, notes } = await request.json(); if (!courtId || !date || !timeSlot) { return NextResponse.json( { error: 'Missing required fields: courtId, date, timeSlot', }, { status: 400 } ); } // Parse timeSlot (e.g., "14:00") to get start and end times const startTime = timeSlot; const [hours, minutes] = timeSlot.split(':').map(Number); const endTime = `${String(hours + 1).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; // Check if user is admin (for bypassing certain restrictions) const isAdmin = session.role === 'admin'; // Validate booking date is not in the past const bookingDate = new Date(date); const today = new Date(); today.setHours(0, 0, 0, 0); if (bookingDate < today) { return NextResponse.json( { error: 'Cannot book dates in the past', }, { status: 400 } ); } // Check if court exists and is active const court = await db.select().from(courts).where(eq(courts.id, courtId)).limit(1); if (court.length === 0 || !court[0].isActive) { return NextResponse.json( { error: 'Court not found or inactive', }, { status: 400 } ); } // CHECK FOR BLOCKS - applies to everyone including admins // A block prevents any booking on that court/time (admins should remove the block first) const requestedHour = parseInt(startTime.split(':')[0]); const activeBlocks = await db .select() .from(courtBlocks) .where( and( eq(courtBlocks.date, date), or( eq(courtBlocks.courtId, courtId), // Block for this specific court isNull(courtBlocks.courtId) // Block for all courts ) ) ); // Check if any block covers this time slot const isBlockedSlot = activeBlocks.some((block) => { const blockStartHour = parseInt(block.startTime.split(':')[0]); const blockEndHour = parseInt(block.endTime.split(':')[0]); return requestedHour >= blockStartHour && requestedHour < blockEndHour; }); if (isBlockedSlot) { const blockingBlock = activeBlocks.find((block) => { const blockStartHour = parseInt(block.startTime.split(':')[0]); const blockEndHour = parseInt(block.endTime.split(':')[0]); return requestedHour >= blockStartHour && requestedHour < blockEndHour; }); return NextResponse.json( { error: `This slot is blocked: ${ blockingBlock?.reason || 'Court unavailable' }. Please choose a different time.`, }, { status: 400 } ); } // CRITICAL: Validate that booking is allowed for this day and time const dayOfWeek = bookingDate.getDay(); const availableTimeSlots = await db .select() .from(timeSlots) .where(and(eq(timeSlots.dayOfWeek, dayOfWeek), eq(timeSlots.isActive, true))); // Check if any time slots are configured for this day (admins can bypass if needed) if (availableTimeSlots.length === 0 && !isAdmin) { 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.`, }, { status: 400 } ); } // Check if the requested time slot is within any of the allowed time ranges // Admins can bypass time slot restrictions if (!isAdmin) { 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(', '); return NextResponse.json( { 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 // Admins bypass this restriction if (!isAdmin) { 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; // 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 } ); } } // Check if slot is already booked const existingBooking = await db .select() .from(bookings) .where( and( eq(bookings.courtId, courtId), eq(bookings.date, date), eq(bookings.startTime, startTime), eq(bookings.status, 'active') ) ) .limit(1); if (existingBooking.length > 0) { return NextResponse.json( { error: 'Time slot already booked', }, { status: 400 } ); } // Create the booking const [newBooking] = await db .insert(bookings) .values({ id: crypto.randomUUID(), userId: session.userId, courtId, date, startTime, endTime, status: 'active', notes: notes || null, // Include notes from the request createdAt: new Date(), updatedAt: new Date(), }) .returning(); // Log the activity await logActivity({ userId: session.userId, action: ACTIONS.BOOKING_CREATE, entityType: ENTITY_TYPES.BOOKING, entityId: newBooking.id, details: { courtId, courtName: court[0].name, date, startTime, endTime, }, 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', }); } catch (error) { console.error('Error creating booking:', error); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } }