Files
GTI_GERENCIADOR_FISCAL/docs/superpowers/plans/2026-08-20-tabela-clientes-ui.md
T

10 KiB

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:

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:

  <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:

  <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:

.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:

    <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
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.