From 9d3b4d37b78b3e92127dab7d4378759d33c809b5 Mon Sep 17 00:00:00 2001 From: Frederico Castro Date: Fri, 26 Dec 2025 23:35:32 -0300 Subject: [PATCH] feat(backend/frontend): integrar dados do Lattes no ranking - Adicionar endpoint /consultor/{id}/lattes para buscar producoes - Incluir id_lattes e titulacoes na resposta do ranking paginado - Adicionar LattesSchema no backend - Adicionar funcao getLattes no servico frontend - Simplificar botao Producoes removendo estado loading desnecessario --- backend/.gitignore | 1 + backend/src/interface/api/routes.py | 85 +++++++++- .../src/interface/schemas/ranking_schema.py | 7 + frontend/src/components/ConsultorCard.jsx | 154 ++++++++++++++++++ frontend/src/services/api.js | 6 + 5 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 backend/.gitignore diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..ef4f894 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1 @@ +instantclient_23_7/ diff --git a/backend/src/interface/api/routes.py b/backend/src/interface/api/routes.py index 33403ac..59557cc 100644 --- a/backend/src/interface/api/routes.py +++ b/backend/src/interface/api/routes.py @@ -236,13 +236,16 @@ async def ranking_paginado( if not d.get("idiomas"): faltando_idiomas.append((c.id_pessoa, d)) - if faltando_idiomas: - ids = [item[0] for item in faltando_idiomas] + faltando_lattes = [(c.id_pessoa, d) for c, d in consultores_dados if not d.get("lattes")] + ids_buscar = list(set([item[0] for item in faltando_idiomas] + [item[0] for item in faltando_lattes])) + + if ids_buscar: docs = await es_client.buscar_por_ids( - ids, - source_fields=["id", "dadosPessoais", "idiomas", "atuacoes", "formacoes"], + ids_buscar, + source_fields=["id", "dadosPessoais", "idiomas", "atuacoes", "formacoes", "identificadorLattes", "titulacoes"], ) docs_map = {int(doc.get("id")): doc for doc in docs if doc.get("id")} + for id_pessoa, detalhes in faltando_idiomas: doc = docs_map.get(int(id_pessoa)) if not doc: @@ -264,6 +267,32 @@ async def ranking_paginado( if titulacao: detalhes["titulacao"] = titulacao + for id_pessoa, detalhes in faltando_lattes: + doc = docs_map.get(int(id_pessoa)) + if not doc: + continue + id_lattes_obj = doc.get("identificadorLattes") + titulacoes_raw = doc.get("titulacoes", []) + if id_lattes_obj and id_lattes_obj.get("descricao"): + id_lattes = id_lattes_obj.get("descricao") + titulacoes_formatadas = [] + for t in titulacoes_raw: + grau_obj = t.get("grauAcademico", {}) + ies_obj = t.get("ies", {}) + titulacoes_formatadas.append({ + "grau": grau_obj.get("nome", ""), + "ano": t.get("ano"), + "ies_nome": ies_obj.get("nome"), + "ies_sigla": ies_obj.get("sigla"), + "area": t.get("areaConhecimento", {}).get("nome"), + "pais": "Brasil", + }) + detalhes["lattes"] = { + "id_lattes": id_lattes, + "url": f"http://lattes.cnpq.br/{id_lattes}", + "titulacoes": titulacoes_formatadas, + } + for c, d in consultores_dados: tipos_atuacao = RankingMapper._extrair_tipos_atuacao(d) consultores_schema.append( @@ -294,6 +323,7 @@ async def ranking_paginado( idiomas=d.get("idiomas"), titulacao=d.get("titulacao"), pontuacao=d.get("pontuacao"), + lattes=d.get("lattes"), ) ) @@ -436,6 +466,53 @@ async def obter_consultor_raw( raise HTTPException(status_code=500, detail=str(e)) +@router.get("/consultor/{id_pessoa}/lattes") +async def obter_lattes( + id_pessoa: int, + es_client: ElasticsearchClient = Depends(get_es_client), +): + docs = await es_client.buscar_por_ids( + [id_pessoa], + source_fields=["id", "identificadorLattes", "titulacoes"], + ) + if not docs: + return {"encontrado": False, "motivo": "Consultor não encontrado"} + + doc = docs[0] + id_lattes_obj = doc.get("identificadorLattes") + + if not id_lattes_obj or not id_lattes_obj.get("descricao"): + return {"encontrado": False, "motivo": "Currículo Lattes não cadastrado"} + + id_lattes = id_lattes_obj.get("descricao") + titulacoes_raw = doc.get("titulacoes", []) + + titulacoes = [] + for t in titulacoes_raw: + grau_obj = t.get("grauAcademico", {}) + ies_obj = t.get("ies", {}) + area_obj = t.get("areaConhecimento", {}) + titulacoes.append({ + "grau": grau_obj.get("nome", ""), + "ano": t.get("ano"), + "inicio": t.get("inicio"), + "fim": t.get("fim"), + "ies_nome": ies_obj.get("nome"), + "ies_sigla": ies_obj.get("sigla"), + "area": area_obj.get("nome"), + "area_avaliacao": area_obj.get("areaAvaliacao", {}).get("nome") if area_obj.get("areaAvaliacao") else None, + "programa": t.get("programa", {}).get("nome") if t.get("programa") else None, + }) + + return { + "encontrado": True, + "id_lattes": id_lattes, + "url": f"http://lattes.cnpq.br/{id_lattes}", + "titulacoes": titulacoes, + "orientacoes_lattes": len([t for t in titulacoes if t.get("grau") in ["Doutorado", "Mestrado"]]), + } + + @router.get("/consultor/{id_pessoa}/pdf") async def exportar_ficha_pdf( id_pessoa: int, diff --git a/backend/src/interface/schemas/ranking_schema.py b/backend/src/interface/schemas/ranking_schema.py index 65c7b63..cf4a0fa 100644 --- a/backend/src/interface/schemas/ranking_schema.py +++ b/backend/src/interface/schemas/ranking_schema.py @@ -3,6 +3,12 @@ from typing import Optional, List from datetime import datetime +class LattesSchema(BaseModel): + id_lattes: Optional[str] = None + url: Optional[str] = None + titulacoes: Optional[list] = None + + class ConsultorRankingResumoSchema(BaseModel): id_pessoa: int nome: str @@ -30,6 +36,7 @@ class ConsultorRankingResumoSchema(BaseModel): idiomas: Optional[list] = None titulacao: Optional[str] = None pontuacao: Optional[dict] = None + lattes: Optional[LattesSchema] = None class RankingPaginadoResponseSchema(BaseModel): diff --git a/frontend/src/components/ConsultorCard.jsx b/frontend/src/components/ConsultorCard.jsx index d946ec1..0047bab 100644 --- a/frontend/src/components/ConsultorCard.jsx +++ b/frontend/src/components/ConsultorCard.jsx @@ -697,6 +697,8 @@ const ItemDetalheModal = ({ item, tipo, onClose }) => { const getTitulo = () => { switch (tipo) { + case 'titulacao': return 'Titulação'; + case 'producoes_lattes': return 'Produções Lattes'; case 'vinculo': return 'Vínculo de Consultoria'; case 'coordenacao': return 'Coordenação CAPES'; case 'premiacao': return 'Premiação'; @@ -710,6 +712,8 @@ const ItemDetalheModal = ({ item, tipo, onClose }) => { const getIcone = () => { switch (tipo) { + case 'titulacao': return '🎓'; + case 'producoes_lattes': return '📚'; case 'vinculo': return '💼'; case 'coordenacao': return '🎯'; case 'premiacao': return '🏆'; @@ -723,6 +727,90 @@ const ItemDetalheModal = ({ item, tipo, onClose }) => { const renderContent = () => { switch (tipo) { + case 'titulacao': { + return ( +
+
+ Grau + {item.grau || 'N/A'} +
+ {item.area && ( +
+ Área + {item.area} +
+ )} + {(item.ies_sigla || item.ies_nome) && ( +
+ Instituição + + {item.ies_sigla && item.ies_nome ? `${item.ies_sigla} - ${item.ies_nome}` : (item.ies_sigla || item.ies_nome)} + +
+ )} + {item.pais && ( +
+ País + {item.pais} +
+ )} + {item.ano && ( +
+ Ano + {item.ano} +
+ )} +
+ ); + } + + case 'producoes_lattes': { + const producoes = item.producoes || item.producoes_recentes || []; + return ( +
+
+ Total + {item.total_producoes ?? 0} +
+
+ Bibliográfica + {item.producao_bibliografica ?? 0} +
+
+ Técnica + {item.producao_tecnica ?? 0} +
+
+ Orientações + {item.orientacoes_lattes ?? 0} +
+ {item.data_atualizacao_lattes && ( +
+ Atualização + {formatDate(item.data_atualizacao_lattes)} +
+ )} +
+ Produções recentes + {producoes.length} +
+ {producoes.length > 0 ? ( + + ) : ( +

Nenhuma produção encontrada.

+ )} +
+ ); + } + case 'vinculo': { const periodo = item.periodo || {}; const isAtivo = periodo.ativo ?? !periodo.fim; @@ -1366,6 +1454,7 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio const [tipoAtuacaoModal, setTipoAtuacaoModal] = useState(null); const [seloModal, setSeloModal] = useState(null); const [itemDetalhe, setItemDetalhe] = useState(null); + const [loadingLattes, setLoadingLattes] = useState(false); const [pontuacaoModal, setPontuacaoModal] = useState(null); const [showInsights, setShowInsights] = useState(false); const cardRef = useRef(null); @@ -1618,6 +1707,71 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio )} + {consultor.lattes?.id_lattes && ( +
+

Curriculo Lattes

+
+ + 📄 + {consultor.lattes.id_lattes} + + +
+ {consultor.lattes.titulacoes + ?.sort((a, b) => { + const ordem = { 'Pos-Doutorado': 1, 'Doutorado': 2, 'Mestrado': 3, 'Graduacao': 4 }; + return (ordem[a.grau] || 99) - (ordem[b.grau] || 99); + }) + .map((t, idx) => ( + { + e.stopPropagation(); + setItemDetalhe({ + item: t, + tipo: 'titulacao' + }); + }} + title={`${t.grau}${t.area ? ` em ${t.area}` : ''}${t.ies_nome ? ` - ${t.ies_nome}` : ''}${t.pais ? ` (${t.pais})` : ''}`} + > + {t.grau}{t.ies_sigla ? ` (${t.ies_sigla})` : ''}{t.ano ? ` - ${t.ano}` : ''} + + ))} + { + e.stopPropagation(); + try { + const data = await rankingService.getLattes(consultor.id_pessoa); + if (data.encontrado) { + setItemDetalhe({ + item: { ...data, filtro: null, titulo: 'Producoes Lattes' }, + tipo: 'producoes_lattes' + }); + } else if (data.motivo) { + alert(data.motivo); + } else { + alert('Nenhuma producao encontrada no Lattes'); + } + } catch (err) { + alert('Erro ao buscar Lattes: ' + err.message); + } + }} + > + 📚 Producoes + +
+
+
+ )} + {consultoria?.vinculos?.length > 0 && (

Vinculos de Consultoria

diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index b0105fa..5d8aedc 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -92,6 +92,7 @@ export const rankingService = { membros_banca: c.membros_banca || [], idiomas: c.idiomas || [], titulacao: c.titulacao || '', + lattes: c.lattes || null, }; }); @@ -175,6 +176,11 @@ export const rankingService = { const response = await api.get('/consultores/areas-avaliacao'); return response.data; }, + + async getLattes(idPessoa) { + const response = await api.get(`/consultor/${idPessoa}/lattes`); + return response.data; + }, }; export default api;