fix: corrigir filtro de ativos, remover count de areas e navegacao ao clicar

This commit is contained in:
Frederico Castro
2025-12-20 12:22:45 -03:00
parent 45ab7412fe
commit 342e52880e
10 changed files with 2179 additions and 15 deletions

Binary file not shown.

View File

@@ -614,14 +614,20 @@ class ElasticsearchClient:
"query": {
"bool": {
"must": [
{"term": {"atuacoes.tipo": "Consultor"}}
{"term": {"atuacoes.tipo": "Consultor"}},
{
"bool": {
"should": [
{"match_phrase": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Atividade Contínua"}},
{"term": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Ativo"}}
],
"minimum_should_match": 1
}
}
],
"should": [
{"match": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Atividade Contínua"}},
{"match": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Ativo"}},
{"match": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Contínua"}}
],
"minimum_should_match": 1
"must_not": [
{"match_phrase": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Inatividade"}}
]
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

View File

@@ -46,13 +46,14 @@ function App() {
const handleSugestaoSelecionada = async (idPessoa) => {
try {
const resultados = await rankingService.searchConsultor(String(idPessoa), 1);
if (resultados && resultados.length > 0) {
const alvo = resultados[0];
const pos = alvo.posicao || 1;
const pagina = Math.ceil(pos / pageSize);
setHighlightId(alvo.id_pessoa);
setPage(pagina);
const response = await fetch(`/api/v1/ranking/posicao/${idPessoa}`);
if (response.ok) {
const alvo = await response.json();
if (alvo.encontrado && alvo.posicao) {
const pagina = Math.ceil(alvo.posicao / pageSize);
setHighlightId(alvo.id_pessoa);
setPage(pagina);
}
}
} catch (err) {
console.error('Erro ao navegar para consultor:', err);

View File

@@ -96,7 +96,7 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
<option value="">Todas as areas</option>
{areas.map((area) => (
<option key={area.nome} value={area.nome}>
{area.nome} ({area.count})
{area.nome}
</option>
))}
</select>

BIN
logo_capes.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,70 @@
from __future__ import annotations
from pathlib import Path
from openpyxl import load_workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
INPUT_PATH = Path("/home/fred/Downloads/Definição Ranking_ATUACAPES - Aba1 a Aba4(7).xlsx")
OUTPUT_PATH = Path("/home/fred/projetos/ranking/docs/Definicao_Ranking_ATUACAPES_estilizada.xlsx")
def apply_styles() -> None:
wb = load_workbook(INPUT_PATH)
header_fill = PatternFill("solid", fgColor="E6F0FF")
header_font = Font(bold=True, color="102A43")
alt_fill = PatternFill("solid", fgColor="F7FAFC")
thin = Side(style="thin", color="CBD5E0")
border = Border(left=thin, right=thin, top=thin, bottom=thin)
for ws in wb.worksheets:
max_col = ws.max_column
max_row = ws.max_row
# Header styling
for col in range(1, max_col + 1):
cell = ws.cell(row=1, column=col)
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
cell.border = border
# Data rows styling
for row in range(2, max_row + 1):
row_fill = alt_fill if row % 2 == 0 else None
for col in range(1, max_col + 1):
cell = ws.cell(row=row, column=col)
cell.border = border
cell.alignment = Alignment(vertical="top", wrap_text=True)
if row_fill:
cell.fill = row_fill
# Freeze header row
ws.freeze_panes = "A2"
# Auto-filter across the used range
ws.auto_filter.ref = ws.dimensions
# Adjust column widths with caps
for col in ws.columns:
col_letter = col[0].column_letter
max_len = 0
for cell in col[: min(max_row, 200)]:
if cell.value is None:
continue
text = str(cell.value)
if len(text) > max_len:
max_len = len(text)
width = max(12, min(45, int(max_len * 0.9)))
ws.column_dimensions[col_letter].width = width
# Slightly taller header
ws.row_dimensions[1].height = 28
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
wb.save(OUTPUT_PATH)
if __name__ == "__main__":
apply_styles()

View File

@@ -0,0 +1,262 @@
from __future__ import annotations
from pathlib import Path
import datetime
from html import escape
from openpyxl import load_workbook
from weasyprint import HTML, CSS
XLSX_PATH = Path("/home/fred/Downloads/Definição Ranking_ATUACAPES - Aba1 a Aba4(7).xlsx")
PDF_PATH = Path("/home/fred/projetos/ranking/docs/Criterios_Ranking_Consultores.pdf")
LOGO_PATH = Path("/home/fred/projetos/ranking/docs/assets/logo-capes.png")
def _fmt(cell) -> str:
if cell is None:
return ""
return str(cell).replace("\n", " ").strip()
def _table_html(headers, rows, code_cols=None) -> str:
code_cols = set(code_cols or [])
thead = "<tr>" + "".join(f"<th>{escape(_fmt(h))}</th>" for h in headers) + "</tr>"
body_rows = []
for row in rows:
cols = []
for idx, cell in enumerate(row):
text = _fmt(cell)
if idx in code_cols and text:
text = f"<code>{escape(text)}</code>"
else:
text = escape(text)
cols.append(f"<td>{text}</td>")
body_rows.append("<tr>" + "".join(cols) + "</tr>")
tbody = "\n".join(body_rows)
return f"<table class=\"criteria-table\"><thead>{thead}</thead><tbody>{tbody}</tbody></table>"
def load_planilha() -> dict:
wb = load_workbook(XLSX_PATH, data_only=True)
def extract(sheet, header_cols, row_cols, skip_score=False, formula=False):
ws = wb[sheet]
headers = [_fmt(c) for c in ws[1][:header_cols]]
rows = []
formula_row = ""
for r in ws.iter_rows(min_row=2, values_only=True):
if not any(r):
continue
if formula and r[0] is None and isinstance(r[1], str) and "tempo =" in r[1]:
formula_row = r[1]
continue
if skip_score and _fmt(r[0]).lower() == "score":
continue
rows.append(r[:row_cols])
return headers, rows, formula_row
h1, r1, _ = extract("Aba1_Mapa_Atuacoes", 9, 9)
h2, r2, _ = extract("Aba2_Pontuacao_Base", 4, 4)
h3, r3, f3 = extract("Aba3_Regras_Tempo", 7, 7, formula=True)
h4, r4, _ = extract("Aba4_Bonus_Extras", 12, 12)
h5, r5, _ = extract("Aba5_Detalh. Perfil_Indicadores", 6, 6, skip_score=True)
return {
"aba1": {"headers": h1, "rows": r1},
"aba2": {"headers": h2, "rows": r2},
"aba3": {"headers": h3, "rows": r3, "formula": f3},
"aba4": {"headers": h4, "rows": r4},
"aba5": {"headers": h5, "rows": r5},
}
def build_html(data: dict) -> str:
hoje = datetime.date.today().isoformat()
logo_html = ""
if LOGO_PATH.exists():
logo_html = f"<img class=\"cover-logo\" src=\"{LOGO_PATH.as_posix()}\" alt=\"CAPES\" />"
return f"""
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<title>Critérios de Pontuação e Ordenação (Versão Executiva)</title>
<style>
@page {{
size: A4;
margin: 2.1cm 2.1cm 2.3cm 2.1cm;
}}
@page landscape {{
size: A4 landscape;
margin: 1.5cm 1.7cm 1.8cm 1.7cm;
}}
body {{
font-family: "Liberation Serif", "Times New Roman", serif;
color: #0f172a;
font-size: 11pt;
line-height: 1.4;
}}
h1, h2 {{
font-family: "Liberation Sans", "Arial", sans-serif;
color: #0b1f3a;
margin: 0 0 0.4cm 0;
}}
h1 {{
font-size: 25pt;
letter-spacing: 0.3px;
}}
h2 {{
font-size: 16pt;
margin-top: 0;
}}
p {{
margin: 0.3cm 0;
}}
.cover {{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 92vh;
border: 2px solid #0b1f3a;
padding: 2.2cm;
text-align: center;
}}
.cover-logo {{
width: 6.0cm;
height: auto;
margin-bottom: 0.7cm;
}}
.cover-subtitle {{
font-size: 14pt;
color: #1f3b63;
margin-bottom: 1.1cm;
max-width: 14cm;
}}
.cover-meta {{
font-size: 10.6pt;
color: #0f172a;
border-top: 1px solid #1f3b63;
padding-top: 0.5cm;
width: 100%;
text-align: left;
}}
.label {{
font-weight: 700;
color: #0b1f3a;
}}
section {{
break-before: page;
}}
section.first {{
break-before: auto;
}}
.landscape {{
page: landscape;
}}
table.criteria-table {{
width: 100%;
border-collapse: collapse;
margin: 0.35cm 0 0.5cm 0;
font-size: 9.3pt;
table-layout: fixed;
}}
table.criteria-table th, table.criteria-table td {{
border: 1px solid #cbd5f0;
padding: 5px 6px;
vertical-align: top;
word-break: break-word;
hyphens: auto;
}}
table.criteria-table th {{
background: #eef2ff;
text-align: left;
font-weight: 700;
}}
table.criteria-table tr:nth-child(even) td {{
background: #f8fafc;
}}
.landscape table.criteria-table {{
font-size: 8.2pt;
}}
.landscape table.criteria-table th,
.landscape table.criteria-table td {{
padding: 4px 5px;
}}
code {{
font-family: "Liberation Mono", "Courier New", monospace;
background: #f1f5f9;
padding: 0 3px;
border-radius: 3px;
font-size: 9.2pt;
}}
</style>
</head>
<body>
<section class="cover">
{logo_html}
<h1>Sistema de Ranking de Consultores</h1>
<div class="cover-subtitle">Critérios de Pontuação e Ordenação (Versão Executiva)</div>
<div class="cover-meta">
<div><span class="label">Versão:</span> 1.0</div>
<div><span class="label">Data:</span> {hoje}</div>
<div><span class="label">Finalidade:</span> Documento executivo para alta gestão</div>
<div><span class="label">Escopo:</span> Critérios oficiais do ranking conforme planilha de definição (Abas 1 a 5)</div>
</div>
</section>
<section class="first">
<h2>Sumário Executivo</h2>
<p>Este documento consolida, de forma hierárquica e fiel à planilha oficial, todos os critérios utilizados no ranking de consultores. A estrutura está organizada por abas: mapeamento das atuações (Aba 1), pontuação base e tetos (Aba 2), regras de tempo (Aba 3), bônus e selos (Aba 4) e indicadores não pontuáveis (Aba 5).</p>
</section>
<section class="landscape">
<h2>1. Aba 1 — Mapa de Atuações</h2>
{_table_html(data["aba1"]["headers"], data["aba1"]["rows"], code_cols=[3])}
</section>
<section>
<h2>2. Aba 2 — Pontuação Base e Teto por Atuação</h2>
{_table_html(data["aba2"]["headers"], data["aba2"]["rows"], code_cols=[0])}
</section>
<section>
<h2>3. Aba 3 — Regras de Tempo</h2>
{_table_html(data["aba3"]["headers"], data["aba3"]["rows"], code_cols=[0])}
<p><span class="label">Fórmula da planilha:</span> <code>{escape(_fmt(data["aba3"]["formula"]))}</code></p>
</section>
<section class="landscape">
<h2>4. Aba 4 — Bônus e Selos</h2>
{_table_html(data["aba4"]["headers"], data["aba4"]["rows"], code_cols=[0])}
</section>
<section>
<h2>5. Aba 5 — Indicadores Não Pontuáveis</h2>
<p>Os itens abaixo constam na Aba 5 e <strong>não impactam a pontuação</strong>. O indicador <strong>Score</strong> é pontuável e já está coberto pelas regras das Abas 2 a 4.</p>
{_table_html(data["aba5"]["headers"], data["aba5"]["rows"])}
</section>
<section>
<h2>6. Fonte Oficial</h2>
<p>Planilha: <code>{escape(str(XLSX_PATH))}</code></p>
</section>
</body>
</html>
"""
def main() -> None:
data = load_planilha()
html = build_html(data)
HTML(string=html, base_url=str(PDF_PATH.parent)).write_pdf(
target=str(PDF_PATH),
stylesheets=[CSS(string="@page { size: A4; }")],
)
print(f"PDF: {PDF_PATH}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,221 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import datetime as dt
import os
import subprocess
import tempfile
from pathlib import Path
from typing import Iterable
from openpyxl import load_workbook
def _excel_col_letter(index_1_based: int) -> str:
result = ""
n = index_1_based
while n:
n, rem = divmod(n - 1, 26)
result = chr(ord("A") + rem) + result
return result
def _stringify_cell(value) -> str:
if value is None:
return ""
if isinstance(value, (dt.datetime, dt.date)):
return value.isoformat()
return str(value)
def _troff_escape(text: str) -> str:
text = text.replace("\\", "\\\\")
text = text.replace("\t", " ")
text = text.replace("|", "¦")
return text
def _troff_field(text: str) -> str:
text = _troff_escape(text)
if not text:
return "T{\n\\&\nT}"
lines = text.splitlines() or [text]
safe_lines: list[str] = []
for line in lines:
if line.startswith((".", "'")):
safe_lines.append(r"\&" + line)
else:
safe_lines.append(line)
body = "\n".join(safe_lines)
return f"T{{\n{body}\nT}}"
def _used_bounds(ws) -> tuple[int, int]:
last_row = 0
last_col = 0
for row_idx, row in enumerate(ws.iter_rows(values_only=True), start=1):
if any(v not in (None, "") for v in row):
last_row = row_idx
for col_idx, v in enumerate(row, start=1):
if v not in (None, ""):
last_col = max(last_col, col_idx)
return last_row, last_col
def _iter_rows(ws, max_row: int, max_col: int) -> Iterable[list[str]]:
for row in ws.iter_rows(min_row=1, max_row=max_row, min_col=1, max_col=max_col, values_only=True):
yield [_stringify_cell(v) for v in row]
def _emit_table(rows: list[list[str]], *, col_slice: slice, title: str) -> str:
cols = list(range(col_slice.start or 0, col_slice.stop or len(rows[0])))
ncols = len(cols)
if ncols <= 0:
return ""
spec = " ".join(["l"] * ncols) + "."
out: list[str] = []
out.append(".LP")
out.append(rf"\fB{_troff_escape(title)}\fP")
out.append(".TS")
out.append("tab(|) expand;")
out.append(spec)
header = rows[0]
header_fields = []
for i in cols:
header_fields.append(_troff_field(header[i]))
out.append("|".join(header_fields))
for row in rows[1:]:
fields = [_troff_field(row[i]) for i in cols]
out.append("|".join(fields))
out.append(".TE")
out.append(".LP")
return "\n".join(out) + "\n"
def build_troff_from_xlsx(xlsx_path: Path, generated_at: dt.datetime) -> str:
wb = load_workbook(xlsx_path, data_only=False)
lines: list[str] = []
lines.append('.\" Auto-generated from XLSX')
lines.append(".po 1i")
lines.append(".ll 6.5i")
lines.append(".ps 10")
lines.append(".vs 12")
lines.append("")
lines.append(".ps 18")
lines.append(".vs 22")
lines.append(".ce")
lines.append(r"\fBDefinição de Ranking — AtuaCAPES\fP")
lines.append(".sp 0.5")
lines.append(".ps 11")
lines.append(".vs 14")
lines.append(".ce")
lines.append("Documento gerado automaticamente")
lines.append(".sp 0.5")
lines.append(".ce")
lines.append(_troff_escape(generated_at.strftime("%Y-%m-%d %H:%M")))
lines.append(".sp 1")
lines.append(rf"Fonte: \fB{_troff_escape(str(xlsx_path))}\fP")
lines.append(".LP")
lines.append("Conteúdo: todas as abas da planilha, com critérios e regras conforme definidos no arquivo de entrada.")
lines.append(".bp")
lines.append(".ps 14")
lines.append(".vs 18")
lines.append(r"\fBSumário de Abas\fP")
lines.append(".ps 10")
lines.append(".vs 12")
lines.append(".sp 0.5")
for i, name in enumerate(wb.sheetnames, start=1):
lines.append(rf"{i}. \fB{_troff_escape(name)}\fP")
lines.append(".br")
lines.append(".bp")
for sheet_name in wb.sheetnames:
ws = wb[sheet_name]
max_row, max_col = _used_bounds(ws)
if max_row == 0 or max_col == 0:
continue
rows = list(_iter_rows(ws, max_row=max_row, max_col=max_col))
if not rows:
continue
lines.append(".ps 14")
lines.append(".vs 18")
lines.append(rf"\fB{_troff_escape(sheet_name)}\fP")
lines.append(".ps 10")
lines.append(".vs 12")
lines.append(".LP")
lines.append(rf"Linhas: \fB{max_row}\fP — Colunas: \fB{max_col}\fP")
max_cols_per_table = 8
if max_col <= max_cols_per_table:
start_letter = _excel_col_letter(1)
end_letter = _excel_col_letter(max_col)
lines.append(_emit_table(rows, col_slice=slice(0, max_col), title=f"Colunas {start_letter}{end_letter}"))
else:
start = 0
while start < max_col:
end = min(start + max_cols_per_table, max_col)
start_letter = _excel_col_letter(start + 1)
end_letter = _excel_col_letter(end)
lines.append(
_emit_table(
rows,
col_slice=slice(start, end),
title=f"Colunas {start_letter}{end_letter}",
)
)
start = end
lines.append(".bp")
return "\n".join(lines)
def render_pdf_from_ms(ms_text: str, pdf_path: Path, *, workdir: Path) -> None:
pdf_path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(dir=workdir) as td:
td_path = Path(td)
ms_path = td_path / "doc.ms"
ps_path = td_path / "doc.ps"
ms_path.write_text(ms_text, encoding="utf-8")
groff_cmd = ["groff", "-Kutf8", "-Tps", "-t", str(ms_path)]
ps_bytes = subprocess.check_output(groff_cmd)
ps_path.write_bytes(ps_bytes)
subprocess.check_call(["ps2pdf", str(ps_path), str(pdf_path)])
def main() -> int:
parser = argparse.ArgumentParser(description="Gera PDF (profissional) a partir de planilha XLSX.")
parser.add_argument("xlsx", type=Path, help="Caminho do XLSX de entrada")
parser.add_argument(
"-o",
"--out",
type=Path,
default=Path("out/definicao_ranking_atuacapes.pdf"),
help="Caminho do PDF de saída (default: out/definicao_ranking_atuacapes.pdf)",
)
args = parser.parse_args()
xlsx_path: Path = args.xlsx
out_pdf: Path = args.out
generated_at = dt.datetime.now().astimezone()
ms_text = build_troff_from_xlsx(xlsx_path=xlsx_path, generated_at=generated_at)
render_pdf_from_ms(ms_text=ms_text, pdf_path=out_pdf, workdir=Path(os.getcwd()))
print(out_pdf)
return 0
if __name__ == "__main__":
raise SystemExit(main())