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

@@ -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>

View File

@@ -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);
}