CLAUDE: Add schedule_game_id linking for webapp games to external schedules
Enables precise tracking of which webapp games correspond to specific scheduled matchups from SBA/PD league systems. Backend: - Add schedule_game_id column to games table with index - Create Alembic migration for the new column - Update QuickCreateRequest to accept schedule_game_id - Update GameListItem response to include schedule_game_id - Update DatabaseOperations.create_game() to store the link Frontend: - Pass schedule_game_id when creating game from "Play" button - Add activeScheduleGameMap to track webapp games by schedule ID - Show "In Progress" (green) link for active games - Show "Resume" (green) link for pending games - Show "Play" (blue) button for unstarted games Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
fbbb1cc5da
commit
a22513b053
@ -0,0 +1,37 @@
|
||||
"""add schedule_game_id to games
|
||||
|
||||
Revision ID: 62bd3195c64c
|
||||
Revises: 005
|
||||
Create Date: 2026-01-14 23:44:40.038088
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '62bd3195c64c'
|
||||
down_revision: Union[str, None] = '005'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add schedule_game_id column for linking webapp games to external schedule systems
|
||||
op.add_column(
|
||||
'games',
|
||||
sa.Column('schedule_game_id', sa.Integer(), nullable=True)
|
||||
)
|
||||
# Add index for efficient lookups
|
||||
op.create_index(
|
||||
'ix_games_schedule_game_id',
|
||||
'games',
|
||||
['schedule_game_id']
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index('ix_games_schedule_game_id', table_name='games')
|
||||
op.drop_column('games', 'schedule_game_id')
|
||||
@ -33,6 +33,8 @@ class GameListItem(BaseModel):
|
||||
away_score: int = 0
|
||||
inning: int | None = None
|
||||
half: str | None = None # 'top' or 'bottom'
|
||||
# External schedule reference (for linking to SBA/PD schedule systems)
|
||||
schedule_game_id: int | None = None
|
||||
|
||||
|
||||
class CreateGameRequest(BaseModel):
|
||||
@ -59,6 +61,7 @@ class QuickCreateRequest(BaseModel):
|
||||
|
||||
home_team_id: int | None = Field(None, description="Home team ID (uses default if not provided)")
|
||||
away_team_id: int | None = Field(None, description="Away team ID (uses default if not provided)")
|
||||
schedule_game_id: int | None = Field(None, description="External schedule game ID for linking")
|
||||
|
||||
|
||||
class LineupPlayerRequest(BaseModel):
|
||||
@ -219,6 +222,7 @@ async def list_games():
|
||||
away_score=game.away_score or 0,
|
||||
inning=game.current_inning,
|
||||
half=game.current_half,
|
||||
schedule_game_id=game.schedule_game_id,
|
||||
)
|
||||
)
|
||||
|
||||
@ -358,7 +362,7 @@ async def quick_create_game(
|
||||
game_id = uuid4()
|
||||
league_id = "sba"
|
||||
|
||||
# Determine team IDs
|
||||
# Determine team IDs and schedule link
|
||||
use_custom_teams = (
|
||||
request is not None
|
||||
and request.home_team_id is not None
|
||||
@ -368,10 +372,12 @@ async def quick_create_game(
|
||||
if use_custom_teams:
|
||||
home_team_id = request.home_team_id
|
||||
away_team_id = request.away_team_id
|
||||
schedule_game_id = request.schedule_game_id
|
||||
else:
|
||||
# Default demo teams
|
||||
home_team_id = 35
|
||||
away_team_id = 38
|
||||
schedule_game_id = None
|
||||
|
||||
# Get creator's discord_id from authenticated user
|
||||
creator_discord_id = user.get("discord_id") if user else None
|
||||
@ -399,6 +405,7 @@ async def quick_create_game(
|
||||
away_team_id=away_team_id,
|
||||
game_mode="friendly",
|
||||
visibility="public",
|
||||
schedule_game_id=schedule_game_id,
|
||||
)
|
||||
|
||||
if use_custom_teams:
|
||||
|
||||
@ -102,6 +102,7 @@ class DatabaseOperations:
|
||||
home_team_is_ai: bool = False,
|
||||
away_team_is_ai: bool = False,
|
||||
ai_difficulty: str | None = None,
|
||||
schedule_game_id: int | None = None,
|
||||
) -> Game:
|
||||
"""
|
||||
Create new game in database.
|
||||
@ -116,6 +117,7 @@ class DatabaseOperations:
|
||||
home_team_is_ai: Whether home team is AI
|
||||
away_team_is_ai: Whether away team is AI
|
||||
ai_difficulty: AI difficulty if applicable
|
||||
schedule_game_id: External schedule game ID for linking (SBA, PD, etc.)
|
||||
|
||||
Returns:
|
||||
Created Game model
|
||||
@ -134,6 +136,7 @@ class DatabaseOperations:
|
||||
home_team_is_ai=home_team_is_ai,
|
||||
away_team_is_ai=away_team_is_ai,
|
||||
ai_difficulty=ai_difficulty,
|
||||
schedule_game_id=schedule_game_id,
|
||||
status="pending",
|
||||
)
|
||||
session.add(game)
|
||||
|
||||
@ -99,6 +99,9 @@ class Game(Base):
|
||||
away_team_is_ai = Column(Boolean, default=False)
|
||||
ai_difficulty = Column(String(20), nullable=True)
|
||||
|
||||
# External schedule reference (league-agnostic - works for SBA, PD, etc.)
|
||||
schedule_game_id = Column(Integer, nullable=True, index=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(
|
||||
DateTime, default=lambda: pendulum.now("UTC").naive(), index=True
|
||||
|
||||
@ -144,11 +144,21 @@
|
||||
Final
|
||||
</span>
|
||||
</template>
|
||||
<!-- Active webapp game: show "In Progress" link -->
|
||||
<template v-else-if="activeScheduleGameMap.get(game.id)">
|
||||
<span class="flex-1"></span>
|
||||
<NuxtLink
|
||||
:to="`/games/${activeScheduleGameMap.get(game.id)!.game_id}`"
|
||||
class="px-2 py-1 text-xs bg-green-600 hover:bg-green-700 text-white font-medium rounded transition"
|
||||
>
|
||||
{{ activeScheduleGameMap.get(game.id)!.status === 'active' ? 'In Progress' : 'Resume' }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<!-- Incomplete game: show Play button -->
|
||||
<template v-else>
|
||||
<span class="flex-1"></span>
|
||||
<button
|
||||
@click="handlePlayScheduledGame(game.home_team.id, game.away_team.id)"
|
||||
@click="handlePlayScheduledGame(game.home_team.id, game.away_team.id, game.id)"
|
||||
:disabled="isCreatingQuickGame"
|
||||
class="px-2 py-1 text-xs bg-primary hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium rounded transition disabled:cursor-not-allowed"
|
||||
>
|
||||
@ -382,6 +392,23 @@ const groupedScheduleGames = computed<ScheduleGroup[]>(() => {
|
||||
return Array.from(groups.values()).sort((a, b) => a.games[0].id - b.games[0].id)
|
||||
})
|
||||
|
||||
// Map of schedule_game_id -> webapp game for active games
|
||||
// Used to show indicators on schedule entries that have webapp games in progress
|
||||
const activeScheduleGameMap = computed(() => {
|
||||
const map = new Map<number, { game_id: string; status: string }>()
|
||||
if (!games.value) return map
|
||||
|
||||
for (const game of games.value) {
|
||||
if (game.schedule_game_id && (game.status === 'active' || game.status === 'pending')) {
|
||||
map.set(game.schedule_game_id, {
|
||||
game_id: game.game_id,
|
||||
status: game.status,
|
||||
})
|
||||
}
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
// Quick-create a demo game with pre-configured lineups
|
||||
async function handleQuickCreate() {
|
||||
try {
|
||||
@ -411,12 +438,12 @@ async function handleQuickCreate() {
|
||||
}
|
||||
|
||||
// Create a game from a scheduled matchup
|
||||
async function handlePlayScheduledGame(homeTeamId: number, awayTeamId: number) {
|
||||
async function handlePlayScheduledGame(homeTeamId: number, awayTeamId: number, scheduleGameId: number) {
|
||||
try {
|
||||
isCreatingQuickGame.value = true
|
||||
error.value = null
|
||||
|
||||
console.log(`[Games Page] Creating game: ${awayTeamId} @ ${homeTeamId}`)
|
||||
console.log(`[Games Page] Creating game: ${awayTeamId} @ ${homeTeamId} (schedule_game_id: ${scheduleGameId})`)
|
||||
|
||||
const response = await $fetch<{ game_id: string; message: string; status: string }>(
|
||||
`${config.public.apiUrl}/api/games/quick-create`,
|
||||
@ -426,6 +453,7 @@ async function handlePlayScheduledGame(homeTeamId: number, awayTeamId: number) {
|
||||
body: {
|
||||
home_team_id: homeTeamId,
|
||||
away_team_id: awayTeamId,
|
||||
schedule_game_id: scheduleGameId,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@ -301,6 +301,7 @@ export interface ManualOutcomeSubmission {
|
||||
|
||||
/**
|
||||
* Game list item for active/completed games
|
||||
* Note: Field names match backend GameListItem response
|
||||
*/
|
||||
export interface GameListItem {
|
||||
game_id: string
|
||||
@ -308,12 +309,17 @@ export interface GameListItem {
|
||||
home_team_id: number
|
||||
away_team_id: number
|
||||
status: GameStatus
|
||||
current_inning: number
|
||||
current_half: InningHalf
|
||||
// Enriched team info from backend
|
||||
home_team_name?: string
|
||||
away_team_name?: string
|
||||
home_team_abbrev?: string
|
||||
away_team_abbrev?: string
|
||||
home_score: number
|
||||
away_score: number
|
||||
started_at: string | null
|
||||
completed_at: string | null
|
||||
inning?: number
|
||||
half?: InningHalf
|
||||
// External schedule reference (for linking to SBA/PD schedule systems)
|
||||
schedule_game_id?: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user