diff --git a/backend/.env.example b/backend/.env.example
index ce549fd..0c1fad2 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -2,7 +2,6 @@ ES_URL=http://localhost:9200
ES_INDEX=atuacapes
ES_USER=seu_usuario_elastic
ES_PASSWORD=sua_senha_elastic
-ES_PASS=sua_senha_elastic
ORACLE_USER=seu_usuario_oracle
ORACLE_PASSWORD=sua_senha_oracle
diff --git a/backend/src/infrastructure/elasticsearch/client.py b/backend/src/infrastructure/elasticsearch/client.py
index ec90a50..b078ecf 100644
--- a/backend/src/infrastructure/elasticsearch/client.py
+++ b/backend/src/infrastructure/elasticsearch/client.py
@@ -68,6 +68,33 @@ class ElasticsearchClient:
except Exception as e:
raise RuntimeError(f"Erro ao buscar consultor {id_pessoa}: {e}")
+ async def buscar_documento_completo(self, id_pessoa: int) -> Optional[dict]:
+ try:
+ query = {
+ "query": {"term": {"id": id_pessoa}},
+ "size": 1,
+ }
+
+ response = await self.client.post(
+ f"{self.url}/{self.index}/_search",
+ json=query
+ )
+ response.raise_for_status()
+
+ data = response.json()
+ hits = data.get("hits", {}).get("hits", [])
+ if hits:
+ hit = hits[0]
+ return {
+ "_index": hit.get("_index"),
+ "_id": hit.get("_id"),
+ "_score": hit.get("_score"),
+ "_source": hit.get("_source"),
+ }
+ return None
+ except Exception as e:
+ raise RuntimeError(f"Erro ao buscar documento completo {id_pessoa}: {e}")
+
async def buscar_com_atuacoes(self, size: int = 1000, from_: int = 0) -> list:
try:
query = {
diff --git a/backend/src/interface/api/dependencies.py b/backend/src/interface/api/dependencies.py
index ca958da..df8e868 100644
--- a/backend/src/interface/api/dependencies.py
+++ b/backend/src/interface/api/dependencies.py
@@ -48,6 +48,10 @@ def get_oracle_client() -> OracleClient:
return oracle_client
+def get_es_client() -> ElasticsearchClient:
+ return es_client
+
+
def get_ranking_oracle_repo() -> RankingOracleRepository:
return ranking_oracle_repo
diff --git a/backend/src/interface/api/routes.py b/backend/src/interface/api/routes.py
index 0d72a6e..8c471c4 100644
--- a/backend/src/interface/api/routes.py
+++ b/backend/src/interface/api/routes.py
@@ -21,7 +21,8 @@ from ..schemas.ranking_schema import (
ProcessarRankingResponseSchema,
ConsultaNomeSchema,
)
-from .dependencies import get_repository, get_ranking_store, get_processar_job
+from .dependencies import get_repository, get_ranking_store, get_processar_job, get_es_client
+from ...infrastructure.elasticsearch.client import ElasticsearchClient
from ...application.jobs.job_status import job_status
router = APIRouter(prefix="/api/v1", tags=["ranking"])
@@ -305,3 +306,17 @@ async def processar_ranking(
mensagem="Processamento do ranking iniciado em background",
job_id="ranking_job"
)
+
+
+@router.get("/consultor/{id_pessoa}/raw")
+async def obter_consultor_raw(
+ id_pessoa: int,
+ es_client: ElasticsearchClient = Depends(get_es_client),
+):
+ try:
+ documento = await es_client.buscar_documento_completo(id_pessoa)
+ if not documento:
+ raise HTTPException(status_code=404, detail=f"Consultor {id_pessoa} não encontrado no Elasticsearch")
+ return documento
+ except RuntimeError as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/frontend/src/components/ConsultorCard.css b/frontend/src/components/ConsultorCard.css
index 2ed7660..b555ace 100644
--- a/frontend/src/components/ConsultorCard.css
+++ b/frontend/src/components/ConsultorCard.css
@@ -239,6 +239,31 @@
background-clip: text;
}
+.btn-raw-data {
+ padding: 0.4rem 0.6rem;
+ background: rgba(99, 102, 241, 0.1);
+ border: 1px solid rgba(99, 102, 241, 0.3);
+ border-radius: 6px;
+ color: #a5b4fc;
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
+ font-size: 0.75rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ margin-left: 0.5rem;
+}
+
+.btn-raw-data:hover {
+ background: rgba(99, 102, 241, 0.25);
+ border-color: rgba(99, 102, 241, 0.5);
+ color: #c7d2fe;
+ transform: scale(1.05);
+}
+
+.btn-raw-data:active {
+ transform: scale(0.95);
+}
+
.expand-icon {
margin-left: 0.5rem;
color: var(--muted);
diff --git a/frontend/src/components/ConsultorCard.jsx b/frontend/src/components/ConsultorCard.jsx
index 42a6945..a502429 100644
--- a/frontend/src/components/ConsultorCard.jsx
+++ b/frontend/src/components/ConsultorCard.jsx
@@ -1,5 +1,6 @@
import React, { useState, useRef, useEffect, useMemo, memo } from 'react';
import './ConsultorCard.css';
+import RawDataModal from './RawDataModal';
const SELOS = {
PRESID_CAMARA: { label: 'Presidente Camara', cor: 'selo-camara', icone: '👑' },
@@ -185,6 +186,7 @@ const ScoreItemWithTooltip = ({ value, label, formula, style }) => (
const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecionado }) => {
const [expanded, setExpanded] = useState(false);
+ const [showRawModal, setShowRawModal] = useState(false);
const cardRef = useRef(null);
useEffect(() => {
@@ -213,6 +215,11 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
onToggleSelecionado(consultor);
};
+ const handleRawDataClick = (e) => {
+ e.stopPropagation();
+ setShowRawModal(true);
+ };
+
const { consultoria, pontuacao } = consultor;
const blocoA = pontuacao?.bloco_a || { total: consultor.bloco_a || 0 };
const blocoB = pontuacao?.bloco_b || { total: consultor.bloco_b || 0 };
@@ -278,6 +285,13 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
{pontuacaoTotal}
Score
+
{expanded ? '▲' : '▼'}
@@ -474,6 +488,14 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
)}
)}
+
+ {showRawModal && (
+ setShowRawModal(false)}
+ />
+ )}
);
});
diff --git a/frontend/src/components/Header.css b/frontend/src/components/Header.css
index 3355293..9ba4a15 100644
--- a/frontend/src/components/Header.css
+++ b/frontend/src/components/Header.css
@@ -27,6 +27,12 @@
font-weight: 700;
letter-spacing: -0.5px;
margin-bottom: 0.35rem;
+ background: linear-gradient(135deg, #fff 0%, #a5b4fc 50%, #60a5fa 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ text-shadow: none;
+ filter: drop-shadow(0 0 20px rgba(99, 102, 241, 0.4)) drop-shadow(0 0 40px rgba(96, 165, 250, 0.2));
}
.subtitle {
@@ -34,6 +40,7 @@
font-size: 0.95rem;
line-height: 1.6;
margin-bottom: 0.6rem;
+ opacity: 0.85;
}
.meta {
@@ -41,14 +48,16 @@
align-items: center;
gap: 0.5rem;
margin-top: 0.6rem;
- padding: 0.45rem 0.85rem;
- background: rgba(0, 0, 0, 0.22);
- border: 1px solid rgba(255,255,255,0.28);
+ padding: 0.5rem 1rem;
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(96, 165, 250, 0.1));
+ border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 999px;
font-size: 0.9rem;
font-weight: 600;
- color: #f4f7ff;
- text-shadow: 0 1px 8px rgba(0,0,0,0.25);
+ color: #e0e7ff;
+ box-shadow: 0 0 20px rgba(99, 102, 241, 0.15), inset 0 1px 0 rgba(255,255,255,0.1);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
}
.criteria-box {
@@ -61,9 +70,26 @@
.criteria-box h3 {
color: var(--accent-2);
- font-size: 0.9rem;
- margin-bottom: 0.75rem;
+ font-size: 0.95rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
text-align: center;
+ text-transform: uppercase;
+ letter-spacing: 1.5px;
+ position: relative;
+ display: inline-block;
+ width: 100%;
+}
+
+.criteria-box h3::after {
+ content: "";
+ position: absolute;
+ bottom: -6px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 60px;
+ height: 2px;
+ background: linear-gradient(90deg, transparent, var(--accent-2), transparent);
}
.criteria-grid {
@@ -73,10 +99,80 @@
}
.criteria-section {
- background: rgba(255,255,255,0.04);
- border: 1px solid var(--stroke);
- border-radius: 10px;
- padding: 0.9rem;
+ background: rgba(255,255,255,0.03);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border: 1px solid rgba(255,255,255,0.08);
+ border-radius: 14px;
+ padding: 1rem;
+ position: relative;
+ overflow: hidden;
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.criteria-section::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ border-radius: 14px 14px 0 0;
+}
+
+.criteria-section:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+}
+
+.criteria-section.bloco-a {
+ border-color: rgba(99, 102, 241, 0.3);
+ background: linear-gradient(180deg, rgba(99, 102, 241, 0.08) 0%, rgba(255,255,255,0.02) 100%);
+ box-shadow: 0 4px 20px rgba(99, 102, 241, 0.1), inset 0 1px 0 rgba(255,255,255,0.05);
+}
+
+.criteria-section.bloco-a::before {
+ background: linear-gradient(90deg, #6366f1, #818cf8, #6366f1);
+}
+
+.criteria-section.bloco-a h4 {
+ color: #a5b4fc;
+}
+
+.criteria-section.bloco-c {
+ border-color: rgba(234, 179, 8, 0.3);
+ background: linear-gradient(180deg, rgba(234, 179, 8, 0.08) 0%, rgba(255,255,255,0.02) 100%);
+ box-shadow: 0 4px 20px rgba(234, 179, 8, 0.1), inset 0 1px 0 rgba(255,255,255,0.05);
+}
+
+.criteria-section.bloco-c::before {
+ background: linear-gradient(90deg, #eab308, #fbbf24, #eab308);
+}
+
+.criteria-section.bloco-c h4 {
+ color: #fcd34d;
+}
+
+.criteria-section.bloco-c .pts-value {
+ color: #fcd34d !important;
+}
+
+.criteria-section.bloco-d {
+ border-color: rgba(217, 119, 6, 0.3);
+ background: linear-gradient(180deg, rgba(217, 119, 6, 0.08) 0%, rgba(255,255,255,0.02) 100%);
+ box-shadow: 0 4px 20px rgba(217, 119, 6, 0.1), inset 0 1px 0 rgba(255,255,255,0.05);
+}
+
+.criteria-section.bloco-d::before {
+ background: linear-gradient(90deg, #d97706, #f59e0b, #d97706);
+}
+
+.criteria-section.bloco-d h4 {
+ color: #fbbf24;
+}
+
+.criteria-section.bloco-d .pts-value {
+ color: #fbbf24 !important;
}
.criteria-section h4 {
@@ -90,12 +186,29 @@
.max-pts {
display: inline-block;
margin-left: 0.5rem;
- padding: 0.15rem 0.5rem;
- background: rgba(79,70,229,0.3);
- border-radius: 4px;
+ padding: 0.2rem 0.6rem;
+ border-radius: 6px;
font-size: 0.7rem;
- color: var(--silver);
- font-weight: 500;
+ font-weight: 600;
+ letter-spacing: 0.3px;
+}
+
+.bloco-a .max-pts {
+ background: rgba(99, 102, 241, 0.25);
+ color: #c7d2fe;
+ border: 1px solid rgba(99, 102, 241, 0.4);
+}
+
+.bloco-c .max-pts {
+ background: rgba(234, 179, 8, 0.2);
+ color: #fef08a;
+ border: 1px solid rgba(234, 179, 8, 0.4);
+}
+
+.bloco-d .max-pts {
+ background: rgba(217, 119, 6, 0.2);
+ color: #fed7aa;
+ border: 1px solid rgba(217, 119, 6, 0.4);
}
.criteria-table {
diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx
index 6da873b..e5cbcf9 100644
--- a/frontend/src/components/Header.jsx
+++ b/frontend/src/components/Header.jsx
@@ -22,7 +22,7 @@ const Header = ({ total }) => {
Blocos de Pontuacao
-
+
A - Coordenacao CAPES
max 450 pts
@@ -44,7 +44,7 @@ const Header = ({ total }) => {
+ Retorno (20)
-
+
C - Consultoria
max 230 pts
@@ -61,7 +61,7 @@ const Header = ({ total }) => {
-
+
D - Premiacoes e Avaliacoes
max 180 pts
diff --git a/frontend/src/components/RawDataModal.css b/frontend/src/components/RawDataModal.css
new file mode 100644
index 0000000..81b3095
--- /dev/null
+++ b/frontend/src/components/RawDataModal.css
@@ -0,0 +1,535 @@
+.raw-modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.85);
+ backdrop-filter: blur(8px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 99999;
+ padding: 1rem;
+}
+
+.raw-modal {
+ background: linear-gradient(180deg, #1a1d2e 0%, #12141f 100%);
+ border: 1px solid rgba(99, 102, 241, 0.3);
+ border-radius: 16px;
+ width: 95%;
+ max-width: 1100px;
+ max-height: 90vh;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0 25px 80px rgba(0, 0, 0, 0.8), 0 0 60px rgba(99, 102, 241, 0.2);
+ overflow: hidden;
+}
+
+.raw-modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1.25rem 1.5rem;
+ background: linear-gradient(90deg, rgba(99, 102, 241, 0.15), rgba(96, 165, 250, 0.1));
+ border-bottom: 1px solid rgba(99, 102, 241, 0.2);
+}
+
+.raw-modal-title h2 {
+ margin: 0;
+ font-size: 1.3rem;
+ font-weight: 600;
+ color: #fff;
+}
+
+.raw-modal-subtitle {
+ font-size: 0.9rem;
+ color: #94a3b8;
+ margin-top: 0.25rem;
+ display: block;
+}
+
+.raw-modal-header-actions {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.view-toggle {
+ display: flex;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 8px;
+ padding: 3px;
+}
+
+.view-toggle button {
+ padding: 0.5rem 1rem;
+ border: none;
+ background: transparent;
+ color: #94a3b8;
+ font-size: 0.85rem;
+ font-weight: 500;
+ cursor: pointer;
+ border-radius: 6px;
+ transition: all 0.2s ease;
+}
+
+.view-toggle button:hover {
+ color: #fff;
+}
+
+.view-toggle button.active {
+ background: rgba(99, 102, 241, 0.3);
+ color: #c7d2fe;
+}
+
+.raw-modal-close {
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ color: #94a3b8;
+ width: 40px;
+ height: 40px;
+ border-radius: 10px;
+ cursor: pointer;
+ font-size: 1.2rem;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.raw-modal-close:hover {
+ background: rgba(239, 68, 68, 0.2);
+ border-color: rgba(239, 68, 68, 0.4);
+ color: #fca5a5;
+}
+
+.raw-modal-content {
+ flex: 1;
+ overflow: auto;
+ padding: 1.5rem;
+ min-height: 400px;
+}
+
+.raw-modal-loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 1rem;
+ padding: 4rem;
+ color: #94a3b8;
+}
+
+.spinner {
+ width: 48px;
+ height: 48px;
+ border: 4px solid rgba(99, 102, 241, 0.2);
+ border-top-color: #6366f1;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.raw-modal-error {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1.25rem 1.5rem;
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid rgba(239, 68, 68, 0.3);
+ border-radius: 10px;
+ color: #fca5a5;
+}
+
+.error-icon {
+ font-size: 1.75rem;
+}
+
+.raw-modal-footer {
+ padding: 0.85rem 1.5rem;
+ background: rgba(0, 0, 0, 0.3);
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.raw-modal-info {
+ font-size: 0.8rem;
+ color: #64748b;
+}
+
+.formatted-view {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.data-section {
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 12px;
+ overflow: hidden;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 1rem 1.25rem;
+ background: rgba(255, 255, 255, 0.02);
+ cursor: pointer;
+ transition: background 0.2s ease;
+}
+
+.section-header:hover {
+ background: rgba(255, 255, 255, 0.05);
+}
+
+.section-icon {
+ font-size: 1.25rem;
+}
+
+.section-header h3 {
+ margin: 0;
+ font-size: 1rem;
+ font-weight: 600;
+ color: #e2e8f0;
+ flex: 1;
+}
+
+.section-count {
+ background: rgba(99, 102, 241, 0.2);
+ color: #a5b4fc;
+ padding: 0.25rem 0.6rem;
+ border-radius: 20px;
+ font-size: 0.8rem;
+ font-weight: 600;
+}
+
+.section-toggle {
+ color: #64748b;
+ font-size: 0.75rem;
+ transition: transform 0.2s ease;
+}
+
+.data-section.open .section-toggle {
+ transform: rotate(0deg);
+}
+
+.section-content {
+ padding: 1.25rem;
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.dados-pessoais-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: 1rem;
+}
+
+.data-field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.data-field.destaque .data-value {
+ font-size: 1.1rem;
+ color: #fff;
+ font-weight: 600;
+}
+
+.data-label {
+ font-size: 0.75rem;
+ color: #64748b;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.data-value {
+ font-size: 0.95rem;
+ color: #cbd5e1;
+ word-break: break-word;
+}
+
+.data-value.array {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.array-item {
+ background: rgba(99, 102, 241, 0.1);
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.85rem;
+}
+
+.nested-object {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+ gap: 0.75rem;
+ padding: 0.75rem;
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 8px;
+ margin-top: 0.5rem;
+}
+
+.nested-display {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 1rem;
+}
+
+.nested-display.depth-1,
+.nested-display.depth-2 {
+ background: rgba(0, 0, 0, 0.15);
+ padding: 0.75rem;
+ border-radius: 8px;
+ margin-top: 0.5rem;
+}
+
+.atuacoes-summary {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+ padding-bottom: 1rem;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.tipo-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.4rem 0.75rem;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 20px;
+ font-size: 0.75rem;
+ color: #94a3b8;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.tipo-badge:hover {
+ background: rgba(99, 102, 241, 0.15);
+ border-color: rgba(99, 102, 241, 0.3);
+ color: #c7d2fe;
+}
+
+.tipo-badge.active {
+ background: rgba(99, 102, 241, 0.25);
+ border-color: rgba(99, 102, 241, 0.5);
+ color: #c7d2fe;
+}
+
+.tipo-count {
+ background: rgba(0, 0, 0, 0.3);
+ padding: 0.15rem 0.4rem;
+ border-radius: 10px;
+ font-size: 0.7rem;
+ font-weight: 600;
+}
+
+.clear-filter {
+ padding: 0.4rem 0.75rem;
+ background: rgba(239, 68, 68, 0.15);
+ border: 1px solid rgba(239, 68, 68, 0.3);
+ border-radius: 20px;
+ font-size: 0.75rem;
+ color: #fca5a5;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.clear-filter:hover {
+ background: rgba(239, 68, 68, 0.25);
+}
+
+.atuacoes-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.atuacao-card {
+ background: rgba(255, 255, 255, 0.02);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ border-radius: 10px;
+ overflow: hidden;
+ transition: all 0.2s ease;
+}
+
+.atuacao-card:hover {
+ border-color: rgba(255, 255, 255, 0.12);
+}
+
+.atuacao-card.atuacao-coordenacao { border-left: 3px solid #6366f1; }
+.atuacao-card.atuacao-consultoria { border-left: 3px solid #eab308; }
+.atuacao-card.atuacao-premiacao { border-left: 3px solid #22c55e; }
+.atuacao-card.atuacao-avaliacao { border-left: 3px solid #f97316; }
+.atuacao-card.atuacao-inscricao { border-left: 3px solid #06b6d4; }
+.atuacao-card.atuacao-bolsa { border-left: 3px solid #ec4899; }
+.atuacao-card.atuacao-orientacao { border-left: 3px solid #8b5cf6; }
+.atuacao-card.atuacao-outros { border-left: 3px solid #64748b; }
+
+.atuacao-header {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 0.85rem 1rem;
+ cursor: pointer;
+ transition: background 0.2s ease;
+}
+
+.atuacao-header:hover {
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.atuacao-index {
+ font-size: 0.75rem;
+ color: #64748b;
+ font-weight: 600;
+ min-width: 35px;
+}
+
+.atuacao-tipo {
+ flex: 1;
+ font-size: 0.9rem;
+ color: #e2e8f0;
+ font-weight: 500;
+}
+
+.atuacao-periodo {
+ font-size: 0.8rem;
+ color: #94a3b8;
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+.atuacao-periodo .ativo {
+ color: #4ade80;
+ font-weight: 600;
+}
+
+.atuacao-toggle {
+ color: #64748b;
+ font-size: 1.1rem;
+ font-weight: 300;
+ width: 24px;
+ text-align: center;
+}
+
+.atuacao-dados {
+ padding: 1rem;
+ background: rgba(0, 0, 0, 0.2);
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 1rem;
+}
+
+.atuacao-dados .data-field.nested {
+ grid-column: 1 / -1;
+}
+
+.empty-message {
+ text-align: center;
+ color: #64748b;
+ padding: 2rem;
+ font-style: italic;
+}
+
+.json-view {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.json-toolbar {
+ display: flex;
+ justify-content: flex-end;
+}
+
+.json-toolbar button {
+ padding: 0.5rem 1rem;
+ background: rgba(99, 102, 241, 0.15);
+ border: 1px solid rgba(99, 102, 241, 0.3);
+ border-radius: 6px;
+ color: #c7d2fe;
+ font-size: 0.85rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.json-toolbar button:hover {
+ background: rgba(99, 102, 241, 0.25);
+}
+
+.json-toolbar button.copied {
+ background: rgba(34, 197, 94, 0.2);
+ border-color: rgba(34, 197, 94, 0.4);
+ color: #86efac;
+}
+
+.json-content {
+ background: rgba(0, 0, 0, 0.3);
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ border-radius: 10px;
+ padding: 1.25rem;
+ overflow: auto;
+ max-height: 60vh;
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
+ font-size: 0.8rem;
+ line-height: 1.6;
+ color: #94a3b8;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+@media (max-width: 768px) {
+ .raw-modal {
+ width: 100%;
+ max-height: 100vh;
+ border-radius: 0;
+ }
+
+ .raw-modal-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1rem;
+ }
+
+ .raw-modal-header-actions {
+ width: 100%;
+ justify-content: space-between;
+ }
+
+ .dados-pessoais-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .atuacao-header {
+ flex-wrap: wrap;
+ }
+
+ .atuacao-tipo {
+ order: 1;
+ flex: 1 1 100%;
+ }
+
+ .atuacao-index {
+ order: 2;
+ }
+
+ .atuacao-periodo {
+ order: 3;
+ flex: 1;
+ }
+
+ .atuacao-toggle {
+ order: 4;
+ }
+}
diff --git a/frontend/src/components/RawDataModal.jsx b/frontend/src/components/RawDataModal.jsx
new file mode 100644
index 0000000..1f59872
--- /dev/null
+++ b/frontend/src/components/RawDataModal.jsx
@@ -0,0 +1,420 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import ReactDOM from 'react-dom';
+import { rankingService } from '../services/api';
+import './RawDataModal.css';
+
+const decodeHtmlEntities = (str) => {
+ if (typeof str !== 'string') return str;
+ const textarea = document.createElement('textarea');
+ textarea.innerHTML = str;
+ return textarea.value;
+};
+
+const formatDate = (dateStr) => {
+ if (!dateStr) return null;
+ try {
+ if (dateStr.includes('/')) {
+ return dateStr.split(' ')[0];
+ }
+ const date = new Date(dateStr);
+ return date.toLocaleDateString('pt-BR');
+ } catch {
+ return dateStr;
+ }
+};
+
+const formatValue = (value) => {
+ if (value === null || value === undefined || value === '') return null;
+ if (typeof value === 'boolean') return value ? 'Sim' : 'Não';
+ if (typeof value === 'number') return String(value);
+ if (typeof value === 'string') return decodeHtmlEntities(value);
+ if (Array.isArray(value)) {
+ if (value.length === 0) return null;
+ return value.map(item => {
+ if (typeof item === 'object' && item !== null) {
+ const val = item.nome || item.descricao || item.sigla || item.tipo || JSON.stringify(item);
+ return decodeHtmlEntities(val);
+ }
+ return decodeHtmlEntities(String(item));
+ }).join(', ');
+ }
+ if (typeof value === 'object') {
+ if (value.nome) return decodeHtmlEntities(value.nome);
+ if (value.descricao) return decodeHtmlEntities(value.descricao);
+ if (value.sigla) return decodeHtmlEntities(value.sigla);
+ return null;
+ }
+ return decodeHtmlEntities(String(value));
+};
+
+const LABEL_MAP = {
+ tipo: 'Tipo',
+ nome: 'Nome',
+ sigla: 'Sigla',
+ codigo: 'Código',
+ situacao: 'Situação',
+ situacaoConsultoria: 'Situação',
+ areaAvaliacao: 'Área de Avaliação',
+ areaConhecimento: 'Área de Conhecimento',
+ areaConhecimentoPos: 'Área de Conhecimento Pós',
+ areaPesquisa: 'Área de Pesquisa',
+ colegio: 'Colégio',
+ ies: 'IES',
+ programa: 'Programa',
+ inicioVinculacao: 'Início Vinculação',
+ fimVinculacao: 'Fim Vinculação',
+ inicioSituacao: 'Início Situação',
+ inativacaoSituacao: 'Inativação',
+ portaria: 'Portaria',
+ dataPortaria: 'Data Portaria',
+ premio: 'Prêmio',
+ evento: 'Evento',
+ premiacao: 'Premiação',
+ ano: 'Ano',
+ edicao: 'Edição',
+ papelPessoa: 'Papel',
+ comissao: 'Comissão',
+ produto: 'Produto',
+ nivel: 'Nível',
+ modalidade: 'Modalidade',
+ camaraTematica: 'Câmara Temática',
+};
+
+const formatLabel = (key) => LABEL_MAP[key] || key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase());
+
+const DataField = ({ label, value, className = '' }) => {
+ const formattedValue = formatValue(value);
+ if (formattedValue === null) return null;
+ return (
+
+ {formatLabel(label)}
+ {formattedValue}
+
+ );
+};
+
+const NestedObjectDisplay = ({ data, depth = 0 }) => {
+ if (!data || typeof data !== 'object') return null;
+
+ const entries = Object.entries(data).filter(([key, value]) => {
+ if (value === null || value === undefined || value === '') return false;
+ if (Array.isArray(value) && value.length === 0) return false;
+ return true;
+ });
+
+ if (entries.length === 0) return null;
+
+ return (
+
+ {entries.map(([key, value]) => {
+ if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
+ const simpleValue = value.nome || value.descricao || value.sigla;
+ if (simpleValue) {
+ return
;
+ }
+ return (
+
+ {formatLabel(key)}
+
+
+ );
+ }
+ if (Array.isArray(value)) {
+ const formatted = value.map(item => {
+ if (typeof item === 'object' && item !== null) {
+ return item.nome || item.descricao || item.sigla || item.tipo || Object.values(item).filter(v => typeof v === 'string')[0] || '';
+ }
+ return String(item);
+ }).filter(Boolean).join(', ');
+ if (!formatted) return null;
+ return
;
+ }
+ return
;
+ })}
+
+ );
+};
+
+const Section = ({ title, icon, children, defaultOpen = true, count }) => {
+ const [isOpen, setIsOpen] = useState(defaultOpen);
+ return (
+
+
setIsOpen(!isOpen)}>
+ {icon}
+
{title}
+ {count !== undefined && {count}}
+ {isOpen ? '▼' : '▶'}
+
+ {isOpen &&
{children}
}
+
+ );
+};
+
+const AtuacaoCard = ({ atuacao, index }) => {
+ const [expanded, setExpanded] = useState(false);
+ const tipo = atuacao.tipo || 'Tipo não informado';
+
+ const getAtuacaoColor = (tipo) => {
+ if (tipo.includes('Coordenação')) return 'atuacao-coordenacao';
+ if (tipo.includes('Consultor')) return 'atuacao-consultoria';
+ if (tipo.includes('Premiação')) return 'atuacao-premiacao';
+ if (tipo.includes('Avaliação')) return 'atuacao-avaliacao';
+ if (tipo.includes('Inscrição')) return 'atuacao-inscricao';
+ if (tipo.includes('Bolsista')) return 'atuacao-bolsa';
+ if (tipo.includes('Orientação')) return 'atuacao-orientacao';
+ return 'atuacao-outros';
+ };
+
+ const getAllDados = () => {
+ const allData = {};
+
+ const dataKeys = [
+ 'dadosCoordenacaoArea',
+ 'dadosHistoricoCoordenacaoArea',
+ 'dadosConsultoria',
+ 'dadosPremiacaoPremio',
+ 'dadosParticipacaoPremio',
+ 'dadosParticipacaoInscricaoPremio',
+ 'dadosBolsistaCNPq',
+ 'dadosOrientacao',
+ 'dadosParticipacao',
+ 'dadosGestaoPrograma',
+ ];
+
+ const dateKeys = ['inicio', 'fim', 'inicioVinculacao', 'fimVinculacao', 'inicioSituacao', 'inativacaoSituacao'];
+ const dateData = {};
+
+ dataKeys.forEach(key => {
+ if (atuacao[key]) {
+ Object.entries(atuacao[key]).forEach(([k, v]) => {
+ if (dateKeys.includes(k)) {
+ dateData[k] = v;
+ } else {
+ allData[k] = v;
+ }
+ });
+ }
+ });
+
+ if (Object.keys(allData).length === 0) {
+ if (atuacao.inicio) allData.inicio = atuacao.inicio;
+ if (atuacao.fim) allData.fim = atuacao.fim;
+ Object.assign(allData, dateData);
+ }
+
+ return allData;
+ };
+
+ const dados = getAllDados();
+ const hasData = Object.keys(dados).length > 0;
+
+ return (
+
+
setExpanded(!expanded)}>
+
#{index + 1}
+
{tipo}
+
+ {atuacao.inicio && {formatDate(atuacao.inicio)}}
+ {atuacao.inicio && atuacao.fim && - }
+ {atuacao.fim ? {formatDate(atuacao.fim)} : atuacao.inicio && Atual}
+
+
{expanded ? '−' : '+'}
+
+ {expanded && (
+
+ {hasData ? (
+
+ ) : (
+
Sem dados adicionais
+ )}
+
+ )}
+
+ );
+};
+
+const RawDataModal = ({ idPessoa, nome, onClose }) => {
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [viewMode, setViewMode] = useState('formatted');
+ const [filterType, setFilterType] = useState('all');
+ const [copyFeedback, setCopyFeedback] = useState(false);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const result = await rankingService.getConsultorRaw(idPessoa);
+ setData(result);
+ } catch (err) {
+ setError(err.response?.data?.detail || err.message || 'Erro ao carregar dados');
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchData();
+ }, [idPessoa]);
+
+ const handleKeyDown = useCallback((e) => {
+ if (e.key === 'Escape') onClose();
+ }, [onClose]);
+
+ useEffect(() => {
+ document.addEventListener('keydown', handleKeyDown);
+ document.body.style.overflow = 'hidden';
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown);
+ document.body.style.overflow = '';
+ };
+ }, [handleKeyDown]);
+
+ const copyToClipboard = async () => {
+ try {
+ await navigator.clipboard.writeText(JSON.stringify(data, null, 2));
+ setCopyFeedback(true);
+ setTimeout(() => setCopyFeedback(false), 2000);
+ } catch (err) {
+ console.error('Erro ao copiar:', err);
+ }
+ };
+
+ const source = data?._source || {};
+ const dadosPessoais = source.dadosPessoais || {};
+ const atuacoes = source.atuacoes || [];
+
+ const tiposAtuacao = [...new Set(atuacoes.map(a => a.tipo))].sort();
+ const atuacoesFiltradas = filterType === 'all'
+ ? atuacoes
+ : atuacoes.filter(a => a.tipo === filterType);
+
+ const atuacoesPorTipo = tiposAtuacao.reduce((acc, tipo) => {
+ acc[tipo] = atuacoes.filter(a => a.tipo === tipo).length;
+ return acc;
+ }, {});
+
+ const modalContent = (
+ e.target.classList.contains('raw-modal-overlay') && onClose()}>
+
+
+
+
Dados Completos ATUACAPES
+ {nome} (ID: {idPessoa})
+
+
+
+
+
+
+
+
+
+
+
+ {loading && (
+
+
+
Carregando dados do Elasticsearch...
+
+ )}
+
+ {error && (
+
+ ⚠
+ {error}
+
+ )}
+
+ {!loading && !error && data && viewMode === 'formatted' && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {atuacoes.length > 0 && (
+ <>
+
+ {tiposAtuacao.map(tipo => (
+ setFilterType(filterType === tipo ? 'all' : tipo)}
+ >
+ {tipo.replace('Histórico de ', '').substring(0, 25)}
+ {atuacoesPorTipo[tipo]}
+
+ ))}
+
+
+ {atuacoesFiltradas.map((atuacao, idx) => (
+
+ ))}
+
+ >
+ )}
+ {atuacoes.length === 0 && (
+ Nenhuma atuação registrada
+ )}
+
+
+
+
+ )}
+
+ {!loading && !error && data && viewMode === 'json' && (
+
+
+
+
+
{JSON.stringify(data, null, 2)}
+
+ )}
+
+
+
+
+ Fonte: Elasticsearch ATUACAPES | Index: {data?._index || 'atuacapes'} | {atuacoes.length} atuações
+
+
+
+
+ );
+
+ return ReactDOM.createPortal(modalContent, document.body);
+};
+
+export default RawDataModal;
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index 5da7572..c85ca13 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -128,6 +128,11 @@ export const rankingService = {
const response = await api.get('/ranking/selos');
return response.data.selos;
},
+
+ async getConsultorRaw(idPessoa) {
+ const response = await api.get(`/consultor/${idPessoa}/raw`);
+ return response.data;
+ },
};
export default api;