feat: botao de gerar chave de reativacao por linha na lista de clientes
This commit is contained in:
@@ -242,6 +242,10 @@ a.botao:hover { border-color: var(--cor-acento); color: var(--cor-acento); }
|
|||||||
.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-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); }
|
.paginacao-reticencias { padding: 0 4px; color: var(--cor-texto-secundario); }
|
||||||
|
|
||||||
|
.chave-feedback { margin-left: 6px; font-size: 0.85rem; color: var(--cor-sucesso); }
|
||||||
|
.chave-feedback-erro { color: var(--cor-perigo); }
|
||||||
|
.botao-gerar-chave { margin-left: 6px; }
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
*, *::before, *::after { transition: none !important; }
|
*, *::before, *::after { transition: none !important; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,11 @@
|
|||||||
<td>{{ c.RAZAO }}</td>
|
<td>{{ c.RAZAO }}</td>
|
||||||
<td>{{ c.FANTASIA }}</td>
|
<td>{{ c.FANTASIA }}</td>
|
||||||
<td>{{ c.CNPJ }}</td>
|
<td>{{ c.CNPJ }}</td>
|
||||||
<td><a class="botao" href="/clientes/{{ c.ID }}">Abrir</a></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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
@@ -80,4 +84,68 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Gera a chave de reativação do mês vigente pro cliente da linha
|
||||||
|
// clicada, reaproveitando o endpoint já usado pelo gerador manual
|
||||||
|
// (/gerador-chave/gerar) — o algoritmo de cifra roda só no backend
|
||||||
|
// (app/gerador_chave.py), esta página só manda código+mês/ano e
|
||||||
|
// copia o resultado.
|
||||||
|
const CSRF_TOKEN = "{{ csrf_token }}";
|
||||||
|
|
||||||
|
function _mesAnoAtual() {
|
||||||
|
const hoje = new Date();
|
||||||
|
return `${hoje.getFullYear()}-${String(hoje.getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gerarChaveReativacao(botao) {
|
||||||
|
const codigo = botao.dataset.codigo;
|
||||||
|
const feedback = botao.parentElement.querySelector('.chave-feedback');
|
||||||
|
botao.disabled = true;
|
||||||
|
feedback.textContent = '';
|
||||||
|
feedback.classList.remove('chave-feedback-erro');
|
||||||
|
|
||||||
|
const corpo = new URLSearchParams({
|
||||||
|
csrf_token: CSRF_TOKEN,
|
||||||
|
tipo: 'reativacao',
|
||||||
|
codigo: codigo,
|
||||||
|
referencia: _mesAnoAtual(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let resposta, dados;
|
||||||
|
try {
|
||||||
|
resposta = await fetch('/gerador-chave/gerar', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: corpo,
|
||||||
|
});
|
||||||
|
dados = await resposta.json();
|
||||||
|
} catch (e) {
|
||||||
|
feedback.textContent = 'Falha de rede — tente de novo.';
|
||||||
|
feedback.classList.add('chave-feedback-erro');
|
||||||
|
botao.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resposta.ok) {
|
||||||
|
feedback.textContent = dados.erro || 'Não foi possível gerar a chave.';
|
||||||
|
feedback.classList.add('chave-feedback-erro');
|
||||||
|
botao.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(dados.chave);
|
||||||
|
feedback.textContent = 'Copiado!';
|
||||||
|
} catch (e) {
|
||||||
|
feedback.textContent = 'Chave gerada, mas não copiou automaticamente.';
|
||||||
|
feedback.classList.add('chave-feedback-erro');
|
||||||
|
}
|
||||||
|
botao.disabled = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
feedback.textContent = '';
|
||||||
|
feedback.classList.remove('chave-feedback-erro');
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -187,6 +187,23 @@ def test_listar_clientes_inclui_csrf_token_no_contexto(monkeypatch):
|
|||||||
assert 'const CSRF_TOKEN = "";' in resposta.text
|
assert 'const CSRF_TOKEN = "";' in resposta.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_listar_clientes_tem_botao_de_gerar_chave_por_linha(monkeypatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
router_clientes.db_clientes,
|
||||||
|
"listar_clientes",
|
||||||
|
lambda busca=None, limite=100, offset=0, ordenar_por="razao", direcao="asc": [
|
||||||
|
{"ID": 1, "CODIGO": "000042", "RAZAO": "Cliente Um", "FANTASIA": "Um", "CNPJ": "11222333000181"}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(router_clientes.db_clientes, "contar_clientes", lambda busca=None, ordenar_por="razao": 1)
|
||||||
|
|
||||||
|
resposta = client.get("/clientes")
|
||||||
|
assert resposta.status_code == 200
|
||||||
|
assert 'data-codigo="000042"' in resposta.text
|
||||||
|
assert "gerarChaveReativacao(this)" in resposta.text
|
||||||
|
assert "/gerador-chave/gerar" in resposta.text
|
||||||
|
|
||||||
|
|
||||||
def test_listar_clientes_pagina_com_controles_de_paginacao(monkeypatch):
|
def test_listar_clientes_pagina_com_controles_de_paginacao(monkeypatch):
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
router_clientes.db_clientes,
|
router_clientes.db_clientes,
|
||||||
|
|||||||
Reference in New Issue
Block a user