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:
@@ -165,6 +165,49 @@ class PDFService:
|
|||||||
|
|
||||||
return result
|
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:
|
class ConsultorWrapper:
|
||||||
def __init__(self, data: Dict):
|
def __init__(self, data: Dict):
|
||||||
|
|||||||
184
backend/src/infrastructure/pdf/templates/equipe_consultores.html
Normal file
184
backend/src/infrastructure/pdf/templates/equipe_consultores.html
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Equipe de Consultores - {{ tema }}</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="documento">
|
||||||
|
<section class="cover">
|
||||||
|
<div>
|
||||||
|
<table class="cover-header">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>MINISTÉRIO DA EDUCAÇÃO</strong><br>
|
||||||
|
Coordenação de Aperfeiçoamento de Pessoal de Nível Superior
|
||||||
|
</td>
|
||||||
|
<td class="right small">Sugestão de Equipe</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="cover-title">Equipe de Consultores</div>
|
||||||
|
<div class="cover-subtitle">Sistema de Ranking AtuaCAPES · Seleção Inteligente</div>
|
||||||
|
|
||||||
|
<div class="cover-name">{{ tema }}</div>
|
||||||
|
{% if area_avaliacao %}
|
||||||
|
<div class="tag-list">
|
||||||
|
<span class="badge">{{ area_avaliacao }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cover-stats">
|
||||||
|
<table class="stats-table">
|
||||||
|
<tr>
|
||||||
|
<td class="stat-cell">
|
||||||
|
<div class="stat-value">{{ estatisticas.total }}</div>
|
||||||
|
<div class="stat-label">Consultores</div>
|
||||||
|
</td>
|
||||||
|
<td class="stat-cell">
|
||||||
|
<div class="stat-value">{{ estatisticas.ativos }}</div>
|
||||||
|
<div class="stat-label">Ativos</div>
|
||||||
|
</td>
|
||||||
|
<td class="stat-cell">
|
||||||
|
<div class="stat-value">{{ estatisticas.coordenadores }}</div>
|
||||||
|
<div class="stat-label">Coordenadores</div>
|
||||||
|
</td>
|
||||||
|
<td class="stat-cell">
|
||||||
|
<div class="stat-value">{{ estatisticas.premiados }}</div>
|
||||||
|
<div class="stat-label">Premiados</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="stat-cell" colspan="2">
|
||||||
|
<div class="stat-value">{{ estatisticas.ies_distintas }}</div>
|
||||||
|
<div class="stat-label">IES Distintas</div>
|
||||||
|
</td>
|
||||||
|
<td class="stat-cell" colspan="2">
|
||||||
|
<div class="stat-value">{{ estatisticas.areas_cobertas }}</div>
|
||||||
|
<div class="stat-label">Áreas Cobertas</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cover-footer">
|
||||||
|
<p>Documento gerado em {{ data_geracao }}</p>
|
||||||
|
<p class="small">Ranking de Consultores CAPES · Geração Automática</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="summary-section">
|
||||||
|
<h2>Resumo da Equipe</h2>
|
||||||
|
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div class="summary-box">
|
||||||
|
<h4>Instituições Representadas</h4>
|
||||||
|
<div class="tag-list">
|
||||||
|
{% for ies in estatisticas.lista_ies %}
|
||||||
|
<span class="badge">{{ ies }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-box">
|
||||||
|
<h4>Áreas de Avaliação</h4>
|
||||||
|
<div class="tag-list">
|
||||||
|
{% for area in estatisticas.lista_areas %}
|
||||||
|
<span class="badge info">{{ area }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="team-section">
|
||||||
|
<h2>Membros da Equipe</h2>
|
||||||
|
|
||||||
|
{% for consultor in consultores %}
|
||||||
|
<div class="team-member">
|
||||||
|
<div class="member-header">
|
||||||
|
<div class="member-rank">#{{ loop.index }}</div>
|
||||||
|
<div class="member-info">
|
||||||
|
<div class="member-name">{{ consultor.nome }}</div>
|
||||||
|
<div class="member-ies">{{ consultor.ies or '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="member-badges">
|
||||||
|
{% if consultor.foi_coordenador %}
|
||||||
|
<span class="badge success">Coordenador</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if consultor.foi_premiado %}
|
||||||
|
<span class="badge warning">Premiado</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if consultor.situacao and 'atividade' in consultor.situacao.lower() %}
|
||||||
|
<span class="badge">Ativo</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge muted">Histórico</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="member-stats">
|
||||||
|
{% if consultor.posicao_ranking %}
|
||||||
|
<div class="stat-inline">
|
||||||
|
<span class="stat-label">Ranking:</span>
|
||||||
|
<span class="stat-value-inline">#{{ consultor.posicao_ranking }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if consultor.pontuacao_ranking %}
|
||||||
|
<div class="stat-inline">
|
||||||
|
<span class="stat-label">Pontos:</span>
|
||||||
|
<span class="stat-value-inline">{{ consultor.pontuacao_ranking|int }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if consultor.motivos_match %}
|
||||||
|
<div class="member-motivos">
|
||||||
|
<strong>Por que este consultor:</strong>
|
||||||
|
<div class="tag-list">
|
||||||
|
{% for motivo in consultor.motivos_match %}
|
||||||
|
<span class="badge info small">{{ motivo }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if consultor.areas_avaliacao %}
|
||||||
|
<div class="member-areas">
|
||||||
|
<strong>Áreas de Avaliação:</strong>
|
||||||
|
<span>{{ consultor.areas_avaliacao | join(', ') }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if consultor.linhas_pesquisa %}
|
||||||
|
<div class="member-pesquisa">
|
||||||
|
<strong>Linhas de Pesquisa:</strong>
|
||||||
|
<ul>
|
||||||
|
{% for linha in consultor.linhas_pesquisa[:5] %}
|
||||||
|
<li>{{ linha }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if consultor.linhas_pesquisa|length > 5 %}
|
||||||
|
<li class="muted">... e mais {{ consultor.linhas_pesquisa|length - 5 }} linhas</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="footer-section">
|
||||||
|
<p class="disclaimer">
|
||||||
|
Este documento foi gerado automaticamente pelo Sistema de Ranking de Consultores CAPES.
|
||||||
|
As sugestões são baseadas em análise de relevância temática, posição no ranking geral e diversidade institucional.
|
||||||
|
</p>
|
||||||
|
<p class="generation-info">
|
||||||
|
Gerado em {{ data_geracao }} · Tema: "{{ tema }}"
|
||||||
|
{% if area_avaliacao %} · Área: {{ area_avaliacao }}{% endif %}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -305,3 +305,206 @@ h1, h2, h3, h4 {
|
|||||||
.center {
|
.center {
|
||||||
text-align: 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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from typing import List
|
|||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
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_URL: str = "http://localhost:9200"
|
||||||
ES_INDEX: str = "atuacapes"
|
ES_INDEX: str = "atuacapes"
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import html
|
||||||
|
import unicodedata
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
from typing import Optional, List
|
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_ranking import ObterRankingUseCase
|
||||||
from ...application.use_cases.obter_consultor import ObterConsultorUseCase
|
from ...application.use_cases.obter_consultor import ObterConsultorUseCase
|
||||||
from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
|
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)
|
@router.get("/consultores/sugerir", response_model=SugerirConsultoresResponseSchema)
|
||||||
async def sugerir_consultores(
|
async def sugerir_consultores(
|
||||||
tema: str = Query(..., min_length=2, description="Tema ou assunto para buscar consultores"),
|
tema: str = Query(..., min_length=2, description="Tema ou assunto para buscar consultores"),
|
||||||
@@ -429,10 +496,10 @@ async def sugerir_consultores(
|
|||||||
tema=tema,
|
tema=tema,
|
||||||
area_avaliacao=area_avaliacao,
|
area_avaliacao=area_avaliacao,
|
||||||
apenas_ativos=apenas_ativos,
|
apenas_ativos=apenas_ativos,
|
||||||
size=quantidade
|
size=quantidade * 3
|
||||||
)
|
)
|
||||||
|
|
||||||
consultores = []
|
consultores_raw = []
|
||||||
for doc in resultados:
|
for doc in resultados:
|
||||||
id_pessoa = doc.get("id")
|
id_pessoa = doc.get("id")
|
||||||
nome = doc.get("dadosPessoais", {}).get("nome", "")
|
nome = doc.get("dadosPessoais", {}).get("nome", "")
|
||||||
@@ -445,6 +512,10 @@ async def sugerir_consultores(
|
|||||||
ies = None
|
ies = None
|
||||||
foi_coordenador = False
|
foi_coordenador = False
|
||||||
foi_premiado = False
|
foi_premiado = False
|
||||||
|
motivos_match = set()
|
||||||
|
|
||||||
|
tema_norm = normalizar_texto(tema)
|
||||||
|
tema_palavras = tema_norm.split()
|
||||||
|
|
||||||
for atuacao in doc.get("atuacoes", []):
|
for atuacao in doc.get("atuacoes", []):
|
||||||
tipo = atuacao.get("tipo", "")
|
tipo = atuacao.get("tipo", "")
|
||||||
@@ -457,14 +528,26 @@ async def sugerir_consultores(
|
|||||||
|
|
||||||
for area in dados.get("areaConhecimentoPos", []):
|
for area in dados.get("areaConhecimentoPos", []):
|
||||||
if area.get("nome"):
|
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", {})
|
area_aval = area.get("areaAvaliacao", {})
|
||||||
if area_aval and area_aval.get("nome"):
|
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", []):
|
for pesq in dados.get("areaPesquisa", []):
|
||||||
if pesq.get("descricao"):
|
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:
|
elif "Coordenação" in tipo:
|
||||||
foi_coordenador = True
|
foi_coordenador = True
|
||||||
@@ -473,23 +556,79 @@ async def sugerir_consultores(
|
|||||||
foi_premiado = True
|
foi_premiado = True
|
||||||
|
|
||||||
posicao_ranking = None
|
posicao_ranking = None
|
||||||
|
pontuacao_ranking = 0
|
||||||
if store.is_ready():
|
if store.is_ready():
|
||||||
entry = store.get_by_id(id_pessoa)
|
entry = store.get_by_id(id_pessoa)
|
||||||
if entry:
|
if entry:
|
||||||
posicao_ranking = entry.posicao
|
posicao_ranking = entry.posicao
|
||||||
|
pontuacao_ranking = entry.pontuacao_total
|
||||||
|
|
||||||
consultores.append(SugestaoConsultorSchema(
|
score_final = score_match
|
||||||
id_pessoa=id_pessoa,
|
if posicao_ranking:
|
||||||
nome=nome,
|
bonus_ranking = max(0, (10000 - posicao_ranking) / 100)
|
||||||
score_match=score_match,
|
score_final += bonus_ranking
|
||||||
areas_avaliacao=list(areas_avaliacao),
|
if foi_coordenador:
|
||||||
areas_conhecimento=list(areas_conhecimento),
|
score_final += 20
|
||||||
linhas_pesquisa=list(linhas_pesquisa),
|
motivos_match.add("Foi Coordenador de Area")
|
||||||
situacao=situacao,
|
if foi_premiado:
|
||||||
ies=ies,
|
score_final += 10
|
||||||
foi_coordenador=foi_coordenador,
|
motivos_match.add("Foi Premiado")
|
||||||
foi_premiado=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(
|
return SugerirConsultoresResponseSchema(
|
||||||
tema_buscado=tema,
|
tema_buscado=tema,
|
||||||
|
|||||||
@@ -104,6 +104,9 @@ class SugestaoConsultorSchema(BaseModel):
|
|||||||
ies: Optional[str] = None
|
ies: Optional[str] = None
|
||||||
foi_coordenador: bool = False
|
foi_coordenador: bool = False
|
||||||
foi_premiado: bool = False
|
foi_premiado: bool = False
|
||||||
|
posicao_ranking: Optional[int] = None
|
||||||
|
pontuacao_ranking: float = 0
|
||||||
|
motivos_match: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
class SugerirConsultoresRequestSchema(BaseModel):
|
class SugerirConsultoresRequestSchema(BaseModel):
|
||||||
|
|||||||
@@ -32,6 +32,10 @@
|
|||||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(96, 165, 250, 0.05));
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(96, 165, 250, 0.05));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sugerir-header-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.sugerir-header h2 {
|
.sugerir-header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
@@ -39,6 +43,13 @@
|
|||||||
color: var(--accent-2);
|
color: var(--accent-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sugerir-subtitle {
|
||||||
|
margin: 0.25rem 0 0 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
.sugerir-close {
|
.sugerir-close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -173,13 +184,25 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sugerir-resultados h3 {
|
.sugerir-resultados-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sugerir-resultados h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--silver);
|
color: var(--silver);
|
||||||
border-bottom: 1px solid var(--stroke);
|
}
|
||||||
|
|
||||||
|
.sugerir-hint {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sugerir-lista {
|
.sugerir-lista {
|
||||||
@@ -192,16 +215,56 @@
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
background: rgba(255, 255, 255, 0.02);
|
background: rgba(255, 255, 255, 0.02);
|
||||||
border: 1px solid var(--stroke);
|
border: 2px solid var(--stroke);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
cursor: pointer;
|
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 {
|
.sugerir-item:hover {
|
||||||
background: rgba(99, 102, 241, 0.1);
|
background: rgba(99, 102, 241, 0.1);
|
||||||
border-color: rgba(99, 102, 241, 0.3);
|
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 {
|
.sugerir-item-header {
|
||||||
@@ -262,6 +325,72 @@
|
|||||||
border: 1px solid rgba(107, 114, 128, 0.4);
|
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 {
|
.sugerir-item-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -271,8 +400,11 @@
|
|||||||
|
|
||||||
.sugerir-ies {
|
.sugerir-ies {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--muted);
|
color: var(--silver);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sugerir-areas {
|
.sugerir-areas {
|
||||||
@@ -316,6 +448,126 @@
|
|||||||
white-space: nowrap;
|
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) {
|
@media (max-width: 600px) {
|
||||||
.sugerir-modal {
|
.sugerir-modal {
|
||||||
max-height: 95vh;
|
max-height: 95vh;
|
||||||
@@ -334,4 +586,18 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.equipe-flutuante {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipe-acoes {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gerar-pdf {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
|
|||||||
const [loadingAreas, setLoadingAreas] = useState(true);
|
const [loadingAreas, setLoadingAreas] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [buscaRealizada, setBuscaRealizada] = useState(false);
|
const [buscaRealizada, setBuscaRealizada] = useState(false);
|
||||||
|
const [selecionados, setSelecionados] = useState([]);
|
||||||
|
const [gerandoPdf, setGerandoPdf] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const carregarAreas = async () => {
|
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) => {
|
const handleClickConsultor = (consultor) => {
|
||||||
if (onSelectConsultor) {
|
if (onSelectConsultor) {
|
||||||
onSelectConsultor(consultor.id_pessoa);
|
onSelectConsultor(consultor.id_pessoa);
|
||||||
@@ -58,17 +71,81 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
|
|||||||
onClose();
|
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) => {
|
const handleKeyDown = (e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isSelecionado = (id) => selecionados.some((c) => c.id_pessoa === id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sugerir-overlay" onClick={onClose} onKeyDown={handleKeyDown}>
|
<div className="sugerir-overlay" onClick={onClose} onKeyDown={handleKeyDown}>
|
||||||
<div className="sugerir-modal" onClick={(e) => e.stopPropagation()}>
|
<div className="sugerir-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="sugerir-header">
|
<div className="sugerir-header">
|
||||||
<h2>Sugerir Consultores por Tema</h2>
|
<div className="sugerir-header-content">
|
||||||
|
<h2>Montar Equipe de Consultores</h2>
|
||||||
|
<p className="sugerir-subtitle">Selecione consultores para gerar documento da equipe</p>
|
||||||
|
</div>
|
||||||
<button className="sugerir-close" onClick={onClose}>×</button>
|
<button className="sugerir-close" onClick={onClose}>×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -140,21 +217,33 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
|
|||||||
|
|
||||||
{buscaRealizada && (
|
{buscaRealizada && (
|
||||||
<div className="sugerir-resultados">
|
<div className="sugerir-resultados">
|
||||||
|
<div className="sugerir-resultados-header">
|
||||||
<h3>
|
<h3>
|
||||||
{consultores.length > 0
|
{consultores.length > 0
|
||||||
? `${consultores.length} consultores encontrados`
|
? `${consultores.length} consultores para "${tema}"`
|
||||||
: 'Nenhum consultor encontrado'}
|
: 'Nenhum consultor encontrado'}
|
||||||
</h3>
|
</h3>
|
||||||
|
{consultores.length > 0 && (
|
||||||
|
<span className="sugerir-hint">Selecione os consultores para a equipe</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{consultores.length > 0 && (
|
{consultores.length > 0 && (
|
||||||
<div className="sugerir-lista">
|
<div className="sugerir-lista">
|
||||||
{consultores.map((c, index) => (
|
{consultores.map((c, index) => (
|
||||||
<div
|
<div
|
||||||
key={c.id_pessoa}
|
key={c.id_pessoa}
|
||||||
className="sugerir-item"
|
className={`sugerir-item ${isSelecionado(c.id_pessoa) ? 'selecionado' : ''}`}
|
||||||
onClick={() => handleClickConsultor(c)}
|
onClick={(e) => toggleSelecionado(c, e)}
|
||||||
>
|
>
|
||||||
<div className="sugerir-item-header">
|
<div className="sugerir-item-header">
|
||||||
|
<label className="sugerir-checkbox-item" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelecionado(c.id_pessoa)}
|
||||||
|
onChange={(e) => toggleSelecionado(c, e)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<span className="sugerir-rank">#{index + 1}</span>
|
<span className="sugerir-rank">#{index + 1}</span>
|
||||||
<span className="sugerir-nome">{c.nome}</span>
|
<span className="sugerir-nome">{c.nome}</span>
|
||||||
<div className="sugerir-badges">
|
<div className="sugerir-badges">
|
||||||
@@ -166,9 +255,45 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
|
|||||||
<span className="badge inativo">Inativo</span>
|
<span className="badge inativo">Inativo</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn-ver-ranking"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleClickConsultor(c);
|
||||||
|
}}
|
||||||
|
title="Ver no ranking"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="sugerir-item-details">
|
|
||||||
|
<div className="sugerir-item-stats">
|
||||||
|
{c.posicao_ranking && (
|
||||||
|
<span className="stat-ranking" title="Posicao no ranking geral">
|
||||||
|
<span className="stat-icon">📊</span>
|
||||||
|
#{c.posicao_ranking.toLocaleString()} no ranking
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{c.pontuacao_ranking > 0 && (
|
||||||
|
<span className="stat-pontuacao" title="Pontuacao no ranking">
|
||||||
|
{c.pontuacao_ranking.toFixed(0)} pts
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{c.ies && <span className="sugerir-ies">{c.ies}</span>}
|
{c.ies && <span className="sugerir-ies">{c.ies}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{c.motivos_match && c.motivos_match.length > 0 && (
|
||||||
|
<div className="sugerir-motivos">
|
||||||
|
<span className="motivos-label">Por que este consultor:</span>
|
||||||
|
<div className="motivos-lista">
|
||||||
|
{c.motivos_match.map((motivo, idx) => (
|
||||||
|
<span key={idx} className="tag motivo">{motivo}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="sugerir-item-details">
|
||||||
{c.areas_avaliacao.length > 0 && (
|
{c.areas_avaliacao.length > 0 && (
|
||||||
<div className="sugerir-areas">
|
<div className="sugerir-areas">
|
||||||
{c.areas_avaliacao.slice(0, 3).map((area) => (
|
{c.areas_avaliacao.slice(0, 3).map((area) => (
|
||||||
@@ -195,6 +320,50 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{selecionados.length > 0 && (
|
||||||
|
<div className="equipe-flutuante">
|
||||||
|
<div className="equipe-info">
|
||||||
|
<div className="equipe-header-row">
|
||||||
|
<span className="equipe-count">{selecionados.length} selecionado{selecionados.length > 1 ? 's' : ''}</span>
|
||||||
|
{(() => {
|
||||||
|
const temas = [...new Set(selecionados.map((c) => c._busca_tema).filter(Boolean))];
|
||||||
|
return temas.length > 1 && (
|
||||||
|
<span className="equipe-interdisciplinar">Interdisciplinar</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<div className="equipe-temas">
|
||||||
|
{(() => {
|
||||||
|
const temas = [...new Set(selecionados.map((c) => c._busca_tema).filter(Boolean))];
|
||||||
|
return temas.map((t) => (
|
||||||
|
<span key={t} className="equipe-tema">{t}</span>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<div className="equipe-nomes">
|
||||||
|
{selecionados.slice(0, 5).map((c) => (
|
||||||
|
<span key={c.id_pessoa} className="equipe-nome">{c.nome.split(' ')[0]}</span>
|
||||||
|
))}
|
||||||
|
{selecionados.length > 5 && (
|
||||||
|
<span className="equipe-nome">+{selecionados.length - 5}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="equipe-acoes">
|
||||||
|
<button className="btn-limpar" onClick={() => setSelecionados([])}>
|
||||||
|
Limpar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn-gerar-pdf"
|
||||||
|
onClick={handleGerarPdf}
|
||||||
|
disabled={gerandoPdf}
|
||||||
|
>
|
||||||
|
{gerandoPdf ? 'Gerando...' : 'Gerar PDF da Equipe'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user