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

128
backend/app/models.py Normal file
View File

@@ -0,0 +1,128 @@
from datetime import datetime
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, ForeignKey, JSON, Text
from sqlalchemy.orm import relationship
from .database import Base
class Player(Base):
__tablename__ = "players"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False)
contact = Column(String(100), nullable=True)
skill_level = Column(String(20), nullable=False, default="Beginner") # Beginner/Intermediate/Advanced/Elite
matches_played = Column(Integer, default=0)
wins = Column(Integer, default=0)
losses = Column(Integer, default=0)
elo = Column(Float, default=1000.0)
created_at = Column(DateTime, default=datetime.utcnow)
event_players = relationship("EventPlayer", back_populates="player")
class Court(Base):
__tablename__ = "courts"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False)
court_type = Column(String(100), default="Sport Court")
status = Column(String(20), default="available") # available/in_use/reserved/maintenance
created_at = Column(DateTime, default=datetime.utcnow)
reservations = relationship("CourtReservation", back_populates="court")
matches = relationship("Match", back_populates="court")
class CourtReservation(Base):
__tablename__ = "court_reservations"
id = Column(Integer, primary_key=True, index=True)
court_id = Column(Integer, ForeignKey("courts.id"), nullable=False)
player_id = Column(Integer, ForeignKey("players.id"), nullable=True)
group_name = Column(String(100), nullable=True)
start_time = Column(DateTime, nullable=False)
end_time = Column(DateTime, nullable=False)
notes = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
court = relationship("Court", back_populates="reservations")
player = relationship("Player")
class Event(Base):
__tablename__ = "events"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(200), nullable=False)
format = Column(String(20), default="Singles") # Singles/Doubles
courts_count = Column(Integer, default=1)
status = Column(String(20), default="setup") # setup/active/completed
current_stage_id = Column(Integer, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
started_at = Column(DateTime, nullable=True)
completed_at = Column(DateTime, nullable=True)
event_players = relationship("EventPlayer", back_populates="event", cascade="all, delete-orphan")
stages = relationship("Stage", back_populates="event", order_by="Stage.order", cascade="all, delete-orphan")
class EventPlayer(Base):
__tablename__ = "event_players"
id = Column(Integer, primary_key=True, index=True)
event_id = Column(Integer, ForeignKey("events.id"), nullable=False)
player_id = Column(Integer, ForeignKey("players.id"), nullable=False)
skill_level_override = Column(String(20), nullable=True)
event = relationship("Event", back_populates="event_players")
player = relationship("Player", back_populates="event_players")
class Stage(Base):
__tablename__ = "stages"
id = Column(Integer, primary_key=True, index=True)
event_id = Column(Integer, ForeignKey("events.id"), nullable=False)
name = Column(String(100), nullable=False)
match_type = Column(String(30), nullable=False) # Open/Skill-Based/Round Robin/Tournament
rounds = Column(Integer, default=1)
advance_count = Column(Integer, default=0) # 0 = all advance
status = Column(String(20), default="pending") # pending/active/completed
order = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
event = relationship("Event", back_populates="stages")
matches = relationship("Match", back_populates="stage", cascade="all, delete-orphan")
class Match(Base):
__tablename__ = "matches"
id = Column(Integer, primary_key=True, index=True)
stage_id = Column(Integer, ForeignKey("stages.id"), nullable=False)
court_id = Column(Integer, ForeignKey("courts.id"), nullable=True)
round_number = Column(Integer, default=1)
match_number = Column(Integer, default=1)
# Singles: player1_id, player2_id
# Doubles: team1_players and team2_players are JSON lists
player1_id = Column(Integer, ForeignKey("players.id"), nullable=True)
player2_id = Column(Integer, ForeignKey("players.id"), nullable=True)
team1_players = Column(JSON, nullable=True) # [player_id, player_id]
team2_players = Column(JSON, nullable=True)
score1 = Column(Integer, nullable=True)
score2 = Column(Integer, nullable=True)
winner_id = Column(Integer, ForeignKey("players.id"), nullable=True) # for singles
winner_team = Column(Integer, nullable=True) # 1 or 2 for doubles
status = Column(String(20), default="scheduled") # scheduled/in_progress/completed/bye
bracket_position = Column(String(20), nullable=True) # winners/losers
next_winner_match_id = Column(Integer, nullable=True)
next_loser_match_id = Column(Integer, nullable=True)
scheduled_time = Column(DateTime, nullable=True)
started_at = Column(DateTime, nullable=True)
completed_at = Column(DateTime, nullable=True)
stage = relationship("Stage", back_populates="matches")
court = relationship("Court", back_populates="matches")
player1 = relationship("Player", foreign_keys=[player1_id])
player2 = relationship("Player", foreign_keys=[player2_id])
winner = relationship("Player", foreign_keys=[winner_id])