diff --git a/backend/src/application/services/pdf_service.py b/backend/src/application/services/pdf_service.py
index 176670b..97e7b4f 100644
--- a/backend/src/application/services/pdf_service.py
+++ b/backend/src/application/services/pdf_service.py
@@ -165,6 +165,49 @@ class PDFService:
return result
+ def gerar_pdf_equipe(self, tema: str, area_avaliacao: str, consultores: list) -> bytes:
+ from weasyprint import HTML, CSS
+
+ template = self.env.get_template("equipe_consultores.html")
+
+ ies_set = set()
+ areas_set = set()
+ for c in consultores:
+ if c.get('ies'):
+ ies_set.add(c['ies'])
+ for area in c.get('areas_avaliacao', []):
+ areas_set.add(area)
+
+ estatisticas = {
+ 'total': len(consultores),
+ 'ativos': sum(1 for c in consultores if c.get('situacao', '').lower().find('atividade') >= 0),
+ 'coordenadores': sum(1 for c in consultores if c.get('foi_coordenador')),
+ 'premiados': sum(1 for c in consultores if c.get('foi_premiado')),
+ 'ies_distintas': len(ies_set),
+ 'areas_cobertas': len(areas_set),
+ 'lista_ies': sorted(list(ies_set)),
+ 'lista_areas': sorted(list(areas_set)),
+ }
+
+ html_content = template.render(
+ tema=tema,
+ area_avaliacao=area_avaliacao,
+ consultores=consultores,
+ estatisticas=estatisticas,
+ data_geracao=datetime.now().strftime("%d/%m/%Y às %H:%M"),
+ )
+
+ css_path = self.template_dir / "styles.css"
+
+ pdf_bytes = HTML(
+ string=html_content,
+ base_url=str(self.template_dir)
+ ).write_pdf(
+ stylesheets=[CSS(filename=str(css_path))]
+ )
+
+ return pdf_bytes
+
class ConsultorWrapper:
def __init__(self, data: Dict):
diff --git a/backend/src/infrastructure/pdf/templates/equipe_consultores.html b/backend/src/infrastructure/pdf/templates/equipe_consultores.html
new file mode 100644
index 0000000..076ec6b
--- /dev/null
+++ b/backend/src/infrastructure/pdf/templates/equipe_consultores.html
@@ -0,0 +1,184 @@
+
+
+
+
+ Equipe de Consultores - {{ tema }}
+
+
+
+
+
+
+
+
+
Equipe de Consultores
+
Sistema de Ranking AtuaCAPES · Seleção Inteligente
+
+
{{ tema }}
+ {% if area_avaliacao %}
+
+ {{ area_avaliacao }}
+
+ {% endif %}
+
+
+
+
+
+
+ {{ estatisticas.total }}
+ Consultores
+
+
+ {{ estatisticas.ativos }}
+ Ativos
+
+
+ {{ estatisticas.coordenadores }}
+ Coordenadores
+
+
+ {{ estatisticas.premiados }}
+ Premiados
+
+
+
+
+ {{ estatisticas.ies_distintas }}
+ IES Distintas
+
+
+ {{ estatisticas.areas_cobertas }}
+ Áreas Cobertas
+
+
+
+
+
+
+
+
+
+ Resumo da Equipe
+
+
+
+
Instituições Representadas
+
+ {% for ies in estatisticas.lista_ies %}
+ {{ ies }}
+ {% endfor %}
+
+
+
+
+
Áreas de Avaliação
+
+ {% for area in estatisticas.lista_areas %}
+ {{ area }}
+ {% endfor %}
+
+
+
+
+
+
+ Membros da Equipe
+
+ {% for consultor in consultores %}
+
+
+
+
+ {% if consultor.posicao_ranking %}
+
+ Ranking:
+ #{{ consultor.posicao_ranking }}
+
+ {% endif %}
+ {% if consultor.pontuacao_ranking %}
+
+ Pontos:
+ {{ consultor.pontuacao_ranking|int }}
+
+ {% endif %}
+
+
+ {% if consultor.motivos_match %}
+
+
Por que este consultor:
+
+ {% for motivo in consultor.motivos_match %}
+ {{ motivo }}
+ {% endfor %}
+
+
+ {% endif %}
+
+ {% if consultor.areas_avaliacao %}
+
+ Áreas de Avaliação:
+ {{ consultor.areas_avaliacao | join(', ') }}
+
+ {% endif %}
+
+ {% if consultor.linhas_pesquisa %}
+
+
Linhas de Pesquisa:
+
+ {% for linha in consultor.linhas_pesquisa[:5] %}
+ {{ linha }}
+ {% endfor %}
+ {% if consultor.linhas_pesquisa|length > 5 %}
+ ... e mais {{ consultor.linhas_pesquisa|length - 5 }} linhas
+ {% endif %}
+
+
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+
+
diff --git a/backend/src/infrastructure/pdf/templates/styles.css b/backend/src/infrastructure/pdf/templates/styles.css
index ef60fab..1440cb9 100644
--- a/backend/src/infrastructure/pdf/templates/styles.css
+++ b/backend/src/infrastructure/pdf/templates/styles.css
@@ -305,3 +305,206 @@ h1, h2, h3, h4 {
.center {
text-align: center;
}
+
+.cover-stats {
+ margin: 24px 0;
+}
+
+.stats-table {
+ width: 100%;
+ border-collapse: collapse;
+ background: white;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+}
+
+.stat-cell {
+ padding: 16px 12px;
+ text-align: center;
+ border: 1px solid var(--border);
+}
+
+.stat-value {
+ font-size: 24pt;
+ font-weight: 700;
+ color: var(--accent);
+}
+
+.stat-label {
+ font-size: 9pt;
+ color: var(--muted);
+ margin-top: 4px;
+}
+
+.summary-section {
+ page-break-before: always;
+ padding-top: 20px;
+}
+
+.summary-section h2 {
+ color: var(--accent);
+ font-size: 14pt;
+ margin-bottom: 16px;
+ padding-bottom: 8px;
+ border-bottom: 2px solid var(--accent);
+}
+
+.summary-grid {
+ display: flex;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.summary-box {
+ flex: 1;
+ min-width: 200px;
+ background: var(--bg-soft);
+ border: 1px solid var(--border);
+ padding: 16px;
+ border-radius: 8px;
+}
+
+.summary-box h4 {
+ font-size: 10pt;
+ color: var(--muted);
+ margin-bottom: 12px;
+}
+
+.team-section {
+ margin-top: 24px;
+}
+
+.team-section h2 {
+ color: var(--accent);
+ font-size: 14pt;
+ margin-bottom: 16px;
+ padding-bottom: 8px;
+ border-bottom: 2px solid var(--accent);
+}
+
+.team-member {
+ background: var(--bg-soft);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 16px;
+ margin-bottom: 16px;
+ page-break-inside: avoid;
+}
+
+.member-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
+.member-rank {
+ background: var(--accent);
+ color: white;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ font-size: 11pt;
+}
+
+.member-info {
+ flex: 1;
+}
+
+.member-name {
+ font-size: 12pt;
+ font-weight: 700;
+ color: var(--ink);
+}
+
+.member-ies {
+ font-size: 9pt;
+ color: var(--muted);
+}
+
+.member-badges {
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+
+.member-stats {
+ display: flex;
+ gap: 16px;
+ margin-bottom: 10px;
+}
+
+.stat-inline {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 9pt;
+ color: var(--muted);
+ background: var(--bg-soft);
+ padding: 4px 8px;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+}
+
+.stat-label {
+ font-weight: 600;
+ color: var(--muted);
+}
+
+.stat-value-inline {
+ font-weight: 700;
+ color: var(--accent);
+}
+
+.member-motivos {
+ background: white;
+ border: 1px solid var(--border);
+ padding: 10px 12px;
+ border-radius: 6px;
+ margin-bottom: 10px;
+ font-size: 9pt;
+}
+
+.member-areas {
+ font-size: 9pt;
+ margin-bottom: 8px;
+}
+
+.member-pesquisa {
+ font-size: 9pt;
+}
+
+.member-pesquisa ul {
+ margin-left: 16px;
+ margin-top: 4px;
+}
+
+.member-pesquisa li {
+ margin-bottom: 2px;
+}
+
+.footer-section {
+ margin-top: 32px;
+ padding-top: 16px;
+ border-top: 1px solid var(--border);
+}
+
+.disclaimer {
+ font-size: 8pt;
+ color: var(--muted);
+ font-style: italic;
+}
+
+.generation-info {
+ font-size: 8pt;
+ color: var(--muted);
+ margin-top: 8px;
+}
+
+.muted {
+ color: var(--muted);
+}
diff --git a/backend/src/interface/api/config.py b/backend/src/interface/api/config.py
index f1ee067..60e0c55 100644
--- a/backend/src/interface/api/config.py
+++ b/backend/src/interface/api/config.py
@@ -3,7 +3,7 @@ from typing import List
class Settings(BaseSettings):
- model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
+ model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
ES_URL: str = "http://localhost:9200"
ES_INDEX: str = "atuacapes"
diff --git a/backend/src/interface/api/routes.py b/backend/src/interface/api/routes.py
index 980e23a..332c8c2 100644
--- a/backend/src/interface/api/routes.py
+++ b/backend/src/interface/api/routes.py
@@ -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,
diff --git a/backend/src/interface/schemas/ranking_schema.py b/backend/src/interface/schemas/ranking_schema.py
index fbc3b4d..32689da 100644
--- a/backend/src/interface/schemas/ranking_schema.py
+++ b/backend/src/interface/schemas/ranking_schema.py
@@ -104,6 +104,9 @@ class SugestaoConsultorSchema(BaseModel):
ies: Optional[str] = None
foi_coordenador: bool = False
foi_premiado: bool = False
+ posicao_ranking: Optional[int] = None
+ pontuacao_ranking: float = 0
+ motivos_match: List[str] = []
class SugerirConsultoresRequestSchema(BaseModel):
diff --git a/frontend/src/components/SugerirConsultores.css b/frontend/src/components/SugerirConsultores.css
index 922ede4..145e98e 100644
--- a/frontend/src/components/SugerirConsultores.css
+++ b/frontend/src/components/SugerirConsultores.css
@@ -32,6 +32,10 @@
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(96, 165, 250, 0.05));
}
+.sugerir-header-content {
+ flex: 1;
+}
+
.sugerir-header h2 {
margin: 0;
font-size: 1.25rem;
@@ -39,6 +43,13 @@
color: var(--accent-2);
}
+.sugerir-subtitle {
+ margin: 0.25rem 0 0 0;
+ font-size: 0.75rem;
+ color: var(--muted);
+ font-weight: 400;
+}
+
.sugerir-close {
background: none;
border: none;
@@ -173,13 +184,25 @@
flex-direction: column;
}
-.sugerir-resultados h3 {
+.sugerir-resultados-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
padding: 1rem 1.5rem;
+ border-bottom: 1px solid var(--stroke);
+}
+
+.sugerir-resultados h3 {
margin: 0;
font-size: 0.95rem;
font-weight: 600;
color: var(--silver);
- border-bottom: 1px solid var(--stroke);
+}
+
+.sugerir-hint {
+ font-size: 0.75rem;
+ color: var(--muted);
+ font-style: italic;
}
.sugerir-lista {
@@ -192,16 +215,56 @@
padding: 1rem;
margin: 0.5rem;
background: rgba(255, 255, 255, 0.02);
- border: 1px solid var(--stroke);
+ border: 2px solid var(--stroke);
border-radius: 10px;
cursor: pointer;
- transition: background 0.2s, border-color 0.2s, transform 0.2s;
+ transition: background 0.2s, border-color 0.2s, transform 0.2s, box-shadow 0.2s;
}
.sugerir-item:hover {
background: rgba(99, 102, 241, 0.1);
border-color: rgba(99, 102, 241, 0.3);
- transform: translateX(4px);
+}
+
+.sugerir-item.selecionado {
+ background: rgba(16, 185, 129, 0.1);
+ border-color: rgba(16, 185, 129, 0.5);
+ box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.3);
+}
+
+.sugerir-item.selecionado:hover {
+ background: rgba(16, 185, 129, 0.15);
+}
+
+.sugerir-checkbox-item {
+ display: flex;
+ align-items: center;
+ margin-right: 0.5rem;
+}
+
+.sugerir-checkbox-item input[type="checkbox"] {
+ width: 1.2rem;
+ height: 1.2rem;
+ accent-color: var(--accent);
+ cursor: pointer;
+}
+
+.btn-ver-ranking {
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid var(--stroke);
+ color: var(--silver);
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.9rem;
+ transition: all 0.2s;
+ margin-left: auto;
+}
+
+.btn-ver-ranking:hover {
+ background: var(--accent);
+ color: white;
+ border-color: var(--accent);
}
.sugerir-item-header {
@@ -262,6 +325,72 @@
border: 1px solid rgba(107, 114, 128, 0.4);
}
+.sugerir-item-stats {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.stat-ranking {
+ display: flex;
+ align-items: center;
+ gap: 0.3rem;
+ font-size: 0.8rem;
+ color: #22d3ee;
+ font-weight: 600;
+ background: rgba(34, 211, 238, 0.15);
+ padding: 0.25rem 0.6rem;
+ border-radius: 6px;
+ border: 1px solid rgba(34, 211, 238, 0.3);
+}
+
+.stat-icon {
+ font-size: 0.9rem;
+}
+
+.stat-pontuacao {
+ font-size: 0.75rem;
+ color: #fcd34d;
+ font-weight: 600;
+ background: rgba(234, 179, 8, 0.15);
+ padding: 0.2rem 0.5rem;
+ border-radius: 4px;
+ border: 1px solid rgba(234, 179, 8, 0.3);
+}
+
+.sugerir-motivos {
+ background: rgba(16, 185, 129, 0.05);
+ border: 1px solid rgba(16, 185, 129, 0.15);
+ border-radius: 8px;
+ padding: 0.6rem 0.8rem;
+ margin-bottom: 0.5rem;
+}
+
+.motivos-label {
+ font-size: 0.7rem;
+ color: var(--muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ display: block;
+ margin-bottom: 0.4rem;
+}
+
+.motivos-lista {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem;
+}
+
+.tag.motivo {
+ background: rgba(16, 185, 129, 0.15);
+ color: #6ee7b7;
+ border: 1px solid rgba(16, 185, 129, 0.3);
+ font-size: 0.7rem;
+ padding: 0.2rem 0.5rem;
+}
+
.sugerir-item-details {
display: flex;
align-items: center;
@@ -271,8 +400,11 @@
.sugerir-ies {
font-size: 0.8rem;
- color: var(--muted);
+ color: var(--silver);
font-weight: 500;
+ background: rgba(255, 255, 255, 0.05);
+ padding: 0.2rem 0.5rem;
+ border-radius: 4px;
}
.sugerir-areas {
@@ -316,6 +448,126 @@
white-space: nowrap;
}
+.equipe-flutuante {
+ position: sticky;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.95), rgba(5, 150, 105, 0.95));
+ backdrop-filter: blur(8px);
+ padding: 1rem 1.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ border-top: 1px solid rgba(255, 255, 255, 0.2);
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
+}
+
+.equipe-info {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.equipe-count {
+ font-size: 1rem;
+ font-weight: 700;
+ color: white;
+}
+
+.equipe-nomes {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.equipe-nome {
+ font-size: 0.8rem;
+ color: rgba(255, 255, 255, 0.8);
+ background: rgba(255, 255, 255, 0.15);
+ padding: 0.15rem 0.5rem;
+ border-radius: 4px;
+}
+
+.equipe-acoes {
+ display: flex;
+ gap: 0.75rem;
+}
+
+.btn-limpar {
+ background: rgba(255, 255, 255, 0.15);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ color: white;
+ padding: 0.6rem 1rem;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.9rem;
+ font-weight: 500;
+ transition: all 0.2s;
+}
+
+.btn-limpar:hover {
+ background: rgba(255, 255, 255, 0.25);
+}
+
+.btn-gerar-pdf {
+ background: white;
+ border: none;
+ color: #059669;
+ padding: 0.6rem 1.25rem;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.95rem;
+ font-weight: 700;
+ transition: all 0.2s;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+
+.btn-gerar-pdf:hover:not(:disabled) {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+}
+
+.btn-gerar-pdf:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+}
+
+.equipe-header-row {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.equipe-interdisciplinar {
+ font-size: 0.7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ background: rgba(255, 255, 255, 0.25);
+ color: white;
+ padding: 0.2rem 0.6rem;
+ border-radius: 4px;
+ border: 1px solid rgba(255, 255, 255, 0.4);
+}
+
+.equipe-temas {
+ display: flex;
+ gap: 0.4rem;
+ flex-wrap: wrap;
+ margin-top: 0.25rem;
+}
+
+.equipe-tema {
+ font-size: 0.75rem;
+ color: rgba(255, 255, 255, 0.9);
+ background: rgba(0, 0, 0, 0.2);
+ padding: 0.15rem 0.5rem;
+ border-radius: 4px;
+ font-style: italic;
+}
+
@media (max-width: 600px) {
.sugerir-modal {
max-height: 95vh;
@@ -334,4 +586,18 @@
width: 100%;
margin-top: 0.5rem;
}
+
+ .equipe-flutuante {
+ flex-direction: column;
+ padding: 1rem;
+ }
+
+ .equipe-acoes {
+ width: 100%;
+ justify-content: stretch;
+ }
+
+ .btn-gerar-pdf {
+ flex: 1;
+ }
}
diff --git a/frontend/src/components/SugerirConsultores.jsx b/frontend/src/components/SugerirConsultores.jsx
index 7e0b31f..02181ec 100644
--- a/frontend/src/components/SugerirConsultores.jsx
+++ b/frontend/src/components/SugerirConsultores.jsx
@@ -13,6 +13,8 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
const [loadingAreas, setLoadingAreas] = useState(true);
const [error, setError] = useState(null);
const [buscaRealizada, setBuscaRealizada] = useState(false);
+ const [selecionados, setSelecionados] = useState([]);
+ const [gerandoPdf, setGerandoPdf] = useState(false);
useEffect(() => {
const carregarAreas = async () => {
@@ -51,6 +53,17 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
}
};
+ const toggleSelecionado = (consultor, e) => {
+ e.stopPropagation();
+ setSelecionados((prev) => {
+ const existe = prev.find((c) => c.id_pessoa === consultor.id_pessoa);
+ if (existe) {
+ return prev.filter((c) => c.id_pessoa !== consultor.id_pessoa);
+ }
+ return [...prev, { ...consultor, _busca_tema: tema, _busca_area: areaAvaliacao }];
+ });
+ };
+
const handleClickConsultor = (consultor) => {
if (onSelectConsultor) {
onSelectConsultor(consultor.id_pessoa);
@@ -58,17 +71,81 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
onClose();
};
+ const handleGerarPdf = async () => {
+ if (selecionados.length === 0) return;
+
+ try {
+ setGerandoPdf(true);
+ setError(null);
+
+ const temasUnicos = [...new Set(selecionados.map((c) => c._busca_tema).filter(Boolean))];
+ const areasUnicas = [...new Set(selecionados.map((c) => c._busca_area).filter(Boolean))];
+ const temasCombinados = temasUnicos.length > 0 ? temasUnicos.join(' + ') : 'Equipe Selecionada';
+ const areasCombinadas = areasUnicas.length > 0 ? areasUnicas.join(', ') : null;
+
+ const response = await fetch('/api/v1/equipe/pdf', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ tema: temasCombinados,
+ area_avaliacao: areasCombinadas,
+ consultores: selecionados.map((c) => ({
+ id_pessoa: c.id_pessoa,
+ nome: c.nome,
+ ies: c.ies,
+ areas_avaliacao: c.areas_avaliacao,
+ areas_conhecimento: c.areas_conhecimento,
+ linhas_pesquisa: c.linhas_pesquisa,
+ situacao: c.situacao,
+ 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,
+ })),
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error('Erro ao gerar PDF');
+ }
+
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ const nomeArquivo = temasUnicos.length > 1
+ ? `equipe_interdisciplinar_${new Date().toISOString().slice(0, 10)}.pdf`
+ : `equipe_${temasCombinados.replace(/\s+/g, '_')}_${new Date().toISOString().slice(0, 10)}.pdf`;
+ a.download = nomeArquivo;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ a.remove();
+ } catch (err) {
+ console.error('Erro ao gerar PDF:', err);
+ setError('Erro ao gerar PDF da equipe. Tente novamente.');
+ } finally {
+ setGerandoPdf(false);
+ }
+ };
+
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
onClose();
}
};
+ const isSelecionado = (id) => selecionados.some((c) => c.id_pessoa === id);
+
return (
e.stopPropagation()}>
-
Sugerir Consultores por Tema
+
+
Montar Equipe de Consultores
+
Selecione consultores para gerar documento da equipe
+
×
@@ -140,21 +217,33 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
{buscaRealizada && (
-
- {consultores.length > 0
- ? `${consultores.length} consultores encontrados`
- : 'Nenhum consultor encontrado'}
-
+
+
+ {consultores.length > 0
+ ? `${consultores.length} consultores para "${tema}"`
+ : 'Nenhum consultor encontrado'}
+
+ {consultores.length > 0 && (
+ Selecione os consultores para a equipe
+ )}
+
{consultores.length > 0 && (
{consultores.map((c, index) => (
handleClickConsultor(c)}
+ className={`sugerir-item ${isSelecionado(c.id_pessoa) ? 'selecionado' : ''}`}
+ onClick={(e) => toggleSelecionado(c, e)}
>
+
e.stopPropagation()}>
+ toggleSelecionado(c, e)}
+ />
+
#{index + 1}
{c.nome}
@@ -166,9 +255,45 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
Inativo
)}
+
{
+ e.stopPropagation();
+ handleClickConsultor(c);
+ }}
+ title="Ver no ranking"
+ >
+ →
+
-
+
+
+ {c.posicao_ranking && (
+
+ 📊
+ #{c.posicao_ranking.toLocaleString()} no ranking
+
+ )}
+ {c.pontuacao_ranking > 0 && (
+
+ {c.pontuacao_ranking.toFixed(0)} pts
+
+ )}
{c.ies && {c.ies} }
+
+
+ {c.motivos_match && c.motivos_match.length > 0 && (
+
+
Por que este consultor:
+
+ {c.motivos_match.map((motivo, idx) => (
+ {motivo}
+ ))}
+
+
+ )}
+
+
{c.areas_avaliacao.length > 0 && (
{c.areas_avaliacao.slice(0, 3).map((area) => (
@@ -195,6 +320,50 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
)}
)}
+
+ {selecionados.length > 0 && (
+
+
+
+ {selecionados.length} selecionado{selecionados.length > 1 ? 's' : ''}
+ {(() => {
+ const temas = [...new Set(selecionados.map((c) => c._busca_tema).filter(Boolean))];
+ return temas.length > 1 && (
+ Interdisciplinar
+ );
+ })()}
+
+
+ {(() => {
+ const temas = [...new Set(selecionados.map((c) => c._busca_tema).filter(Boolean))];
+ return temas.map((t) => (
+ {t}
+ ));
+ })()}
+
+
+ {selecionados.slice(0, 5).map((c) => (
+ {c.nome.split(' ')[0]}
+ ))}
+ {selecionados.length > 5 && (
+ +{selecionados.length - 5}
+ )}
+
+
+
+ setSelecionados([])}>
+ Limpar
+
+
+ {gerandoPdf ? 'Gerando...' : 'Gerar PDF da Equipe'}
+
+
+
+ )}
);