feat(raw-data): adicionar visualização de dados brutos do ATUACAPES
- Novo endpoint GET /api/v1/consultor/{id}/raw para buscar documento completo do ES
- Novo componente RawDataModal com formatação inteligente de campos
- Botão de acesso rápido no ConsultorCard (ícone ⋮)
- Melhorias de estilo no Header e ConsultorCard
This commit is contained in:
@@ -2,7 +2,6 @@ ES_URL=http://localhost:9200
|
|||||||
ES_INDEX=atuacapes
|
ES_INDEX=atuacapes
|
||||||
ES_USER=seu_usuario_elastic
|
ES_USER=seu_usuario_elastic
|
||||||
ES_PASSWORD=sua_senha_elastic
|
ES_PASSWORD=sua_senha_elastic
|
||||||
ES_PASS=sua_senha_elastic
|
|
||||||
|
|
||||||
ORACLE_USER=seu_usuario_oracle
|
ORACLE_USER=seu_usuario_oracle
|
||||||
ORACLE_PASSWORD=sua_senha_oracle
|
ORACLE_PASSWORD=sua_senha_oracle
|
||||||
|
|||||||
@@ -68,6 +68,33 @@ class ElasticsearchClient:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"Erro ao buscar consultor {id_pessoa}: {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:
|
async def buscar_com_atuacoes(self, size: int = 1000, from_: int = 0) -> list:
|
||||||
try:
|
try:
|
||||||
query = {
|
query = {
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ def get_oracle_client() -> OracleClient:
|
|||||||
return oracle_client
|
return oracle_client
|
||||||
|
|
||||||
|
|
||||||
|
def get_es_client() -> ElasticsearchClient:
|
||||||
|
return es_client
|
||||||
|
|
||||||
|
|
||||||
def get_ranking_oracle_repo() -> RankingOracleRepository:
|
def get_ranking_oracle_repo() -> RankingOracleRepository:
|
||||||
return ranking_oracle_repo
|
return ranking_oracle_repo
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ from ..schemas.ranking_schema import (
|
|||||||
ProcessarRankingResponseSchema,
|
ProcessarRankingResponseSchema,
|
||||||
ConsultaNomeSchema,
|
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
|
from ...application.jobs.job_status import job_status
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v1", tags=["ranking"])
|
router = APIRouter(prefix="/api/v1", tags=["ranking"])
|
||||||
@@ -305,3 +306,17 @@ async def processar_ranking(
|
|||||||
mensagem="Processamento do ranking iniciado em background",
|
mensagem="Processamento do ranking iniciado em background",
|
||||||
job_id="ranking_job"
|
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))
|
||||||
|
|||||||
@@ -239,6 +239,31 @@
|
|||||||
background-clip: text;
|
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 {
|
.expand-icon {
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useRef, useEffect, useMemo, memo } from 'react';
|
import React, { useState, useRef, useEffect, useMemo, memo } from 'react';
|
||||||
import './ConsultorCard.css';
|
import './ConsultorCard.css';
|
||||||
|
import RawDataModal from './RawDataModal';
|
||||||
|
|
||||||
const SELOS = {
|
const SELOS = {
|
||||||
PRESID_CAMARA: { label: 'Presidente Camara', cor: 'selo-camara', icone: '👑' },
|
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 ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecionado }) => {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [showRawModal, setShowRawModal] = useState(false);
|
||||||
const cardRef = useRef(null);
|
const cardRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -213,6 +215,11 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
|
|||||||
onToggleSelecionado(consultor);
|
onToggleSelecionado(consultor);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRawDataClick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowRawModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
const { consultoria, pontuacao } = consultor;
|
const { consultoria, pontuacao } = consultor;
|
||||||
const blocoA = pontuacao?.bloco_a || { total: consultor.bloco_a || 0 };
|
const blocoA = pontuacao?.bloco_a || { total: consultor.bloco_a || 0 };
|
||||||
const blocoB = pontuacao?.bloco_b || { total: consultor.bloco_b || 0 };
|
const blocoB = pontuacao?.bloco_b || { total: consultor.bloco_b || 0 };
|
||||||
@@ -278,6 +285,13 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
|
|||||||
<div className="score-value">{pontuacaoTotal}</div>
|
<div className="score-value">{pontuacaoTotal}</div>
|
||||||
<div className="stat-label">Score</div>
|
<div className="stat-label">Score</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn-raw-data"
|
||||||
|
onClick={handleRawDataClick}
|
||||||
|
title="Ver dados completos do ATUACAPES"
|
||||||
|
>
|
||||||
|
⋮
|
||||||
|
</button>
|
||||||
<div className="expand-icon">{expanded ? '▲' : '▼'}</div>
|
<div className="expand-icon">{expanded ? '▲' : '▼'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -474,6 +488,14 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showRawModal && (
|
||||||
|
<RawDataModal
|
||||||
|
idPessoa={consultor.id_pessoa}
|
||||||
|
nome={consultor.nome}
|
||||||
|
onClose={() => setShowRawModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,6 +27,12 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: -0.5px;
|
letter-spacing: -0.5px;
|
||||||
margin-bottom: 0.35rem;
|
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 {
|
.subtitle {
|
||||||
@@ -34,6 +40,7 @@
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin-bottom: 0.6rem;
|
margin-bottom: 0.6rem;
|
||||||
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta {
|
.meta {
|
||||||
@@ -41,14 +48,16 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 0.6rem;
|
margin-top: 0.6rem;
|
||||||
padding: 0.45rem 0.85rem;
|
padding: 0.5rem 1rem;
|
||||||
background: rgba(0, 0, 0, 0.22);
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(96, 165, 250, 0.1));
|
||||||
border: 1px solid rgba(255,255,255,0.28);
|
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #f4f7ff;
|
color: #e0e7ff;
|
||||||
text-shadow: 0 1px 8px rgba(0,0,0,0.25);
|
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 {
|
.criteria-box {
|
||||||
@@ -61,9 +70,26 @@
|
|||||||
|
|
||||||
.criteria-box h3 {
|
.criteria-box h3 {
|
||||||
color: var(--accent-2);
|
color: var(--accent-2);
|
||||||
font-size: 0.9rem;
|
font-size: 0.95rem;
|
||||||
margin-bottom: 0.75rem;
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
text-align: center;
|
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 {
|
.criteria-grid {
|
||||||
@@ -73,10 +99,80 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.criteria-section {
|
.criteria-section {
|
||||||
background: rgba(255,255,255,0.04);
|
background: rgba(255,255,255,0.03);
|
||||||
border: 1px solid var(--stroke);
|
backdrop-filter: blur(12px);
|
||||||
border-radius: 10px;
|
-webkit-backdrop-filter: blur(12px);
|
||||||
padding: 0.9rem;
|
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 {
|
.criteria-section h4 {
|
||||||
@@ -90,12 +186,29 @@
|
|||||||
.max-pts {
|
.max-pts {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
padding: 0.15rem 0.5rem;
|
padding: 0.2rem 0.6rem;
|
||||||
background: rgba(79,70,229,0.3);
|
border-radius: 6px;
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: var(--silver);
|
font-weight: 600;
|
||||||
font-weight: 500;
|
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 {
|
.criteria-table {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const Header = ({ total }) => {
|
|||||||
<div className="criteria-box">
|
<div className="criteria-box">
|
||||||
<h3>Blocos de Pontuacao</h3>
|
<h3>Blocos de Pontuacao</h3>
|
||||||
<div className="criteria-grid">
|
<div className="criteria-grid">
|
||||||
<div className="criteria-section">
|
<div className="criteria-section bloco-a">
|
||||||
<h4>A - Coordenacao CAPES</h4>
|
<h4>A - Coordenacao CAPES</h4>
|
||||||
<span className="max-pts">max 450 pts</span>
|
<span className="max-pts">max 450 pts</span>
|
||||||
<table className="criteria-table">
|
<table className="criteria-table">
|
||||||
@@ -44,7 +44,7 @@ const Header = ({ total }) => {
|
|||||||
<div className="criteria-note">+ Retorno (20)</div>
|
<div className="criteria-note">+ Retorno (20)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="criteria-section">
|
<div className="criteria-section bloco-c">
|
||||||
<h4>C - Consultoria</h4>
|
<h4>C - Consultoria</h4>
|
||||||
<span className="max-pts">max 230 pts</span>
|
<span className="max-pts">max 230 pts</span>
|
||||||
<table className="criteria-table">
|
<table className="criteria-table">
|
||||||
@@ -61,7 +61,7 @@ const Header = ({ total }) => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="criteria-section">
|
<div className="criteria-section bloco-d">
|
||||||
<h4>D - Premiacoes e Avaliacoes</h4>
|
<h4>D - Premiacoes e Avaliacoes</h4>
|
||||||
<span className="max-pts">max 180 pts</span>
|
<span className="max-pts">max 180 pts</span>
|
||||||
<table className="criteria-table">
|
<table className="criteria-table">
|
||||||
|
|||||||
535
frontend/src/components/RawDataModal.css
Normal file
535
frontend/src/components/RawDataModal.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
420
frontend/src/components/RawDataModal.jsx
Normal file
420
frontend/src/components/RawDataModal.jsx
Normal file
@@ -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 (
|
||||||
|
<div className={`data-field ${className}`}>
|
||||||
|
<span className="data-label">{formatLabel(label)}</span>
|
||||||
|
<span className="data-value">{formattedValue}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={`nested-display depth-${depth}`}>
|
||||||
|
{entries.map(([key, value]) => {
|
||||||
|
if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
|
||||||
|
const simpleValue = value.nome || value.descricao || value.sigla;
|
||||||
|
if (simpleValue) {
|
||||||
|
return <DataField key={key} label={key} value={simpleValue} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div key={key} className="data-field nested">
|
||||||
|
<span className="data-label">{formatLabel(key)}</span>
|
||||||
|
<NestedObjectDisplay data={value} depth={depth + 1} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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 <DataField key={key} label={key} value={formatted} />;
|
||||||
|
}
|
||||||
|
return <DataField key={key} label={key} value={value} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Section = ({ title, icon, children, defaultOpen = true, count }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||||
|
return (
|
||||||
|
<div className={`data-section ${isOpen ? 'open' : ''}`}>
|
||||||
|
<div className="section-header" onClick={() => setIsOpen(!isOpen)}>
|
||||||
|
<span className="section-icon">{icon}</span>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
{count !== undefined && <span className="section-count">{count}</span>}
|
||||||
|
<span className="section-toggle">{isOpen ? '▼' : '▶'}</span>
|
||||||
|
</div>
|
||||||
|
{isOpen && <div className="section-content">{children}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={`atuacao-card ${getAtuacaoColor(tipo)}`}>
|
||||||
|
<div className="atuacao-header" onClick={() => setExpanded(!expanded)}>
|
||||||
|
<span className="atuacao-index">#{index + 1}</span>
|
||||||
|
<span className="atuacao-tipo">{tipo}</span>
|
||||||
|
<div className="atuacao-periodo">
|
||||||
|
{atuacao.inicio && <span>{formatDate(atuacao.inicio)}</span>}
|
||||||
|
{atuacao.inicio && atuacao.fim && <span> - </span>}
|
||||||
|
{atuacao.fim ? <span>{formatDate(atuacao.fim)}</span> : atuacao.inicio && <span className="ativo">Atual</span>}
|
||||||
|
</div>
|
||||||
|
<span className="atuacao-toggle">{expanded ? '−' : '+'}</span>
|
||||||
|
</div>
|
||||||
|
{expanded && (
|
||||||
|
<div className="atuacao-dados">
|
||||||
|
{hasData ? (
|
||||||
|
<NestedObjectDisplay data={dados} />
|
||||||
|
) : (
|
||||||
|
<p className="empty-message">Sem dados adicionais</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = (
|
||||||
|
<div className="raw-modal-overlay" onClick={(e) => e.target.classList.contains('raw-modal-overlay') && onClose()}>
|
||||||
|
<div className="raw-modal">
|
||||||
|
<div className="raw-modal-header">
|
||||||
|
<div className="raw-modal-title">
|
||||||
|
<h2>Dados Completos ATUACAPES</h2>
|
||||||
|
<span className="raw-modal-subtitle">{nome} (ID: {idPessoa})</span>
|
||||||
|
</div>
|
||||||
|
<div className="raw-modal-header-actions">
|
||||||
|
<div className="view-toggle">
|
||||||
|
<button
|
||||||
|
className={viewMode === 'formatted' ? 'active' : ''}
|
||||||
|
onClick={() => setViewMode('formatted')}
|
||||||
|
>
|
||||||
|
Formatado
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={viewMode === 'json' ? 'active' : ''}
|
||||||
|
onClick={() => setViewMode('json')}
|
||||||
|
>
|
||||||
|
JSON
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button className="raw-modal-close" onClick={onClose}>✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="raw-modal-content">
|
||||||
|
{loading && (
|
||||||
|
<div className="raw-modal-loading">
|
||||||
|
<div className="spinner"></div>
|
||||||
|
<span>Carregando dados do Elasticsearch...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="raw-modal-error">
|
||||||
|
<span className="error-icon">⚠</span>
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && !error && data && viewMode === 'formatted' && (
|
||||||
|
<div className="formatted-view">
|
||||||
|
<Section title="Dados Pessoais" icon="👤" defaultOpen={true}>
|
||||||
|
<div className="dados-pessoais-grid">
|
||||||
|
<DataField label="ID" value={source.id} />
|
||||||
|
<DataField label="Nome" value={dadosPessoais.nome} className="destaque" />
|
||||||
|
<DataField label="CPF" value={dadosPessoais.cpf} />
|
||||||
|
<DataField label="Email" value={dadosPessoais.email} />
|
||||||
|
<DataField label="Sexo" value={dadosPessoais.sexo} />
|
||||||
|
<DataField label="Nascimento" value={formatDate(dadosPessoais.nascimento)} />
|
||||||
|
<DataField label="Ano Óbito" value={dadosPessoais.anoObito} />
|
||||||
|
<DataField label="Nacionalidade" value={dadosPessoais.nacionalidade} />
|
||||||
|
<DataField label="País Nascimento" value={dadosPessoais.paisNascimento} />
|
||||||
|
<DataField label="UF Nascimento" value={dadosPessoais.ufNascimento} />
|
||||||
|
<DataField label="Cidade Nascimento" value={dadosPessoais.cidadeNascimento} />
|
||||||
|
<DataField label="Lattes" value={dadosPessoais.lattes} />
|
||||||
|
<DataField label="ORCID" value={dadosPessoais.orcid} />
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Atuações" icon="📋" defaultOpen={true} count={atuacoes.length}>
|
||||||
|
{atuacoes.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="atuacoes-summary">
|
||||||
|
{tiposAtuacao.map(tipo => (
|
||||||
|
<span
|
||||||
|
key={tipo}
|
||||||
|
className={`tipo-badge ${filterType === tipo ? 'active' : ''}`}
|
||||||
|
onClick={() => setFilterType(filterType === tipo ? 'all' : tipo)}
|
||||||
|
>
|
||||||
|
{tipo.replace('Histórico de ', '').substring(0, 25)}
|
||||||
|
<span className="tipo-count">{atuacoesPorTipo[tipo]}</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="atuacoes-list">
|
||||||
|
{atuacoesFiltradas.map((atuacao, idx) => (
|
||||||
|
<AtuacaoCard key={idx} atuacao={atuacao} index={idx} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{atuacoes.length === 0 && (
|
||||||
|
<p className="empty-message">Nenhuma atuação registrada</p>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Metadados Elasticsearch" icon="🔍" defaultOpen={false}>
|
||||||
|
<div className="dados-pessoais-grid">
|
||||||
|
<DataField label="Index" value={data._index} />
|
||||||
|
<DataField label="Document ID" value={data._id} />
|
||||||
|
<DataField label="Score" value={data._score} />
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && !error && data && viewMode === 'json' && (
|
||||||
|
<div className="json-view">
|
||||||
|
<div className="json-toolbar">
|
||||||
|
<button onClick={copyToClipboard} className={copyFeedback ? 'copied' : ''}>
|
||||||
|
{copyFeedback ? '✓ Copiado!' : 'Copiar JSON'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre className="json-content">{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="raw-modal-footer">
|
||||||
|
<span className="raw-modal-info">
|
||||||
|
Fonte: Elasticsearch ATUACAPES | Index: {data?._index || 'atuacapes'} | {atuacoes.length} atuações
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(modalContent, document.body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RawDataModal;
|
||||||
@@ -128,6 +128,11 @@ export const rankingService = {
|
|||||||
const response = await api.get('/ranking/selos');
|
const response = await api.get('/ranking/selos');
|
||||||
return response.data.selos;
|
return response.data.selos;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getConsultorRaw(idPessoa) {
|
||||||
|
const response = await api.get(`/consultor/${idPessoa}/raw`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
Reference in New Issue
Block a user