feat: seletor de linhas por pagina e paginacao numerada na listagem de clientes

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-20 09:01:12 -03:00
co-authored by Claude Sonnet 5
parent 5a3c4ac092
commit 9ce6bcdcad
3 changed files with 77 additions and 16 deletions
+6
View File
@@ -235,6 +235,12 @@ a.botao:hover { border-color: var(--cor-acento); color: var(--cor-acento); }
}
.paginacao { display: flex; align-items: center; gap: 12px; justify-content: center; margin-top: 16px; }
.paginacao-rodape { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-top: 16px; }
.paginacao-rodape .paginacao { margin-top: 0; }
.paginacao-por-pagina { display: flex; align-items: center; gap: 8px; }
.paginacao-por-pagina label { margin: 0; white-space: nowrap; }
.paginacao-atual { display: inline-flex; align-items: center; justify-content: center; min-width: 32px; height: 32px; padding: 0 6px; border-radius: 6px; background: var(--cor-acento); color: var(--cor-acento-texto); font-weight: 600; }
.paginacao-reticencias { padding: 0 4px; color: var(--cor-texto-secundario); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition: none !important; }
+39 -12
View File
@@ -17,6 +17,8 @@
</form>
</div>
<p><a class="botao" href="/clientes/novo">+ Novo cliente</a></p>
{% set busca_qs = ('&busca=' + (busca | urlencode)) if busca else '' %}
{% set qs_base = ('&ordenar_por=' + ordenar_por + '&direcao=' + direcao + '&por_pagina=' + (por_pagina | string) + busca_qs) | safe %}
<div class="tabela-wrap">
<table>
<tr>
@@ -24,7 +26,7 @@
{% for campo, rotulo in colunas_tabela %}
{% set nova_direcao = "desc" if (ordenar_por == campo and direcao == "asc") else "asc" %}
<th>
<a href="/clientes?ordenar_por={{ campo }}&direcao={{ nova_direcao }}&pagina=1{{ '&busca=' + (busca | urlencode) if busca else '' }}">
<a href="/clientes?ordenar_por={{ campo }}&direcao={{ nova_direcao }}&pagina=1&por_pagina={{ por_pagina }}{{ busca_qs }}">
{{ rotulo }}{% if ordenar_por == campo %}{{ " ▲" if direcao == "asc" else " ▼" }}{% endif %}
</a>
</th>
@@ -42,15 +44,40 @@
{% endfor %}
</table>
</div>
{% if total_paginas and total_paginas > 1 %}
<div class="paginacao">
{% if pagina > 1 %}
<a class="botao" href="/clientes?pagina={{ pagina - 1 }}{{ '&busca=' + (busca | urlencode) if busca else '' }}&ordenar_por={{ ordenar_por }}&direcao={{ direcao }}">&larr; Anterior</a>
{% endif %}
<span>Página {{ pagina }} de {{ total_paginas }} ({{ total }} clientes)</span>
{% if pagina < total_paginas %}
<a class="botao" href="/clientes?pagina={{ pagina + 1 }}{{ '&busca=' + (busca | urlencode) if busca else '' }}&ordenar_por={{ ordenar_por }}&direcao={{ direcao }}">Próxima &rarr;</a>
{% endif %}
</div>
{% endif %}
<div class="paginacao-rodape">
<form method="get" action="/clientes" class="paginacao-por-pagina">
<input type="hidden" name="ordenar_por" value="{{ ordenar_por }}">
<input type="hidden" name="direcao" value="{{ direcao }}">
<input type="hidden" name="pagina" value="1">
{% if busca %}<input type="hidden" name="busca" value="{{ busca }}">{% endif %}
<label for="por_pagina">Linhas por página</label>
<select id="por_pagina" name="por_pagina" onchange="this.form.submit()">
{% for opcao in opcoes_por_pagina %}
<option value="{{ opcao }}" {% if opcao == por_pagina %}selected{% endif %}>{{ opcao }}</option>
{% endfor %}
</select>
</form>
<span>Exibindo {{ primeiro_item }} a {{ ultimo_item }} de {{ total }} clientes</span>
{% if total_paginas and total_paginas > 1 %}
<div class="paginacao">
{% if pagina > 1 %}
<a class="botao" href="/clientes?pagina=1{{ qs_base }}" aria-label="Primeira página">&laquo;</a>
<a class="botao" href="/clientes?pagina={{ pagina - 1 }}{{ qs_base }}" aria-label="Página anterior">&lsaquo;</a>
{% endif %}
{% for p in janela_paginas %}
{% if p is none %}
<span class="paginacao-reticencias">&hellip;</span>
{% elif p == pagina %}
<span class="paginacao-atual">{{ p }}</span>
{% else %}
<a class="botao" href="/clientes?pagina={{ p }}{{ qs_base }}">{{ p }}</a>
{% endif %}
{% endfor %}
{% if pagina < total_paginas %}
<a class="botao" href="/clientes?pagina={{ pagina + 1 }}{{ qs_base }}" aria-label="Próxima página">&rsaquo;</a>
<a class="botao" href="/clientes?pagina={{ total_paginas }}{{ qs_base }}" aria-label="Última página">&raquo;</a>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}
+32 -4
View File
@@ -186,10 +186,38 @@ def test_listar_clientes_pagina_com_controles_de_paginacao(monkeypatch):
resposta = client.get("/clientes?pagina=2&por_pagina=100")
assert resposta.status_code == 200
assert "Página 2 de 3" in resposta.text
assert "250 clientes" in resposta.text
assert "Anterior" in resposta.text
assert "Próxima" in resposta.text
assert "Exibindo 101 a 200 de 250 clientes" in resposta.text
assert "pagina=1&ordenar_por=razao&direcao=asc&por_pagina=100" in resposta.text
assert "pagina=3&ordenar_por=razao&direcao=asc&por_pagina=100" in resposta.text
def test_listar_clientes_footer_com_selecao_de_por_pagina(monkeypatch):
monkeypatch.setattr(
router_clientes.db_clientes,
"listar_clientes",
lambda busca=None, limite=100, offset=0, ordenar_por="razao", direcao="asc": [
{"ID": i, "CODIGO": f"{i:06d}", "RAZAO": f"Cliente {i}", "FANTASIA": f"C{i}", "CNPJ": "1"}
for i in range(1, 16)
],
)
monkeypatch.setattr(router_clientes.db_clientes, "contar_clientes", lambda busca=None, ordenar_por="razao": 98)
resposta = client.get("/clientes?pagina=1")
assert resposta.status_code == 200
assert 'value="15" selected' in resposta.text
assert "Exibindo 1 a 15 de 98 clientes" in resposta.text
# 98 / 15 = 7 paginas -> <=7, sem reticencias, todas visiveis
assert "pagina=7&ordenar_por=razao&direcao=asc&por_pagina=15" in resposta.text
def test_listar_clientes_paginacao_com_reticencias_quando_muitas_paginas(monkeypatch):
monkeypatch.setattr(router_clientes.db_clientes, "listar_clientes", lambda **kwargs: [])
monkeypatch.setattr(router_clientes.db_clientes, "contar_clientes", lambda busca=None, ordenar_por="razao": 500)
# 500 / 5 = 100 paginas, pedindo a pagina 10 (no meio) -> tem reticencias
resposta = client.get("/clientes?pagina=10&por_pagina=5")
assert resposta.status_code == 200
assert "&hellip;" in resposta.text
def test_listar_clientes_ordenar_por_invalido_cai_no_padrao(monkeypatch):