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
This commit is contained in:
1
backend/.gitignore
vendored
Normal file
1
backend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
instantclient_23_7/
|
||||||
@@ -236,13 +236,16 @@ async def ranking_paginado(
|
|||||||
if not d.get("idiomas"):
|
if not d.get("idiomas"):
|
||||||
faltando_idiomas.append((c.id_pessoa, d))
|
faltando_idiomas.append((c.id_pessoa, d))
|
||||||
|
|
||||||
if faltando_idiomas:
|
faltando_lattes = [(c.id_pessoa, d) for c, d in consultores_dados if not d.get("lattes")]
|
||||||
ids = [item[0] for item in faltando_idiomas]
|
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(
|
docs = await es_client.buscar_por_ids(
|
||||||
ids,
|
ids_buscar,
|
||||||
source_fields=["id", "dadosPessoais", "idiomas", "atuacoes", "formacoes"],
|
source_fields=["id", "dadosPessoais", "idiomas", "atuacoes", "formacoes", "identificadorLattes", "titulacoes"],
|
||||||
)
|
)
|
||||||
docs_map = {int(doc.get("id")): doc for doc in docs if doc.get("id")}
|
docs_map = {int(doc.get("id")): doc for doc in docs if doc.get("id")}
|
||||||
|
|
||||||
for id_pessoa, detalhes in faltando_idiomas:
|
for id_pessoa, detalhes in faltando_idiomas:
|
||||||
doc = docs_map.get(int(id_pessoa))
|
doc = docs_map.get(int(id_pessoa))
|
||||||
if not doc:
|
if not doc:
|
||||||
@@ -264,6 +267,32 @@ async def ranking_paginado(
|
|||||||
if titulacao:
|
if titulacao:
|
||||||
detalhes["titulacao"] = 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:
|
for c, d in consultores_dados:
|
||||||
tipos_atuacao = RankingMapper._extrair_tipos_atuacao(d)
|
tipos_atuacao = RankingMapper._extrair_tipos_atuacao(d)
|
||||||
consultores_schema.append(
|
consultores_schema.append(
|
||||||
@@ -294,6 +323,7 @@ async def ranking_paginado(
|
|||||||
idiomas=d.get("idiomas"),
|
idiomas=d.get("idiomas"),
|
||||||
titulacao=d.get("titulacao"),
|
titulacao=d.get("titulacao"),
|
||||||
pontuacao=d.get("pontuacao"),
|
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))
|
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")
|
@router.get("/consultor/{id_pessoa}/pdf")
|
||||||
async def exportar_ficha_pdf(
|
async def exportar_ficha_pdf(
|
||||||
id_pessoa: int,
|
id_pessoa: int,
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ from typing import Optional, List
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class LattesSchema(BaseModel):
|
||||||
|
id_lattes: Optional[str] = None
|
||||||
|
url: Optional[str] = None
|
||||||
|
titulacoes: Optional[list] = None
|
||||||
|
|
||||||
|
|
||||||
class ConsultorRankingResumoSchema(BaseModel):
|
class ConsultorRankingResumoSchema(BaseModel):
|
||||||
id_pessoa: int
|
id_pessoa: int
|
||||||
nome: str
|
nome: str
|
||||||
@@ -30,6 +36,7 @@ class ConsultorRankingResumoSchema(BaseModel):
|
|||||||
idiomas: Optional[list] = None
|
idiomas: Optional[list] = None
|
||||||
titulacao: Optional[str] = None
|
titulacao: Optional[str] = None
|
||||||
pontuacao: Optional[dict] = None
|
pontuacao: Optional[dict] = None
|
||||||
|
lattes: Optional[LattesSchema] = None
|
||||||
|
|
||||||
|
|
||||||
class RankingPaginadoResponseSchema(BaseModel):
|
class RankingPaginadoResponseSchema(BaseModel):
|
||||||
|
|||||||
@@ -697,6 +697,8 @@ const ItemDetalheModal = ({ item, tipo, onClose }) => {
|
|||||||
|
|
||||||
const getTitulo = () => {
|
const getTitulo = () => {
|
||||||
switch (tipo) {
|
switch (tipo) {
|
||||||
|
case 'titulacao': return 'Titulação';
|
||||||
|
case 'producoes_lattes': return 'Produções Lattes';
|
||||||
case 'vinculo': return 'Vínculo de Consultoria';
|
case 'vinculo': return 'Vínculo de Consultoria';
|
||||||
case 'coordenacao': return 'Coordenação CAPES';
|
case 'coordenacao': return 'Coordenação CAPES';
|
||||||
case 'premiacao': return 'Premiação';
|
case 'premiacao': return 'Premiação';
|
||||||
@@ -710,6 +712,8 @@ const ItemDetalheModal = ({ item, tipo, onClose }) => {
|
|||||||
|
|
||||||
const getIcone = () => {
|
const getIcone = () => {
|
||||||
switch (tipo) {
|
switch (tipo) {
|
||||||
|
case 'titulacao': return '🎓';
|
||||||
|
case 'producoes_lattes': return '📚';
|
||||||
case 'vinculo': return '💼';
|
case 'vinculo': return '💼';
|
||||||
case 'coordenacao': return '🎯';
|
case 'coordenacao': return '🎯';
|
||||||
case 'premiacao': return '🏆';
|
case 'premiacao': return '🏆';
|
||||||
@@ -723,6 +727,90 @@ const ItemDetalheModal = ({ item, tipo, onClose }) => {
|
|||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (tipo) {
|
switch (tipo) {
|
||||||
|
case 'titulacao': {
|
||||||
|
return (
|
||||||
|
<div className="modal-detalhe-content">
|
||||||
|
<div className="modal-detalhe-row">
|
||||||
|
<span className="modal-detalhe-label">Grau</span>
|
||||||
|
<span className="modal-detalhe-value">{item.grau || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
{item.area && (
|
||||||
|
<div className="modal-detalhe-row">
|
||||||
|
<span className="modal-detalhe-label">Área</span>
|
||||||
|
<span className="modal-detalhe-value">{item.area}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(item.ies_sigla || item.ies_nome) && (
|
||||||
|
<div className="modal-detalhe-row">
|
||||||
|
<span className="modal-detalhe-label">Instituição</span>
|
||||||
|
<span className="modal-detalhe-value">
|
||||||
|
{item.ies_sigla && item.ies_nome ? `${item.ies_sigla} - ${item.ies_nome}` : (item.ies_sigla || item.ies_nome)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item.pais && (
|
||||||
|
<div className="modal-detalhe-row">
|
||||||
|
<span className="modal-detalhe-label">País</span>
|
||||||
|
<span className="modal-detalhe-value">{item.pais}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item.ano && (
|
||||||
|
<div className="modal-detalhe-row">
|
||||||
|
<span className="modal-detalhe-label">Ano</span>
|
||||||
|
<span className="modal-detalhe-value">{item.ano}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'producoes_lattes': {
|
||||||
|
const producoes = item.producoes || item.producoes_recentes || [];
|
||||||
|
return (
|
||||||
|
<div className="modal-detalhe-content">
|
||||||
|
<div className="modal-detalhe-row">
|
||||||
|
<span className="modal-detalhe-label">Total</span>
|
||||||
|
<span className="modal-detalhe-value">{item.total_producoes ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="modal-detalhe-row">
|
||||||
|
<span className="modal-detalhe-label">Bibliográfica</span>
|
||||||
|
<span className="modal-detalhe-value">{item.producao_bibliografica ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="modal-detalhe-row">
|
||||||
|
<span className="modal-detalhe-label">Técnica</span>
|
||||||
|
<span className="modal-detalhe-value">{item.producao_tecnica ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="modal-detalhe-row">
|
||||||
|
<span className="modal-detalhe-label">Orientações</span>
|
||||||
|
<span className="modal-detalhe-value">{item.orientacoes_lattes ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
{item.data_atualizacao_lattes && (
|
||||||
|
<div className="modal-detalhe-row">
|
||||||
|
<span className="modal-detalhe-label">Atualização</span>
|
||||||
|
<span className="modal-detalhe-value">{formatDate(item.data_atualizacao_lattes)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="modal-detalhe-row">
|
||||||
|
<span className="modal-detalhe-label">Produções recentes</span>
|
||||||
|
<span className="modal-detalhe-value">{producoes.length}</span>
|
||||||
|
</div>
|
||||||
|
{producoes.length > 0 ? (
|
||||||
|
<ul className="modal-list" style={{ marginTop: '0.6rem' }}>
|
||||||
|
{producoes.map((prod, idx) => (
|
||||||
|
<li key={idx} className="modal-item">
|
||||||
|
<span className="modal-item-main">{prod.titulo || 'Sem título'}</span>
|
||||||
|
<span className="muted">{prod.ano || '-'}</span>
|
||||||
|
{prod.natureza && <span className="badge">{prod.natureza}</span>}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p className="modal-empty">Nenhuma produção encontrada.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
case 'vinculo': {
|
case 'vinculo': {
|
||||||
const periodo = item.periodo || {};
|
const periodo = item.periodo || {};
|
||||||
const isAtivo = periodo.ativo ?? !periodo.fim;
|
const isAtivo = periodo.ativo ?? !periodo.fim;
|
||||||
@@ -1366,6 +1454,7 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
|
|||||||
const [tipoAtuacaoModal, setTipoAtuacaoModal] = useState(null);
|
const [tipoAtuacaoModal, setTipoAtuacaoModal] = useState(null);
|
||||||
const [seloModal, setSeloModal] = useState(null);
|
const [seloModal, setSeloModal] = useState(null);
|
||||||
const [itemDetalhe, setItemDetalhe] = useState(null);
|
const [itemDetalhe, setItemDetalhe] = useState(null);
|
||||||
|
const [loadingLattes, setLoadingLattes] = useState(false);
|
||||||
const [pontuacaoModal, setPontuacaoModal] = useState(null);
|
const [pontuacaoModal, setPontuacaoModal] = useState(null);
|
||||||
const [showInsights, setShowInsights] = useState(false);
|
const [showInsights, setShowInsights] = useState(false);
|
||||||
const cardRef = useRef(null);
|
const cardRef = useRef(null);
|
||||||
@@ -1618,6 +1707,71 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{consultor.lattes?.id_lattes && (
|
||||||
|
<div className="detail-section lattes-section">
|
||||||
|
<h4>Curriculo Lattes</h4>
|
||||||
|
<div className="lattes-content">
|
||||||
|
<a
|
||||||
|
href={consultor.lattes.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="lattes-link"
|
||||||
|
>
|
||||||
|
<span className="lattes-icon">📄</span>
|
||||||
|
<span className="lattes-id">{consultor.lattes.id_lattes}</span>
|
||||||
|
<span className="lattes-external">↗</span>
|
||||||
|
</a>
|
||||||
|
<div className="titulacoes-resumo">
|
||||||
|
{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) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="titulacao-badge titulacao-clicavel"
|
||||||
|
onClick={(e) => {
|
||||||
|
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}` : ''}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
<span
|
||||||
|
className="titulacao-badge titulacao-clicavel lattes-producoes"
|
||||||
|
title="Ver producoes do Lattes"
|
||||||
|
onClick={async (e) => {
|
||||||
|
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
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{consultoria?.vinculos?.length > 0 && (
|
{consultoria?.vinculos?.length > 0 && (
|
||||||
<div className="extra-details">
|
<div className="extra-details">
|
||||||
<h4>Vinculos de Consultoria</h4>
|
<h4>Vinculos de Consultoria</h4>
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export const rankingService = {
|
|||||||
membros_banca: c.membros_banca || [],
|
membros_banca: c.membros_banca || [],
|
||||||
idiomas: c.idiomas || [],
|
idiomas: c.idiomas || [],
|
||||||
titulacao: c.titulacao || '',
|
titulacao: c.titulacao || '',
|
||||||
|
lattes: c.lattes || null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,6 +176,11 @@ export const rankingService = {
|
|||||||
const response = await api.get('/consultores/areas-avaliacao');
|
const response = await api.get('/consultores/areas-avaliacao');
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getLattes(idPessoa) {
|
||||||
|
const response = await api.get(`/consultor/${idPessoa}/lattes`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
Reference in New Issue
Block a user