diff --git a/backend/src/infrastructure/oracle/client.py b/backend/src/infrastructure/oracle/client.py index ccf07b5..610968b 100644 --- a/backend/src/infrastructure/oracle/client.py +++ b/backend/src/infrastructure/oracle/client.py @@ -1,4 +1,5 @@ import logging +import time import oracledb from typing import List, Dict, Any, Optional @@ -15,32 +16,40 @@ class OracleClient: self._pool: Optional[oracledb.ConnectionPool] = None self._connected = False - def connect(self) -> None: - try: - test_conn = oracledb.connect( - user=self.user, - password=self.password, - dsn=self.dsn, - ) - test_conn.ping() - test_conn.close() + def connect(self, max_retries: int = 10, initial_delay: float = 2.0) -> None: + for attempt in range(max_retries): + try: + test_conn = oracledb.connect( + user=self.user, + password=self.password, + dsn=self.dsn, + ) + test_conn.ping() + test_conn.close() - self._pool = oracledb.create_pool( - user=self.user, - password=self.password, - dsn=self.dsn, - min=1, - max=10, - increment=1, - ) - self._connected = True - logger.info(f"Pool Oracle conectado: {self.dsn}") - except oracledb.Error as e: - logger.error(f"Oracle database error: {e}") - self._connected = False - except Exception as e: - logger.error(f"Oracle connection error: {e}") - self._connected = False + self._pool = oracledb.create_pool( + user=self.user, + password=self.password, + dsn=self.dsn, + min=1, + max=10, + increment=1, + ) + self._connected = True + logger.info(f"Pool Oracle conectado: {self.dsn}") + return + except oracledb.Error as e: + delay = initial_delay * (2 ** attempt) + if attempt < max_retries - 1: + logger.warning(f"Oracle tentativa {attempt + 1}/{max_retries} falhou: {e}. Retry em {delay:.1f}s...") + 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: if self._pool: diff --git a/docker-compose.yml b/docker-compose.yml index fe6c9a1..99e7b1a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,7 +64,7 @@ services: volumes: - oracle_data:/opt/oracle/oradata 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 timeout: 5s retries: 30 diff --git a/frontend/src/components/ConsultorCard.css b/frontend/src/components/ConsultorCard.css index 5d0713e..2c18937 100644 --- a/frontend/src/components/ConsultorCard.css +++ b/frontend/src/components/ConsultorCard.css @@ -503,6 +503,83 @@ 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) { .details-grid { grid-template-columns: repeat(3, 1fr); @@ -749,21 +826,21 @@ color: var(--accent-2); } -.tipos-expandido { +.tipos-section .tipos-atuacao-container { gap: 0.5rem; } .tipos-expandido .tipo-atuacao { - padding: 0.25rem 0.5rem; + padding: 0.35rem 0.6rem; font-size: 0.7rem; } .tipos-expandido .tipo-icone { - font-size: 0.8rem; + font-size: 1rem; } .tipos-expandido .tipo-label { - font-size: 0.65rem; + font-size: 0.72rem; } .selos-section { diff --git a/frontend/src/components/ConsultorCard.jsx b/frontend/src/components/ConsultorCard.jsx index 7cd6e9c..36670f8 100644 --- a/frontend/src/components/ConsultorCard.jsx +++ b/frontend/src/components/ConsultorCard.jsx @@ -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 ( +
+
+ Status + + {isAtivo ? 'ATIVO' : 'ENCERRADO'} + +
+
+ Instituição + + {item.ies ? (item.ies.sigla ? `${item.ies.sigla} - ${item.ies.nome || ''}` : item.ies.nome) : 'Não informada'} + +
+
+ Início + {formatDate(periodo.inicio)} +
+
+ Fim + {isAtivo ? 'Em andamento' : formatDate(periodo.fim)} +
+ {item.situacao && ( +
+ Situação + {item.situacao} +
+ )} +
+ ); + } + + case 'coordenacao': { + const isAtivo = item.ativo ?? !item.fim; + return ( +
+
+ Tipo + {item.codigo || item.tipo} +
+
+ Área de Avaliação + {item.area_avaliacao || 'N/A'} +
+
+ Status + + {isAtivo ? 'VIGENTE' : 'ENCERRADO'} + +
+
+ Início + {formatDate(item.inicio || item.periodo?.inicio)} +
+
+ Fim + {isAtivo ? 'Em andamento' : formatDate(item.fim || item.periodo?.fim)} +
+ {item.presidente && ( +
+ Função + 👑 Presidente de Câmara +
+ )} +
+ Pontuação Base + {PONTOS_BASE[item.codigo] || 0} pts +
+
+ ); + } + + case 'premiacao': + return ( +
+
+ Código + {item.codigo} +
+
+ Prêmio + {item.nome_premio || item.premio || 'N/A'} +
+
+ Ano + {item.ano || 'N/A'} +
+ {item.papel && ( +
+ Papel + {item.papel} +
+ )} + {item.tipo && ( +
+ Tipo + {item.tipo} +
+ )} +
+ Pontuação Base + {PONTOS_BASE[item.codigo] || 0} pts +
+
+ ); + + case 'avaliacao': + return ( +
+
+ Código + {item.codigo} +
+
+ Prêmio + {item.premio || 'N/A'} +
+ {item.nome_comissao && ( +
+ Comissão + {item.nome_comissao} +
+ )} + {item.comissao_tipo && ( +
+ Tipo Comissão + {item.comissao_tipo} +
+ )} + {item.tipo && ( +
+ Função + {item.tipo} +
+ )} +
+ Ano + {item.ano || 'N/A'} +
+
+ Pontuação Base + {PONTOS_BASE[item.codigo] || 0} pts +
+
+ ); + + case 'inscricao': + return ( +
+
+ Código + {item.codigo} +
+
+ Prêmio + {item.premio || 'N/A'} +
+ {item.tipo && ( +
+ Tipo Inscrição + {item.tipo} +
+ )} + {item.situacao && ( +
+ Situação + {item.situacao} +
+ )} +
+ Ano + {item.ano || 'N/A'} +
+
+ Pontuação Base + {PONTOS_BASE[item.codigo] || 0} pts +
+
+ ); + + case 'participacao': + return ( +
+
+ Tipo + {item.codigo} +
+
+ Descrição + {item.descricao || item.tipo || 'N/A'} +
+
+ Ano + {item.ano || 'N/A'} +
+
+ Pontuação Base + {PONTOS_BASE[item.codigo] || 0} pts +
+
+ ); + + case 'orientacao': + return ( +
+
+ Tipo + {item.codigo} +
+
+ Categoria + + {item.codigo?.includes('TESE') ? 'Doutorado' : item.codigo?.includes('DISS') ? 'Mestrado' : 'Pós-Doutorado'} + +
+
+ Função + + {item.coorientacao || item.codigo?.startsWith('CO_') ? 'Coorientador' : 'Orientador'} + +
+ {item.premiada && ( +
+ Destaque + 🏆 Premiada +
+ )} +
+ Pontuação + Apenas selo (sem pontuação) +
+
+ ); + + default: + return

Sem detalhes disponíveis

; + } + }; + + return createPortal( +
+
e.stopPropagation()}> +
+ + {getIcone()} + {getTitulo()} + + +
+
+ {renderContent()} +
+
+
, + document.body + ); +}; + const FORMULAS = { bloco_a: { titulo: 'Coordenacao CAPES', @@ -623,6 +920,7 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio const [showRawModal, setShowRawModal] = useState(false); const [tipoAtuacaoModal, setTipoAtuacaoModal] = useState(null); const [seloModal, setSeloModal] = useState(null); + const [itemDetalhe, setItemDetalhe] = useState(null); const cardRef = useRef(null); useEffect(() => { @@ -840,7 +1138,11 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio const periodo = vinculo.periodo || {}; const isAtivo = periodo.ativo ?? !periodo.fim; return ( -
+
setItemDetalhe({ item: vinculo, tipo: 'vinculo' })} + > {isAtivo ? 'ATIVO' : 'ENCERRADO'} @@ -868,7 +1170,11 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio {[...consultor.coordenacoes_capes] .sort((a, b) => new Date(b.inicio || b.periodo?.inicio || 0) - new Date(a.inicio || a.periodo?.inicio || 0)) .map((coord, idx) => ( -
+
setItemDetalhe({ item: coord, tipo: 'coordenacao' })} + > {coord.codigo || coord.tipo} {PONTOS_BASE[coord.codigo] || 0} pts {coord.area_avaliacao} @@ -888,7 +1194,11 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio {[...consultor.premiacoes] .sort((a, b) => (b.ano || 0) - (a.ano || 0)) .map((prem, idx) => ( -
+
setItemDetalhe({ item: prem, tipo: 'premiacao' })} + > {prem.codigo} {PONTOS_BASE[prem.codigo] || 0} pts {prem.nome_premio} @@ -906,7 +1216,11 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio {[...consultor.avaliacoes_comissao] .sort((a, b) => (b.ano || 0) - (a.ano || 0)) .map((aval, idx) => ( -
+
setItemDetalhe({ item: aval, tipo: 'avaliacao' })} + > {aval.codigo} {PONTOS_BASE[aval.codigo] || 0} pts {aval.nome_comissao || aval.premio} @@ -924,7 +1238,11 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio {[...consultor.inscricoes] .sort((a, b) => (b.ano || 0) - (a.ano || 0)) .map((insc, idx) => ( -
+
setItemDetalhe({ item: insc, tipo: 'inscricao' })} + > {insc.codigo} {PONTOS_BASE[insc.codigo] || 0} pts {insc.premio} @@ -943,7 +1261,11 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio .sort((a, b) => (b.ano || 0) - (a.ano || 0)) .slice(0, 10) .map((part, idx) => ( -
+
setItemDetalhe({ item: part, tipo: 'participacao' })} + > {part.codigo} {PONTOS_BASE[part.codigo] || 0} pts {part.descricao || part.tipo} @@ -1025,6 +1347,14 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio onClose={() => setSeloModal(null)} /> )} + + {itemDetalhe && ( + setItemDetalhe(null)} + /> + )}
); });