feat(filtros): adicionar filtro multi-select por selos no ranking

- Backend: extrair selos de detalhes e filtrar por eles
- API: endpoint /ranking/selos e parâmetro selos em /ranking/paginado
- Frontend: componente FiltroSelos com dropdown e seleção múltipla
- Selos disponíveis: funções, premiações, orientações
This commit is contained in:
Frederico Castro
2025-12-15 12:32:24 -03:00
parent d215e9ac76
commit c294d4cc77
6 changed files with 527 additions and 11 deletions

View File

@@ -3,7 +3,82 @@ from __future__ import annotations
import asyncio
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Set, Tuple
SELOS_DISPONIVEIS = [
"PRESID_CAMARA",
"COORD_PPG",
"BPQ",
"AUTOR_GP",
"AUTOR_PREMIO",
"AUTOR_MENCAO",
"ORIENT_GP",
"ORIENT_PREMIO",
"ORIENT_MENCAO",
"COORIENT_GP",
"COORIENT_PREMIO",
"COORIENT_MENCAO",
"ORIENT_POS_DOC",
"ORIENT_TESE",
"ORIENT_DISS",
"CO_ORIENT_POS_DOC",
"CO_ORIENT_TESE",
"CO_ORIENT_DISS",
]
def extrair_selos_entry(detalhes: Dict[str, Any]) -> Set[str]:
selos = set()
for c in detalhes.get("coordenacoes_capes", []):
if c.get("presidente"):
selos.add("PRESID_CAMARA")
if detalhes.get("coordenador_ppg"):
selos.add("COORD_PPG")
if detalhes.get("bolsas_cnpq"):
selos.add("BPQ")
for prem in detalhes.get("premiacoes", []):
papel = (prem.get("papel") or "").lower()
codigo = prem.get("codigo", "")
if "GP" in codigo or "grande" in codigo.lower():
tipo_prem = "GP"
elif "MENCAO" in codigo or "menção" in codigo.lower():
tipo_prem = "MENCAO"
else:
tipo_prem = "PREMIO"
if "autor" in papel:
selos.add(f"AUTOR_{tipo_prem}")
elif "orientador" in papel:
selos.add(f"ORIENT_{tipo_prem}")
elif "coorientador" in papel or "co-orientador" in papel:
selos.add(f"COORIENT_{tipo_prem}")
for orient in detalhes.get("orientacoes", []):
codigo = orient.get("codigo", "")
is_coorient = orient.get("coorientacao", False)
if is_coorient:
if "POS_DOC" in codigo:
selos.add("CO_ORIENT_POS_DOC")
elif "TESE" in codigo:
selos.add("CO_ORIENT_TESE")
elif "DISS" in codigo:
selos.add("CO_ORIENT_DISS")
else:
if "POS_DOC" in codigo:
selos.add("ORIENT_POS_DOC")
elif "TESE" in codigo:
selos.add("ORIENT_TESE")
elif "DISS" in codigo:
selos.add("ORIENT_DISS")
return selos
@dataclass(frozen=True)
@@ -51,17 +126,28 @@ class RankingStore:
return sum(1 for e in self._entries if e.ativo == filtro_ativo)
def get_page(
self, page: int, size: int, filtro_ativo: Optional[bool] = None
self,
page: int,
size: int,
filtro_ativo: Optional[bool] = None,
filtro_selos: Optional[List[str]] = None,
) -> Tuple[int, List[RankingEntry]]:
if page < 1:
page = 1
if size < 1:
size = 1
if filtro_ativo is None:
entries = self._entries
else:
entries = [e for e in self._entries if e.ativo == filtro_ativo]
entries = self._entries
if filtro_ativo is not None:
entries = [e for e in entries if e.ativo == filtro_ativo]
if filtro_selos:
selos_set = set(filtro_selos)
entries = [
e for e in entries
if selos_set & extrair_selos_entry(e.detalhes)
]
total = len(entries)
start = (page - 1) * size

View File

@@ -110,11 +110,43 @@ async def health_check():
return {"status": "ok", "message": "API Ranking CAPES funcionando"}
@router.get("/ranking/selos")
async def listar_selos():
from ...infrastructure.ranking_store import SELOS_DISPONIVEIS
selos_info = {
"PRESID_CAMARA": {"label": "Presidente Câmara", "icone": "👑", "grupo": "funcoes"},
"COORD_PPG": {"label": "Coord. PPG", "icone": "🎓", "grupo": "funcoes"},
"BPQ": {"label": "Bolsista PQ", "icone": "🏅", "grupo": "funcoes"},
"AUTOR_GP": {"label": "Autor Grande Prêmio", "icone": "🏆", "grupo": "premiacoes"},
"AUTOR_PREMIO": {"label": "Autor Prêmio", "icone": "🥇", "grupo": "premiacoes"},
"AUTOR_MENCAO": {"label": "Autor Menção", "icone": "🥈", "grupo": "premiacoes"},
"ORIENT_GP": {"label": "Orientador GP", "icone": "🏆", "grupo": "premiacoes"},
"ORIENT_PREMIO": {"label": "Orientador Prêmio", "icone": "🎖️", "grupo": "premiacoes"},
"ORIENT_MENCAO": {"label": "Orientador Menção", "icone": "📜", "grupo": "premiacoes"},
"COORIENT_GP": {"label": "Coorientador GP", "icone": "🏆", "grupo": "premiacoes"},
"COORIENT_PREMIO": {"label": "Coorientador Prêmio", "icone": "🎖️", "grupo": "premiacoes"},
"COORIENT_MENCAO": {"label": "Coorientador Menção", "icone": "📜", "grupo": "premiacoes"},
"ORIENT_POS_DOC": {"label": "Orient. Pós-Doc", "icone": "🔬", "grupo": "orientacoes"},
"ORIENT_TESE": {"label": "Orient. Tese", "icone": "📚", "grupo": "orientacoes"},
"ORIENT_DISS": {"label": "Orient. Dissertação", "icone": "📄", "grupo": "orientacoes"},
"CO_ORIENT_POS_DOC": {"label": "Coorient. Pós-Doc", "icone": "🔬", "grupo": "coorientacoes"},
"CO_ORIENT_TESE": {"label": "Coorient. Tese", "icone": "📚", "grupo": "coorientacoes"},
"CO_ORIENT_DISS": {"label": "Coorient. Dissertação", "icone": "📄", "grupo": "coorientacoes"},
}
return {
"selos": [
{"codigo": s, **selos_info.get(s, {"label": s, "icone": "🏷️", "grupo": "outros"})}
for s in SELOS_DISPONIVEIS
]
}
@router.get("/ranking/paginado", response_model=RankingPaginadoResponseSchema)
async def ranking_paginado(
page: int = Query(default=1, ge=1, description="Número da página"),
size: int = Query(default=50, ge=1, le=1000, description="Tamanho da página (máx 1000)"),
ativo: Optional[bool] = Query(default=None, description="Filtrar por status ativo"),
selos: Optional[str] = Query(default=None, description="Filtrar por selos (separados por vírgula)"),
store = Depends(get_ranking_store),
):
if not store.is_ready():
@@ -123,7 +155,8 @@ async def ranking_paginado(
detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.",
)
total, entries = store.get_page(page=page, size=size, filtro_ativo=ativo)
filtro_selos = [s.strip() for s in selos.split(",") if s.strip()] if selos else None
total, entries = store.get_page(page=page, size=size, filtro_ativo=ativo, filtro_selos=filtro_selos)
total_pages = (total + size - 1) // size