- Complete rewrite for single Court Manager user - Feature 1: Player Management (CRUD, ELO, skill level) - Feature 2: Court Reservation (timeline, override) - Feature 3: Match Event Builder (3-step wizard) - Step 1: Event setup with player selection + stage calculator - Step 2: Stage configurator (Open/Skill/RR/Tournament) - Step 3: Auto bracket generation - Feature 4: Live Court View (main dashboard, 2x2 grid) - Event Control: score entry, bracket advancement, court assignment - Screen view: TV display per court (/screen/:id) - Seed: 8 players, 4 courts, 1 completed + 1 active event - Routes: / players courts events events/new events/:id screen/:id
157 lines
5.6 KiB
Python
157 lines
5.6 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
from datetime import datetime
|
|
from ..database import get_db
|
|
from ..models import Match, Player, Court, Stage
|
|
from ..schemas import MatchScoreUpdate, MatchCourtAssign
|
|
|
|
router = APIRouter(prefix="/api/matches", tags=["matches"])
|
|
|
|
|
|
def _match_detail(match: Match, db: Session) -> dict:
|
|
p1 = db.query(Player).filter(Player.id == match.player1_id).first() if match.player1_id else None
|
|
p2 = db.query(Player).filter(Player.id == match.player2_id).first() if match.player2_id else None
|
|
court = db.query(Court).filter(Court.id == match.court_id).first() if match.court_id else None
|
|
return {
|
|
"id": match.id,
|
|
"stage_id": match.stage_id,
|
|
"court_id": match.court_id,
|
|
"court_name": court.name if court else None,
|
|
"round_number": match.round_number,
|
|
"match_number": match.match_number,
|
|
"player1_id": match.player1_id,
|
|
"player2_id": match.player2_id,
|
|
"player1_name": p1.name if p1 else None,
|
|
"player2_name": p2.name if p2 else None,
|
|
"team1_players": match.team1_players,
|
|
"team2_players": match.team2_players,
|
|
"score1": match.score1,
|
|
"score2": match.score2,
|
|
"winner_id": match.winner_id,
|
|
"winner_team": match.winner_team,
|
|
"status": match.status,
|
|
"bracket_position": match.bracket_position,
|
|
"next_winner_match_id": match.next_winner_match_id,
|
|
"next_loser_match_id": match.next_loser_match_id,
|
|
"started_at": match.started_at,
|
|
"completed_at": match.completed_at,
|
|
}
|
|
|
|
|
|
@router.get("/{match_id}")
|
|
def get_match(match_id: int, db: Session = Depends(get_db)):
|
|
match = db.query(Match).filter(Match.id == match_id).first()
|
|
if not match:
|
|
raise HTTPException(status_code=404, detail="Match not found")
|
|
return _match_detail(match, db)
|
|
|
|
|
|
@router.post("/{match_id}/start")
|
|
def start_match(match_id: int, db: Session = Depends(get_db)):
|
|
match = db.query(Match).filter(Match.id == match_id).first()
|
|
if not match:
|
|
raise HTTPException(status_code=404, detail="Match not found")
|
|
|
|
match.status = "in_progress"
|
|
match.started_at = datetime.utcnow()
|
|
|
|
# Auto-assign available court if none assigned
|
|
if not match.court_id:
|
|
stage = db.query(Stage).filter(Stage.id == match.stage_id).first()
|
|
if stage:
|
|
# Find available court
|
|
in_use_court_ids = db.query(Match.court_id).filter(
|
|
Match.status == "in_progress", Match.court_id.isnot(None)
|
|
).all()
|
|
in_use_ids = [r[0] for r in in_use_court_ids]
|
|
available_court = db.query(Court).filter(
|
|
Court.status.in_(["available", "reserved"]),
|
|
Court.id.notin_(in_use_ids)
|
|
).first()
|
|
if available_court:
|
|
match.court_id = available_court.id
|
|
available_court.status = "in_use"
|
|
|
|
db.commit()
|
|
return _match_detail(match, db)
|
|
|
|
|
|
@router.post("/{match_id}/score")
|
|
def enter_score(match_id: int, data: MatchScoreUpdate, db: Session = Depends(get_db)):
|
|
match = db.query(Match).filter(Match.id == match_id).first()
|
|
if not match:
|
|
raise HTTPException(status_code=404, detail="Match not found")
|
|
|
|
match.score1 = data.score1
|
|
match.score2 = data.score2
|
|
match.status = "completed"
|
|
match.completed_at = datetime.utcnow()
|
|
|
|
# Determine winner
|
|
if data.score1 > data.score2:
|
|
match.winner_id = match.player1_id
|
|
match.winner_team = 1
|
|
winner_id = match.player1_id
|
|
loser_id = match.player2_id
|
|
else:
|
|
match.winner_id = match.player2_id
|
|
match.winner_team = 2
|
|
winner_id = match.player2_id
|
|
loser_id = match.player1_id
|
|
|
|
# Free up court
|
|
if match.court_id:
|
|
court = db.query(Court).filter(Court.id == match.court_id).first()
|
|
if court:
|
|
court.status = "available"
|
|
|
|
# Update player stats
|
|
if winner_id:
|
|
winner = db.query(Player).filter(Player.id == winner_id).first()
|
|
if winner:
|
|
winner.wins += 1
|
|
winner.matches_played += 1
|
|
winner.elo += 20 # Simple ELO gain
|
|
if loser_id:
|
|
loser = db.query(Player).filter(Player.id == loser_id).first()
|
|
if loser:
|
|
loser.losses += 1
|
|
loser.matches_played += 1
|
|
loser.elo = max(800, loser.elo - 15)
|
|
|
|
# Advance bracket: place winner into next match
|
|
if match.next_winner_match_id:
|
|
next_match = db.query(Match).filter(Match.id == match.next_winner_match_id).first()
|
|
if next_match:
|
|
if next_match.player1_id is None:
|
|
next_match.player1_id = winner_id
|
|
elif next_match.player2_id is None:
|
|
next_match.player2_id = winner_id
|
|
|
|
db.commit()
|
|
return _match_detail(match, db)
|
|
|
|
|
|
@router.patch("/{match_id}/court")
|
|
def assign_court(match_id: int, data: MatchCourtAssign, db: Session = Depends(get_db)):
|
|
match = db.query(Match).filter(Match.id == match_id).first()
|
|
if not match:
|
|
raise HTTPException(status_code=404, detail="Match not found")
|
|
|
|
# Free old court
|
|
if match.court_id:
|
|
old_court = db.query(Court).filter(Court.id == match.court_id).first()
|
|
if old_court and old_court.status == "in_use":
|
|
old_court.status = "available"
|
|
|
|
match.court_id = data.court_id
|
|
|
|
# Mark new court in use if match is in progress
|
|
if data.court_id and match.status == "in_progress":
|
|
new_court = db.query(Court).filter(Court.id == data.court_id).first()
|
|
if new_court:
|
|
new_court.status = "in_use"
|
|
|
|
db.commit()
|
|
return {"ok": True}
|