feat(frontend): adicionar link para perfil no ATUACAPES

- Adiciona ícone de link externo antes do nome do consultor
- Link abre perfil no ATUACAPES em nova aba (/perfil/{id_pessoa})
- Variável de ambiente HOST_ATUACAPES configurável
- Adiciona retry automático (10 tentativas) ao carregar ranking
- Corrige espaçamento da seção de selos
- Atualiza arquivos .env.example
This commit is contained in:
Frederico Castro
2025-12-15 11:14:10 -03:00
parent b44582653b
commit 4876d4d9f5
7 changed files with 73 additions and 21 deletions

View File

@@ -6,3 +6,5 @@ ES_VERIFY_SSL=true
SCHEDULER_ENABLED=false SCHEDULER_ENABLED=false
SCHEDULER_HOUR=3 SCHEDULER_HOUR=3
HOST_ATUACAPES=https://atuacapes.capes.gov.br

View File

@@ -1,18 +1,17 @@
ES_URL=http://localhost:9200 ES_URL=http://localhost:9200
ES_INDEX=atuacapes__1763197236 ES_INDEX=atuacapes
ES_USER=seu_usuario_elastic ES_USER=seu_usuario_elastic
ES_PASSWORD=sua_senha_elastic ES_PASSWORD=sua_senha_elastic
ES_VERIFY_SSL=true ES_PASS=sua_senha_elastic
ORACLE_LOCAL_USER=ranking ORACLE_USER=seu_usuario_oracle
ORACLE_LOCAL_PASSWORD=senha_oracle ORACLE_PASSWORD=sua_senha_oracle
ORACLE_LOCAL_DSN=localhost:1521/XEPDB1 ORACLE_DSN=oracle:1521/XEPDB1
API_HOST=0.0.0.0 ORACLE_LOCAL_USER=seu_usuario_local
API_PORT=8000 ORACLE_LOCAL_PASSWORD=sua_senha_local
API_RELOAD=true ORACLE_LOCAL_DSN=XEPDB1
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
LOG_LEVEL=INFO ORACLE_CLIENT=oracle-local
SCHEDULER_ENABLED=false
SCHEDULER_HOUR=3 HOST_ATUACAPES=https://atuacapes.capes.gov.br

View File

@@ -36,6 +36,8 @@ services:
- backend - backend
ports: ports:
- "5173:5173" - "5173:5173"
environment:
- VITE_HOST_ATUACAPES=${HOST_ATUACAPES:-https://atuacapes.capes.gov.br}
volumes: volumes:
- ./frontend/src:/app/src - ./frontend/src:/app/src
- ./frontend/index.html:/app/index.html - ./frontend/index.html:/app/index.html

2
frontend/.env.example Normal file
View File

@@ -0,0 +1,2 @@
VITE_API_URL=http://localhost:8010/api/v1
VITE_HOST_ATUACAPES=https://atuacapes.capes.gov.br

View File

@@ -44,7 +44,10 @@ function App() {
loadRanking(); loadRanking();
}, [page, pageSize]); }, [page, pageSize]);
const loadRanking = async () => { const loadRanking = async (retryCount = 0) => {
const MAX_RETRIES = 10;
const RETRY_DELAY = 2000;
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
@@ -56,6 +59,14 @@ function App() {
setPage(response.page || page); setPage(response.page || page);
} catch (err) { } catch (err) {
const status = err?.response?.status; const status = err?.response?.status;
const isNetworkError = !err?.response || err?.code === 'ERR_NETWORK';
if (isNetworkError && retryCount < MAX_RETRIES) {
setProcessMessage(`Aguardando API... (tentativa ${retryCount + 1}/${MAX_RETRIES})`);
await new Promise((r) => setTimeout(r, RETRY_DELAY));
return loadRanking(retryCount + 1);
}
if (status === 503) { if (status === 503) {
try { try {
setProcessing(true); setProcessing(true);
@@ -146,7 +157,7 @@ function App() {
return ( return (
<div className="container"> <div className="container">
<div className="loading"> <div className="loading">
{processing ? (processMessage || 'Processando ranking...') : 'Carregando ranking...'} {processMessage || (processing ? 'Processando ranking...' : 'Carregando ranking...')}
</div> </div>
</div> </div>
); );

View File

@@ -628,6 +628,7 @@
.selos-section { .selos-section {
grid-column: 1 / -1; grid-column: 1 / -1;
margin-top: 1rem;
} }
.selos-section .selos-container { .selos-section .selos-container {
@@ -651,3 +652,26 @@
display: none; display: none;
} }
} }
.link-atuacapes {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
font-size: 0.9rem;
color: var(--muted);
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--stroke);
border-radius: 6px;
text-decoration: none;
transition: all 200ms ease;
cursor: pointer;
}
.link-atuacapes:hover {
color: var(--accent-2);
background: rgba(79, 70, 229, 0.15);
border-color: rgba(79, 70, 229, 0.4);
transform: scale(1.1);
}

View File

@@ -237,6 +237,18 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
<div className="card-info"> <div className="card-info">
<div className="consultant-name"> <div className="consultant-name">
{import.meta.env.VITE_HOST_ATUACAPES && consultor.id_pessoa && (
<a
href={`${import.meta.env.VITE_HOST_ATUACAPES}/perfil/${consultor.id_pessoa}`}
target="_blank"
rel="noopener noreferrer"
className="link-atuacapes"
onClick={(e) => e.stopPropagation()}
title="Ver perfil no ATUACAPES"
>
</a>
)}
{consultor.nome} {consultor.nome}
{consultor.ativo && <span className="badge badge-ativo">ATIVO</span>} {consultor.ativo && <span className="badge badge-ativo">ATIVO</span>}
{!consultor.ativo && <span className="badge badge-historico">HISTORICO</span>} {!consultor.ativo && <span className="badge badge-historico">HISTORICO</span>}
@@ -325,15 +337,15 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
{blocoD.atuacoes && blocoD.atuacoes.length > 0 && ( {blocoD.atuacoes && blocoD.atuacoes.length > 0 && (
<BlocoDetalhes titulo="D - Premiacoes/Avaliacoes" bloco={blocoD} cor="var(--bronze)" /> <BlocoDetalhes titulo="D - Premiacoes/Avaliacoes" bloco={blocoD} cor="var(--bronze)" />
)} )}
{selos.length > 0 && (
<div className="detail-section selos-section">
<h4>Selos e Reconhecimentos</h4>
<SelosBadges selos={selos} />
</div>
)}
</div> </div>
{selos.length > 0 && (
<div className="detail-section selos-section">
<h4>Selos e Reconhecimentos</h4>
<SelosBadges selos={selos} />
</div>
)}
{consultor.coordenacoes_capes?.length > 0 && ( {consultor.coordenacoes_capes?.length > 0 && (
<div className="extra-details"> <div className="extra-details">
<h4>Coordenacoes CAPES</h4> <h4>Coordenacoes CAPES</h4>