diff --git a/backend/src/infrastructure/elasticsearch/client.py b/backend/src/infrastructure/elasticsearch/client.py
index 685e469..1e623ac 100644
--- a/backend/src/infrastructure/elasticsearch/client.py
+++ b/backend/src/infrastructure/elasticsearch/client.py
@@ -102,3 +102,260 @@ class ElasticsearchClient:
return data.get("count", 0)
except Exception as e:
raise RuntimeError(f"Erro ao contar consultores: {e}")
+
+ async def buscar_candidatos_ranking(self, size: int = 100) -> list:
+ query = {
+ "size": size,
+ "query": {
+ "bool": {
+ "should": [
+ {
+ "nested": {
+ "path": "atuacoes",
+ "query": {
+ "bool": {
+ "should": [
+ {"term": {"atuacoes.tipo": {"value": "Coordenação de Área de Avaliação", "boost": 10}}},
+ {"term": {"atuacoes.tipo": {"value": "Histórico de Coordenação de Área de Avaliação", "boost": 5}}}
+ ]
+ }
+ },
+ "score_mode": "sum"
+ }
+ },
+ {
+ "nested": {
+ "path": "atuacoes",
+ "query": {
+ "bool": {
+ "should": [
+ {"term": {"atuacoes.tipo": {"value": "Consultor", "boost": 2}}},
+ {"term": {"atuacoes.tipo": {"value": "Histórico de Consultoria", "boost": 1}}}
+ ]
+ }
+ },
+ "score_mode": "sum"
+ }
+ },
+ {
+ "nested": {
+ "path": "atuacoes",
+ "query": {
+ "bool": {
+ "should": [
+ {"term": {"atuacoes.tipo": {"value": "Premiação Prêmio", "boost": 3}}},
+ {"term": {"atuacoes.tipo": {"value": "Avaliação Prêmio", "boost": 2}}},
+ {"term": {"atuacoes.tipo": {"value": "Inscrição Prêmio", "boost": 1}}}
+ ]
+ }
+ },
+ "score_mode": "sum"
+ }
+ }
+ ],
+ "minimum_should_match": 1
+ }
+ },
+ "_source": ["id", "dadosPessoais", "atuacoes"],
+ "sort": [{"_score": "desc"}]
+ }
+
+ try:
+ response = await self.client.post(
+ f"{self.url}/{self.index}/_search",
+ json=query,
+ timeout=120.0
+ )
+ response.raise_for_status()
+
+ data = response.json()
+ results = []
+ for hit in data.get("hits", {}).get("hits", []):
+ doc = hit["_source"]
+ doc["_score_es"] = hit.get("_score", 0)
+ results.append(doc)
+ return results
+ except Exception as e:
+ raise RuntimeError(f"Erro ao buscar candidatos ranking: {e}")
+
+ async def buscar_ranking_com_score_old(self, size: int = 100) -> list:
+ painless_script = """
+ double score = 0;
+ long now = System.currentTimeMillis();
+ long doisAnos = 730L * 24 * 60 * 60 * 1000;
+
+ // Componente A - Coordenação CAPES
+ double compA = 0;
+ for (def a : params._source.atuacoes) {
+ String tipo = a.tipo;
+ if (tipo == null) continue;
+
+ if (tipo.contains('Coordenação de Área')) {
+ String inicio = a.inicio;
+ String fim = a.fim;
+ if (inicio == null) continue;
+
+ boolean ativo = (fim == null || fim.length() == 0);
+ if (!ativo) continue;
+
+ // Parse data inicio (formato DD/MM/YYYY ou YYYY-MM-DD)
+ long inicioMs = 0;
+ try {
+ if (inicio.contains('/')) {
+ String[] parts = inicio.substring(0, 10).split('/');
+ int dia = Integer.parseInt(parts[0]);
+ int mes = Integer.parseInt(parts[1]);
+ int ano = Integer.parseInt(parts[2]);
+ inicioMs = (ano - 1970) * 365L * 24 * 60 * 60 * 1000;
+ }
+ } catch (Exception e) {}
+
+ double anos = (now - inicioMs) / (365.25 * 24 * 60 * 60 * 1000);
+
+ // Determinar tipo de coordenação
+ double base = 100;
+ double tempoMax = 50;
+ double bonusAtual = 10;
+
+ def dados = a.dadosCoordenacaoArea;
+ if (dados == null) dados = a.dadosHistoricoCoordenacaoArea;
+ if (dados != null && dados.tipo != null) {
+ String tipoCoord = dados.tipo.toLowerCase();
+ if (tipoCoord.contains('coordenador') && !tipoCoord.contains('adjunt')) {
+ base = 200; tempoMax = 100; bonusAtual = 30;
+ } else if (tipoCoord.contains('adjunt')) {
+ if (tipoCoord.contains('profissional')) {
+ base = 120; tempoMax = 60; bonusAtual = 15;
+ } else {
+ base = 150; tempoMax = 80; bonusAtual = 20;
+ }
+ } else if (tipoCoord.contains('presidente') || tipoCoord.contains('camara')) {
+ base = 100; tempoMax = 50; bonusAtual = 10;
+ }
+ }
+
+ double tempo = Math.min(anos * 10, tempoMax);
+ double thisCompA = base + tempo + bonusAtual;
+ if (thisCompA > compA) compA = thisCompA;
+ }
+ }
+ score += Math.min(compA, 450);
+
+ // Componente C - Consultoria
+ double compC = 0;
+ int totalEventos = 0;
+ int eventosRecentes = 0;
+ int vezesResponsavel = 0;
+ long primeiroEvento = now;
+ def areasConsultoria = new HashSet();
+
+ for (def a : params._source.atuacoes) {
+ String tipo = a.tipo;
+ if (tipo == null) continue;
+
+ if (tipo == 'Consultor' || tipo == 'Histórico de Consultoria') {
+ totalEventos++;
+
+ String inicio = a.inicio;
+ if (inicio != null && inicio.length() >= 10) {
+ try {
+ String[] parts = inicio.substring(0, 10).split('/');
+ if (parts.length == 3) {
+ int ano = Integer.parseInt(parts[2]);
+ long inicioMs = (ano - 1970) * 365L * 24 * 60 * 60 * 1000;
+ if (inicioMs < primeiroEvento) primeiroEvento = inicioMs;
+ }
+ } catch (Exception e) {}
+ }
+
+ String fim = a.fim;
+ if (fim != null && fim.length() >= 10) {
+ try {
+ String[] parts = fim.substring(0, 10).split('/');
+ if (parts.length == 3) {
+ int ano = Integer.parseInt(parts[2]);
+ long fimMs = (ano - 1970) * 365L * 24 * 60 * 60 * 1000;
+ if (now - fimMs < doisAnos) eventosRecentes++;
+ }
+ } catch (Exception e) {}
+ }
+
+ def dados = a.dadosConsultoria;
+ if (dados != null) {
+ if (dados.responsavel == true) vezesResponsavel++;
+ if (dados.areaAvaliacao != null) areasConsultoria.add(dados.areaAvaliacao);
+ }
+ }
+ }
+
+ if (totalEventos > 0) {
+ double base = (eventosRecentes > 0) ? 150 : 100;
+ double anosConsultoria = (now - primeiroEvento) / (365.25 * 24 * 60 * 60 * 1000);
+ double tempo = Math.min(anosConsultoria * 5, 50);
+ double extrasEventos = Math.min(totalEventos * 2, 20);
+ double extrasResp = Math.min(vezesResponsavel * 5, 25);
+ double extrasAreas = (areasConsultoria.size() > 1) ? Math.min((areasConsultoria.size() - 1) * 10, 30) : 0;
+ compC = base + tempo + extrasEventos + extrasResp + extrasAreas;
+ }
+ score += Math.min(compC, 230);
+
+ // Componente D - Premiações
+ double compD = 0;
+ for (def a : params._source.atuacoes) {
+ String tipo = a.tipo;
+ if (tipo == null) continue;
+
+ if (tipo.contains('Prêmio') || tipo.contains('Premio')) {
+ if (tipo.contains('Premiação')) {
+ compD += 60;
+ } else if (tipo.contains('Avaliação')) {
+ compD += 40;
+ } else if (tipo.contains('Inscrição')) {
+ compD += 20;
+ }
+ }
+ }
+ score += Math.min(compD, 180);
+
+ return score;
+ """
+
+ query = {
+ "size": size,
+ "query": {
+ "function_score": {
+ "query": {
+ "nested": {
+ "path": "atuacoes",
+ "query": {"exists": {"field": "atuacoes.tipo"}}
+ }
+ },
+ "script_score": {
+ "script": {
+ "source": painless_script,
+ "lang": "painless"
+ }
+ },
+ "boost_mode": "replace"
+ }
+ },
+ "_source": ["id", "dadosPessoais", "atuacoes"],
+ "sort": [{"_score": "desc"}]
+ }
+
+ try:
+ response = await self.client.post(
+ f"{self.url}/{self.index}/_search",
+ json=query
+ )
+ response.raise_for_status()
+
+ data = response.json()
+ results = []
+ for hit in data.get("hits", {}).get("hits", []):
+ doc = hit["_source"]
+ doc["_score_es"] = hit.get("_score", 0)
+ results.append(doc)
+ return results
+ except Exception as e:
+ raise RuntimeError(f"Erro ao buscar ranking com score: {e}")
diff --git a/backend/src/infrastructure/repositories/consultor_repository_impl.py b/backend/src/infrastructure/repositories/consultor_repository_impl.py
index 4ead580..ee22981 100644
--- a/backend/src/infrastructure/repositories/consultor_repository_impl.py
+++ b/backend/src/infrastructure/repositories/consultor_repository_impl.py
@@ -264,8 +264,16 @@ class ConsultorRepositoryImpl(ConsultorRepository):
if _ranking_cache.is_valid():
return _ranking_cache.get()[:limite]
- tamanho_busca = 1000
- consultores = await self.buscar_todos(limite=tamanho_busca)
+ tamanho_busca = max(limite * 3, 1000)
+ docs = await self.es_client.buscar_candidatos_ranking(size=tamanho_busca)
+
+ consultores = []
+ for doc in docs:
+ consultor = await self._construir_consultor(doc)
+ score_es = doc.get("_score_es", 0)
+ consultor.score_es = score_es
+ consultores.append(consultor)
+
consultores_ordenados = sorted(
consultores, key=lambda c: c.pontuacao_total, reverse=True
)
diff --git a/frontend/src/components/ConsultorCard.jsx b/frontend/src/components/ConsultorCard.jsx
index c49e029..195d5c0 100644
--- a/frontend/src/components/ConsultorCard.jsx
+++ b/frontend/src/components/ConsultorCard.jsx
@@ -55,7 +55,7 @@ const ConsultorCard = ({ consultor }) => {
>
)}
-
{consultor.pontuacao_total}
+
{pontuacao.pontuacao_total}
Score
{expanded ? '▲' : '▼'}
diff --git a/frontend/src/components/Header.css b/frontend/src/components/Header.css
index e837321..369997f 100644
--- a/frontend/src/components/Header.css
+++ b/frontend/src/components/Header.css
@@ -79,27 +79,72 @@
.criteria-section h4 {
color: var(--accent-2);
- font-size: 0.82rem;
- margin-bottom: 0.6rem;
+ font-size: 0.85rem;
+ margin-bottom: 0.25rem;
letter-spacing: 0.4px;
+ display: inline;
}
-.criteria-section ul {
- list-style: none;
- font-size: 0.8rem;
- line-height: 1.7;
+.max-pts {
+ display: inline-block;
+ margin-left: 0.5rem;
+ padding: 0.15rem 0.5rem;
+ background: rgba(79,70,229,0.3);
+ border-radius: 4px;
+ font-size: 0.7rem;
+ color: var(--silver);
+ font-weight: 500;
+}
+
+.criteria-table {
+ width: 100%;
+ margin-top: 0.6rem;
+ font-size: 0.75rem;
+ border-collapse: collapse;
+}
+
+.criteria-table th {
+ text-align: left;
+ color: var(--silver);
+ font-weight: 500;
+ padding: 0.3rem 0.4rem;
+ border-bottom: 1px solid rgba(255,255,255,0.1);
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.criteria-table td {
+ padding: 0.35rem 0.4rem;
color: var(--muted);
-}
-
-.criteria-section li {
- padding: 0.2rem 0;
border-bottom: 1px dashed rgba(255,255,255,0.05);
}
-.criteria-section li:last-child {
+.criteria-table tr:last-child td {
border-bottom: none;
}
+.criteria-table td:first-child {
+ color: var(--silver);
+ font-weight: 500;
+}
+
+.pts-value {
+ text-align: right;
+ color: var(--accent-2) !important;
+ font-weight: 400 !important;
+}
+
+.criteria-note {
+ margin-top: 0.5rem;
+ padding-top: 0.4rem;
+ border-top: 1px dashed rgba(255,255,255,0.1);
+ font-size: 0.7rem;
+ color: var(--muted);
+ text-align: center;
+ font-style: italic;
+}
+
@media (max-width: 768px) {
.criteria-grid {
grid-template-columns: 1fr;
diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx
index f2511e0..344333c 100644
--- a/frontend/src/components/Header.jsx
+++ b/frontend/src/components/Header.jsx
@@ -20,44 +20,65 @@ const Header = ({ total }) => {
Componentes de Pontuação
-
A - Coordenação CAPES (máx 450 pts)
-
- - CA: 200 base + 100 tempo + 100 áreas + 20 retorno + 30 bônus
- - CAJ: 150 base + 80 tempo + 100 áreas + 20 retorno + 20 bônus
- - CAJ-MP: 120 base + 60 tempo + 100 áreas + 20 retorno + 15 bônus
- - CAM: 100 base + 50 tempo + 100 áreas + 20 retorno + 10 bônus
-
+
A - Coordenação CAPES
+
máx 450 pts
+
+
+
+ | Tipo |
+ Base |
+ Tempo |
+ Bônus |
+
+
+
+ | CA | 200 | até 100 | 30 |
+ | CAJ | 150 | até 80 | 20 |
+ | CAJ-MP | 120 | até 60 | 15 |
+ | CAM | 100 | até 50 | 10 |
+
+
+
+ Áreas (até 100) + Retorno (20)
-
B - Coordenação Programa PPG (máx 180 pts)
-
- - Base: 70 pts
- - Tempo: 5 pts/ano (máx 50 pts)
- - Programas extras: 20 pts/programa (máx 40 pts)
- - Bônus ativo: 20 pts
-
+
B - Coordenação PPG
+
máx 180 pts
+
+
+ | Base | 70 pts |
+ | Tempo | 5 pts/ano (máx 50) |
+ | Programas extras | 20 pts/prog (máx 40) |
+ | Bônus ativo | 20 pts |
+
+
-
C - Consultoria (máx 230 pts)
-
- - Ativo: 150 pts base | Histórico: 100 pts base
- - Tempo: 5 pts/ano (máx 50 pts)
- - Eventos: 2 pts/evento (máx 20 pts)
- - Responsável: 5 pts/vez (máx 25 pts)
- - Áreas extras: 10 pts/área (máx 30 pts)
-
+
C - Consultoria
+
máx 230 pts
+
+
+ | Base (ativo) | 150 pts |
+ | Base (histórico) | 100 pts |
+ | Tempo | 5 pts/ano (máx 50) |
+ | Eventos | 2 pts/ev (máx 20) |
+ | Responsável | 5 pts/vez (máx 25) |
+ | Áreas extras | 10 pts/área (máx 30) |
+
+
-
D - Premiações (máx 180 pts)
-
- - Premiação: 60 pts
- - Avaliação: 40 pts
- - Inscrição: 20 pts
- - Total máximo: 180 pts
-
+
D - Premiações
+
máx 180 pts
+
+
+ | Premiação | 60 pts |
+ | Avaliação | 40 pts |
+ | Inscrição | 20 pts |
+
+
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index 3d88954..0e2d0b5 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -5,6 +5,7 @@ const api = axios.create({
headers: {
'Content-Type': 'application/json',
},
+ timeout: 180000,
});
export const rankingService = {