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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user