fix: corrigir filtro de ativos, remover count de areas e navegacao ao clicar
This commit is contained in:
BIN
Ranking_ATUACAPES_Criterios_Gerencia.pdf
Normal file
BIN
Ranking_ATUACAPES_Criterios_Gerencia.pdf
Normal file
Binary file not shown.
@@ -614,16 +614,22 @@ class ElasticsearchClient:
|
|||||||
"query": {
|
"query": {
|
||||||
"bool": {
|
"bool": {
|
||||||
"must": [
|
"must": [
|
||||||
{"term": {"atuacoes.tipo": "Consultor"}}
|
{"term": {"atuacoes.tipo": "Consultor"}},
|
||||||
],
|
{
|
||||||
|
"bool": {
|
||||||
"should": [
|
"should": [
|
||||||
{"match": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Atividade Contínua"}},
|
{"match_phrase": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Atividade Contínua"}},
|
||||||
{"match": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Ativo"}},
|
{"term": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Ativo"}}
|
||||||
{"match": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Contínua"}}
|
|
||||||
],
|
],
|
||||||
"minimum_should_match": 1
|
"minimum_should_match": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"must_not": [
|
||||||
|
{"match_phrase": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Inatividade"}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
BIN
frontend/public/logo_capes.jpg
Normal file
BIN
frontend/public/logo_capes.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
@@ -46,14 +46,15 @@ function App() {
|
|||||||
|
|
||||||
const handleSugestaoSelecionada = async (idPessoa) => {
|
const handleSugestaoSelecionada = async (idPessoa) => {
|
||||||
try {
|
try {
|
||||||
const resultados = await rankingService.searchConsultor(String(idPessoa), 1);
|
const response = await fetch(`/api/v1/ranking/posicao/${idPessoa}`);
|
||||||
if (resultados && resultados.length > 0) {
|
if (response.ok) {
|
||||||
const alvo = resultados[0];
|
const alvo = await response.json();
|
||||||
const pos = alvo.posicao || 1;
|
if (alvo.encontrado && alvo.posicao) {
|
||||||
const pagina = Math.ceil(pos / pageSize);
|
const pagina = Math.ceil(alvo.posicao / pageSize);
|
||||||
setHighlightId(alvo.id_pessoa);
|
setHighlightId(alvo.id_pessoa);
|
||||||
setPage(pagina);
|
setPage(pagina);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erro ao navegar para consultor:', err);
|
console.error('Erro ao navegar para consultor:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
|
|||||||
<option value="">Todas as areas</option>
|
<option value="">Todas as areas</option>
|
||||||
{areas.map((area) => (
|
{areas.map((area) => (
|
||||||
<option key={area.nome} value={area.nome}>
|
<option key={area.nome} value={area.nome}>
|
||||||
{area.nome} ({area.count})
|
{area.nome}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
BIN
logo_capes.jpg
Normal file
BIN
logo_capes.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
1604
ranking_atuacapes_criterios_gerencia.html
Normal file
1604
ranking_atuacapes_criterios_gerencia.html
Normal file
File diff suppressed because it is too large
Load Diff
70
scripts/estilizar_planilha.py
Normal file
70
scripts/estilizar_planilha.py
Normal 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()
|
||||||
262
scripts/gerar_documento_criterios.py
Normal file
262
scripts/gerar_documento_criterios.py
Normal 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()
|
||||||
221
tools/generate_ranking_pdf.py
Normal file
221
tools/generate_ranking_pdf.py
Normal 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())
|
||||||
Reference in New Issue
Block a user