Add 'gerenciador/' from commit '34cc9c75a57dedbf61a6e6a1ba1f35f76e72434b'

git-subtree-dir: gerenciador
git-subtree-mainline: f4f546c928
git-subtree-split: 34cc9c75a5
This commit is contained in:
gti
2026-09-29 14:43:12 -03:00
84 changed files with 8906 additions and 0 deletions
@@ -0,0 +1,235 @@
# Log: Seções Escondidas por Padrão — 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:** Fazer as duas seções da tela `/log` ("Ações no painel" e "Envios
automáticos de chave (n8n)") começarem fechadas ao carregar a página,
exigindo um clique no título ou em um botão "Mostrar" para expandir.
**Architecture:** Envolver cada seção em `<details class="secao-log">`
nativo do HTML (fechado por padrão, sem `open`), com um `<summary>` que
contém o título e um botão `Mostrar`/`Ocultar`. Um script inline pequeno
sincroniza o texto do botão e `aria-expanded` com o evento `toggle` nativo
do `<details>` — a abertura/fechamento em si não precisa de JavaScript,
só o texto do botão.
**Tech Stack:** Jinja2 (`app/templates/log.html`), CSS puro
(`app/static/estilo.css`), JavaScript vanilla inline, pytest +
`fastapi.testclient.TestClient` (`tests/test_router_log.py`).
## Global Constraints
- Nenhuma seção pode abrir automaticamente ao carregar a página (spec:
"Fora de escopo" — abrir automaticamente qualquer seção por padrão).
- Estado aberto/fechado não é persistido entre recarregamentos (sem
localStorage, cookie ou parâmetro de URL).
- Nenhum dado exibido, paginação, limite de 20 registros em "Ações no
painel" ou o botão "Limpar log" (visível só para admin) pode mudar de
comportamento — só o wrapper de exibição muda.
- A ordem das seções continua: "Ações no painel" primeiro, "Envios
automáticos de chave (n8n)" embaixo.
---
### Task 1: Seções `<details>` no template + estilo + teste
**Files:**
- Modify: `app/templates/log.html`
- Modify: `app/static/estilo.css`
- Test: `tests/test_router_log.py`
**Interfaces:**
- Consumes: contexto já passado pela rota `GET /log` em
`app/routers/log.py` (`acoes`, `envios`, `envios_configurado`, `eh_admin`,
`csrf_token`, `mensagem`, `mensagem_tipo`) — 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_log.py`:
```python
def test_pagina_log_secoes_comecam_fechadas(monkeypatch):
monkeypatch.setattr(router_log.db_log, "listar", lambda limite=20: [])
monkeypatch.setattr(router_log.n8n_client, "listar_envios", lambda limite=50: [])
monkeypatch.setattr(router_log.n8n_client, "configurado", lambda: False)
resposta = client.get("/log")
assert resposta.status_code == 200
texto = resposta.text
assert texto.count('<details class="secao-log">') == 2
assert "<details class=\"secao-log\" open>" not in texto
assert "<details class='secao-log' open>" not in texto
```
- [ ] **Step 2: Rodar o teste para confirmar que falha**
Run: `pytest tests/test_router_log.py::test_pagina_log_secoes_comecam_fechadas -v`
Expected: FAIL — o HTML atual não tem `<details class="secao-log">`.
- [ ] **Step 3: Reescrever `app/templates/log.html`**
Substituir o conteúdo do bloco `{% block conteudo %}` inteiro por:
```html
{% block conteudo %}
<h1>Log</h1>
<details class="secao-log">
<summary>
<span class="secao-log-titulo">Ações no painel</span>
<button type="button" class="botao-toggle-log" aria-expanded="false">Mostrar</button>
</summary>
<div class="secao-log-conteudo">
{% if eh_admin %}
<form method="post" action="/log/limpar" style="margin-bottom:14px">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" onclick="return confirm('Limpar todo o histórico de ações? Isso não pode ser desfeito.')">Limpar log</button>
</form>
{% endif %}
{% if acoes %}
<div class="tabela-wrap">
<table>
<tr><th>Quando</th><th>Usuário</th><th>Ação</th><th>Entidade</th><th>Status</th><th>Detalhe</th></tr>
{% for a in acoes %}
<tr>
<td>{{ a.CRIADO_EM }}</td>
<td>{{ a.USUARIO }}</td>
<td>{{ a.ACAO }}</td>
<td>{{ a.ENTIDADE }}{% if a.ENTIDADE_ID %} #{{ a.ENTIDADE_ID }}{% endif %}</td>
<td><span class="badge {{ 'badge-sucesso' if a.SUCESSO else 'badge-neutro' }}">{{ "OK" if a.SUCESSO else "ERRO" }}</span></td>
<td>{{ a.DETALHE or "" }}</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<p>Nenhuma ação registrada ainda.</p>
{% endif %}
</div>
</details>
<details class="secao-log">
<summary>
<span class="secao-log-titulo">Envios automáticos de chave (n8n)</span>
<button type="button" class="botao-toggle-log" aria-expanded="false">Mostrar</button>
</summary>
<div class="secao-log-conteudo">
{% if not envios_configurado %}
<div class="cartao">
<p>
Integração com o n8n não configurada — defina <code>N8N_API_URL</code> e
<code>N8N_API_KEY</code> no ambiente (ver <code>.env.example</code>) para ver
aqui os envios automáticos de chave feitos pelo workflow do n8n.
</p>
</div>
{% elif not envios %}
<p>Nenhum envio encontrado (ou não foi possível consultar o n8n agora).</p>
{% else %}
<div class="tabela-wrap">
<table>
<tr>
<th>Data</th><th>Cliente</th><th>Código</th><th>Competência</th><th>Status</th>
<th>Telefones enviados</th><th>Telefones com erro</th>
</tr>
{% for e in envios %}
<tr>
<td>{{ e.data_processamento }}</td>
<td>{{ e.nome_cliente }}</td>
<td>{{ e.codigo_cliente }}</td>
<td>{{ e.competencia }}</td>
<td>{{ e.status }}</td>
<td>{{ e.telefones_enviados }}</td>
<td>{{ e.telefones_com_erro or "" }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
</div>
</details>
<script>
document.querySelectorAll(".secao-log").forEach(function (secao) {
var botao = secao.querySelector(".botao-toggle-log");
var atualizar = function () {
var aberto = secao.hasAttribute("open");
botao.textContent = aberto ? "Ocultar" : "Mostrar";
botao.setAttribute("aria-expanded", aberto ? "true" : "false");
};
secao.addEventListener("toggle", atualizar);
atualizar();
});
</script>
{% endblock %}
```
Notas importantes:
- O `<button>` fica dentro do `<summary>`: clicar nele já aciona o toggle
nativo do `<details>` (o clique borbulha até o `<summary>`), então não
precisa de `onclick` para abrir/fechar — só o listener do evento
`toggle` para trocar o texto.
- Nenhum `<details>` tem o atributo `open` — as duas começam fechadas.
- O botão "Limpar log" (dentro de "Ações no painel") continua exatamente
igual, só mudou de lugar (dentro do `<div class="secao-log-conteudo">`).
- [ ] **Step 4: Adicionar estilo em `app/static/estilo.css`**
Adicionar ao final do arquivo:
```css
.secao-log { margin-bottom: 20px; }
.secao-log summary {
display: flex; align-items: center; gap: 10px; cursor: pointer;
list-style: none; padding: 10px 0; user-select: none;
}
.secao-log summary::-webkit-details-marker { display: none; }
.secao-log summary::before {
content: "▸"; display: inline-block; color: var(--cor-texto-secundario);
transition: transform 0.15s ease;
}
.secao-log[open] summary::before { transform: rotate(90deg); }
.secao-log-titulo { font-size: 1.17em; font-weight: 700; }
.botao-toggle-log {
margin-top: 0; padding: 6px 12px; font-size: 0.85rem; font-weight: 400;
background: transparent; border: 1px solid var(--cor-borda); color: var(--cor-texto);
}
.botao-toggle-log:hover { border-color: var(--cor-acento); color: var(--cor-acento); }
.secao-log-conteudo { padding-top: 4px; }
```
- [ ] **Step 5: Rodar o teste para confirmar que passa**
Run: `pytest tests/test_router_log.py -v`
Expected: PASS em todos os testes do arquivo (incluindo os 6 já existentes
e o novo).
- [ ] **Step 6: Rodar a suíte completa**
Run: `pytest`
Expected: PASS em todos os testes do projeto (nenhum outro teste referencia
a estrutura HTML de `log.html`, então nada mais deve quebrar).
- [ ] **Step 7: Commit**
```bash
git add app/templates/log.html app/static/estilo.css tests/test_router_log.py
git commit -m "feat: secoes da tela de log comecam fechadas, com clique para abrir"
```
---
## Self-Review Notes
- **Spec coverage:** as duas seções ficam em `<details>` fechado (spec:
"estrutura"); título clicável com seta (spec: "controles — título");
botão Mostrar/Ocultar dentro do summary, toggle nativo sem duplicar lógica
(spec: "controles — botão"); nenhum estado persistido (spec: "estado não
é persistido"); ordem mantida; nenhuma mudança em dados/paginação/limite/
botão limpar (spec: "sem mudança de comportamento"). Teste cobre que as
duas seções carregam sem `open`.
- **Placeholder scan:** nenhum "TBD"/"implementar depois" — todo HTML, CSS
e JS estão completos e prontos para colar.
- **Type consistency:** não há funções/assinaturas cruzando tasks (task
única, sem mudança de router).
@@ -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.
@@ -0,0 +1,66 @@
# Log: seções escondidas por padrão, com clique para abrir
## Contexto
A tela `/log` mostra duas tabelas: "Ações no painel" (histórico de ações do
painel, já limitado aos 20 registros mais recentes) e "Envios automáticos de
chave (n8n)" (log dos envios automáticos de chave via WhatsApp, sem limite).
Como ambas aparecem sempre expandidas, a tela fica grande e atrapalha quando
o usuário só quer olhar rapidamente um dos dois logs.
## Objetivo
Cada seção da tela `/log` deve começar **escondida** (fechada) ao carregar a
página, exigindo um clique para expandir. Isso facilita, por exemplo, olhar
só o log de envios de chave sem precisar rolar past o log inteiro do painel.
## Design
**Estrutura:** cada seção (`Ações no painel` e `Envios automáticos de chave
(n8n)`) é envolvida em um elemento `<details class="secao-log">` HTML
nativo, **fechado por padrão** (sem o atributo `open`). A ordem das seções
não muda: Ações no painel primeiro, Envios automáticos de chave embaixo.
**Controles (dois, redundantes, ambos alternam o mesmo estado):**
- O título da seção (dentro de `<summary>`) é clicável — clicar nele
abre/fecha a seção. Uma seta (▸ fechado / ▾ aberto) ao lado do título
indica visualmente o estado atual.
- Um botão no estilo dos demais botões do painel (`Mostrar` / `Ocultar`)
fica ao lado do título, dentro do mesmo `<summary>`. Como ele está dentro
do `<summary>`, clicar nele já aciona o toggle nativo do `<details>` —
não precisa de lógica de toggle separada. Um script pequeno escuta o
evento `toggle` de cada `<details>` e só atualiza o texto do botão
(`Mostrar`↔`Ocultar`) e o atributo `aria-expanded`, para acessibilidade.
**Estado não é persistido:** toda vez que a página `/log` é carregada, as
duas seções começam fechadas — não guarda se o usuário tinha deixado aberta
antes (sem localStorage, sem cookie, sem parâmetro de URL). Simplicidade,
sem necessidade adicional hoje.
**Sem mudança de comportamento:** os dados carregados por seção (limite de
20 em "Ações no painel", botão "Limpar log" restrito a administradores
dentro dessa seção, listagem de envios do n8n) continuam exatamente como
estão hoje — só o wrapper de exibição (fechado/aberto) muda.
## Fora de escopo
- Lembrar o estado aberto/fechado entre recarregamentos de página.
- Abrir automaticamente alguma seção por padrão (as duas começam fechadas).
- Qualquer mudança nos dados exibidos, na paginação, no limite de 20 ou no
botão de limpar log.
## Arquivos afetados
- `app/templates/log.html` — envolver cada seção em `<details>`/`<summary>`,
adicionar o botão Mostrar/Ocultar e a seta indicadora dentro do `summary`.
- `app/static/estilo.css` — estilo do `<details>`/`<summary>` (remoção do
marcador padrão do navegador, seta customizada que gira com `[open]`,
espaçamento do botão dentro do `summary`).
- Um pequeno `<script>` inline (ou incluído no bloco já existente do
template) para sincronizar o texto do botão com o evento `toggle`.
## Testes
- Teste de router (`tests/test_router_log.py`) verificando que a resposta
de `GET /log` contém os elementos `<details>` para as duas seções, sem o
atributo `open`, confirmando que ambas carregam fechadas por padrão.
@@ -0,0 +1,71 @@
# Tabela de Clientes: UI/UX — Colunas, Centralização e Densidade
## Contexto
A tabela em `/clientes` usa o estilo genérico de `table`/`th`/`td` do
projeto (compartilhado com outras telas como `/funcionarios` e `/log`).
Isso deixa a tabela de clientes com padding grande demais para o volume de
linhas que ela normalmente exibe, todas as colunas alinhadas à esquerda
sem distinção (mesmo colunas curtas como Código e Status), e a coluna CNPJ
estreita a ponto do valor quebrar em duas linhas.
## Objetivo
Melhorar o layout da tabela de `/clientes` — ajuste de largura de colunas,
alinhamento/centralização e densidade das linhas — igual para os dois
perfis de usuário (admin e padrão), já que hoje não existe distinção de
permissão nessa tela e este trabalho não introduz nenhuma.
## Fora de escopo
- Qualquer restrição de ação por perfil (admin vs. padrão) — confirmado
explicitamente que não é para mudar quem vê/usa o quê nesta tela.
- Mudança em dados exibidos, busca, filtro de status, paginação ou nas
ações em si (Abrir, Gerar chave, alternar status).
- Mudança no estilo genérico de `table`/`th`/`td` usado por outras telas
(`/funcionarios`, `/log`, etc.) — os ajustes ficam restritos a uma classe
específica da tabela de clientes, sem tocar nas regras compartilhadas.
## Design
**Escopo do CSS:** a tabela de `/clientes` ganha uma classe própria (ex.:
`table-clientes`) para que os ajustes de largura/alinhamento/densidade não
vazem para as demais tabelas do painel.
**Larguras e alinhamento por coluna:**
| Coluna | Largura | Alinhamento |
|---|---|---|
| Código | estreita e fixa | centralizado, `tabular-nums` |
| Razão | flexível (resto do espaço) | esquerda |
| Fantasia | flexível (resto do espaço) | esquerda |
| CNPJ | fixa, larga o bastante para não quebrar linha (18 caracteres) | centralizado, `tabular-nums`, `white-space: nowrap` |
| Status | estreita e fixa | centralizado |
| Ações (Abrir / Gerar chave) | fixa | direita |
Razão e Fantasia dividem o espaço restante igualmente entre si (cada uma
recebendo a mesma largura flexível), já que são as colunas de leitura mais
importantes da tabela.
**Cabeçalhos:** cada `<th>` reflete o alinhamento da coluna correspondente
(Código/CNPJ/Status centralizados, Razão/Fantasia à esquerda, Ações à
direita) — hoje todos os cabeçalhos ficam forçados à esquerda
independente do conteúdo da coluna.
**Densidade:** o padding vertical das células dessa tabela cai de `10px`
para `7px` (mantendo o padding horizontal), permitindo mais linhas visíveis
sem rolagem. Os botões de ação (Abrir / Gerar chave) ganham uma classe
específica de tabela que remove a margem superior de 14px do botão padrão
(pensada para formulários, não para dentro de células) e usa padding mais
compacto, mantendo o espaçamento consistente entre os dois botões.
**Sem mudança de comportamento:** os links de ordenação nos cabeçalhos
(`ordenar_por`/`direcao`), a bolinha de status clicável (toggle
ativo/inativo), o botão "Gerar chave" e a paginação continuam funcionando
exatamente como hoje — só a apresentação visual muda.
## Testes
- Teste de router (`tests/test_router_clientes.py`) verificando que a
tabela renderizada usa a classe `table-clientes` (confirma que o CSS
com escopo está sendo aplicado, sem checar detalhes visuais que testes
automatizados não conseguem verificar de forma confiável).