strat-gameplay-webapp/backend/app/services/lineup_service.py
Cal Corum 52706bed40 CLAUDE: Mobile drag-drop lineup builder and touch-friendly UI improvements
- Add vuedraggable for mobile-friendly lineup building
- Add touch delay and threshold settings for better mobile UX
- Add drag ghost/chosen/dragging visual states
- Add replacement mode visual feedback when dragging over occupied slots
- Add getBench action to useGameActions for substitution panel
- Add BN (bench) to valid positions in LineupPlayerState
- Update lineup service to load full lineup (active + bench)
- Add touch-manipulation CSS to UI components (ActionButton, ButtonGroup, ToggleSwitch)
- Add select-none to prevent text selection during touch interactions
- Add mobile touch patterns documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 22:17:16 -06:00

212 lines
7.0 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. Fetches player data from SBA API (for SBA league)
3. 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: Fetch player data for SBA league
player_data = {}
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:
player_data = await sba_api_client.get_players_batch(player_ids)
logger.info(
f"Loaded {len(player_data)}/{len(player_ids)} players for team {team_id}"
)
except Exception as e:
logger.warning(
f"Failed to fetch player data for team {team_id}: {e}"
)
# 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 and player_data.get(p.player_id): # type: ignore[arg-type]
player = player_data.get(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()