docs: plano da melhoria de ui/ux da tabela de clientes
This commit is contained in:
@@ -0,0 +1,249 @@
|
|||||||
|
# Tabela de Clientes UI/UX Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Ajustar largura de colunas, alinhamento/centralização e densidade
|
||||||
|
das linhas da tabela em `/clientes`, igual para os dois perfis de usuário,
|
||||||
|
sem alterar comportamento, dados ou permissões.
|
||||||
|
|
||||||
|
**Architecture:** Adicionar uma classe `table-clientes` só nessa tabela
|
||||||
|
(escopando o CSS pra não afetar outras telas), colunas com largura e
|
||||||
|
alinhamento fixos via CSS (`nth-child` ou classes por coluna), padding
|
||||||
|
vertical reduzido, e uma classe de botão mais compacta pras ações da linha.
|
||||||
|
|
||||||
|
**Tech Stack:** Jinja2 (`app/templates/clientes_lista.html`), CSS puro
|
||||||
|
(`app/static/estilo.css`), pytest + `fastapi.testclient.TestClient`
|
||||||
|
(`tests/test_router_clientes.py`).
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Nenhuma mudança de permissão/visibilidade por perfil (admin vs. padrão)
|
||||||
|
nesta tela — confirmado explicitamente fora de escopo.
|
||||||
|
- Nenhuma mudança em dados exibidos, busca, filtro de status, ordenação,
|
||||||
|
paginação, no toggle de status ou no botão "Gerar chave" — só a
|
||||||
|
apresentação visual muda.
|
||||||
|
- Os ajustes de CSS ficam restritos à classe `table-clientes` — não alterar
|
||||||
|
as regras genéricas `table`/`th`/`td` (usadas por `/funcionarios`,
|
||||||
|
`/log`, etc.).
|
||||||
|
- Larguras/alinhamento por coluna (da spec): Código centralizado/estreito,
|
||||||
|
Razão e Fantasia flexíveis à esquerda (dividindo o espaço igualmente),
|
||||||
|
CNPJ centralizado/fixo sem quebrar linha, Status centralizado/estreito,
|
||||||
|
Ações à direita.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Classe de escopo, larguras/alinhamento e densidade
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `app/templates/clientes_lista.html`
|
||||||
|
- Modify: `app/static/estilo.css`
|
||||||
|
- Test: `tests/test_router_clientes.py`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: contexto já passado pela rota `GET /clientes` em
|
||||||
|
`app/routers/clientes.py` (`clientes`, `ordenar_por`, `direcao`, `status`,
|
||||||
|
`busca`, `csrf_token`, `redirecionar_para`, paginação) — nenhuma mudança
|
||||||
|
nesse router nesta tarefa.
|
||||||
|
- Produces: nada consumido por outras tasks (task única).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Escrever o teste que falha**
|
||||||
|
|
||||||
|
Adicionar ao final de `tests/test_router_clientes.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_listar_clientes_tabela_tem_classe_de_escopo():
|
||||||
|
resposta = client.get("/clientes")
|
||||||
|
assert resposta.status_code == 200
|
||||||
|
assert '<table class="table-clientes">' in resposta.text
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Rodar o teste para confirmar que falha**
|
||||||
|
|
||||||
|
Run: `pytest tests/test_router_clientes.py::test_listar_clientes_tabela_tem_classe_de_escopo -v`
|
||||||
|
Expected: FAIL — a tabela hoje é `<table>` sem classe.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Editar `app/templates/clientes_lista.html`**
|
||||||
|
|
||||||
|
No bloco da tabela (linhas atuais 28-71), aplicar a classe `table-clientes`
|
||||||
|
na tag `<table>` e alinhar os `<th>`/`<td>` com classes de coluna. Substituir
|
||||||
|
o trecho:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="tabela-wrap">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
{% set colunas_tabela = [("codigo", "Código"), ("razao", "Razão"), ("fantasia", "Fantasia"), ("cnpj", "CNPJ")] %}
|
||||||
|
{% 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&por_pagina={{ por_pagina }}&status={{ status }}{{ busca_qs }}">
|
||||||
|
{{ rotulo }}{% if ordenar_por == campo %}{{ " ▲" if direcao == "asc" else " ▼" }}{% endif %}
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
{% endfor %}
|
||||||
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
{% for c in clientes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ c.CODIGO }}</td>
|
||||||
|
<td>{{ c.RAZAO }}</td>
|
||||||
|
<td>{{ c.FANTASIA }}</td>
|
||||||
|
<td>{{ c.CNPJ }}</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="/clientes/{{ c.ID }}/alternar-ativo" style="display:inline">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<input type="hidden" name="redirecionar_para" value="{{ redirecionar_para }}">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="status-dot {{ 'status-dot-ativo' if c.ATIVO else 'status-dot-inativo' }}"
|
||||||
|
title="{{ 'Ativo — clique para desativar' if c.ATIVO else 'Inativo — clique para ativar' }}"
|
||||||
|
onclick='return confirm({{ ((("Desativar" if c.ATIVO else "Reativar") + " o cliente " + c.RAZAO + "?")) | tojson }})'
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ "Ativo" if c.ATIVO else "Inativo" }}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a class="botao" href="/clientes/{{ c.ID }}">Abrir</a>
|
||||||
|
<button type="button" class="botao botao-gerar-chave" data-codigo="{{ c.CODIGO }}" onclick="gerarChaveReativacao(this)">Gerar chave</button>
|
||||||
|
<span class="chave-feedback" aria-live="polite"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
por:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="tabela-wrap">
|
||||||
|
<table class="table-clientes">
|
||||||
|
<tr>
|
||||||
|
{% set colunas_tabela = [("codigo", "Código", "col-codigo"), ("razao", "Razão", "col-razao"), ("fantasia", "Fantasia", "col-fantasia"), ("cnpj", "CNPJ", "col-cnpj")] %}
|
||||||
|
{% for campo, rotulo, classe_coluna in colunas_tabela %}
|
||||||
|
{% set nova_direcao = "desc" if (ordenar_por == campo and direcao == "asc") else "asc" %}
|
||||||
|
<th class="{{ classe_coluna }}">
|
||||||
|
<a href="/clientes?ordenar_por={{ campo }}&direcao={{ nova_direcao }}&pagina=1&por_pagina={{ por_pagina }}&status={{ status }}{{ busca_qs }}">
|
||||||
|
{{ rotulo }}{% if ordenar_por == campo %}{{ " ▲" if direcao == "asc" else " ▼" }}{% endif %}
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
{% endfor %}
|
||||||
|
<th class="col-status">Status</th>
|
||||||
|
<th class="col-acoes"></th>
|
||||||
|
</tr>
|
||||||
|
{% for c in clientes %}
|
||||||
|
<tr>
|
||||||
|
<td class="col-codigo">{{ c.CODIGO }}</td>
|
||||||
|
<td class="col-razao">{{ c.RAZAO }}</td>
|
||||||
|
<td class="col-fantasia">{{ c.FANTASIA }}</td>
|
||||||
|
<td class="col-cnpj">{{ c.CNPJ }}</td>
|
||||||
|
<td class="col-status">
|
||||||
|
<form method="post" action="/clientes/{{ c.ID }}/alternar-ativo" style="display:inline">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<input type="hidden" name="redirecionar_para" value="{{ redirecionar_para }}">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="status-dot {{ 'status-dot-ativo' if c.ATIVO else 'status-dot-inativo' }}"
|
||||||
|
title="{{ 'Ativo — clique para desativar' if c.ATIVO else 'Inativo — clique para ativar' }}"
|
||||||
|
onclick='return confirm({{ ((("Desativar" if c.ATIVO else "Reativar") + " o cliente " + c.RAZAO + "?")) | tojson }})'
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ "Ativo" if c.ATIVO else "Inativo" }}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td class="col-acoes">
|
||||||
|
<a class="botao botao-tabela" href="/clientes/{{ c.ID }}">Abrir</a>
|
||||||
|
<button type="button" class="botao botao-tabela botao-gerar-chave" data-codigo="{{ c.CODIGO }}" onclick="gerarChaveReativacao(this)">Gerar chave</button>
|
||||||
|
<span class="chave-feedback" aria-live="polite"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Notas:
|
||||||
|
- `colunas_tabela` ganhou um terceiro elemento (a classe da coluna) — o
|
||||||
|
`{% for %}` foi ajustado para desempacotar os 3 valores.
|
||||||
|
- `botao-tabela` foi adicionado junto de `botao` nos dois botões de ação
|
||||||
|
(não substitui `botao`, soma-se a ele — `botao-gerar-chave` continua
|
||||||
|
presente também).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Adicionar CSS em `app/static/estilo.css`**
|
||||||
|
|
||||||
|
Adicionar ao final do arquivo:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.table-clientes th.col-codigo, .table-clientes td.col-codigo,
|
||||||
|
.table-clientes th.col-cnpj, .table-clientes td.col-cnpj,
|
||||||
|
.table-clientes th.col-status, .table-clientes td.col-status {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.table-clientes th.col-acoes, .table-clientes td.col-acoes { text-align: right; }
|
||||||
|
|
||||||
|
.table-clientes td.col-codigo, .table-clientes td.col-cnpj {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.table-clientes td.col-cnpj { white-space: nowrap; }
|
||||||
|
|
||||||
|
.table-clientes col.col-codigo { width: 90px; }
|
||||||
|
.table-clientes col.col-razao, .table-clientes col.col-fantasia { width: 27%; }
|
||||||
|
.table-clientes col.col-cnpj { width: 160px; }
|
||||||
|
.table-clientes col.col-status { width: 70px; }
|
||||||
|
.table-clientes col.col-acoes { width: 230px; }
|
||||||
|
|
||||||
|
.table-clientes th, .table-clientes td { padding: 7px 14px; }
|
||||||
|
|
||||||
|
.botao-tabela { margin-top: 0; padding: 7px 12px; }
|
||||||
|
```
|
||||||
|
|
||||||
|
E, no `<table class="table-clientes">` do template (Step 3), adicionar um
|
||||||
|
`<colgroup>` logo após a abertura da tag, antes da primeira `<tr>`, para
|
||||||
|
que as larguras acima sejam aplicadas:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<table class="table-clientes">
|
||||||
|
<colgroup>
|
||||||
|
<col class="col-codigo">
|
||||||
|
<col class="col-razao">
|
||||||
|
<col class="col-fantasia">
|
||||||
|
<col class="col-cnpj">
|
||||||
|
<col class="col-status">
|
||||||
|
<col class="col-acoes">
|
||||||
|
</colgroup>
|
||||||
|
<tr>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Rodar o teste para confirmar que passa**
|
||||||
|
|
||||||
|
Run: `pytest tests/test_router_clientes.py -v`
|
||||||
|
Expected: PASS em todos os testes do arquivo (existentes + o novo).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Rodar a suíte completa**
|
||||||
|
|
||||||
|
Run: `pytest`
|
||||||
|
Expected: PASS em todos os testes do projeto.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add app/templates/clientes_lista.html app/static/estilo.css tests/test_router_clientes.py
|
||||||
|
git commit -m "feat: ajusta colunas, alinhamento e densidade da tabela de clientes"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review Notes
|
||||||
|
|
||||||
|
- **Spec coverage:** classe de escopo `table-clientes` (spec: "escopo do
|
||||||
|
CSS"); larguras/alinhamento por coluna via `<colgroup>` + classes (spec:
|
||||||
|
tabela de larguras/alinhamento); cabeçalhos alinhados junto com as
|
||||||
|
colunas (spec: "cabeçalhos"); padding `10px→7px` e `.botao-tabela` sem
|
||||||
|
margem superior (spec: "densidade"); nenhuma mudança de dado/ordenação/
|
||||||
|
paginação/permissão (spec: "sem mudança de comportamento" e "fora de
|
||||||
|
escopo"). Teste cobre a classe de escopo aplicada.
|
||||||
|
- **Placeholder scan:** nenhum "TBD"/"implementar depois" — HTML e CSS
|
||||||
|
completos, prontos pra colar.
|
||||||
|
- **Type consistency:** task única, sem assinaturas cruzando tasks.
|
||||||
Reference in New Issue
Block a user