diff --git a/backend/src/application/mappers/ranking_mapper.py b/backend/src/application/mappers/ranking_mapper.py index 455078f..70cde22 100644 --- a/backend/src/application/mappers/ranking_mapper.py +++ b/backend/src/application/mappers/ranking_mapper.py @@ -1,14 +1,47 @@ import json import logging -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional from ...domain.entities.consultor_ranking import ConsultorRanking from ...interface.schemas.ranking_schema import ConsultorRankingResumoSchema logger = logging.getLogger(__name__) +TIPOS_ATUACAO_ORDEM = [ + ("Coordenador", "coordenacoes_capes"), + ("Consultor", "consultoria"), + ("Avaliador", "avaliacoes_comissao"), + ("Premiado", "premiacoes"), + ("Orientador", "orientacoes"), + ("Bolsista CNPq", "bolsas_cnpq"), + ("Inscrito Premio", "inscricoes"), + ("Projeto", "participacoes_projeto"), + ("Evento", "participacoes_evento"), +] + class RankingMapper: + @staticmethod + def _extrair_tipos_atuacao(jd: Dict[str, Any]) -> List[str]: + tipos = [] + for label, campo in TIPOS_ATUACAO_ORDEM: + if campo == "consultoria": + if jd.get("consultoria"): + tipos.append(label) + elif campo == "participacoes_projeto": + participacoes = jd.get("participacoes") or [] + if any(p.get("codigo") == "PROJ" for p in participacoes): + tipos.append(label) + elif campo == "participacoes_evento": + participacoes = jd.get("participacoes") or [] + if any(p.get("codigo") == "EVENTO" for p in participacoes): + tipos.append(label) + else: + dados = jd.get(campo) + if dados and (isinstance(dados, list) and len(dados) > 0): + tipos.append(label) + return tipos + @staticmethod def consultor_ranking_to_schema(c: ConsultorRanking) -> ConsultorRankingResumoSchema: consultoria = None @@ -21,6 +54,7 @@ class RankingMapper: orientacoes = None membros_banca = None pontuacao = None + tipos_atuacao = [] try: jd = json.loads(c.json_detalhes) if c.json_detalhes else {} @@ -35,6 +69,7 @@ class RankingMapper: orientacoes = jd.get("orientacoes") membros_banca = jd.get("membros_banca") pontuacao = jd.get("pontuacao") + tipos_atuacao = RankingMapper._extrair_tipos_atuacao(jd) except (json.JSONDecodeError, TypeError) as e: logger.warning(f"Erro ao parsear json_detalhes do consultor {c.id_pessoa}: {e}") @@ -59,6 +94,7 @@ class RankingMapper: bloco_d=bloco_d, ativo=c.ativo, anos_atuacao=c.anos_atuacao, + tipos_atuacao=tipos_atuacao, consultoria=consultoria, coordenacoes_capes=coordenacoes_capes, inscricoes=inscricoes, diff --git a/backend/src/interface/api/routes.py b/backend/src/interface/api/routes.py index 332c8c2..0c9d8eb 100644 --- a/backend/src/interface/api/routes.py +++ b/backend/src/interface/api/routes.py @@ -21,6 +21,7 @@ def normalizar_texto(texto: str) -> str: from ...application.use_cases.obter_ranking import ObterRankingUseCase from ...application.use_cases.obter_consultor import ObterConsultorUseCase from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl +from ...application.mappers.ranking_mapper import RankingMapper from ..schemas.consultor_schema import ( RankingResponseSchema, RankingDetalhadoResponseSchema, @@ -183,6 +184,7 @@ async def ranking_paginado( consultores_schema = [] for e in entries: d = e.detalhes + tipos_atuacao = RankingMapper._extrair_tipos_atuacao(d) consultores_schema.append( ConsultorRankingResumoSchema( id_pessoa=e.id_pessoa, @@ -195,6 +197,7 @@ async def ranking_paginado( bloco_d=float(e.bloco_d), ativo=e.ativo, anos_atuacao=float(e.anos_atuacao), + tipos_atuacao=tipos_atuacao, coordenador_ppg=bool(d.get("coordenador_ppg", False)), consultoria=d.get("consultoria"), coordenacoes_capes=d.get("coordenacoes_capes"), diff --git a/backend/src/interface/schemas/ranking_schema.py b/backend/src/interface/schemas/ranking_schema.py index 32689da..faead5c 100644 --- a/backend/src/interface/schemas/ranking_schema.py +++ b/backend/src/interface/schemas/ranking_schema.py @@ -14,6 +14,7 @@ class ConsultorRankingResumoSchema(BaseModel): bloco_d: float ativo: bool anos_atuacao: float + tipos_atuacao: List[str] = [] coordenador_ppg: Optional[bool] = None consultoria: Optional[dict] = None coordenacoes_capes: Optional[list] = None diff --git a/frontend/src/components/ConsultorCard.css b/frontend/src/components/ConsultorCard.css index 8e97c07..53c3e96 100644 --- a/frontend/src/components/ConsultorCard.css +++ b/frontend/src/components/ConsultorCard.css @@ -124,6 +124,8 @@ font-size: 1.35rem; white-space: nowrap; line-height: 1; + color: #ffffff; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); } .rank-number.rank-digits-4 { @@ -578,13 +580,17 @@ .selos-container.selos-compacto { display: inline-flex; - margin-left: 0.3rem; + margin-left: 0.25rem; + gap: 0.2rem; } .selos-compacto .selo { background: rgba(148, 163, 184, 0.12); border-color: rgba(148, 163, 184, 0.25); color: #94a3b8; + padding: 0.1rem 0.25rem; + gap: 0.15rem; + font-size: 0.6rem; } .selos-compacto .selo:hover { @@ -596,11 +602,14 @@ .selos-compacto .selo-icone { filter: grayscale(100%) brightness(1.2); opacity: 0.85; + font-size: 0.65rem; } .selos-compacto .selo-qtd { background: rgba(148, 163, 184, 0.25); color: #cbd5e1; + font-size: 0.5rem; + padding: 0.05rem 0.2rem; } .selo { @@ -644,8 +653,9 @@ .selo-mais { background: rgba(255, 255, 255, 0.05); border-style: dashed; - font-size: 0.65rem; + font-size: 0.55rem; color: var(--muted); + padding: 0.1rem 0.2rem; } .selo-coord { @@ -722,6 +732,32 @@ color: #e2e8f0; } +.tipos-section { + grid-column: 1 / -1; + margin-top: 1rem; +} + +.tipos-section h4 { + color: var(--accent-2); +} + +.tipos-expandido { + gap: 0.5rem; +} + +.tipos-expandido .tipo-atuacao { + padding: 0.25rem 0.5rem; + font-size: 0.7rem; +} + +.tipos-expandido .tipo-icone { + font-size: 0.8rem; +} + +.tipos-expandido .tipo-label { + font-size: 0.65rem; +} + .selos-section { grid-column: 1 / -1; margin-top: 1rem; @@ -771,3 +807,123 @@ border-color: rgba(79, 70, 229, 0.4); transform: scale(1.1); } + +.tipos-atuacao-container { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 0.4rem; +} + +.tipo-atuacao { + display: inline-flex; + align-items: center; + gap: 0.2rem; + font-size: 0.65rem; + padding: 0.15rem 0.4rem; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.05); + color: var(--muted); + font-weight: 500; + transition: all 150ms ease; +} + +.tipo-atuacao:hover { + transform: translateY(-1px); + border-color: rgba(255, 255, 255, 0.25); +} + +.tipo-icone { + font-size: 0.7rem; + line-height: 1; + opacity: 0.9; +} + +.tipo-label { + font-size: 0.6rem; + letter-spacing: 0.2px; + text-transform: uppercase; +} + +.tipo-mais { + background: rgba(255, 255, 255, 0.03); + border-style: dashed; + font-size: 0.55rem; + padding: 0.1rem 0.25rem; +} + +.tipo-coordenador { + background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(239, 68, 68, 0.08)); + border-color: rgba(239, 68, 68, 0.35); + color: #fca5a5; +} + +.tipo-consultor { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(59, 130, 246, 0.08)); + border-color: rgba(59, 130, 246, 0.35); + color: #93c5fd; +} + +.tipo-avaliador { + background: linear-gradient(135deg, rgba(168, 85, 247, 0.2), rgba(168, 85, 247, 0.08)); + border-color: rgba(168, 85, 247, 0.35); + color: #d8b4fe; +} + +.tipo-premiado { + background: linear-gradient(135deg, rgba(251, 191, 36, 0.25), rgba(251, 191, 36, 0.1)); + border-color: rgba(251, 191, 36, 0.4); + color: #fcd34d; +} + +.tipo-orientador { + background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(34, 197, 94, 0.08)); + border-color: rgba(34, 197, 94, 0.35); + color: #86efac; +} + +.tipo-bolsista { + background: linear-gradient(135deg, rgba(14, 165, 233, 0.2), rgba(14, 165, 233, 0.08)); + border-color: rgba(14, 165, 233, 0.35); + color: #7dd3fc; +} + +.tipo-inscrito { + background: linear-gradient(135deg, rgba(249, 115, 22, 0.2), rgba(249, 115, 22, 0.08)); + border-color: rgba(249, 115, 22, 0.35); + color: #fdba74; +} + +.tipo-projeto { + background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(99, 102, 241, 0.08)); + border-color: rgba(99, 102, 241, 0.35); + color: #a5b4fc; +} + +.tipo-evento { + background: linear-gradient(135deg, rgba(236, 72, 153, 0.2), rgba(236, 72, 153, 0.08)); + border-color: rgba(236, 72, 153, 0.35); + color: #f9a8d4; +} + +.tipo-default { + background: rgba(148, 163, 184, 0.12); + border-color: rgba(148, 163, 184, 0.25); + color: #94a3b8; +} + +@media (max-width: 900px) { + .tipos-atuacao-container { + margin-top: 0.3rem; + } + + .tipo-atuacao { + font-size: 0.6rem; + padding: 0.1rem 0.3rem; + } + + .tipo-label { + font-size: 0.55rem; + } +} diff --git a/frontend/src/components/ConsultorCard.jsx b/frontend/src/components/ConsultorCard.jsx index e86d587..5864d23 100644 --- a/frontend/src/components/ConsultorCard.jsx +++ b/frontend/src/components/ConsultorCard.jsx @@ -24,6 +24,18 @@ const SELOS = { CO_ORIENT_POS_DOC: { label: 'Coorient. Pos-Doc', cor: 'selo-coorient', icone: '🔬' }, }; +const TIPOS_ATUACAO_CONFIG = { + 'Coordenador': { cor: 'tipo-coordenador', icone: '🎯' }, + 'Consultor': { cor: 'tipo-consultor', icone: '💼' }, + 'Avaliador': { cor: 'tipo-avaliador', icone: '📋' }, + 'Premiado': { cor: 'tipo-premiado', icone: '🏆' }, + 'Orientador': { cor: 'tipo-orientador', icone: '🎓' }, + 'Bolsista CNPq': { cor: 'tipo-bolsista', icone: '🔬' }, + 'Inscrito Premio': { cor: 'tipo-inscrito', icone: '📝' }, + 'Projeto': { cor: 'tipo-projeto', icone: '📊' }, + 'Evento': { cor: 'tipo-evento', icone: '📅' }, +}; + const gerarSelos = (consultor) => { const selos = []; @@ -114,6 +126,32 @@ const SelosBadges = ({ selos, compacto = false }) => { ); }; +const TiposAtuacaoBadges = ({ tipos, exibirTodos = false }) => { + if (!tipos || tipos.length === 0) return null; + + const tiposExibidos = exibirTodos ? tipos : tipos.slice(0, 4); + const tiposOcultos = !exibirTodos && tipos.length > 4 ? tipos.length - 4 : 0; + + return ( +
+ {tiposExibidos.map((tipo, idx) => { + const config = TIPOS_ATUACAO_CONFIG[tipo] || { cor: 'tipo-default', icone: '📌' }; + return ( + + {config.icone} + {tipo} + + ); + })} + {tiposOcultos > 0 && ( + + +{tiposOcultos} + + )} +
+ ); +}; + const FORMULAS = { bloco_a: { titulo: 'Coordenacao CAPES', @@ -372,6 +410,13 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio )} + {consultor.tipos_atuacao?.length > 0 && ( +
+

Tipos de Atuacao

+ +
+ )} + {selos.length > 0 && (

Selos e Reconhecimentos

diff --git a/frontend/src/components/Header.css b/frontend/src/components/Header.css index 884a371..8cedabb 100644 --- a/frontend/src/components/Header.css +++ b/frontend/src/components/Header.css @@ -359,6 +359,96 @@ color: #c4b5fd; } +/* Selos na Legenda */ +.selos-table th:last-child, +.selos-table td:last-child { + text-align: center; + width: 2.5rem; +} + +.selo-legenda { + display: inline-block; + font-size: 0.75rem; + padding: 0.1rem 0.3rem; + border-radius: 4px; + line-height: 1; +} + +.selo-legenda.selo-orient { + background: rgba(34, 211, 238, 0.2); + border: 1px solid rgba(34, 211, 238, 0.4); +} + +.selo-legenda.selo-coorient { + background: rgba(96, 165, 250, 0.2); + border: 1px solid rgba(96, 165, 250, 0.4); +} + +.selo-legenda.selo-banca { + background: rgba(167, 139, 250, 0.2); + border: 1px solid rgba(167, 139, 250, 0.4); +} + +.selo-legenda.selo-bpq { + background: rgba(59, 130, 246, 0.2); + border: 1px solid rgba(59, 130, 246, 0.4); +} + +.selo-legenda.selo-proj { + background: rgba(16, 185, 129, 0.2); + border: 1px solid rgba(16, 185, 129, 0.4); +} + +.selo-legenda.selo-evento { + background: rgba(251, 146, 60, 0.2); + border: 1px solid rgba(251, 146, 60, 0.4); +} + +.selo-legenda.selo-coord { + background: rgba(139, 92, 246, 0.2); + border: 1px solid rgba(139, 92, 246, 0.4); +} + +.selo-legenda.selo-camara { + background: rgba(234, 179, 8, 0.2); + border: 1px solid rgba(234, 179, 8, 0.4); +} + +.selo-legenda.selo-gp { + background: rgba(234, 179, 8, 0.25); + border: 1px solid rgba(234, 179, 8, 0.5); +} + +.selo-legenda.selo-premio { + background: rgba(16, 185, 129, 0.2); + border: 1px solid rgba(16, 185, 129, 0.4); +} + +.selo-legenda.selo-mencao { + background: rgba(156, 163, 175, 0.2); + border: 1px solid rgba(156, 163, 175, 0.4); +} + +.selo-legenda.selo-orient-premio { + background: rgba(236, 72, 153, 0.2); + border: 1px solid rgba(236, 72, 153, 0.4); +} + +.selo-legenda.selo-orient-mencao { + background: rgba(148, 163, 184, 0.2); + border: 1px solid rgba(148, 163, 184, 0.4); +} + +.selo-legenda.selo-coorient-premio { + background: rgba(168, 85, 247, 0.2); + border: 1px solid rgba(168, 85, 247, 0.4); +} + +.selo-legenda.selo-coorient-mencao { + background: rgba(107, 114, 128, 0.2); + border: 1px solid rgba(107, 114, 128, 0.4); +} + @media (max-width: 900px) { .criteria-row { flex-wrap: wrap; diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 802178c..12129b3 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -85,14 +85,29 @@ const Header = ({ total }) => { max 300
- +
- + - - - + + + + + + + + + + + + + + + + + +
PremiacoesBaseTeto
PremiacoesBTAutorOrientCoorient
PREMIACAO_GP100300
PREMIACAO50150
MENCAO3090
GP100300🏆🏆🏆
PREMIO50150🥇🎖️🎖️
MENCAO3090🥈📜📜
@@ -120,61 +135,59 @@ const Header = ({ total }) => {
-
+

C - Orientacoes

selos
-
- +
OrientacaoBT
+ - - - + + +
OrientacaoSelo
POS_DOC00
TESE00
DISS00
POS_DOC🔬
TESE📚
DISS📄
- - +
Co-OrientBT
+ - - - + + +
Co-OrientSelo
POS_DOC00
TESE00
DISS00
POS_DOC🔬
TESE📚
DISS📄
-
+versoes _PREM | Selos no V1
-
+

C - Bancas

selos
- - +
Membro BancaBaseTeto
+ - - - + + +
Membro BancaSelo
MB_POS_DOC00
MB_TESE00
MB_DISS00
MB_POS_DOC🔬
MB_TESE📚
MB_DISS📄
-
+versoes _PREM | Selos no V1
-
+

D - Indicadores

selos
- - +
CodigoBaseTeto
+ - - - - - + + + + +
CodigoBTSelo
BOL_BPQ3060
PROJ1030
EVENTO15
IDIOMA00
TITULACAO00
BOL_BPQ3060🏅
PROJ1030📁
EVENTO15📅
PPG_COORD--🎓
PRES_CAM--👑
diff --git a/frontend/src/components/SugerirConsultores.css b/frontend/src/components/SugerirConsultores.css index 145e98e..7eb02cc 100644 --- a/frontend/src/components/SugerirConsultores.css +++ b/frontend/src/components/SugerirConsultores.css @@ -277,7 +277,7 @@ .sugerir-rank { font-size: 0.8rem; font-weight: 700; - color: var(--accent); + color: #ebeaff; min-width: 2rem; } @@ -286,6 +286,31 @@ font-weight: 600; color: var(--white); font-size: 0.95rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.sugerir-nome .link-atuacapes { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + font-size: 0.8rem; + color: #94a3b8; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 5px; + text-decoration: none; + transition: all 200ms ease; +} + +.sugerir-nome .link-atuacapes:hover { + color: #a5b4fc; + background: rgba(79, 70, 229, 0.2); + border-color: rgba(165, 180, 252, 0.4); + transform: scale(1.1); } .sugerir-badges { @@ -338,12 +363,12 @@ align-items: center; gap: 0.3rem; font-size: 0.8rem; - color: #22d3ee; + color: #67e8f9; font-weight: 600; - background: rgba(34, 211, 238, 0.15); + background: rgba(34, 211, 238, 0.2); padding: 0.25rem 0.6rem; border-radius: 6px; - border: 1px solid rgba(34, 211, 238, 0.3); + border: 1px solid rgba(103, 232, 249, 0.4); } .stat-icon { diff --git a/frontend/src/components/SugerirConsultores.jsx b/frontend/src/components/SugerirConsultores.jsx index 02181ec..5b358a8 100644 --- a/frontend/src/components/SugerirConsultores.jsx +++ b/frontend/src/components/SugerirConsultores.jsx @@ -245,7 +245,21 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => { /> #{index + 1} - {c.nome} + + {c.nome} + {import.meta.env.VITE_HOST_ATUACAPES && c.id_pessoa && ( + e.stopPropagation()} + title="Ver perfil no ATUACAPES" + > + ↗ + + )} +
{c.foi_coordenador && CA} {c.foi_premiado && P} diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 2ba5c6c..45eedc8 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -53,6 +53,7 @@ export const rankingService = { ativo: c.ativo, anos_atuacao: anos, veterano: anos >= 10, + tipos_atuacao: c.tipos_atuacao || [], coordenador_ppg: Boolean(c.coordenador_ppg), pontuacao: c.pontuacao || { pontuacao_total: c.pontuacao_total,