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
|
away_score: int = 0
|
||||||
inning: int | None = None
|
inning: int | None = None
|
||||||
half: str | None = None # 'top' or 'bottom'
|
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):
|
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)")
|
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)")
|
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):
|
class LineupPlayerRequest(BaseModel):
|
||||||
@ -219,6 +222,7 @@ async def list_games():
|
|||||||
away_score=game.away_score or 0,
|
away_score=game.away_score or 0,
|
||||||
inning=game.current_inning,
|
inning=game.current_inning,
|
||||||
half=game.current_half,
|
half=game.current_half,
|
||||||
|
schedule_game_id=game.schedule_game_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -358,7 +362,7 @@ async def quick_create_game(
|
|||||||
game_id = uuid4()
|
game_id = uuid4()
|
||||||
league_id = "sba"
|
league_id = "sba"
|
||||||
|
|
||||||
# Determine team IDs
|
# Determine team IDs and schedule link
|
||||||
use_custom_teams = (
|
use_custom_teams = (
|
||||||
request is not None
|
request is not None
|
||||||
and request.home_team_id is not None
|
and request.home_team_id is not None
|
||||||
@ -368,10 +372,12 @@ async def quick_create_game(
|
|||||||
if use_custom_teams:
|
if use_custom_teams:
|
||||||
home_team_id = request.home_team_id
|
home_team_id = request.home_team_id
|
||||||
away_team_id = request.away_team_id
|
away_team_id = request.away_team_id
|
||||||
|
schedule_game_id = request.schedule_game_id
|
||||||
else:
|
else:
|
||||||
# Default demo teams
|
# Default demo teams
|
||||||
home_team_id = 35
|
home_team_id = 35
|
||||||
away_team_id = 38
|
away_team_id = 38
|
||||||
|
schedule_game_id = None
|
||||||
|
|
||||||
# Get creator's discord_id from authenticated user
|
# Get creator's discord_id from authenticated user
|
||||||
creator_discord_id = user.get("discord_id") if user else None
|
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,
|
away_team_id=away_team_id,
|
||||||
game_mode="friendly",
|
game_mode="friendly",
|
||||||
visibility="public",
|
visibility="public",
|
||||||
|
schedule_game_id=schedule_game_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if use_custom_teams:
|
if use_custom_teams:
|
||||||
|
|||||||
@ -102,6 +102,7 @@ class DatabaseOperations:
|
|||||||
home_team_is_ai: bool = False,
|
home_team_is_ai: bool = False,
|
||||||
away_team_is_ai: bool = False,
|
away_team_is_ai: bool = False,
|
||||||
ai_difficulty: str | None = None,
|
ai_difficulty: str | None = None,
|
||||||
|
schedule_game_id: int | None = None,
|
||||||
) -> Game:
|
) -> Game:
|
||||||
"""
|
"""
|
||||||
Create new game in database.
|
Create new game in database.
|
||||||
@ -116,6 +117,7 @@ class DatabaseOperations:
|
|||||||
home_team_is_ai: Whether home team is AI
|
home_team_is_ai: Whether home team is AI
|
||||||
away_team_is_ai: Whether away team is AI
|
away_team_is_ai: Whether away team is AI
|
||||||
ai_difficulty: AI difficulty if applicable
|
ai_difficulty: AI difficulty if applicable
|
||||||
|
schedule_game_id: External schedule game ID for linking (SBA, PD, etc.)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Created Game model
|
Created Game model
|
||||||
@ -134,6 +136,7 @@ class DatabaseOperations:
|
|||||||
home_team_is_ai=home_team_is_ai,
|
home_team_is_ai=home_team_is_ai,
|
||||||
away_team_is_ai=away_team_is_ai,
|
away_team_is_ai=away_team_is_ai,
|
||||||
ai_difficulty=ai_difficulty,
|
ai_difficulty=ai_difficulty,
|
||||||
|
schedule_game_id=schedule_game_id,
|
||||||
status="pending",
|
status="pending",
|
||||||
)
|
)
|
||||||
session.add(game)
|
session.add(game)
|
||||||
|
|||||||
@ -99,6 +99,9 @@ class Game(Base):
|
|||||||
away_team_is_ai = Column(Boolean, default=False)
|
away_team_is_ai = Column(Boolean, default=False)
|
||||||
ai_difficulty = Column(String(20), nullable=True)
|
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
|
# Timestamps
|
||||||
created_at = Column(
|
created_at = Column(
|
||||||
DateTime, default=lambda: pendulum.now("UTC").naive(), index=True
|
DateTime, default=lambda: pendulum.now("UTC").naive(), index=True
|
||||||
|
|||||||
@ -144,11 +144,21 @@
|
|||||||
Final
|
Final
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</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 -->
|
<!-- Incomplete game: show Play button -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="flex-1"></span>
|
<span class="flex-1"></span>
|
||||||
<button
|
<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"
|
: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"
|
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)
|
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
|
// Quick-create a demo game with pre-configured lineups
|
||||||
async function handleQuickCreate() {
|
async function handleQuickCreate() {
|
||||||
try {
|
try {
|
||||||
@ -411,12 +438,12 @@ async function handleQuickCreate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a game from a scheduled matchup
|
// Create a game from a scheduled matchup
|
||||||
async function handlePlayScheduledGame(homeTeamId: number, awayTeamId: number) {
|
async function handlePlayScheduledGame(homeTeamId: number, awayTeamId: number, scheduleGameId: number) {
|
||||||
try {
|
try {
|
||||||
isCreatingQuickGame.value = true
|
isCreatingQuickGame.value = true
|
||||||
error.value = null
|
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 }>(
|
const response = await $fetch<{ game_id: string; message: string; status: string }>(
|
||||||
`${config.public.apiUrl}/api/games/quick-create`,
|
`${config.public.apiUrl}/api/games/quick-create`,
|
||||||
@ -426,6 +453,7 @@ async function handlePlayScheduledGame(homeTeamId: number, awayTeamId: number) {
|
|||||||
body: {
|
body: {
|
||||||
home_team_id: homeTeamId,
|
home_team_id: homeTeamId,
|
||||||
away_team_id: awayTeamId,
|
away_team_id: awayTeamId,
|
||||||
|
schedule_game_id: scheduleGameId,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -301,6 +301,7 @@ export interface ManualOutcomeSubmission {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Game list item for active/completed games
|
* Game list item for active/completed games
|
||||||
|
* Note: Field names match backend GameListItem response
|
||||||
*/
|
*/
|
||||||
export interface GameListItem {
|
export interface GameListItem {
|
||||||
game_id: string
|
game_id: string
|
||||||
@ -308,12 +309,17 @@ export interface GameListItem {
|
|||||||
home_team_id: number
|
home_team_id: number
|
||||||
away_team_id: number
|
away_team_id: number
|
||||||
status: GameStatus
|
status: GameStatus
|
||||||
current_inning: number
|
// Enriched team info from backend
|
||||||
current_half: InningHalf
|
home_team_name?: string
|
||||||
|
away_team_name?: string
|
||||||
|
home_team_abbrev?: string
|
||||||
|
away_team_abbrev?: string
|
||||||
home_score: number
|
home_score: number
|
||||||
away_score: number
|
away_score: number
|
||||||
started_at: string | null
|
inning?: number
|
||||||
completed_at: string | null
|
half?: InningHalf
|
||||||
|
// External schedule reference (for linking to SBA/PD schedule systems)
|
||||||
|
schedule_game_id?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user