feat: Rebuild as Court Manager Dashboard v2
- 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
This commit is contained in:
156
backend/app/routers/matches.py
Normal file
156
backend/app/routers/matches.py
Normal file
@@ -0,0 +1,156 @@
|
||||
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}
|
||||
Reference in New Issue
Block a user