diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 554c1ac..56cd00f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -9,6 +9,8 @@ function App() { const [consultores, setConsultores] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [processing, setProcessing] = useState(false); + const [processMessage, setProcessMessage] = useState(''); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(50); @@ -45,12 +47,44 @@ function App() { try { setLoading(true); setError(null); + setProcessMessage(''); const response = await rankingService.getRanking(page, pageSize); setConsultores(response.consultores); setTotal(response.total); setTotalPages(response.total_pages || 0); setPage(response.page || page); } catch (err) { + const status = err?.response?.status; + if (status === 503) { + try { + setProcessing(true); + setProcessMessage('Ranking ainda não processado. Iniciando processamento...'); + await rankingService.processarRanking(true); + + while (true) { + const st = await rankingService.getStatus(); + setProcessMessage(st.mensagem || `Processando... ${st.progress || 0}%`); + if (!st.running) { + if (st.erro) throw new Error(st.erro); + break; + } + await new Promise((r) => setTimeout(r, 4000)); + } + + const response = await rankingService.getRanking(page, pageSize); + setConsultores(response.consultores); + setTotal(response.total); + setTotalPages(response.total_pages || 0); + setPage(response.page || page); + } catch (e) { + console.error('Erro ao processar ranking:', e); + setError('Erro ao processar ranking. Verifique a conexão com o Elasticsearch.'); + } finally { + setProcessing(false); + } + return; + } + console.error('Erro ao carregar ranking:', err); setError('Erro ao carregar ranking. Verifique se a API está rodando.'); } finally { @@ -88,7 +122,9 @@ function App() { if (loading) { return (
-
Carregando ranking...
+
+ {processing ? (processMessage || 'Processando ranking...') : 'Carregando ranking...'} +
); } diff --git a/frontend/src/components/ConsultorCard.jsx b/frontend/src/components/ConsultorCard.jsx index de2cc80..6837229 100644 --- a/frontend/src/components/ConsultorCard.jsx +++ b/frontend/src/components/ConsultorCard.jsx @@ -2,12 +2,12 @@ import React, { useState, useRef, useEffect } from 'react'; import './ConsultorCard.css'; const SELOS = { - COORD_PPG: { label: 'Coord. PPG', cor: 'selo-coord', icone: '🎓' }, - PRESID_CAMARA: { label: 'Presid. Câmara', cor: 'selo-camara', icone: '🏛️' }, - BPQ: { label: 'Bolsista PQ', cor: 'selo-bpq', icone: '🔬' }, - GRANDE_PREMIO: { label: 'Grande Prêmio', cor: 'selo-gp', icone: '🏆' }, - PREMIO: { label: 'Prêmio', cor: 'selo-premio', icone: '🥇' }, - MENCAO: { label: 'Menção Honrosa', cor: 'selo-mencao', icone: '🎖️' }, + PRESID_CAMARA: { label: 'Presidente Câmara Temática', cor: 'selo-camara', icone: '🏛️' }, + COORD_PPG: { label: 'Coordenador de PPG', cor: 'selo-coord', icone: '🎓' }, + BPQ: { label: 'BPQ', cor: 'selo-bpq', icone: '🔬' }, + AUTOR_GP: { label: 'Autor - Grande Prêmio', cor: 'selo-gp', icone: '🏆' }, + AUTOR_PREMIO: { label: 'Autor - Prêmio', cor: 'selo-premio', icone: '🥇' }, + AUTOR_MENCAO: { label: 'Autor - Menção Honrosa', cor: 'selo-mencao', icone: '🎖️' }, ORIENT_POS_DOC: { label: 'Orient. Pós-Doc', cor: 'selo-orient', icone: '📚' }, ORIENT_POS_DOC_PREM: { label: 'Orient. Pós-Doc Premiada', cor: 'selo-orient-prem', icone: '📚🏆' }, ORIENT_TESE: { label: 'Orient. Tese', cor: 'selo-orient', icone: '📖' }, @@ -25,42 +25,86 @@ const SELOS = { const gerarSelos = (consultor) => { const selos = []; - if (consultor.coordenacoes_capes?.some(c => c.codigo === 'CAM' && c.presidente)) { - selos.push({ ...SELOS.PRESID_CAMARA, qtd: 1 }); + const isPresidCamaraVigente = consultor.coordenacoes_capes?.some( + (c) => c.codigo === 'CAM' && c.presidente && (c.ativo ?? !c.fim) + ); + if (isPresidCamaraVigente) { + selos.push({ ...SELOS.PRESID_CAMARA, qtd: 1, hint: 'Presidente Câmara Temática: mandato vigente como presidente.' }); } if (consultor.coordenador_ppg) { - selos.push({ ...SELOS.COORD_PPG, qtd: 1 }); + selos.push({ ...SELOS.COORD_PPG, qtd: 1, hint: 'Coordenador de PPG: possui perfil/atuação de coordenação de programa no ATUACAPES.' }); } - if (consultor.bolsas_cnpq?.length > 0) { - selos.push({ ...SELOS.BPQ, qtd: consultor.bolsas_cnpq.length }); + const bolsas = Array.isArray(consultor.bolsas_cnpq) ? consultor.bolsas_cnpq : []; + if (bolsas.length > 0) { + const porNivel = {}; + for (const b of bolsas) { + const nivel = (b.nivel || 'N/A').toString().trim(); + porNivel[nivel] = (porNivel[nivel] || 0) + 1; + } + const niveis = Object.keys(porNivel); + const label = niveis.length === 1 ? `BPQ ${niveis[0]}` : 'BPQ'; + const breakdown = niveis + .sort() + .map((n) => `${n}=${porNivel[n]}`) + .join(' | '); + selos.push({ ...SELOS.BPQ, label, qtd: bolsas.length, hint: `BPQ NIVEL: ${breakdown}` }); } - const gp = consultor.premiacoes?.filter(p => p.codigo === 'PREMIACAO')?.length || 0; - const premio = consultor.premiacoes?.filter(p => p.codigo === 'PREMIACAO_GP')?.length || 0; - const mencao = consultor.premiacoes?.filter(p => p.codigo === 'MENCAO')?.length || 0; + const premiacoes = Array.isArray(consultor.premiacoes) ? consultor.premiacoes : []; + const premiacoesAutor = premiacoes.filter((p) => (p.papel || '').toString().toLowerCase() === 'autor'); + const autorGp = premiacoesAutor.filter((p) => p.codigo === 'PREMIACAO').length; + const autorPremio = premiacoesAutor.filter((p) => p.codigo === 'PREMIACAO_GP').length; + const autorMencao = premiacoesAutor.filter((p) => p.codigo === 'MENCAO').length; - if (gp > 0) selos.push({ ...SELOS.GRANDE_PREMIO, qtd: gp }); - if (premio > 0) selos.push({ ...SELOS.PREMIO, qtd: premio }); - if (mencao > 0) selos.push({ ...SELOS.MENCAO, qtd: mencao }); + if (autorGp > 0) selos.push({ ...SELOS.AUTOR_GP, qtd: autorGp, hint: `Autor - Grande Prêmio: ${autorGp} ocorrência(s).` }); + if (autorPremio > 0) selos.push({ ...SELOS.AUTOR_PREMIO, qtd: autorPremio, hint: `Autor - Prêmio: ${autorPremio} ocorrência(s).` }); + if (autorMencao > 0) selos.push({ ...SELOS.AUTOR_MENCAO, qtd: autorMencao, hint: `Autor - Menção Honrosa: ${autorMencao} ocorrência(s).` }); - const orientPosDoc = consultor.orientacoes?.filter(o => o.codigo === 'ORIENT_POS_DOC')?.length || 0; - const orientTese = consultor.orientacoes?.filter(o => o.codigo === 'ORIENT_TESE')?.length || 0; - const orientDiss = consultor.orientacoes?.filter(o => o.codigo === 'ORIENT_DISS')?.length || 0; + const orientacoes = Array.isArray(consultor.orientacoes) ? consultor.orientacoes : []; + const contarPremiadas = (lista) => { + const acc = { GP: 0, PREMIO: 0, MENCAO: 0 }; + for (const o of lista) { + if (!o?.premiada) continue; + const t = (o.premiacao_tipo || '').toString().toUpperCase(); + if (t.includes('GP')) acc.GP += 1; + else if (t.includes('MENCAO')) acc.MENCAO += 1; + else acc.PREMIO += 1; + } + return acc; + }; + const hintPremiadas = (labelBase, counts) => + `${labelBase} (GP / Prêmio / Menção): GP=${counts.GP} | Prêmio=${counts.PREMIO} | Menção=${counts.MENCAO}`; - const orientPosDocPrem = consultor.orientacoes?.filter(o => o.codigo === 'ORIENT_POS_DOC' && o.premiada)?.length || 0; - const orientTesePrem = consultor.orientacoes?.filter(o => o.codigo === 'ORIENT_TESE' && o.premiada)?.length || 0; - const orientDissPrem = consultor.orientacoes?.filter(o => o.codigo === 'ORIENT_DISS' && o.premiada)?.length || 0; + const selosOrientacao = (codigo, seloNormal, seloPrem) => { + const base = orientacoes.filter((o) => o.codigo === codigo && !o.coorientacao); + const prem = base.filter((o) => o.premiada); + const naoPrem = base.filter((o) => !o.premiada); + if (prem.length > 0) { + selos.push({ ...seloPrem, qtd: prem.length, hint: hintPremiadas(seloPrem.label, contarPremiadas(prem)) }); + } else if (naoPrem.length > 0) { + selos.push({ ...seloNormal, qtd: naoPrem.length, hint: `${seloNormal.label}: ${naoPrem.length} ocorrência(s).` }); + } + }; - if (orientPosDocPrem > 0) selos.push({ ...SELOS.ORIENT_POS_DOC_PREM, qtd: orientPosDocPrem }); - else if (orientPosDoc > 0) selos.push({ ...SELOS.ORIENT_POS_DOC, qtd: orientPosDoc }); + const selosCoorientacao = (codigo, seloNormal, seloPrem) => { + const base = orientacoes.filter((o) => o.codigo === codigo && o.coorientacao); + const prem = base.filter((o) => o.premiada); + const naoPrem = base.filter((o) => !o.premiada); + if (prem.length > 0) { + selos.push({ ...seloPrem, qtd: prem.length, hint: hintPremiadas(seloPrem.label, contarPremiadas(prem)) }); + } else if (naoPrem.length > 0) { + selos.push({ ...seloNormal, qtd: naoPrem.length, hint: `${seloNormal.label}: ${naoPrem.length} ocorrência(s).` }); + } + }; - if (orientTesePrem > 0) selos.push({ ...SELOS.ORIENT_TESE_PREM, qtd: orientTesePrem }); - else if (orientTese > 0) selos.push({ ...SELOS.ORIENT_TESE, qtd: orientTese }); - - if (orientDissPrem > 0) selos.push({ ...SELOS.ORIENT_DISS_PREM, qtd: orientDissPrem }); - else if (orientDiss > 0) selos.push({ ...SELOS.ORIENT_DISS, qtd: orientDiss }); + selosOrientacao('ORIENT_POS_DOC', SELOS.ORIENT_POS_DOC, SELOS.ORIENT_POS_DOC_PREM); + selosOrientacao('ORIENT_TESE', SELOS.ORIENT_TESE, SELOS.ORIENT_TESE_PREM); + selosOrientacao('ORIENT_DISS', SELOS.ORIENT_DISS, SELOS.ORIENT_DISS_PREM); + selosCoorientacao('CO_ORIENT_POS_DOC', SELOS.CO_ORIENT_POS_DOC, SELOS.CO_ORIENT_POS_DOC_PREM); + selosCoorientacao('CO_ORIENT_TESE', SELOS.CO_ORIENT_TESE, SELOS.CO_ORIENT_TESE_PREM); + selosCoorientacao('CO_ORIENT_DISS', SELOS.CO_ORIENT_DISS, SELOS.CO_ORIENT_DISS_PREM); return selos; }; @@ -77,7 +121,7 @@ const SelosBadges = ({ selos, compacto = false }) => { 1 ? ` (${selo.qtd}x)` : ''}`} + title={selo.hint || `${selo.label}${selo.qtd > 1 ? ` (${selo.qtd}x)` : ''}`} > {selo.icone} {!compacto && {selo.label}} @@ -94,19 +138,19 @@ const SelosBadges = ({ selos, compacto = false }) => { const FORMULAS = { bloco_a: { titulo: 'Coordenacao CAPES', - descricao: 'CA=200 | CAJ=150 | CAJ_MP=120 | CAM=100\nTempo: multiplicador por ano\nBonus atualidade + Retorno (V2)', + descricao: 'CA=200 | CAJ=150 | CAJ_MP=120 | CAM=100\nTempo: multiplicador por ano (anos completos)\nBônus atualidade (mandato vigente) + Retorno (mandato anterior)', }, bloco_b: { titulo: 'Coordenacao PPG', - descricao: 'Base=70 | Tempo=5 pts/ano (max 50)\nExtras por programas distintos (max 40)\nBonus por maior nota do programa (max 20)', + descricao: 'Reservado no V1: PPG_COORD base=0 | teto=0 (dados incompletos no ATUACAPES para pontuar).', }, bloco_c: { titulo: 'Consultoria', - descricao: 'CONS_ATIVO=150 | CONS_HIST=100 | CONS_FALECIDO=100\nTempo: 5 pts/ano (max 50)\nContinuidade 8a+=20 | Retorno=15 (V2)', + descricao: 'CONS_ATIVO=150 | CONS_HIST=100 | CONS_FALECIDO=100\nTempo: 5 pts/ano (max 50)\nContinuidade (escalonado): 3a=+5 | 5a=+10 | 8a+=+15\nRetorno (reativação): +15 (uma vez)', }, bloco_d: { titulo: 'Premiacoes/Avaliacoes', - descricao: 'GP=100 | Premio=50 | Mencao=30\nAVAL_COMIS=30-50 | COORD_COMIS=50-60\nINSC_AUTOR=10 | INSC_INST=30 (V2)', + descricao: 'Premiações: GP=150 (teto 180) | Prêmio=30 (teto 60) | Menção=10 (teto 20)\nBolsas: BPQ_SUP=30 (teto 60) | BPQ_INT=50 (teto 100)\nInscrições/Avaliações/Comissões/Participações/Orientações/Bancas (com tetos por código)', }, }; @@ -116,8 +160,9 @@ const PONTOS_BASE = { INSC_AUTOR: 10, INSC_INST: 30, AVAL_COMIS_PREMIO: 30, AVAL_COMIS_GP: 50, COORD_COMIS_PREMIO: 50, COORD_COMIS_GP: 60, - PREMIACAO: 100, PREMIACAO_GP: 50, MENCAO: 30, - BOL_BPQ_SUPERIOR: 30, BOL_BPQ_INTERMEDIARIO: 30, + PREMIACAO: 150, PREMIACAO_GP: 30, MENCAO: 10, + BOL_BPQ_SUP: 30, BOL_BPQ_INT: 50, + BOL_BPQ_SUPERIOR: 30, BOL_BPQ_INTERMEDIARIO: 50, EVENTO: 1, PROJ: 10, ORIENT_POS_DOC: 15, ORIENT_TESE: 10, ORIENT_DISS: 5, CO_ORIENT_POS_DOC: 7, CO_ORIENT_TESE: 5, CO_ORIENT_DISS: 3, @@ -128,25 +173,27 @@ const TETOS = { INSC_AUTOR: { teto: 20, doc: '3.3 Inscrições' }, INSC_INST: { teto: 60, doc: '3.3 Inscrições' }, AVAL_COMIS_PREMIO: { teto: 60, doc: '3.4 Avaliação/Comissão', bonus: '+2/ano (max 15)' }, - AVAL_COMIS_GP: { teto: 80, doc: '3.4 Avaliação/Comissão', bonus: '+3/ano (max 20)' }, + AVAL_COMIS_GP: { teto: 100, doc: '3.4 Avaliação/Comissão', bonus: '+3/ano (max 20)' }, COORD_COMIS_PREMIO: { teto: 100, doc: '3.4 Avaliação/Comissão', bonus: '+4/ano (max 20)' }, COORD_COMIS_GP: { teto: 120, doc: '3.4 Avaliação/Comissão', bonus: '+6/ano (max 20)' }, - PREMIACAO: { teto: 180, doc: '3.6 Grande Prêmio' }, - PREMIACAO_GP: { teto: 60, doc: '3.6 Prêmio' }, - MENCAO: { teto: 20, doc: '3.6 Menção Honrosa' }, - EVENTO: { teto: 5, doc: '3.7 Participações' }, - PROJ: { teto: 40, doc: '3.7 Participações' }, - BOL_BPQ_SUPERIOR: { teto: 60, doc: '3.5 Bolsas CNPQ' }, - BOL_BPQ_INTERMEDIARIO: { teto: 60, doc: '3.5 Bolsas CNPQ' }, - ORIENT_POS_DOC: { teto: 0, doc: '3.8 Orientação (sem limite)' }, - ORIENT_TESE: { teto: 0, doc: '3.8 Orientação (sem limite)' }, - ORIENT_DISS: { teto: 0, doc: '3.8 Orientação (sem limite)' }, - CO_ORIENT_POS_DOC: { teto: 0, doc: '3.9 Co-Orientação (sem limite)' }, - CO_ORIENT_TESE: { teto: 0, doc: '3.9 Co-Orientação (sem limite)' }, - CO_ORIENT_DISS: { teto: 0, doc: '3.9 Co-Orientação (sem limite)' }, - MB_BANCA_POS_DOC: { teto: 0, doc: '3.10 Banca (sem limite)' }, - MB_BANCA_TESE: { teto: 0, doc: '3.10 Banca (sem limite)' }, - MB_BANCA_DISS: { teto: 0, doc: '3.10 Banca (sem limite)' }, + PREMIACAO: { teto: 180, doc: '3.4 Premiações e Bolsas' }, + PREMIACAO_GP: { teto: 60, doc: '3.4 Premiações e Bolsas' }, + MENCAO: { teto: 20, doc: '3.4 Premiações e Bolsas' }, + EVENTO: { teto: 5, doc: '3.5 Participações Acadêmicas' }, + PROJ: { teto: 40, doc: '3.5 Participações Acadêmicas' }, + BOL_BPQ_SUP: { teto: 60, doc: '3.4 Premiações e Bolsas' }, + BOL_BPQ_INT: { teto: 100, doc: '3.4 Premiações e Bolsas' }, + BOL_BPQ_SUPERIOR: { teto: 60, doc: '3.4 Premiações e Bolsas' }, + BOL_BPQ_INTERMEDIARIO: { teto: 100, doc: '3.4 Premiações e Bolsas' }, + ORIENT_POS_DOC: { teto: 100, doc: '3.5 Participações Acadêmicas' }, + ORIENT_TESE: { teto: 50, doc: '3.5 Participações Acadêmicas' }, + ORIENT_DISS: { teto: 25, doc: '3.5 Participações Acadêmicas' }, + CO_ORIENT_POS_DOC: { teto: 35, doc: '3.5 Participações Acadêmicas' }, + CO_ORIENT_TESE: { teto: 25, doc: '3.5 Participações Acadêmicas' }, + CO_ORIENT_DISS: { teto: 15, doc: '3.5 Participações Acadêmicas' }, + MB_BANCA_POS_DOC: { teto: 15, doc: '3.5 Participações Acadêmicas' }, + MB_BANCA_TESE: { teto: 15, doc: '3.5 Participações Acadêmicas' }, + MB_BANCA_DISS: { teto: 10, doc: '3.5 Participações Acadêmicas' }, }; const ScoreItemWithTooltip = ({ value, label, formula, style }) => ( diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 712c6c8..6da873b 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -38,7 +38,7 @@ const Header = ({ total }) => { CA20010/ano (max 100)30 CAJ1508/ano (max 80)20 CAJ_MP1206/ano (max 60)15 - CAM1005/ano (max 50)10 + CAM1005/ano (max 50)20
+ Retorno (20)
@@ -53,6 +53,8 @@ const Header = ({ total }) => { CONS_HIST100 pts CONS_FALECIDO100 pts Tempo5 pts/ano (max 50) + Continuidade 3a+5 pts + Continuidade 5a+10 pts Continuidade 8a++15 pts Retorno+15 pts @@ -67,9 +69,9 @@ const Header = ({ total }) => { PREMIACAO (GP)150 pts (max 180) PREMIACAO_GP30 pts (max 60) MENCAO10 pts (max 20) - COORD_COMIS_GP60 pts (max 120) - AVAL_COMIS_GP50 pts (max 100) INSC_INST30 pts (max 60) + AVAL_COMIS_GP50 pts (max 100) + COORD_COMIS_GP60 pts (max 120) diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 3c902af..685236e 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -50,6 +50,7 @@ export const rankingService = { ativo: c.ativo, anos_atuacao: anos, veterano: anos >= 10, + coordenador_ppg: Boolean(c.coordenador_ppg), pontuacao: c.pontuacao || { pontuacao_total: c.pontuacao_total, bloco_a: { total: c.bloco_a, atuacoes: [] }, @@ -100,6 +101,16 @@ export const rankingService = { const response = await api.get('/ranking/busca', { params: { nome, limit } }); return response.data; }, + + async processarRanking(limpar_antes = true) { + const response = await api.post('/ranking/processar', { limpar_antes }); + return response.data; + }, + + async getStatus() { + const response = await api.get('/ranking/status'); + return response.data; + }, }; export default api;