- FastAPI backend with PostgreSQL + Redis - 4 core features: Registration, Courts, Matchmaking, Tournament - Double elimination tournament with bracket visualization - ELO-based matchmaking (Stage 1 Open, Stage 2 Skill-Based) - Real-time WebSocket updates - TV Screen display for courts - Vue 3 + Tailwind CSS frontend - Seed data: 12 players, 4 courts, active matches, tournament in progress - Docker compose stack with Nginx reverse proxy
219 lines
6.6 KiB
Python
219 lines
6.6 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import and_
|
|
from app.database import get_db
|
|
from app.models.court import Court
|
|
from app.models.booking import Booking, BookingStatus
|
|
from app.models.player import Player
|
|
from app.models.match import Match, MatchStatus
|
|
from pydantic import BaseModel
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional, List
|
|
|
|
router = APIRouter(prefix="/courts", tags=["courts"])
|
|
|
|
|
|
class BookingCreate(BaseModel):
|
|
player_id: int
|
|
court_id: int
|
|
start_time: datetime
|
|
duration_hours: float = 1.0
|
|
|
|
|
|
class CourtResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
court_number: int
|
|
hourly_rate: float
|
|
is_active: bool
|
|
surface_type: str
|
|
features: str
|
|
current_status: str
|
|
current_match: Optional[dict] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class BookingResponse(BaseModel):
|
|
id: int
|
|
player_id: int
|
|
player_name: str
|
|
court_id: int
|
|
court_name: str
|
|
start_time: datetime
|
|
end_time: datetime
|
|
duration_hours: float
|
|
total_cost: float
|
|
status: str
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
def get_court_status(court: Court, db: Session) -> tuple:
|
|
"""Get current status of a court"""
|
|
# Check if match is in progress
|
|
match = db.query(Match).filter(
|
|
Match.court_id == court.id,
|
|
Match.status == MatchStatus.IN_PROGRESS
|
|
).first()
|
|
|
|
if match:
|
|
team1 = [mp.player.name for mp in match.match_players if mp.team == 1]
|
|
team2 = [mp.player.name for mp in match.match_players if mp.team == 2]
|
|
return "occupied", {
|
|
"match_id": match.id,
|
|
"team1": team1,
|
|
"team2": team2,
|
|
"team1_score": match.team1_score,
|
|
"team2_score": match.team2_score,
|
|
"stage": match.stage.value,
|
|
"started_at": match.started_at.isoformat() if match.started_at else None,
|
|
}
|
|
|
|
# Check if there's a booking now
|
|
now = datetime.utcnow()
|
|
booking = db.query(Booking).filter(
|
|
Booking.court_id == court.id,
|
|
Booking.start_time <= now,
|
|
Booking.end_time >= now,
|
|
Booking.status.in_([BookingStatus.CONFIRMED, BookingStatus.IN_PROGRESS])
|
|
).first()
|
|
|
|
if booking:
|
|
return "booked", None
|
|
|
|
return "available", None
|
|
|
|
|
|
@router.get("/", response_model=List[CourtResponse])
|
|
def get_courts(db: Session = Depends(get_db)):
|
|
courts = db.query(Court).filter(Court.is_active == True).order_by(Court.court_number).all()
|
|
result = []
|
|
for court in courts:
|
|
status, match_info = get_court_status(court, db)
|
|
result.append(CourtResponse(
|
|
id=court.id,
|
|
name=court.name,
|
|
court_number=court.court_number,
|
|
hourly_rate=court.hourly_rate,
|
|
is_active=court.is_active,
|
|
surface_type=court.surface_type,
|
|
features=court.features,
|
|
current_status=status,
|
|
current_match=match_info,
|
|
))
|
|
return result
|
|
|
|
|
|
@router.get("/{court_id}")
|
|
def get_court(court_id: int, db: Session = Depends(get_db)):
|
|
court = db.query(Court).filter(Court.id == court_id).first()
|
|
if not court:
|
|
raise HTTPException(status_code=404, detail="Court not found")
|
|
status, match_info = get_court_status(court, db)
|
|
return {
|
|
"id": court.id,
|
|
"name": court.name,
|
|
"court_number": court.court_number,
|
|
"hourly_rate": court.hourly_rate,
|
|
"surface_type": court.surface_type,
|
|
"features": court.features,
|
|
"current_status": status,
|
|
"current_match": match_info,
|
|
}
|
|
|
|
|
|
@router.get("/{court_id}/schedule")
|
|
def get_court_schedule(court_id: int, db: Session = Depends(get_db)):
|
|
"""Get bookings for a court for the next 7 days"""
|
|
now = datetime.utcnow()
|
|
end = now + timedelta(days=7)
|
|
bookings = db.query(Booking).filter(
|
|
Booking.court_id == court_id,
|
|
Booking.start_time >= now,
|
|
Booking.end_time <= end,
|
|
Booking.status.in_([BookingStatus.CONFIRMED, BookingStatus.IN_PROGRESS])
|
|
).order_by(Booking.start_time).all()
|
|
|
|
return [
|
|
{
|
|
"id": b.id,
|
|
"player_name": b.player.name,
|
|
"start_time": b.start_time.isoformat(),
|
|
"end_time": b.end_time.isoformat(),
|
|
"duration_hours": b.duration_hours,
|
|
"is_match_booking": b.is_match_booking,
|
|
}
|
|
for b in bookings
|
|
]
|
|
|
|
|
|
@router.post("/book")
|
|
def book_court(booking_data: BookingCreate, db: Session = Depends(get_db)):
|
|
court = db.query(Court).filter(Court.id == booking_data.court_id).first()
|
|
if not court:
|
|
raise HTTPException(status_code=404, detail="Court not found")
|
|
|
|
player = db.query(Player).filter(Player.id == booking_data.player_id).first()
|
|
if not player:
|
|
raise HTTPException(status_code=404, detail="Player not found")
|
|
|
|
end_time = booking_data.start_time + timedelta(hours=booking_data.duration_hours)
|
|
|
|
# Check for conflicts
|
|
conflict = db.query(Booking).filter(
|
|
Booking.court_id == booking_data.court_id,
|
|
Booking.status.in_([BookingStatus.CONFIRMED, BookingStatus.IN_PROGRESS]),
|
|
and_(Booking.start_time < end_time, Booking.end_time > booking_data.start_time)
|
|
).first()
|
|
|
|
if conflict:
|
|
raise HTTPException(status_code=400, detail="Court is already booked for this time slot")
|
|
|
|
total_cost = court.hourly_rate * booking_data.duration_hours
|
|
booking = Booking(
|
|
player_id=booking_data.player_id,
|
|
court_id=booking_data.court_id,
|
|
start_time=booking_data.start_time,
|
|
end_time=end_time,
|
|
duration_hours=booking_data.duration_hours,
|
|
total_cost=total_cost,
|
|
status=BookingStatus.CONFIRMED,
|
|
)
|
|
db.add(booking)
|
|
db.commit()
|
|
db.refresh(booking)
|
|
|
|
return {
|
|
"id": booking.id,
|
|
"court_name": court.name,
|
|
"player_name": player.name,
|
|
"start_time": booking.start_time.isoformat(),
|
|
"end_time": booking.end_time.isoformat(),
|
|
"total_cost": booking.total_cost,
|
|
"status": booking.status.value,
|
|
}
|
|
|
|
|
|
@router.get("/bookings/upcoming")
|
|
def get_upcoming_bookings(db: Session = Depends(get_db)):
|
|
now = datetime.utcnow()
|
|
bookings = db.query(Booking).filter(
|
|
Booking.start_time >= now,
|
|
Booking.status == BookingStatus.CONFIRMED
|
|
).order_by(Booking.start_time).limit(20).all()
|
|
|
|
return [
|
|
{
|
|
"id": b.id,
|
|
"player_name": b.player.name,
|
|
"court_name": b.court.name,
|
|
"start_time": b.start_time.isoformat(),
|
|
"end_time": b.end_time.isoformat(),
|
|
"total_cost": b.total_cost,
|
|
}
|
|
for b in bookings
|
|
]
|