fix(backend): corrigir exibicao de idiomas e selos multilingue

- Adicionar idiomas e formacoes ao _source das queries ES (client.py)
- Corrigir type mismatch int/str no endpoint paginado (routes.py)
- Adicionar campo evento nas inscricoes para nome do premio
- Implementar extracao de idiomas do ES no repository
- Ajustar frontend para exibir selo multilingue corretamente
This commit is contained in:
Frederico Castro
2025-12-24 18:12:22 -03:00
parent 0d355e705e
commit 919d95d1e8
14 changed files with 266 additions and 12 deletions

View File

@@ -87,7 +87,6 @@ const DADOS_BLOCOS = {
],
selos: [
{ cod: 'PPG_COORD', nome: 'Coordenador PPG', obs: 'Indicador (sem pontuação V1)' },
{ cod: 'IDIOMA_BILINGUE', nome: 'Bilíngue', obs: '2+ idiomas' },
{ cod: 'IDIOMA_MULTILINGUE', nome: 'Multilíngue', obs: '3+ idiomas' },
{ cod: 'TITULACAO_MESTRE', nome: 'Mestre', obs: 'Maior titulação' },
{ cod: 'TITULACAO_DOUTOR', nome: 'Doutor', obs: 'Maior titulação' },

View File

@@ -28,7 +28,6 @@ const SELOS = {
MB_BANCA_DISS: { codigo: 'MB_BANCA_DISS', label: 'Banca Diss.', cor: 'selo-banca', icone: '📄' },
EVENTO: { codigo: 'EVENTO', label: 'Evento', cor: 'selo-evento', icone: '📅' },
PROJ: { codigo: 'PROJ', label: 'Projeto', cor: 'selo-proj', icone: '📁' },
IDIOMA_BILINGUE: { codigo: 'IDIOMA_BILINGUE', label: 'Bilingue', cor: 'selo-idioma', icone: '🌍' },
IDIOMA_MULTILINGUE: { codigo: 'IDIOMA_MULTILINGUE', label: 'Multilingue', cor: 'selo-idioma', icone: '🌐' },
TITULACAO_MESTRE: { codigo: 'TITULACAO_MESTRE', label: 'Mestre', cor: 'selo-titulacao', icone: '🎓' },
TITULACAO_DOUTOR: { codigo: 'TITULACAO_DOUTOR', label: 'Doutor', cor: 'selo-titulacao', icone: '🎓' },
@@ -49,6 +48,13 @@ const TIPOS_ATUACAO_CONFIG = {
const gerarSelos = (consultor) => {
const selos = [];
const normalizarIdioma = (valor) => (valor || '')
.toString()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.trim();
const normalizarCodigoIdioma = (valor) => normalizarIdioma(valor).replace(/[^a-z0-9]/g, '');
const isPresidCamaraVigente = consultor.coordenacoes_capes?.some(
(c) => c.codigo === 'CAM' && c.presidente && (c.ativo ?? !c.fim)
@@ -132,6 +138,40 @@ const gerarSelos = (consultor) => {
selos.push({ ...SELOS.PROJ, qtd: projetos.length, hint: `Projetos (${projetos.length}x)` });
}
const idiomas = Array.isArray(consultor.idiomas) ? consultor.idiomas : [];
const idiomasUnicosMap = new Map();
let temPortugues = false;
for (const idioma of idiomas) {
const nome = idioma?.idioma || idioma?.nome || idioma?.descricao || '';
const chave = normalizarIdioma(nome);
if (!chave) continue;
if (!idiomasUnicosMap.has(chave)) {
idiomasUnicosMap.set(chave, nome);
}
if (chave.includes('portugues') || chave.includes('portuguese')) {
temPortugues = true;
}
}
const idiomasUnicos = Array.from(idiomasUnicosMap.values());
const totalIdiomas = idiomasUnicos.length + (!temPortugues && idiomasUnicos.length > 0 ? 1 : 0);
if (totalIdiomas >= 3) {
selos.push({
...SELOS.IDIOMA_MULTILINGUE,
qtd: totalIdiomas,
hint: `Multilingue: ${idiomasUnicos.join(', ')}`,
});
}
const titulacao = consultor.titulacao || '';
const titulacaoLower = titulacao.toLowerCase();
if (titulacaoLower.includes('pós-doutorado') || titulacaoLower.includes('pos-doutorado') || titulacaoLower.includes('posdoc') || titulacaoLower.includes('pós-doc')) {
selos.push({ ...SELOS.TITULACAO_POS_DOUTOR, qtd: 1, hint: 'Pós-Doutorado' });
} else if (titulacaoLower.includes('doutorado') || titulacaoLower.includes('doutor')) {
selos.push({ ...SELOS.TITULACAO_DOUTOR, qtd: 1, hint: 'Doutorado' });
} else if (titulacaoLower.includes('mestrado') || titulacaoLower.includes('mestre')) {
selos.push({ ...SELOS.TITULACAO_MESTRE, qtd: 1, hint: 'Mestrado' });
}
return selos;
};
@@ -144,6 +184,8 @@ const SELOS_COM_DADOS = [
'CO_ORIENT_TESE', 'CO_ORIENT_DISS', 'CO_ORIENT_POS_DOC',
'MB_BANCA_POS_DOC', 'MB_BANCA_TESE', 'MB_BANCA_DISS',
'EVENTO', 'PROJ',
'IDIOMA_MULTILINGUE',
'TITULACAO_MESTRE', 'TITULACAO_DOUTOR', 'TITULACAO_POS_DOUTOR',
];
const SelosBadges = ({ selos, compacto = false, onSeloClick }) => {
@@ -355,7 +397,7 @@ const TipoAtuacaoModal = ({ tipo, consultor, onClose }) => {
{[...inscs].sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((ins, i) => (
<div key={i} className="modal-item">
<span className="badge">{ins.codigo}</span>
<span className="modal-item-main">{ins.premio || ins.descricao || '-'}</span>
<span className="modal-item-main">{ins.evento || ins.premio || ins.descricao || '-'}</span>
<span className="muted">{ins.ano || '-'}</span>
</div>
))}
@@ -837,6 +879,12 @@ const ItemDetalheModal = ({ item, tipo, onClose }) => {
<span className="modal-detalhe-label">Código</span>
<span className="badge">{item.codigo}</span>
</div>
{item.evento && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Evento</span>
<span className="modal-detalhe-value">{item.evento}</span>
</div>
)}
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Prêmio</span>
<span className="modal-detalhe-value">{item.premio || 'N/A'}</span>
@@ -1461,7 +1509,7 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
onClick={handleRawDataClick}
title="Ver dados completos do ATUACAPES"
>
📋
</button>
<div className="expand-icon">{expanded ? '▲' : '▼'}</div>
</div>
@@ -1687,7 +1735,7 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
>
<span className="badge">{insc.codigo}</span>
<span className="pontos">{PONTOS_BASE[insc.codigo] || 0} pts</span>
<span>{insc.premio}</span>
<span>{insc.evento || insc.premio}</span>
<span className="muted">{insc.ano}</span>
</div>
))}

View File

@@ -19,6 +19,9 @@ const SELOS_CONFIG = {
{ codigo: 'ORIENT_GP', label: 'Orient. GP', icone: '🏆' },
{ codigo: 'ORIENT_PREMIO', label: 'Orient. Prêmio', icone: '🎖️' },
{ codigo: 'ORIENT_MENCAO', label: 'Orient. Menção', icone: '📜' },
{ codigo: 'COORIENT_GP', label: 'Coorient. GP', icone: '🏆' },
{ codigo: 'COORIENT_PREMIO', label: 'Coorient. Prêmio', icone: '🎖️' },
{ codigo: 'COORIENT_MENCAO', label: 'Coorient. Menção', icone: '📜' },
],
},
orientacoes: {

View File

@@ -493,6 +493,9 @@
display: flex;
flex-direction: column;
gap: 0.25rem;
flex: 1;
min-width: 0;
overflow: hidden;
}
.equipe-count {
@@ -518,6 +521,7 @@
.equipe-acoes {
display: flex;
gap: 0.75rem;
flex-shrink: 0;
}
.btn-limpar {
@@ -530,6 +534,7 @@
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s;
white-space: nowrap;
}
.btn-limpar:hover {
@@ -547,6 +552,7 @@
font-weight: 700;
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
white-space: nowrap;
}
.btn-gerar-pdf:hover:not(:disabled) {

View File

@@ -90,6 +90,8 @@ export const rankingService = {
participacoes: c.participacoes || [],
orientacoes: c.orientacoes || [],
membros_banca: c.membros_banca || [],
idiomas: c.idiomas || [],
titulacao: c.titulacao || '',
};
});