feat(frontend): adicionar modais de detalhes para itens das listas

- Criar componente ItemDetalheModal para exibir detalhes de atuações
- Adicionar modais clicáveis em: vínculos, coordenações, premiações,
  avaliações, inscrições e participações
- Melhorar healthcheck do Oracle no docker-compose
- Adicionar retry com backoff na conexão Oracle
- Padronizar tamanho dos badges de tipos de atuação
This commit is contained in:
Frederico Castro
2025-12-22 04:17:12 -03:00
parent 061a3e8768
commit 32b404d1a8
4 changed files with 452 additions and 36 deletions

View File

@@ -1,4 +1,5 @@
import logging import logging
import time
import oracledb import oracledb
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
@@ -15,32 +16,40 @@ class OracleClient:
self._pool: Optional[oracledb.ConnectionPool] = None self._pool: Optional[oracledb.ConnectionPool] = None
self._connected = False self._connected = False
def connect(self) -> None: def connect(self, max_retries: int = 10, initial_delay: float = 2.0) -> None:
try: for attempt in range(max_retries):
test_conn = oracledb.connect( try:
user=self.user, test_conn = oracledb.connect(
password=self.password, user=self.user,
dsn=self.dsn, password=self.password,
) dsn=self.dsn,
test_conn.ping() )
test_conn.close() test_conn.ping()
test_conn.close()
self._pool = oracledb.create_pool( self._pool = oracledb.create_pool(
user=self.user, user=self.user,
password=self.password, password=self.password,
dsn=self.dsn, dsn=self.dsn,
min=1, min=1,
max=10, max=10,
increment=1, increment=1,
) )
self._connected = True self._connected = True
logger.info(f"Pool Oracle conectado: {self.dsn}") logger.info(f"Pool Oracle conectado: {self.dsn}")
except oracledb.Error as e: return
logger.error(f"Oracle database error: {e}") except oracledb.Error as e:
self._connected = False delay = initial_delay * (2 ** attempt)
except Exception as e: if attempt < max_retries - 1:
logger.error(f"Oracle connection error: {e}") logger.warning(f"Oracle tentativa {attempt + 1}/{max_retries} falhou: {e}. Retry em {delay:.1f}s...")
self._connected = False time.sleep(delay)
else:
logger.error(f"Oracle database error: {e}")
self._connected = False
except Exception as e:
logger.error(f"Oracle connection error: {e}")
self._connected = False
return
def close(self) -> None: def close(self) -> None:
if self._pool: if self._pool:

View File

