Features: - PlayerCardModal: Tap any player to view full playing card image - OutcomeWizard: Progressive 3-step outcome selection (On Base/Out/X-Check) - GameBoard: Expandable view showing all 9 fielder positions - Post-roll card display: Shows batter/pitcher card based on d6 roll - CurrentSituation: Tappable player cards with modal integration Bug fixes: - Fix batter not advancing after play (state_manager recovery logic) - Add dark mode support for buttons and panels (partial - iOS issue noted) New files: - PlayerCardModal.vue, OutcomeWizard.vue, BottomSheet.vue - outcomeFlow.ts constants for outcome category mapping - TEST_PLAN_UI_OVERHAUL.md with 23/24 tests passing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
243 lines
8.5 KiB
Python
243 lines
8.5 KiB
Python
"""
|
|
Lineup Service - High-level lineup operations with player data.
|
|
|
|
Combines database operations with API calls to provide complete
|
|
lineup entries with player data.
|
|
|
|
Author: Claude
|
|
Date: 2025-01-10
|
|
"""
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from uuid import UUID
|
|
|
|
from app.database.operations import DatabaseOperations
|
|
from app.models.game_models import LineupPlayerState, TeamLineupState
|
|
from app.services.sba_api_client import sba_api_client
|
|
|
|
logger = logging.getLogger(f"{__name__}.LineupService")
|
|
|
|
|
|
@dataclass
|
|
class LineupEntryWithPlayer:
|
|
"""Lineup entry with associated player data."""
|
|
|
|
lineup_id: int
|
|
player_id: int
|
|
position: str
|
|
batting_order: int | None
|
|
is_starter: bool
|
|
is_active: bool
|
|
# Player data from API
|
|
player_name: str
|
|
player_image: str # Card image
|
|
player_headshot: str # Headshot for UI circles
|
|
|
|
|
|
class LineupService:
|
|
"""
|
|
Service for lineup operations that include player data.
|
|
|
|
Combines database operations with SBA API calls to provide
|
|
complete lineup entries with player names and images.
|
|
"""
|
|
|
|
def __init__(self, db_ops: DatabaseOperations | None = None):
|
|
self.db_ops = db_ops or DatabaseOperations()
|
|
|
|
async def add_sba_player_to_lineup(
|
|
self,
|
|
game_id: UUID,
|
|
team_id: int,
|
|
player_id: int,
|
|
position: str,
|
|
batting_order: int | None = None,
|
|
is_starter: bool = True,
|
|
) -> LineupEntryWithPlayer:
|
|
"""
|
|
Add SBA player to lineup with player data.
|
|
|
|
1. Creates lineup entry in database
|
|
2. Fetches player data from SBA API
|
|
3. Returns combined result
|
|
|
|
Args:
|
|
game_id: Game identifier
|
|
team_id: Team identifier
|
|
player_id: SBA player ID
|
|
position: Defensive position
|
|
batting_order: Batting order (1-9) if applicable
|
|
is_starter: Whether player is in starting lineup
|
|
|
|
Returns:
|
|
LineupEntryWithPlayer with lineup data + player info
|
|
|
|
Raises:
|
|
HTTPError: If SBA API call fails
|
|
SQLAlchemyError: If database operation fails
|
|
"""
|
|
# Step 1: Create lineup entry in database
|
|
lineup = await self.db_ops.add_sba_lineup_player(
|
|
game_id=game_id,
|
|
team_id=team_id,
|
|
player_id=player_id,
|
|
position=position,
|
|
batting_order=batting_order,
|
|
is_starter=is_starter,
|
|
)
|
|
|
|
# Step 2: Fetch player data from SBA API
|
|
player_name = f"Player #{player_id}"
|
|
player_image = ""
|
|
player_headshot = ""
|
|
|
|
try:
|
|
player = await sba_api_client.get_player(player_id)
|
|
player_name = player.name
|
|
player_image = player.get_image_url()
|
|
player_headshot = player.headshot or ""
|
|
logger.info(f"Loaded player data for {player_id}: {player_name}")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to fetch player data for {player_id}: {e}")
|
|
# Continue with defaults - lineup entry is still valid
|
|
|
|
# Step 3: Return combined result
|
|
return LineupEntryWithPlayer(
|
|
lineup_id=lineup.id, # type: ignore[arg-type]
|
|
player_id=player_id,
|
|
position=position,
|
|
batting_order=batting_order,
|
|
is_starter=is_starter,
|
|
is_active=True,
|
|
player_name=player_name,
|
|
player_image=player_image,
|
|
player_headshot=player_headshot,
|
|
)
|
|
|
|
async def load_team_lineup_with_player_data(
|
|
self, game_id: UUID, team_id: int, league_id: str
|
|
) -> TeamLineupState | None:
|
|
"""
|
|
Load existing team lineup from database with player data.
|
|
|
|
1. Fetches full lineup (active + bench) from database
|
|
2. Tries to get player data from RosterLink cache first
|
|
3. Falls back to SBA API if cache misses (for SBA league)
|
|
4. Returns TeamLineupState with player info populated
|
|
|
|
Args:
|
|
game_id: Game identifier
|
|
team_id: Team identifier
|
|
league_id: League identifier ('sba' or 'pd')
|
|
|
|
Returns:
|
|
TeamLineupState with player data (including bench), or None if no lineup found
|
|
"""
|
|
# Step 1: Get full lineup from database (active + bench)
|
|
lineup_entries = await self.db_ops.get_full_lineup(game_id, team_id)
|
|
if not lineup_entries:
|
|
return None
|
|
|
|
# Step 2: Get player data - try RosterLink cache first, then API fallback
|
|
player_data_cache: dict[int, dict] = {}
|
|
api_player_data: dict = {}
|
|
|
|
if league_id == "sba":
|
|
player_ids = [p.player_id for p in lineup_entries if p.player_id] # type: ignore[misc]
|
|
if player_ids:
|
|
# Try RosterLink cache first
|
|
player_data_cache = await self.db_ops.get_roster_player_data(
|
|
game_id, team_id
|
|
)
|
|
cached_count = len(player_data_cache)
|
|
|
|
# Find players missing from cache
|
|
missing_ids = [
|
|
pid for pid in player_ids if pid not in player_data_cache
|
|
]
|
|
|
|
if missing_ids:
|
|
# Fall back to SBA API for missing players
|
|
try:
|
|
api_player_data = await sba_api_client.get_players_batch(
|
|
missing_ids
|
|
)
|
|
logger.info(
|
|
f"Loaded {cached_count} players from cache, "
|
|
f"{len(api_player_data)} from API for team {team_id}"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"Failed to fetch player data from API for team {team_id}: {e}"
|
|
)
|
|
else:
|
|
logger.info(
|
|
f"Loaded all {cached_count} players from RosterLink cache "
|
|
f"for team {team_id} (no API call needed)"
|
|
)
|
|
|
|
# Step 3: Build TeamLineupState with player data
|
|
players = []
|
|
for p in lineup_entries:
|
|
player_name = None
|
|
player_image = None
|
|
player_headshot = None
|
|
|
|
if league_id == "sba" and p.player_id: # type: ignore[arg-type]
|
|
# Check cache first, then API data
|
|
if p.player_id in player_data_cache: # type: ignore[arg-type]
|
|
cached = player_data_cache[p.player_id] # type: ignore[arg-type]
|
|
player_name = cached.get("name")
|
|
player_image = cached.get("image")
|
|
player_headshot = cached.get("headshot")
|
|
elif p.player_id in api_player_data: # type: ignore[arg-type]
|
|
player = api_player_data[p.player_id] # type: ignore[arg-type]
|
|
player_name = player.name
|
|
player_image = player.get_image_url()
|
|
player_headshot = player.headshot
|
|
|
|
players.append(
|
|
LineupPlayerState(
|
|
lineup_id=p.id, # type: ignore[arg-type]
|
|
card_id=p.card_id if p.card_id else (p.player_id or 0), # type: ignore[arg-type]
|
|
position=p.position, # type: ignore[arg-type]
|
|
batting_order=p.batting_order, # type: ignore[arg-type]
|
|
is_active=p.is_active, # type: ignore[arg-type]
|
|
is_starter=p.is_starter, # type: ignore[arg-type]
|
|
player_name=player_name,
|
|
player_image=player_image,
|
|
player_headshot=player_headshot,
|
|
)
|
|
)
|
|
|
|
return TeamLineupState(team_id=team_id, players=players)
|
|
|
|
async def get_sba_player_data(self, player_id: int) -> tuple[str, str]:
|
|
"""
|
|
Fetch player data for a single SBA player.
|
|
|
|
Args:
|
|
player_id: SBA player ID
|
|
|
|
Returns:
|
|
Tuple of (player_name, player_image)
|
|
Returns defaults if API call fails
|
|
"""
|
|
player_name = f"Player #{player_id}"
|
|
player_image = ""
|
|
|
|
try:
|
|
player = await sba_api_client.get_player(player_id)
|
|
player_name = player.name
|
|
player_image = player.get_image_url()
|
|
logger.info(f"Loaded player data for {player_id}: {player_name}")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to fetch player data for {player_id}: {e}")
|
|
|
|
return player_name, player_image
|
|
|
|
|
|
# Singleton instance
|
|
lineup_service = LineupService()
|