feat(equipe): implementar montagem de equipe interdisciplinar com PDF

- Adicionar seleção múltipla de consultores entre diferentes buscas
- Criar endpoint POST /api/v1/equipe/pdf para gerar documento da equipe
- Implementar template HTML profissional para PDF da equipe
- Exibir estatísticas: total, ativos, coordenadores, premiados, IES
- Persistir seleção ao trocar termos de busca (equipe interdisciplinar)
- Mostrar temas combinados na barra flutuante e no PDF
- Corrigir renderização de emojis no PDF (substituir por texto)
- Melhorar contraste da cor do ranking no frontend (roxo → ciano)
- Corrigir validação Pydantic para ignorar campos extras do .env
This commit is contained in:
Frederico Castro
2025-12-20 18:30:37 -03:00
parent 81acf1895f
commit bb36961b4c
8 changed files with 1040 additions and 33 deletions

View File

@@ -1,11 +1,23 @@
import asyncio
import html
import unicodedata
from io import BytesIO
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import Optional, List
def normalizar_texto(texto: str) -> str:
if not texto:
return ""
texto = html.unescape(texto)
texto = unicodedata.normalize('NFD', texto)
texto = ''.join(c for c in texto if unicodedata.category(c) != 'Mn')
return texto.lower()
from ...application.use_cases.obter_ranking import ObterRankingUseCase
from ...application.use_cases.obter_consultor import ObterConsultorUseCase
from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
@@ -415,6 +427,61 @@ async def exportar_ficha_pdf(
)
class ConsultorEquipeSchema(BaseModel):
id_pessoa: int
nome: str
ies: Optional[str] = None
areas_avaliacao: List[str] = []
areas_conhecimento: List[str] = []
linhas_pesquisa: List[str] = []
situacao: str = ""
foi_coordenador: bool = False
foi_premiado: bool = False
posicao_ranking: Optional[int] = None
pontuacao_ranking: float = 0
motivos_match: List[str] = []
class GerarEquipePdfRequest(BaseModel):
tema: str
area_avaliacao: Optional[str] = None
consultores: List[ConsultorEquipeSchema]
@router.post("/equipe/pdf")
async def gerar_pdf_equipe(
request: GerarEquipePdfRequest,
):
from ...application.services.pdf_service import PDFService
if not request.consultores:
raise HTTPException(status_code=400, detail="Nenhum consultor selecionado")
try:
pdf_service = PDFService()
consultores_dict = [c.model_dump() for c in request.consultores]
pdf_bytes = pdf_service.gerar_pdf_equipe(
tema=request.tema,
area_avaliacao=request.area_avaliacao,
consultores=consultores_dict
)
except Exception as e:
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Erro ao gerar PDF: {str(e)}")
tema_sanitizado = "".join(c if c.isalnum() or c in " -_" else "_" for c in request.tema)
nome_arquivo = f"equipe_{tema_sanitizado[:30]}_{datetime.now().strftime('%Y%m%d')}.pdf"
return StreamingResponse(
BytesIO(pdf_bytes),
media_type="application/pdf",
headers={
"Content-Disposition": f'attachment; filename="{nome_arquivo}"'
}
)
@router.get("/consultores/sugerir", response_model=SugerirConsultoresResponseSchema)
async def sugerir_consultores(
tema: str = Query(..., min_length=2, description="Tema ou assunto para buscar consultores"),
@@ -429,10 +496,10 @@ async def sugerir_consultores(
tema=tema,
area_avaliacao=area_avaliacao,
apenas_ativos=apenas_ativos,
size=quantidade
size=quantidade * 3
)
consultores = []
consultores_raw = []
for doc in resultados:
id_pessoa = doc.get("id")
nome = doc.get("dadosPessoais", {}).get("nome", "")
@@ -445,6 +512,10 @@ async def sugerir_consultores(
ies = None
foi_coordenador = False
foi_premiado = False
motivos_match = set()
tema_norm = normalizar_texto(tema)
tema_palavras = tema_norm.split()
for atuacao in doc.get("atuacoes", []):
tipo = atuacao.get("tipo", "")
@@ -457,14 +528,26 @@ async def sugerir_consultores(
for area in dados.get("areaConhecimentoPos", []):
if area.get("nome"):
areas_conhecimento.add(area["nome"])
area_nome = html.unescape(area["nome"])
areas_conhecimento.add(area_nome)
area_norm = normalizar_texto(area["nome"])
if tema_norm in area_norm or any(p in area_norm for p in tema_palavras if len(p) > 3):
motivos_match.add(f"Area: {area_nome}")
area_aval = area.get("areaAvaliacao", {})
if area_aval and area_aval.get("nome"):
areas_avaliacao.add(area_aval["nome"])
aval_nome = html.unescape(area_aval["nome"])
areas_avaliacao.add(aval_nome)
aval_norm = normalizar_texto(area_aval["nome"])
if tema_norm in aval_norm or any(p in aval_norm for p in tema_palavras if len(p) > 3):
motivos_match.add(f"Area Avaliacao: {aval_nome}")
for pesq in dados.get("areaPesquisa", []):
if pesq.get("descricao"):
linhas_pesquisa.add(pesq["descricao"])
pesq_desc = html.unescape(pesq["descricao"])
linhas_pesquisa.add(pesq_desc)
pesq_norm = normalizar_texto(pesq["descricao"])
if tema_norm in pesq_norm or any(p in pesq_norm for p in tema_palavras if len(p) > 3):
motivos_match.add(f"Pesquisa: {pesq_desc[:50]}...")
elif "Coordenação" in tipo:
foi_coordenador = True
@@ -473,23 +556,79 @@ async def sugerir_consultores(
foi_premiado = True
posicao_ranking = None
pontuacao_ranking = 0
if store.is_ready():
entry = store.get_by_id(id_pessoa)
if entry:
posicao_ranking = entry.posicao
pontuacao_ranking = entry.pontuacao_total
consultores.append(SugestaoConsultorSchema(
id_pessoa=id_pessoa,
nome=nome,
score_match=score_match,
areas_avaliacao=list(areas_avaliacao),
areas_conhecimento=list(areas_conhecimento),
linhas_pesquisa=list(linhas_pesquisa),
situacao=situacao,
ies=ies,
foi_coordenador=foi_coordenador,
foi_premiado=foi_premiado,
))
score_final = score_match
if posicao_ranking:
bonus_ranking = max(0, (10000 - posicao_ranking) / 100)
score_final += bonus_ranking
if foi_coordenador:
score_final += 20
motivos_match.add("Foi Coordenador de Area")
if foi_premiado:
score_final += 10
motivos_match.add("Foi Premiado")
consultores_raw.append({
"id_pessoa": id_pessoa,
"nome": nome,
"score_match": score_match,
"score_final": score_final,
"posicao_ranking": posicao_ranking,
"pontuacao_ranking": pontuacao_ranking,
"areas_avaliacao": list(areas_avaliacao),
"areas_conhecimento": list(areas_conhecimento),
"linhas_pesquisa": list(linhas_pesquisa),
"situacao": situacao,
"ies": ies,
"foi_coordenador": foi_coordenador,
"foi_premiado": foi_premiado,
"motivos_match": list(motivos_match)[:5],
})
consultores_raw.sort(key=lambda x: x["score_final"], reverse=True)
ies_selecionadas = set()
consultores_finais = []
for c in consultores_raw:
if len(consultores_finais) >= quantidade:
break
if c["ies"] and c["ies"] in ies_selecionadas and len(consultores_finais) < quantidade // 2:
continue
consultores_finais.append(c)
if c["ies"]:
ies_selecionadas.add(c["ies"])
while len(consultores_finais) < quantidade and len(consultores_finais) < len(consultores_raw):
for c in consultores_raw:
if c not in consultores_finais:
consultores_finais.append(c)
if len(consultores_finais) >= quantidade:
break
consultores = [
SugestaoConsultorSchema(
id_pessoa=c["id_pessoa"],
nome=c["nome"],
score_match=c["score_final"],
areas_avaliacao=c["areas_avaliacao"],
areas_conhecimento=c["areas_conhecimento"],
linhas_pesquisa=c["linhas_pesquisa"],
situacao=c["situacao"],
ies=c["ies"],
foi_coordenador=c["foi_coordenador"],
foi_premiado=c["foi_premiado"],
posicao_ranking=c["posicao_ranking"],
pontuacao_ranking=c["pontuacao_ranking"],
motivos_match=c["motivos_match"],
)
for c in consultores_finais
]
return SugerirConsultoresResponseSchema(
tema_buscado=tema,