import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; import { db } from '@/lib/db'; import { createBookingSchema } from '@smashpoint/shared'; import { Decimal } from '@prisma/client/runtime/library'; // Helper function to check if a time is premium (after 18:00 or weekend) function isPremiumTime(date: Date, timeMinutes: number): boolean { const dayOfWeek = date.getDay(); // 0 = Sunday, 6 = Saturday const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; const isEveningTime = timeMinutes >= 18 * 60; // After 18:00 return isWeekend || isEveningTime; } // GET /api/bookings - List bookings with filters export async function GET(request: NextRequest) { try { const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } const { searchParams } = new URL(request.url); const siteId = searchParams.get('siteId'); const date = searchParams.get('date'); const status = searchParams.get('status'); const clientId = searchParams.get('clientId'); const courtId = searchParams.get('courtId'); // Build the where clause interface BookingWhereClause { site: { organizationId: string; id?: string; }; status?: 'PENDING' | 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NO_SHOW'; clientId?: string; courtId?: string; startTime?: { gte: Date; lt: Date; }; } const whereClause: BookingWhereClause = { site: { organizationId: session.user.organizationId, }, }; // Filter by site if (siteId) { whereClause.site.id = siteId; } else if (session.user.siteId) { whereClause.site.id = session.user.siteId; } // Filter by status if (status) { const validStatuses = ['PENDING', 'CONFIRMED', 'CANCELLED', 'COMPLETED', 'NO_SHOW'] as const; if (validStatuses.includes(status.toUpperCase() as typeof validStatuses[number])) { whereClause.status = status.toUpperCase() as typeof validStatuses[number]; } } // Filter by client if (clientId) { whereClause.clientId = clientId; } // Filter by court if (courtId) { whereClause.courtId = courtId; } // Filter by date if (date) { const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (dateRegex.test(date)) { const startOfDay = new Date(date); startOfDay.setHours(0, 0, 0, 0); const endOfDay = new Date(date); endOfDay.setHours(23, 59, 59, 999); whereClause.startTime = { gte: startOfDay, lt: new Date(endOfDay.getTime() + 1), }; } } const bookings = await db.booking.findMany({ where: whereClause, include: { court: { select: { id: true, name: true, type: true, pricePerHour: true, }, }, client: { select: { id: true, firstName: true, lastName: true, email: true, phone: true, }, }, site: { select: { id: true, name: true, timezone: true, }, }, createdBy: { select: { id: true, firstName: true, lastName: true, }, }, }, orderBy: [ { startTime: 'asc' }, ], }); return NextResponse.json(bookings); } catch (error) { console.error('Error fetching bookings:', error); return NextResponse.json( { error: 'Error fetching bookings' }, { status: 500 } ); } } // POST /api/bookings - Create a new booking export async function POST(request: NextRequest) { try { const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } const body = await request.json(); // Validate input with Zod schema const validationResult = createBookingSchema.safeParse(body); if (!validationResult.success) { return NextResponse.json( { error: 'Invalid booking data', details: validationResult.error.flatten().fieldErrors, }, { status: 400 } ); } const { courtId, clientId, startTime, endTime, paymentType, notes, participants } = validationResult.data; // Check court exists and belongs to user's organization const court = await db.court.findFirst({ where: { id: courtId, site: { organizationId: session.user.organizationId, }, }, include: { site: { select: { id: true, name: true, organizationId: true, }, }, }, }); if (!court) { return NextResponse.json( { error: 'Court not found or does not belong to your organization' }, { status: 404 } ); } if (court.status !== 'AVAILABLE' || !court.isActive) { return NextResponse.json( { error: 'The court is not available for bookings' }, { status: 400 } ); } // Check client exists and belongs to user's organization const client = await db.client.findFirst({ where: { id: clientId, organizationId: session.user.organizationId, }, include: { memberships: { where: { status: 'ACTIVE', endDate: { gte: new Date(), }, }, include: { plan: true, }, orderBy: { endDate: 'desc', }, take: 1, }, }, }); if (!client) { return NextResponse.json( { error: 'Client not found or does not belong to your organization' }, { status: 404 } ); } // Check availability - no existing booking at that time const bookingStartTime = new Date(startTime); const bookingEndTime = new Date(endTime); const existingBooking = await db.booking.findFirst({ where: { courtId, status: { in: ['PENDING', 'CONFIRMED'], }, OR: [ // New booking starts during existing booking { startTime: { lte: bookingStartTime }, endTime: { gt: bookingStartTime }, }, // New booking ends during existing booking { startTime: { lt: bookingEndTime }, endTime: { gte: bookingEndTime }, }, // New booking encompasses existing booking { startTime: { gte: bookingStartTime }, endTime: { lte: bookingEndTime }, }, ], }, }); if (existingBooking) { return NextResponse.json( { error: 'A booking already exists for that time slot. Please select another time.' }, { status: 409 } ); } // Calculate duration in minutes const durationMs = bookingEndTime.getTime() - bookingStartTime.getTime(); const durationMinutes = durationMs / (1000 * 60); const durationHours = durationMinutes / 60; // Calculate price (regular vs premium) const startMinutes = bookingStartTime.getHours() * 60 + bookingStartTime.getMinutes(); const isPremium = isPremiumTime(bookingStartTime, startMinutes); const basePrice = Number(court.pricePerHour); const premiumMultiplier = 1.25; const hourlyRate = isPremium ? basePrice * premiumMultiplier : basePrice; let totalPrice = hourlyRate * durationHours; // Check client membership for discounts const activeMembership = client.memberships[0]; let usedFreeHours = false; let appliedDiscount = 0; let finalPaymentType = paymentType.toUpperCase() as 'CASH' | 'CARD' | 'TRANSFER' | 'MEMBERSHIP' | 'FREE'; if (activeMembership) { const plan = activeMembership.plan; // Check if membership has free hours remaining if (plan.courtHours && activeMembership.remainingHours && activeMembership.remainingHours > 0) { // Client has free hours, use them const hoursToUse = Math.min(activeMembership.remainingHours, durationHours); if (hoursToUse >= durationHours) { // Fully covered by free hours totalPrice = 0; usedFreeHours = true; finalPaymentType = 'FREE'; } else { // Partial coverage - calculate remaining const remainingHours = durationHours - hoursToUse; totalPrice = hourlyRate * remainingHours; usedFreeHours = true; } } else if (plan.discountPercent) { // Apply booking discount percentage const discountPercent = Number(plan.discountPercent); appliedDiscount = (totalPrice * discountPercent) / 100; totalPrice = totalPrice - appliedDiscount; finalPaymentType = 'MEMBERSHIP'; } } // Round price to 2 decimal places totalPrice = Math.round(totalPrice * 100) / 100; // Create booking with transaction (to update membership hours if needed) const booking = await db.$transaction(async (tx) => { // Update membership hours if free hours were used if (usedFreeHours && activeMembership) { const hoursUsed = Math.min(activeMembership.remainingHours || 0, durationHours); await tx.membership.update({ where: { id: activeMembership.id }, data: { remainingHours: { decrement: hoursUsed, }, }, }); } // Create the booking const newBooking = await tx.booking.create({ data: { siteId: court.siteId, courtId, clientId, createdById: session.user.id, startTime: bookingStartTime, endTime: bookingEndTime, status: 'PENDING', paymentType: finalPaymentType, totalPrice: new Decimal(totalPrice), paidAmount: new Decimal(0), notes: notes || null, playerNames: participants?.map(p => p.name) || [], isRecurring: false, }, include: { court: { select: { id: true, name: true, type: true, pricePerHour: true, }, }, client: { select: { id: true, firstName: true, lastName: true, email: true, phone: true, }, }, site: { select: { id: true, name: true, timezone: true, }, }, }, }); return newBooking; }); return NextResponse.json(booking, { status: 201 }); } catch (error) { console.error('Error creating booking:', error); return NextResponse.json( { error: 'Error creating booking' }, { status: 500 } ); } }