From 09518c53352050f0d61d20ba56d6142260d13a4a Mon Sep 17 00:00:00 2001 From: Ivan Date: Mon, 2 Mar 2026 03:54:38 +0000 Subject: [PATCH] feat: add court session and live courts API routes Co-Authored-By: Claude Opus 4.6 --- apps/web/app/api/court-sessions/[id]/route.ts | 124 ++++++++++++++ apps/web/app/api/court-sessions/route.ts | 159 ++++++++++++++++++ apps/web/app/api/live/route.ts | 97 +++++++++++ 3 files changed, 380 insertions(+) create mode 100644 apps/web/app/api/court-sessions/[id]/route.ts create mode 100644 apps/web/app/api/court-sessions/route.ts create mode 100644 apps/web/app/api/live/route.ts diff --git a/apps/web/app/api/court-sessions/[id]/route.ts b/apps/web/app/api/court-sessions/[id]/route.ts new file mode 100644 index 0000000..88ba3b0 --- /dev/null +++ b/apps/web/app/api/court-sessions/[id]/route.ts @@ -0,0 +1,124 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { db } from '@/lib/db'; + +interface RouteContext { + params: Promise<{ id: string }>; +} + +// PUT /api/court-sessions/[id] - End a court session +export async function PUT( + request: NextRequest, + context: RouteContext +) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + const { id } = await context.params; + + // Verify session exists and belongs to user's organization + const existingSession = await db.courtSession.findFirst({ + where: { + id, + court: { + site: { + organizationId: session.user.organizationId, + }, + }, + }, + }); + + if (!existingSession) { + return NextResponse.json( + { error: 'Court session not found' }, + { status: 404 } + ); + } + + if (!existingSession.isActive) { + return NextResponse.json( + { error: 'Court session is already ended' }, + { status: 400 } + ); + } + + // End the session + const updatedSession = await db.courtSession.update({ + where: { id }, + data: { + isActive: false, + endTime: new Date(), + }, + include: { + court: { select: { id: true, name: true, type: true, isOpenPlay: true, siteId: true } }, + client: { select: { id: true, firstName: true, lastName: true, phone: true } }, + }, + }); + + return NextResponse.json(updatedSession); + } catch (error) { + console.error('Error ending court session:', error); + return NextResponse.json( + { error: 'Error ending court session' }, + { status: 500 } + ); + } +} + +// DELETE /api/court-sessions/[id] - Remove a court session entirely +export async function DELETE( + request: NextRequest, + context: RouteContext +) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + const { id } = await context.params; + + // Verify session exists and belongs to user's organization + const existingSession = await db.courtSession.findFirst({ + where: { + id, + court: { + site: { + organizationId: session.user.organizationId, + }, + }, + }, + }); + + if (!existingSession) { + return NextResponse.json( + { error: 'Court session not found' }, + { status: 404 } + ); + } + + await db.courtSession.delete({ + where: { id }, + }); + + return NextResponse.json({ message: 'Court session deleted successfully' }); + } catch (error) { + console.error('Error deleting court session:', error); + return NextResponse.json( + { error: 'Error deleting court session' }, + { status: 500 } + ); + } +} diff --git a/apps/web/app/api/court-sessions/route.ts b/apps/web/app/api/court-sessions/route.ts new file mode 100644 index 0000000..25b1d8d --- /dev/null +++ b/apps/web/app/api/court-sessions/route.ts @@ -0,0 +1,159 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { db } from '@/lib/db'; + +// GET /api/court-sessions - List all active court sessions +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 courtId = searchParams.get('courtId'); + const siteId = searchParams.get('siteId'); + + // Build where clause + const whereClause: { + isActive: boolean; + courtId?: string; + court?: { siteId?: string; site: { organizationId: string } }; + } = { + isActive: true, + court: { + site: { + organizationId: session.user.organizationId, + }, + }, + }; + + if (courtId) { + whereClause.courtId = courtId; + } + + if (siteId) { + whereClause.court = { + ...whereClause.court!, + siteId, + }; + } else if (session.user.siteId) { + whereClause.court = { + ...whereClause.court!, + siteId: session.user.siteId, + }; + } + + const sessions = await db.courtSession.findMany({ + where: whereClause, + include: { + court: { select: { id: true, name: true, type: true, isOpenPlay: true, siteId: true } }, + client: { select: { id: true, firstName: true, lastName: true, phone: true } }, + }, + orderBy: { startTime: 'desc' }, + }); + + return NextResponse.json(sessions); + } catch (error) { + console.error('Error fetching court sessions:', error); + return NextResponse.json( + { error: 'Error fetching court sessions' }, + { status: 500 } + ); + } +} + +// POST /api/court-sessions - Check in a player to a court +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(); + const { courtId, clientId, walkInName, notes } = body; + + // Validate required fields + if (!courtId) { + return NextResponse.json( + { error: 'courtId is required' }, + { status: 400 } + ); + } + + // Must have either clientId or walkInName + if (!clientId && !walkInName) { + return NextResponse.json( + { error: 'Either clientId or walkInName is required' }, + { status: 400 } + ); + } + + // Verify court exists and belongs to user's organization + const court = await db.court.findFirst({ + where: { + id: courtId, + site: { + organizationId: session.user.organizationId, + }, + }, + }); + + if (!court) { + return NextResponse.json( + { error: 'Court not found or does not belong to your organization' }, + { status: 404 } + ); + } + + // If clientId is provided, verify client exists + if (clientId) { + const client = await db.client.findFirst({ + where: { + id: clientId, + organizationId: session.user.organizationId, + }, + }); + + if (!client) { + return NextResponse.json( + { error: 'Client not found or does not belong to your organization' }, + { status: 404 } + ); + } + } + + // Create the court session + const courtSession = await db.courtSession.create({ + data: { + courtId, + clientId: clientId || null, + walkInName: walkInName || null, + notes: notes || null, + isActive: true, + }, + include: { + court: { select: { id: true, name: true, type: true, isOpenPlay: true, siteId: true } }, + client: { select: { id: true, firstName: true, lastName: true, phone: true } }, + }, + }); + + return NextResponse.json(courtSession, { status: 201 }); + } catch (error) { + console.error('Error creating court session:', error); + return NextResponse.json( + { error: 'Error creating court session' }, + { status: 500 } + ); + } +} diff --git a/apps/web/app/api/live/route.ts b/apps/web/app/api/live/route.ts new file mode 100644 index 0000000..1588158 --- /dev/null +++ b/apps/web/app/api/live/route.ts @@ -0,0 +1,97 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { db } from '@/lib/db'; + +// GET /api/live - Get complete live court status +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'); + + // Build where clause for courts + const courtWhere: { + isActive: boolean; + site: { organizationId: string; id?: string }; + } = { + isActive: true, + site: { + organizationId: session.user.organizationId, + }, + }; + + if (siteId) { + courtWhere.site.id = siteId; + } else if (session.user.siteId) { + courtWhere.site.id = session.user.siteId; + } + + const now = new Date(); + const endOfHour = new Date(now.getTime() + 30 * 60 * 1000); // 30 minutes from now + + // Get all courts with their active sessions AND current bookings + const courts = await db.court.findMany({ + where: courtWhere, + include: { + sessions: { + where: { isActive: true }, + include: { + client: { select: { id: true, firstName: true, lastName: true, phone: true } }, + }, + }, + bookings: { + where: { + startTime: { lte: endOfHour }, + endTime: { gte: now }, + status: { in: ['CONFIRMED', 'PENDING'] }, + }, + include: { + client: { select: { id: true, firstName: true, lastName: true } }, + }, + }, + site: { select: { id: true, name: true } }, + }, + orderBy: { displayOrder: 'asc' }, + }); + + // Compute status for each court + const courtsWithStatus = courts.map((court) => { + let status: 'available' | 'active' | 'open-play' | 'booked'; + + if (court.sessions.length > 0) { + // Court has active sessions + if (court.isOpenPlay) { + status = 'open-play'; + } else { + status = 'active'; + } + } else if (court.bookings.length > 0) { + status = 'booked'; + } else { + status = 'available'; + } + + return { + ...court, + status, + }; + }); + + return NextResponse.json(courtsWithStatus); + } catch (error) { + console.error('Error fetching live court status:', error); + return NextResponse.json( + { error: 'Error fetching live court status' }, + { status: 500 } + ); + } +}