Backend (FastAPI + DDD):
- Arquitetura DDD com camadas Domain, Application, Infrastructure, Interface
- Integração com Elasticsearch (ATUACAPES) para dados de consultores
- Integração com Oracle (SUCUPIRA_PAINEL) para coordenações PPG
- Cálculo dos 4 componentes de pontuação (A, B, C, D)
- Cache em memória para otimização de performance
- API REST com endpoints /ranking, /ranking/detalhado, /consultor/{id}
Frontend (React + Vite):
- Interface responsiva com cards expansíveis
- Visualização detalhada de pontuação por componente
- Filtro por quantidade de consultores (Top 10, 50, 100, etc)
Docker:
- docker-compose com shared_network externa
- Backend com Oracle Instant Client
- Frontend com Vite dev server
217 lines
8.2 KiB
JavaScript
217 lines
8.2 KiB
JavaScript
import React, { useState } from 'react';
|
|
import './ConsultorCard.css';
|
|
|
|
const ConsultorCard = ({ consultor }) => {
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
const getRankClass = (rank) => {
|
|
if (rank === 1) return 'rank-1';
|
|
if (rank === 2) return 'rank-2';
|
|
if (rank === 3) return 'rank-3';
|
|
return '';
|
|
};
|
|
|
|
const formatDate = (dateStr) => {
|
|
if (!dateStr) return 'Atual';
|
|
return new Date(dateStr).toLocaleDateString('pt-BR');
|
|
};
|
|
|
|
const { pontuacao } = consultor;
|
|
const { consultoria } = consultor;
|
|
|
|
return (
|
|
<div className={`ranking-card ${expanded ? 'expanded' : ''}`} onClick={() => setExpanded(!expanded)}>
|
|
<div className="card-main">
|
|
<div className={`rank ${getRankClass(consultor.rank)}`}>#{consultor.rank}</div>
|
|
|
|
<div className="card-info">
|
|
<div className="consultant-name">
|
|
{consultor.nome}
|
|
{consultor.ativo && <span className="badge badge-ativo">ATIVO</span>}
|
|
{!consultor.ativo && <span className="badge badge-historico">HISTÓRICO</span>}
|
|
{consultor.veterano && <span className="badge badge-veterano">VETERANO</span>}
|
|
</div>
|
|
<div className="consultant-area">
|
|
{consultor.anos_atuacao} anos de atuação
|
|
{consultoria && ` | Desde ${formatDate(consultoria.primeiro_evento)}`}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card-stats">
|
|
{consultoria && (
|
|
<>
|
|
<div className="stat">
|
|
<div className="stat-value">{consultoria.total_eventos}</div>
|
|
<div className="stat-label">Eventos</div>
|
|
</div>
|
|
<div className="stat">
|
|
<div className="stat-value">{consultoria.eventos_recentes}</div>
|
|
<div className="stat-label">Recentes</div>
|
|
</div>
|
|
<div className="stat">
|
|
<div className="stat-value">{consultoria.vezes_responsavel}</div>
|
|
<div className="stat-label">Responsável</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
<div className="stat">
|
|
<div className="score-value">{consultor.pontuacao_total}</div>
|
|
<div className="stat-label">Score</div>
|
|
</div>
|
|
<div className="expand-icon">{expanded ? '▲' : '▼'}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{expanded && (
|
|
<div className="card-details">
|
|
<div className="details-grid">
|
|
<div className="detail-section">
|
|
<h4>Pontuação Total</h4>
|
|
<div className="score-breakdown-total">
|
|
<div className="score-item">
|
|
<div className="score-item-value" style={{ color: pontuacao.componente_a.total > 0 ? 'var(--accent-2)' : 'var(--muted)' }}>
|
|
{pontuacao.componente_a.total}
|
|
</div>
|
|
<div className="score-item-label">COMP A</div>
|
|
</div>
|
|
<div className="score-item">
|
|
<div className="score-item-value" style={{ color: pontuacao.componente_b.total > 0 ? 'var(--success)' : 'var(--muted)' }}>
|
|
{pontuacao.componente_b.total}
|
|
</div>
|
|
<div className="score-item-label">COMP B</div>
|
|
</div>
|
|
<div className="score-item">
|
|
<div className="score-item-value" style={{ color: pontuacao.componente_c.total > 0 ? 'var(--gold)' : 'var(--muted)' }}>
|
|
{pontuacao.componente_c.total}
|
|
</div>
|
|
<div className="score-item-label">COMP C</div>
|
|
</div>
|
|
<div className="score-item">
|
|
<div className="score-item-value" style={{ color: pontuacao.componente_d.total > 0 ? 'var(--bronze)' : 'var(--muted)' }}>
|
|
{pontuacao.componente_d.total}
|
|
</div>
|
|
<div className="score-item-label">COMP D</div>
|
|
</div>
|
|
<div className="score-item score-total">
|
|
<div className="score-item-value">{pontuacao.pontuacao_total}</div>
|
|
<div className="score-item-label">TOTAL</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<ComponenteDetalhes
|
|
titulo="A - Coordenação CAPES"
|
|
componente={pontuacao.componente_a}
|
|
cor="var(--accent-2)"
|
|
/>
|
|
|
|
<ComponenteDetalhes
|
|
titulo="B - Coordenação PPG"
|
|
componente={pontuacao.componente_b}
|
|
cor="var(--success)"
|
|
/>
|
|
|
|
<ComponenteDetalhes
|
|
titulo="C - Consultoria"
|
|
componente={pontuacao.componente_c}
|
|
cor="var(--gold)"
|
|
/>
|
|
|
|
<ComponenteDetalhes
|
|
titulo="D - Premiações"
|
|
componente={pontuacao.componente_d}
|
|
cor="var(--bronze)"
|
|
/>
|
|
</div>
|
|
|
|
{consultor.coordenacoes_capes?.length > 0 && (
|
|
<div className="extra-details">
|
|
<h4>Coordenações CAPES</h4>
|
|
<div className="list-items">
|
|
{consultor.coordenacoes_capes.map((coord, idx) => (
|
|
<div key={idx} className="list-item">
|
|
<span className="badge">{coord.tipo}</span>
|
|
<span>{coord.area_avaliacao}</span>
|
|
<span className="muted">
|
|
{formatDate(coord.periodo.inicio)} - {formatDate(coord.periodo.fim)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{consultor.coordenacoes_programas?.length > 0 && (
|
|
<div className="extra-details">
|
|
<h4>Coordenações de Programa (PPG)</h4>
|
|
<div className="list-items">
|
|
{consultor.coordenacoes_programas.map((coord, idx) => (
|
|
<div key={idx} className="list-item">
|
|
<span className="badge">{coord.nota_ppg}</span>
|
|
<span>{coord.nome_programa}</span>
|
|
<span className="muted">{coord.area_avaliacao}</span>
|
|
<span className="muted">
|
|
{formatDate(coord.periodo.inicio)} - {formatDate(coord.periodo.fim)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{consultor.premiacoes?.length > 0 && (
|
|
<div className="extra-details">
|
|
<h4>Premiações</h4>
|
|
<div className="list-items">
|
|
{consultor.premiacoes.map((prem, idx) => (
|
|
<div key={idx} className="list-item">
|
|
<span className="badge">{prem.pontos} pts</span>
|
|
<span>{prem.nome_premio}</span>
|
|
<span className="muted">{prem.ano}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const ComponenteDetalhes = ({ titulo, componente, cor }) => (
|
|
<div className="detail-section">
|
|
<h4 style={{ color: cor }}>{titulo}</h4>
|
|
<div className="score-breakdown">
|
|
<div className="score-item">
|
|
<div className="score-item-value">{componente.base}</div>
|
|
<div className="score-item-label">BASE</div>
|
|
</div>
|
|
<div className="score-item">
|
|
<div className="score-item-value">{componente.tempo}</div>
|
|
<div className="score-item-label">TEMPO</div>
|
|
</div>
|
|
<div className="score-item">
|
|
<div className="score-item-value">{componente.extras}</div>
|
|
<div className="score-item-label">EXTRAS</div>
|
|
</div>
|
|
<div className="score-item">
|
|
<div className="score-item-value">{componente.bonus}</div>
|
|
<div className="score-item-label">BÔNUS</div>
|
|
</div>
|
|
{componente.retorno > 0 && (
|
|
<div className="score-item">
|
|
<div className="score-item-value">{componente.retorno}</div>
|
|
<div className="score-item-label">RETORNO</div>
|
|
</div>
|
|
)}
|
|
<div className="score-item score-total">
|
|
<div className="score-item-value">{componente.total}</div>
|
|
<div className="score-item-label">TOTAL</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
export default ConsultorCard;
|