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:
Nemo
2026-03-19 07:35:51 +08:00
parent e455accc7e
commit 4421f61c24
56 changed files with 2949 additions and 4209 deletions

View File

View File

@@ -0,0 +1,16 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from ..database import get_db
from ..models import Base
from ..database import engine
import subprocess, sys
router = APIRouter(prefix="/api/admin", tags=["admin"])
@router.post("/seed")
def seed_data(db: Session = Depends(get_db)):
"""Re-seed all demo data"""
import subprocess, sys
result = subprocess.run([sys.executable, "/app/seed.py"], capture_output=True, text=True)
return {"ok": result.returncode == 0, "output": result.stdout[-500:], "error": result.stderr[-200:] if result.stderr else None}

View File

@@ -0,0 +1,81 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload
from typing import List
from datetime import datetime
from ..database import get_db
from ..models import Court, CourtReservation
from ..schemas import CourtCreate, CourtStatusUpdate, ReservationCreate, ReservationOut, CourtOut
router = APIRouter(prefix="/api/courts", tags=["courts"])
@router.get("/", response_model=List[CourtOut])
def list_courts(db: Session = Depends(get_db)):
courts = db.query(Court).options(joinedload(Court.reservations).joinedload(CourtReservation.player)).all()
return courts
@router.post("/", response_model=CourtOut)
def create_court(data: CourtCreate, db: Session = Depends(get_db)):
court = Court(**data.model_dump())
db.add(court)
db.commit()
db.refresh(court)
return court
@router.get("/{court_id}", response_model=CourtOut)
def get_court(court_id: int, db: Session = Depends(get_db)):
court = db.query(Court).options(joinedload(Court.reservations).joinedload(CourtReservation.player)).filter(Court.id == court_id).first()
if not court:
raise HTTPException(status_code=404, detail="Court not found")
return court
@router.patch("/{court_id}/status")
def update_court_status(court_id: int, data: CourtStatusUpdate, 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")
valid_statuses = ["available", "in_use", "reserved", "maintenance"]
if data.status not in valid_statuses:
raise HTTPException(status_code=400, detail=f"Status must be one of {valid_statuses}")
court.status = data.status
db.commit()
return {"ok": True, "status": data.status}
@router.get("/{court_id}/reservations", response_model=List[ReservationOut])
def list_reservations(court_id: int, db: Session = Depends(get_db)):
return db.query(CourtReservation).filter(CourtReservation.court_id == court_id).order_by(CourtReservation.start_time).all()
@router.post("/{court_id}/reservations", response_model=ReservationOut)
def create_reservation(court_id: int, data: ReservationCreate, 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")
reservation = CourtReservation(court_id=court_id, **data.model_dump())
db.add(reservation)
# Update court status to reserved if it's currently available
if court.status == "available":
court.status = "reserved"
db.commit()
db.refresh(reservation)
return reservation
@router.delete("/reservations/{reservation_id}")
def delete_reservation(reservation_id: int, db: Session = Depends(get_db)):
reservation = db.query(CourtReservation).filter(CourtReservation.id == reservation_id).first()
if not reservation:
raise HTTPException(status_code=404, detail="Reservation not found")
court = db.query(Court).filter(Court.id == reservation.court_id).first()
db.delete(reservation)
db.commit()
# Check if court has any remaining reservations
remaining = db.query(CourtReservation).filter(CourtReservation.court_id == reservation.court_id).count()
if remaining == 0 and court and court.status == "reserved":
court.status = "available"
db.commit()
return {"ok": True}

View File

@@ -0,0 +1,136 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from datetime import datetime
from ..database import get_db
from ..models import Court, Match, Event, Stage, Player, CourtReservation
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
@router.get("/")
def get_dashboard(db: Session = Depends(get_db)):
courts = db.query(Court).all()
courts_data = []
for court in courts:
current_match = db.query(Match).filter(
Match.court_id == court.id,
Match.status == "in_progress"
).first()
match_data = None
elapsed = None
if current_match:
p1 = db.query(Player).filter(Player.id == current_match.player1_id).first() if current_match.player1_id else None
p2 = db.query(Player).filter(Player.id == current_match.player2_id).first() if current_match.player2_id else None
if current_match.started_at:
elapsed = int((datetime.utcnow() - current_match.started_at).total_seconds())
match_data = {
"id": current_match.id,
"player1_id": current_match.player1_id,
"player2_id": current_match.player2_id,
"player1_name": p1.name if p1 else None,
"player2_name": p2.name if p2 else None,
"score1": current_match.score1,
"score2": current_match.score2,
"status": current_match.status,
"elapsed_seconds": elapsed,
"stage_id": current_match.stage_id,
}
# Next reservation
now = datetime.utcnow()
next_reservation = db.query(CourtReservation).filter(
CourtReservation.court_id == court.id,
CourtReservation.start_time > now
).order_by(CourtReservation.start_time).first()
courts_data.append({
"id": court.id,
"name": court.name,
"court_type": court.court_type,
"status": court.status,
"current_match": match_data,
"elapsed_seconds": elapsed,
"next_reservation": {
"start_time": next_reservation.start_time,
"group_name": next_reservation.group_name,
} if next_reservation else None,
})
# Upcoming scheduled matches (not yet in progress)
upcoming = db.query(Match).filter(
Match.status == "scheduled",
Match.player1_id.isnot(None),
Match.player2_id.isnot(None)
).limit(10).all()
upcoming_data = []
for m in upcoming:
p1 = db.query(Player).filter(Player.id == m.player1_id).first()
p2 = db.query(Player).filter(Player.id == m.player2_id).first()
stage = db.query(Stage).filter(Stage.id == m.stage_id).first()
event = db.query(Event).filter(Event.id == stage.event_id).first() if stage else None
upcoming_data.append({
"id": m.id,
"player1_name": p1.name if p1 else None,
"player2_name": p2.name if p2 else None,
"round_number": m.round_number,
"event_name": event.name if event else None,
"stage_name": stage.name if stage else None,
})
# Active event summary
active_event = db.query(Event).filter(Event.status == "active").first()
active_event_data = None
if active_event:
active_stage = None
if active_event.current_stage_id:
active_stage = db.query(Stage).filter(Stage.id == active_event.current_stage_id).first()
active_event_data = {
"id": active_event.id,
"name": active_event.name,
"format": active_event.format,
"current_stage": active_stage.name if active_stage else None,
"current_stage_id": active_event.current_stage_id,
}
return {
"courts": courts_data,
"upcoming_matches": upcoming_data,
"active_event": active_event_data,
}
@router.get("/screen/{court_id}")
def screen_view(court_id: int, db: Session = Depends(get_db)):
"""TV/screen display for a specific court"""
court = db.query(Court).filter(Court.id == court_id).first()
if not court:
return {"error": "Court not found"}
current_match = db.query(Match).filter(
Match.court_id == court_id,
Match.status == "in_progress"
).first()
match_data = None
if current_match:
p1 = db.query(Player).filter(Player.id == current_match.player1_id).first()
p2 = db.query(Player).filter(Player.id == current_match.player2_id).first()
elapsed = None
if current_match.started_at:
elapsed = int((datetime.utcnow() - current_match.started_at).total_seconds())
match_data = {
"id": current_match.id,
"player1_name": p1.name if p1 else "TBD",
"player2_name": p2.name if p2 else "TBD",
"score1": current_match.score1 if current_match.score1 is not None else 0,
"score2": current_match.score2 if current_match.score2 is not None else 0,
"elapsed_seconds": elapsed,
}
return {
"court": {"id": court.id, "name": court.name, "status": court.status},
"current_match": match_data,
}

View File

@@ -0,0 +1,294 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload
from typing import List
from datetime import datetime
from ..database import get_db
from ..models import Event, EventPlayer, Stage, Match, Player, Court
from ..schemas import (
EventCreate, EventUpdate, EventPlayerAdd, EventPlayerOut,
StageCreate, StageUpdate, StageOut, MatchOut
)
from ..bracket import calculate_stages, generate_single_elim_bracket, generate_open_schedule, generate_round_robin
router = APIRouter(prefix="/api/events", tags=["events"])
def _event_with_details(event_id: int, db: Session):
return db.query(Event).options(
joinedload(Event.event_players).joinedload(EventPlayer.player),
joinedload(Event.stages).joinedload(Stage.matches)
).filter(Event.id == event_id).first()
@router.get("/")
def list_events(db: Session = Depends(get_db)):
events = db.query(Event).order_by(Event.created_at.desc()).all()
return [{"id": e.id, "name": e.name, "format": e.format, "status": e.status,
"courts_count": e.courts_count, "created_at": e.created_at,
"player_count": len(e.event_players), "stage_count": len(e.stages)} for e in events]
@router.post("/")
def create_event(data: EventCreate, db: Session = Depends(get_db)):
event = Event(**data.model_dump())
db.add(event)
db.commit()
db.refresh(event)
return {"id": event.id, "name": event.name, "format": event.format,
"status": event.status, "courts_count": event.courts_count}
@router.get("/calculate")
def calculate_event(players: int, format: str = "Singles", courts: int = 1):
return calculate_stages(players, format, courts)
@router.get("/{event_id}")
def get_event(event_id: int, db: Session = Depends(get_db)):
event = _event_with_details(event_id, db)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
stages_data = []
for stage in event.stages:
matches_data = []
for m in stage.matches:
p1 = db.query(Player).filter(Player.id == m.player1_id).first() if m.player1_id else None
p2 = db.query(Player).filter(Player.id == m.player2_id).first() if m.player2_id else None
court = db.query(Court).filter(Court.id == m.court_id).first() if m.court_id else None
matches_data.append({
"id": m.id, "round_number": m.round_number, "match_number": m.match_number,
"player1_id": m.player1_id, "player2_id": m.player2_id,
"team1_players": m.team1_players, "team2_players": m.team2_players,
"player1_name": p1.name if p1 else None,
"player2_name": p2.name if p2 else None,
"score1": m.score1, "score2": m.score2,
"winner_id": m.winner_id, "winner_team": m.winner_team,
"status": m.status, "bracket_position": m.bracket_position,
"next_winner_match_id": m.next_winner_match_id,
"next_loser_match_id": m.next_loser_match_id,
"court_id": m.court_id,
"court_name": court.name if court else None,
"started_at": m.started_at, "completed_at": m.completed_at,
})
stages_data.append({
"id": stage.id, "name": stage.name, "match_type": stage.match_type,
"rounds": stage.rounds, "advance_count": stage.advance_count,
"status": stage.status, "order": stage.order,
"matches": matches_data,
})
players_data = [{
"id": ep.id, "player_id": ep.player_id,
"skill_level_override": ep.skill_level_override,
"player": {"id": ep.player.id, "name": ep.player.name,
"skill_level": ep.player.skill_level, "elo": ep.player.elo,
"matches_played": ep.player.matches_played,
"wins": ep.player.wins, "losses": ep.player.losses}
} for ep in event.event_players]
return {
"id": event.id, "name": event.name, "format": event.format,
"courts_count": event.courts_count, "status": event.status,
"current_stage_id": event.current_stage_id,
"created_at": event.created_at, "started_at": event.started_at,
"players": players_data,
"stages": stages_data,
}
@router.put("/{event_id}")
def update_event(event_id: int, data: EventUpdate, db: Session = Depends(get_db)):
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
raise HTTPException(status_code=404, detail="Event not found")
for k, v in data.model_dump(exclude_none=True).items():
setattr(event, k, v)
db.commit()
return {"ok": True}
@router.post("/{event_id}/players")
def add_player_to_event(event_id: int, data: EventPlayerAdd, db: Session = Depends(get_db)):
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
raise HTTPException(status_code=404, detail="Event not found")
player = db.query(Player).filter(Player.id == data.player_id).first()
if not player:
raise HTTPException(status_code=404, detail="Player not found")
existing = db.query(EventPlayer).filter(
EventPlayer.event_id == event_id, EventPlayer.player_id == data.player_id
).first()
if existing:
raise HTTPException(status_code=400, detail="Player already in event")
ep = EventPlayer(event_id=event_id, player_id=data.player_id,
skill_level_override=data.skill_level_override)
db.add(ep)
db.commit()
db.refresh(ep)
return {"id": ep.id, "player_id": ep.player_id}
@router.delete("/{event_id}/players/{player_id}")
def remove_player_from_event(event_id: int, player_id: int, db: Session = Depends(get_db)):
ep = db.query(EventPlayer).filter(
EventPlayer.event_id == event_id, EventPlayer.player_id == player_id
).first()
if not ep:
raise HTTPException(status_code=404, detail="Player not in event")
db.delete(ep)
db.commit()
return {"ok": True}
@router.post("/{event_id}/stages")
def add_stage(event_id: int, data: StageCreate, db: Session = Depends(get_db)):
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
raise HTTPException(status_code=404, detail="Event not found")
stage = Stage(event_id=event_id, **data.model_dump())
db.add(stage)
db.commit()
db.refresh(stage)
return {"id": stage.id, "name": stage.name, "match_type": stage.match_type,
"rounds": stage.rounds, "advance_count": stage.advance_count, "status": stage.status}
@router.put("/{event_id}/stages/{stage_id}")
def update_stage(event_id: int, stage_id: int, data: StageUpdate, db: Session = Depends(get_db)):
stage = db.query(Stage).filter(Stage.id == stage_id, Stage.event_id == event_id).first()
if not stage:
raise HTTPException(status_code=404, detail="Stage not found")
for k, v in data.model_dump(exclude_none=True).items():
setattr(stage, k, v)
db.commit()
return {"ok": True}
@router.post("/{event_id}/stages/{stage_id}/generate")
def generate_stage_matches(event_id: int, stage_id: int, db: Session = Depends(get_db)):
"""Generate matches for a stage"""
stage = db.query(Stage).filter(Stage.id == stage_id, Stage.event_id == event_id).first()
if not stage:
raise HTTPException(status_code=404, detail="Stage not found")
event = db.query(Event).filter(Event.id == event_id).first()
# Delete existing matches for this stage
db.query(Match).filter(Match.stage_id == stage_id).delete()
db.commit()
# Get players for this event
event_players = db.query(EventPlayer).filter(EventPlayer.event_id == event_id).all()
player_ids = [ep.player_id for ep in event_players]
if len(player_ids) < 2:
raise HTTPException(status_code=400, detail="Need at least 2 players")
match_type = stage.match_type
matches_to_create = []
if match_type in ["Open", "Open Match"]:
matches_raw = generate_open_schedule(player_ids, stage.rounds, event.courts_count)
for m in matches_raw:
matches_to_create.append(Match(
stage_id=stage_id,
round_number=m["round_number"],
match_number=m["match_number"],
player1_id=m["player1_id"],
player2_id=m["player2_id"],
status=m["status"],
bracket_position=m.get("bracket_position", "main"),
))
elif match_type == "Round Robin":
pairings = generate_round_robin(player_ids)
for i, (p1, p2) in enumerate(pairings):
matches_to_create.append(Match(
stage_id=stage_id,
round_number=1,
match_number=i + 1,
player1_id=p1,
player2_id=p2,
status="scheduled",
bracket_position="main",
))
elif match_type in ["Tournament", "Double Elimination"]:
bracket_matches = generate_single_elim_bracket(player_ids)
# Create matches and track temp IDs
created = {}
# First pass - create all match objects
db_matches = []
for bm in bracket_matches:
m = Match(
stage_id=stage_id,
round_number=bm["round_number"],
match_number=bm["match_number"],
player1_id=bm.get("player1_id"),
player2_id=bm.get("player2_id"),
status=bm["status"],
bracket_position=bm.get("bracket_position", "winners"),
)
db.add(m)
db.flush()
created[bm["temp_id"]] = m
db_matches.append((bm, m))
# Second pass - link next_winner_match_id
for bm, m in db_matches:
if bm.get("next_winner_temp_id") and bm["next_winner_temp_id"] in created:
m.next_winner_match_id = created[bm["next_winner_temp_id"]].id
db.commit()
return {"ok": True, "match_count": len(bracket_matches)}
elif match_type == "Skill-Based":
# Group players by skill level and create matches within same skill groups
players = db.query(Player).filter(Player.id.in_(player_ids)).all()
skill_order = {"Beginner": 0, "Intermediate": 1, "Advanced": 2, "Elite": 3}
players_sorted = sorted(players, key=lambda p: skill_order.get(p.skill_level, 0))
sorted_ids = [p.id for p in players_sorted]
pairings = generate_round_robin(sorted_ids)[:len(sorted_ids)]
for i, (p1, p2) in enumerate(pairings):
matches_to_create.append(Match(
stage_id=stage_id,
round_number=1,
match_number=i + 1,
player1_id=p1,
player2_id=p2,
status="scheduled",
bracket_position="main",
))
for m in matches_to_create:
db.add(m)
db.commit()
return {"ok": True, "match_count": len(matches_to_create)}
@router.post("/{event_id}/start")
def start_event(event_id: int, db: Session = Depends(get_db)):
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
raise HTTPException(status_code=404, detail="Event not found")
event.status = "active"
event.started_at = datetime.utcnow()
# Activate first stage
first_stage = db.query(Stage).filter(Stage.event_id == event_id).order_by(Stage.order).first()
if first_stage:
first_stage.status = "active"
event.current_stage_id = first_stage.id
db.commit()
return {"ok": True}
@router.post("/{event_id}/complete")
def complete_event(event_id: int, db: Session = Depends(get_db)):
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
raise HTTPException(status_code=404, detail="Event not found")
event.status = "completed"
event.completed_at = datetime.utcnow()
db.commit()
return {"ok": True}

View 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}

View File

@@ -0,0 +1,52 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
from ..database import get_db
from ..models import Player
from ..schemas import PlayerCreate, PlayerUpdate, PlayerOut
router = APIRouter(prefix="/api/players", tags=["players"])
@router.get("/", response_model=List[PlayerOut])
def list_players(db: Session = Depends(get_db)):
return db.query(Player).order_by(Player.elo.desc()).all()
@router.post("/", response_model=PlayerOut)
def create_player(data: PlayerCreate, db: Session = Depends(get_db)):
player = Player(**data.model_dump())
db.add(player)
db.commit()
db.refresh(player)
return player
@router.get("/{player_id}", response_model=PlayerOut)
def get_player(player_id: int, db: Session = Depends(get_db)):
player = db.query(Player).filter(Player.id == player_id).first()
if not player:
raise HTTPException(status_code=404, detail="Player not found")
return player
@router.put("/{player_id}", response_model=PlayerOut)
def update_player(player_id: int, data: PlayerUpdate, db: Session = Depends(get_db)):
player = db.query(Player).filter(Player.id == player_id).first()
if not player:
raise HTTPException(status_code=404, detail="Player not found")
for k, v in data.model_dump(exclude_none=True).items():
setattr(player, k, v)
db.commit()
db.refresh(player)
return player
@router.delete("/{player_id}")
def delete_player(player_id: int, db: Session = Depends(get_db)):
player = db.query(Player).filter(Player.id == player_id).first()
if not player:
raise HTTPException(status_code=404, detail="Player not found")
db.delete(player)
db.commit()
return {"ok": True}