@@ -64,7 +64,7 @@ services:
volumes: volumes:
- oracle_data:/opt/oracle/oradata - oracle_data:/opt/oracle/oradata
healthcheck: healthcheck:
test: ["CMD", "bash", "-c", "echo 'SELECT 1 FROM DUAL;' | sqlplus -s SYSTEM/local123@localhost:1521/XEPDB1 | grep -q 1"] test: ["CMD", "bash", "-c", "echo 'SELECT 1 FROM DUAL;' | sqlplus -s local123/local123@localhost:1521/XEPDB1 | grep -q 1"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 30 retries: 30

View File

@@ -503,6 +503,83 @@
font-size: 0.85rem; font-size: 0.85rem;
} }
.list-item-clicavel {
cursor: pointer;
transition: all 0.2s ease;
}
.list-item-clicavel:hover {
background: rgba(6, 182, 212, 0.1);
border-left: 3px solid var(--accent-2);
padding-left: calc(0.5rem - 3px);
}
.modal-detalhe-content {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem 0;
}
.modal-detalhe-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
padding: 0.7rem 0.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 4px;
}
.modal-detalhe-row:last-child {
border-bottom: none;
}
.modal-detalhe-row:hover {
background: rgba(255, 255, 255, 0.02);
}
.modal-detalhe-label {
color: var(--muted);
font-size: 0.85rem;
font-weight: 500;
min-width: 120px;
flex-shrink: 0;
}
.modal-detalhe-value {
color: var(--text);
font-size: 0.9rem;
text-align: right;
flex: 1;
}
.modal-detalhe-value.pontos {
color: var(--accent-2);
font-weight: 700;
}
.modal-detalhe-value.muted {
color: var(--muted);
font-style: italic;
}
.modal-titulo-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.modal-titulo-icone {
font-size: 1.2rem;
}
.modal-empty {
color: var(--muted);
text-align: center;
padding: 1rem;
}
@media (max-width: 1200px) { @media (max-width: 1200px) {
.details-grid { .details-grid {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@@ -749,21 +826,21 @@
color: var(--accent-2); color: var(--accent-2);
} }
.tipos-expandido { .tipos-section .tipos-atuacao-container {
gap: 0.5rem; gap: 0.5rem;
} }
.tipos-expandido .tipo-atuacao { .tipos-expandido .tipo-atuacao {
padding: 0.25rem 0.5rem; padding: 0.35rem 0.6rem;
font-size: 0.7rem; font-size: 0.7rem;
} }
.tipos-expandido .tipo-icone { .tipos-expandido .tipo-icone {
font-size: 0.8rem; font-size: 1rem;
} }
.tipos-expandido .tipo-label { .tipos-expandido .tipo-label {
font-size: 0.65rem; font-size: 0.72rem;
} }
.selos-section { .selos-section {

View File

@@ -551,6 +551,303 @@ const SeloModal = ({ selo, consultor, onClose }) => {
); );
}; };
const ItemDetalheModal = ({ item, tipo, onClose }) => {
if (!item || !tipo) return null;
const formatDate = (dateStr) => {
if (!dateStr) return 'N/A';
return new Date(dateStr).toLocaleDateString('pt-BR');
};
const getTitulo = () => {
switch (tipo) {
case 'vinculo': return 'Vínculo de Consultoria';
case 'coordenacao': return 'Coordenação CAPES';
case 'premiacao': return 'Premiação';
case 'avaliacao': return 'Avaliação de Comissão';
case 'inscricao': return 'Inscrição em Prêmio';
case 'participacao': return item.codigo === 'PROJ' ? 'Projeto' : 'Evento';
case 'orientacao': return 'Orientação';
default: return 'Detalhes';
}
};
const getIcone = () => {
switch (tipo) {
case 'vinculo': return '💼';
case 'coordenacao': return '🎯';
case 'premiacao': return '🏆';
case 'avaliacao': return '📋';
case 'inscricao': return '📝';
case 'participacao': return item.codigo === 'PROJ' ? '📊' : '📅';
case 'orientacao': return '🎓';
default: return '📄';
}
};
const renderContent = () => {
switch (tipo) {
case 'vinculo': {
const periodo = item.periodo || {};
const isAtivo = periodo.ativo ?? !periodo.fim;
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Status</span>
<span className={`badge ${isAtivo ? 'badge-ativo' : 'badge-historico'}`}>
{isAtivo ? 'ATIVO' : 'ENCERRADO'}
</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Instituição</span>
<span className="modal-detalhe-value">
{item.ies ? (item.ies.sigla ? `${item.ies.sigla} - ${item.ies.nome || ''}` : item.ies.nome) : 'Não informada'}
</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Início</span>
<span className="modal-detalhe-value">{formatDate(periodo.inicio)}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Fim</span>
<span className="modal-detalhe-value">{isAtivo ? 'Em andamento' : formatDate(periodo.fim)}</span>
</div>
{item.situacao && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Situação</span>
<span className="modal-detalhe-value">{item.situacao}</span>
</div>
)}
</div>
);
}
case 'coordenacao': {
const isAtivo = item.ativo ?? !item.fim;
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Tipo</span>
<span className="badge">{item.codigo || item.tipo}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Área de Avaliação</span>
<span className="modal-detalhe-value">{item.area_avaliacao || 'N/A'}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Status</span>
<span className={`badge ${isAtivo ? 'badge-ativo' : 'badge-historico'}`}>
{isAtivo ? 'VIGENTE' : 'ENCERRADO'}
</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Início</span>
<span className="modal-detalhe-value">{formatDate(item.inicio || item.periodo?.inicio)}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Fim</span>
<span className="modal-detalhe-value">{isAtivo ? 'Em andamento' : formatDate(item.fim || item.periodo?.fim)}</span>
</div>
{item.presidente && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Função</span>
<span className="badge badge-premiado">👑 Presidente de Câmara</span>
</div>
)}
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação Base</span>
<span className="modal-detalhe-value pontos">{PONTOS_BASE[item.codigo] || 0} pts</span>
</div>
</div>
);
}
case 'premiacao':
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Código</span>
<span className="badge">{item.codigo}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Prêmio</span>
<span className="modal-detalhe-value">{item.nome_premio || item.premio || 'N/A'}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Ano</span>
<span className="modal-detalhe-value">{item.ano || 'N/A'}</span>
</div>
{item.papel && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Papel</span>
<span className="modal-detalhe-value">{item.papel}</span>
</div>
)}
{item.tipo && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Tipo</span>
<span className="modal-detalhe-value">{item.tipo}</span>
</div>
)}
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação Base</span>
<span className="modal-detalhe-value pontos">{PONTOS_BASE[item.codigo] || 0} pts</span>
</div>
</div>
);
case 'avaliacao':
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Código</span>
<span className="badge">{item.codigo}</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>
</div>
{item.nome_comissao && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Comissão</span>
<span className="modal-detalhe-value">{item.nome_comissao}</span>
</div>
)}
{item.comissao_tipo && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Tipo Comissão</span>
<span className="modal-detalhe-value">{item.comissao_tipo}</span>
</div>
)}
{item.tipo && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Função</span>
<span className="modal-detalhe-value">{item.tipo}</span>
</div>
)}
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Ano</span>
<span className="modal-detalhe-value">{item.ano || 'N/A'}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação Base</span>
<span className="modal-detalhe-value pontos">{PONTOS_BASE[item.codigo] || 0} pts</span>
</div>
</div>
);
case 'inscricao':
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Código</span>
<span className="badge">{item.codigo}</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>
</div>
{item.tipo && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Tipo Inscrição</span>
<span className="modal-detalhe-value">{item.tipo}</span>
</div>
)}
{item.situacao && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Situação</span>
<span className="modal-detalhe-value">{item.situacao}</span>
</div>
)}
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Ano</span>
<span className="modal-detalhe-value">{item.ano || 'N/A'}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação Base</span>
<span className="modal-detalhe-value pontos">{PONTOS_BASE[item.codigo] || 0} pts</span>
</div>
</div>
);
case 'participacao':
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Tipo</span>
<span className="badge">{item.codigo}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Descrição</span>
<span className="modal-detalhe-value">{item.descricao || item.tipo || 'N/A'}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Ano</span>
<span className="modal-detalhe-value">{item.ano || 'N/A'}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação Base</span>
<span className="modal-detalhe-value pontos">{PONTOS_BASE[item.codigo] || 0} pts</span>
</div>
</div>
);
case 'orientacao':
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Tipo</span>
<span className="badge">{item.codigo}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Categoria</span>
<span className="modal-detalhe-value">
{item.codigo?.includes('TESE') ? 'Doutorado' : item.codigo?.includes('DISS') ? 'Mestrado' : 'Pós-Doutorado'}
</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Função</span>
<span className="modal-detalhe-value">
{item.coorientacao || item.codigo?.startsWith('CO_') ? 'Coorientador' : 'Orientador'}
</span>
</div>
{item.premiada && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Destaque</span>
<span className="badge badge-premiado">🏆 Premiada</span>
</div>
)}
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação</span>
<span className="modal-detalhe-value muted">Apenas selo (sem pontuação)</span>
</div>
</div>
);
default:
return <p className="modal-empty">Sem detalhes disponíveis</p>;
}
};
return createPortal(
<div className="tipo-modal-overlay" onClick={onClose}>
<div className="tipo-modal" onClick={(e) => e.stopPropagation()}>
<div className="tipo-modal-header">
<span className="modal-titulo-item">
<span className="modal-titulo-icone">{getIcone()}</span>
<span>{getTitulo()}</span>
</span>
<button className="tipo-modal-close" onClick={onClose}></button>
</div>
<div className="tipo-modal-body">
{renderContent()}
</div>
</div>
</div>,
document.body
);
};
const FORMULAS = { const FORMULAS = {
bloco_a: { bloco_a: {
titulo: 'Coordenacao CAPES', titulo: 'Coordenacao CAPES',
@@ -623,6 +920,7 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
const [showRawModal, setShowRawModal] = useState(false); const [showRawModal, setShowRawModal] = useState(false);
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 cardRef = useRef(null); const cardRef = useRef(null);
useEffect(() => { useEffect(() => {
@@ -840,7 +1138,11 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
const periodo = vinculo.periodo || {}; const periodo = vinculo.periodo || {};
const isAtivo = periodo.ativo ?? !periodo.fim; const isAtivo = periodo.ativo ?? !periodo.fim;
return ( return (
<div key={idx} className="list-item"> <div
key={idx}
className="list-item list-item-clicavel"
onClick={() => setItemDetalhe({ item: vinculo, tipo: 'vinculo' })}
>
<span className={`badge ${isAtivo ? 'badge-ativo' : 'badge-historico'}`}> <span className={`badge ${isAtivo ? 'badge-ativo' : 'badge-historico'}`}>
{isAtivo ? 'ATIVO' : 'ENCERRADO'} {isAtivo ? 'ATIVO' : 'ENCERRADO'}
</span> </span>
@@ -868,7 +1170,11 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
{[...consultor.coordenacoes_capes] {[...consultor.coordenacoes_capes]
.sort((a, b) => new Date(b.inicio || b.periodo?.inicio || 0) - new Date(a.inicio || a.periodo?.inicio || 0)) .sort((a, b) => new Date(b.inicio || b.periodo?.inicio || 0) - new Date(a.inicio || a.periodo?.inicio || 0))
.map((coord, idx) => ( .map((coord, idx) => (
<div key={idx} className="list-item"> <div
key={idx}
className="list-item list-item-clicavel"
onClick={() => setItemDetalhe({ item: coord, tipo: 'coordenacao' })}
>
<span className="badge">{coord.codigo || coord.tipo}</span> <span className="badge">{coord.codigo || coord.tipo}</span>
<span className="pontos">{PONTOS_BASE[coord.codigo] || 0} pts</span> <span className="pontos">{PONTOS_BASE[coord.codigo] || 0} pts</span>
<span>{coord.area_avaliacao}</span> <span>{coord.area_avaliacao}</span>
@@ -888,7 +1194,11 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
{[...consultor.premiacoes] {[...consultor.premiacoes]
.sort((a, b) => (b.ano || 0) - (a.ano || 0)) .sort((a, b) => (b.ano || 0) - (a.ano || 0))
.map((prem, idx) => ( .map((prem, idx) => (
<div key={idx} className="list-item"> <div
key={idx}
className="list-item list-item-clicavel"
onClick={() => setItemDetalhe({ item: prem, tipo: 'premiacao' })}
>
<span className="badge">{prem.codigo}</span> <span className="badge">{prem.codigo}</span>
<span className="pontos">{PONTOS_BASE[prem.codigo] || 0} pts</span> <span className="pontos">{PONTOS_BASE[prem.codigo] || 0} pts</span>
<span>{prem.nome_premio}</span> <span>{prem.nome_premio}</span>
@@ -906,7 +1216,11 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
{[...consultor.avaliacoes_comissao] {[...consultor.avaliacoes_comissao]
.sort((a, b) => (b.ano || 0) - (a.ano || 0)) .sort((a, b) => (b.ano || 0) - (a.ano || 0))
.map((aval, idx) => ( .map((aval, idx) => (
<div key={idx} className="list-item"> <div
key={idx}
className="list-item list-item-clicavel"
onClick={() => setItemDetalhe({ item: aval, tipo: 'avaliacao' })}
>
<span className="badge">{aval.codigo}</span> <span className="badge">{aval.codigo}</span>
<span className="pontos">{PONTOS_BASE[aval.codigo] || 0} pts</span> <span className="pontos">{PONTOS_BASE[aval.codigo] || 0} pts</span>
<span>{aval.nome_comissao || aval.premio}</span> <span>{aval.nome_comissao || aval.premio}</span>
@@ -924,7 +1238,11 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
{[...consultor.inscricoes] {[...consultor.inscricoes]
.sort((a, b) => (b.ano || 0) - (a.ano || 0)) .sort((a, b) => (b.ano || 0) - (a.ano || 0))
.map((insc, idx) => ( .map((insc, idx) => (
<div key={idx} className="list-item"> <div
key={idx}
className="list-item list-item-clicavel"
onClick={() => setItemDetalhe({ item: insc, tipo: 'inscricao' })}
>
<span className="badge">{insc.codigo}</span> <span className="badge">{insc.codigo}</span>
<span className="pontos">{PONTOS_BASE[insc.codigo] || 0} pts</span> <span className="pontos">{PONTOS_BASE[insc.codigo] || 0} pts</span>
<span>{insc.premio}</span> <span>{insc.premio}</span>
@@ -943,7 +1261,11 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
.sort((a, b) => (b.ano || 0) - (a.ano || 0)) .sort((a, b) => (b.ano || 0) - (a.ano || 0))
.slice(0, 10) .slice(0, 10)
.map((part, idx) => ( .map((part, idx) => (
<div key={idx} className="list-item"> <div
key={idx}
className="list-item list-item-clicavel"
onClick={() => setItemDetalhe({ item: part, tipo: 'participacao' })}
>
<span className="badge">{part.codigo}</span> <span className="badge">{part.codigo}</span>
<span className="pontos">{PONTOS_BASE[part.codigo] || 0} pts</span> <span className="pontos">{PONTOS_BASE[part.codigo] || 0} pts</span>
<span>{part.descricao || part.tipo}</span> <span>{part.descricao || part.tipo}</span>
@@ -1025,6 +1347,14 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
onClose={() => setSeloModal(null)} onClose={() => setSeloModal(null)}
/> />
)} )}
{itemDetalhe && (
<ItemDetalheModal
item={itemDetalhe.item}
tipo={itemDetalhe.tipo}
onClose={() => setItemDetalhe(null)}
/>
)}
</div> </div>
); );
}); });