From c388509acb2f507ca91d22d03fed5c7d71b1c8e2 Mon Sep 17 00:00:00 2001 From: Frederico Castro Date: Wed, 17 Dec 2025 23:28:11 -0300 Subject: [PATCH] =?UTF-8?q?feat(raw-data):=20adicionar=20visualiza=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20dados=20brutos=20do=20ATUACAPES?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/.env.example | 1 - .../infrastructure/elasticsearch/client.py | 27 + backend/src/interface/api/dependencies.py | 4 + backend/src/interface/api/routes.py | 17 +- frontend/src/components/ConsultorCard.css | 25 + frontend/src/components/ConsultorCard.jsx | 22 + frontend/src/components/Header.css | 145 ++++- frontend/src/components/Header.jsx | 6 +- frontend/src/components/RawDataModal.css | 535 ++++++++++++++++++ frontend/src/components/RawDataModal.jsx | 420 ++++++++++++++ frontend/src/services/api.js | 5 + 11 files changed, 1186 insertions(+), 21 deletions(-) create mode 100644 frontend/src/components/RawDataModal.css create mode 100644 frontend/src/components/RawDataModal.jsx 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;