Add Pokemon detail card with tabbed encounter/evolution views
Pokemon edit modal now shows three tabs (Details, Evolutions, Encounters)
instead of a single long form. Evolution chain entries are clickable to
open the EvolutionFormModal for direct editing. Encounter locations link
to admin route detail pages. Create mode shows only the form (no tabs).
Backend adds GET /pokemon/{id}/encounter-locations (grouped by game) and
GET /pokemon/{id}/evolution-chain (BFS family discovery). Extracts
formatEvolutionMethod to shared utility.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,13 +8,17 @@ from app.models.evolution import Evolution
|
||||
from app.models.pokemon import Pokemon
|
||||
from app.models.route import Route
|
||||
from app.models.route_encounter import RouteEncounter
|
||||
from app.models.game import Game
|
||||
from app.schemas.pokemon import (
|
||||
BulkImportItem,
|
||||
BulkImportResult,
|
||||
EvolutionAdminResponse,
|
||||
EvolutionResponse,
|
||||
FamiliesResponse,
|
||||
PaginatedPokemonResponse,
|
||||
PokemonCreate,
|
||||
PokemonEncounterLocationItem,
|
||||
PokemonEncounterLocationResponse,
|
||||
PokemonResponse,
|
||||
PokemonUpdate,
|
||||
RouteEncounterCreate,
|
||||
@@ -174,6 +178,103 @@ async def get_pokemon_forms(
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/pokemon/{pokemon_id}/encounter-locations",
|
||||
response_model=list[PokemonEncounterLocationResponse],
|
||||
)
|
||||
async def get_pokemon_encounter_locations(
|
||||
pokemon_id: int, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
pokemon = await session.get(Pokemon, pokemon_id)
|
||||
if pokemon is None:
|
||||
raise HTTPException(status_code=404, detail="Pokemon not found")
|
||||
|
||||
result = await session.execute(
|
||||
select(RouteEncounter)
|
||||
.where(RouteEncounter.pokemon_id == pokemon_id)
|
||||
.options(joinedload(RouteEncounter.route), joinedload(RouteEncounter.game))
|
||||
.order_by(RouteEncounter.game_id, RouteEncounter.route_id)
|
||||
)
|
||||
encounters = result.scalars().unique().all()
|
||||
|
||||
grouped: dict[int, PokemonEncounterLocationResponse] = {}
|
||||
for enc in encounters:
|
||||
if enc.game_id not in grouped:
|
||||
grouped[enc.game_id] = PokemonEncounterLocationResponse(
|
||||
game_id=enc.game_id,
|
||||
game_name=enc.game.name,
|
||||
encounters=[],
|
||||
)
|
||||
grouped[enc.game_id].encounters.append(
|
||||
PokemonEncounterLocationItem(
|
||||
route_id=enc.route_id,
|
||||
route_name=enc.route.name,
|
||||
encounter_method=enc.encounter_method,
|
||||
encounter_rate=enc.encounter_rate,
|
||||
min_level=enc.min_level,
|
||||
max_level=enc.max_level,
|
||||
)
|
||||
)
|
||||
|
||||
return list(grouped.values())
|
||||
|
||||
|
||||
@router.get(
|
||||
"/pokemon/{pokemon_id}/evolution-chain",
|
||||
response_model=list[EvolutionAdminResponse],
|
||||
)
|
||||
async def get_pokemon_evolution_chain(
|
||||
pokemon_id: int, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
from collections import deque
|
||||
|
||||
pokemon = await session.get(Pokemon, pokemon_id)
|
||||
if pokemon is None:
|
||||
raise HTTPException(status_code=404, detail="Pokemon not found")
|
||||
|
||||
# Load all evolutions to build adjacency
|
||||
result = await session.execute(select(Evolution))
|
||||
evolutions = result.scalars().all()
|
||||
|
||||
adj: dict[int, set[int]] = {}
|
||||
for evo in evolutions:
|
||||
adj.setdefault(evo.from_pokemon_id, set()).add(evo.to_pokemon_id)
|
||||
adj.setdefault(evo.to_pokemon_id, set()).add(evo.from_pokemon_id)
|
||||
|
||||
# BFS from pokemon_id to find family members
|
||||
family: set[int] = set()
|
||||
queue = deque([pokemon_id])
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
if current in family:
|
||||
continue
|
||||
family.add(current)
|
||||
for neighbor in adj.get(current, set()):
|
||||
if neighbor not in family:
|
||||
queue.append(neighbor)
|
||||
|
||||
# Filter evolutions to only those in the family
|
||||
family_evo_ids = [
|
||||
evo.id for evo in evolutions
|
||||
if evo.from_pokemon_id in family and evo.to_pokemon_id in family
|
||||
]
|
||||
|
||||
if not family_evo_ids:
|
||||
return []
|
||||
|
||||
# Reload with eager-loaded relationships
|
||||
result = await session.execute(
|
||||
select(Evolution)
|
||||
.where(Evolution.id.in_(family_evo_ids))
|
||||
.options(
|
||||
joinedload(Evolution.from_pokemon),
|
||||
joinedload(Evolution.to_pokemon),
|
||||
)
|
||||
.order_by(Evolution.from_pokemon_id, Evolution.to_pokemon_id)
|
||||
)
|
||||
return result.scalars().unique().all()
|
||||
|
||||
|
||||
@router.get("/pokemon/{pokemon_id}/evolutions", response_model=list[EvolutionResponse])
|
||||
async def get_pokemon_evolutions(
|
||||
pokemon_id: int,
|
||||
|
||||
@@ -50,6 +50,21 @@ class RouteEncounterDetailResponse(RouteEncounterResponse):
|
||||
pokemon: PokemonResponse
|
||||
|
||||
|
||||
class PokemonEncounterLocationItem(CamelModel):
|
||||
route_id: int
|
||||
route_name: str
|
||||
encounter_method: str
|
||||
encounter_rate: int
|
||||
min_level: int
|
||||
max_level: int
|
||||
|
||||
|
||||
class PokemonEncounterLocationResponse(CamelModel):
|
||||
game_id: int
|
||||
game_name: str
|
||||
encounters: list[PokemonEncounterLocationItem]
|
||||
|
||||
|
||||
# --- Admin schemas ---
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user