Programação Por Equipe DevBras

Capa do artigo: Trocar o "Tem certeza?" por um desfazer que funciona de verdade

Trocar o "Tem certeza?" por um desfazer que funciona de verdade

Decida quando usar desfazer e quando confirmar, implemente exclusão reversível com prazo no banco e escreva uma confirmação que nomeia o que será apagado.

Uma analista arquivou por engano o cliente "Padaria Estrela", que tinha 340 notas fiscais vinculadas. O sistema perguntou "Tem certeza?", como pergunta antes de qualquer ação, e ela apertou Enter, como aperta trinta vezes por dia. A confirmação existia e não protegeu nada.

A resposta curta: para ação reversível, não pergunte, deixe desfazer. Execute na hora e mostre "Cliente arquivado. Desfazer" por alguns segundos, com o desfazer funcionando no servidor. Para ação irreversível, confirme nomeando o objeto e exija um gesto diferente do clique automático. Misturar os dois casos com a mesma caixa genérica é o que treina as pessoas a clicar sem ler.

Por que o "Tem certeza?" para de funcionar

Confirmação é uma interrupção. Na primeira vez, a pessoa lê. Na décima, ela já sabe onde fica o botão e o dedo vai sozinho. Quando toda ação pede confirmação, a caixa deixa de carregar informação, porque aparece tanto no trivial quanto no grave.

O texto genérico agrava. "Tem certeza que deseja continuar?" não diz o que vai acontecer. Se a pessoa clicou no cliente errado, a pergunta não mostra isso. Ela confirma a ação certa no objeto errado.

Desfazer funciona melhor porque não depende de atenção prévia. A pessoa percebe o erro depois, ao ver o resultado, que é quando humanos realmente percebem erros. O sistema só precisa permitir voltar.

Como decidir entre desfazer e confirmar

Faça duas perguntas sobre a ação. Dá para reverter tecnicamente? E o efeito sai do sistema, como um e-mail enviado, um pagamento estornado ou um arquivo enviado para o governo?

  • Reversível e interno (arquivar, mover, remover da lista, excluir para a lixeira): execute e ofereça desfazer.
  • Irreversível ou com efeito externo (excluir definitivamente, enviar campanha, cancelar nota): confirme com o nome do objeto e a consequência.
  • Em massa (arquivar 200 clientes): mostre o número antes de executar e ofereça desfazer depois. Número grande inesperado é o melhor alarme que existe.

Como implementar o desfazer no servidor

O desfazer que só esconde a linha na tela e dispara o DELETE depois de cinco segundos é frágil. Se a pessoa fecha a aba, a ação nunca acontece, ou acontece sem ninguém ver. O jeito robusto é executar de verdade de forma reversível e apagar de vez só depois de um prazo.

  1. Adicione uma coluna de exclusão lógica, como arquivado_em, nula enquanto o registro está ativo.
  2. A ação "arquivar" preenche a coluna com a data e hora atuais. Todas as consultas normais filtram arquivado_em IS NULL.
  3. O desfazer limpa a coluna, mas só para o registro daquela pessoa e dentro do prazo.
  4. Uma tarefa agendada apaga ou anonimiza o que passou do prazo de retenção definido pelo negócio.
  5. Registre quem arquivou e quando, para o suporte conseguir reverter sem acesso direto ao banco.

Em MySQL, para a tabela de clientes do exemplo:

ALTER TABLE clientes
  ADD COLUMN arquivado_em DATETIME NULL,
  ADD COLUMN arquivado_por INT NULL;

-- arquivar
UPDATE clientes
   SET arquivado_em = NOW(), arquivado_por = 17
 WHERE id = 4821 AND arquivado_em IS NULL;

-- desfazer, só dentro de 30 dias
UPDATE clientes
   SET arquivado_em = NULL, arquivado_por = NULL
 WHERE id = 4821
   AND arquivado_em >= NOW() - INTERVAL 30 DAY;

-- limpeza agendada
DELETE FROM clientes
 WHERE arquivado_em < NOW() - INTERVAL 30 DAY;

A limpeza agendada precisa respeitar chaves estrangeiras. Se o cliente tem notas fiscais, você provavelmente não apaga o cliente, só o mantém arquivado. Revise isso antes de rodar o DELETE, e leia apagar um registro sem apagar a tabela para não errar o WHERE justamente na rotina de limpeza.

No front, a mensagem de desfazer chama o endpoint de verdade:

async function arquivarCliente(id, nome) {
  const r = await fetch('/api/clientes/' + id + '/arquivar', { method: 'POST' });
  if (!r.ok) {
    mostrarErro('Não foi possível arquivar ' + nome + '.');
    return;
  }
  removerDaLista(id);
  mostrarAviso('Cliente ' + nome + ' arquivado.', {
    acao: 'Desfazer',
    aoClicar: async () => {
      const d = await fetch('/api/clientes/' + id + '/restaurar', { method: 'POST' });
      if (d.ok) recarregarLista();
      else mostrarErro('Não foi possível restaurar ' + nome + '.');
    }
  });
}

Dois cuidados. A mensagem fica na tela tempo suficiente para ser lida e tem um botão alcançável pelo teclado. E existe um segundo caminho para restaurar, uma tela "Arquivados", porque quem perdeu o aviso também merece voltar. Os detalhes de tempo e acessibilidade desse aviso estão em aviso que some sozinho.

Como escrever a confirmação que protege

Quando a ação não tem volta, a caixa precisa quebrar o automatismo. Três elementos fazem isso.

  1. O título nomeia o objeto e a ação: "Excluir definitivamente Padaria Estrela?" e não "Tem certeza?".
  2. O corpo diz a consequência em números: "As 340 notas fiscais vinculadas perdem o cliente. Isso não pode ser desfeito."
  3. O botão repete o verbo: "Excluir cliente", não "Sim" nem "OK". O botão de cancelar recebe o foco inicial.

Para o caso mais grave, peça que a pessoa digite o nome do objeto. É um gesto que não sai no automático e confirma que ela olhou o nome. Use com parcimônia: se tudo exigir digitação, vira o mesmo hábito com mais passos.

<dialog id="excluir-cliente" aria-labelledby="titulo-excluir">
  <form method="dialog">
    <h2 id="titulo-excluir">Excluir definitivamente Padaria Estrela?</h2>
    <p>As 340 notas fiscais vinculadas perdem o cliente. Isso não pode ser desfeito.</p>
    <label for="confirma-nome">Digite o nome do cliente para confirmar</label>
    <input id="confirma-nome" autocomplete="off">
    <button value="cancelar" autofocus>Cancelar</button>
    <button value="excluir" id="botao-excluir" disabled>Excluir cliente</button>
  </form>
</dialog>

Um script habilita o botão só quando o texto digitado é igual ao nome. Abra com showModal() e, ao fechar, devolva o foco para o botão que abriu a caixa, como explica modal que devolve o foco. E valide de novo no servidor: o botão desabilitado é conveniência, não proteção.

Erros comuns

  • Confirmar tudo, inclusive o trivial. A pessoa aprende a apertar Enter e a confirmação perde valor justamente no caso grave.
  • Desfazer só na tela. Se o DELETE roda depois de um temporizador no navegador, fechar a aba muda o resultado e ninguém sabe o estado real.
  • Botões "Sim" e "Não". A pessoa precisa reler a pergunta para saber qual é qual, e em pergunta negativa erra o sentido.
  • Foco inicial no botão destrutivo. Um Enter apressado apaga o que a caixa deveria proteger.
  • Aviso de desfazer como único caminho de volta. Quem não viu os poucos segundos de aviso fica sem saída e abre chamado.
  • Limpeza agendada sem filtro de prazo bem testado. Um erro no WHERE transforma o recurso de segurança em exclusão em massa.

Checklist

  • Cada ação destrutiva foi classificada como reversível ou irreversível.
  • Ações reversíveis executam na hora e oferecem desfazer que chama o servidor.
  • Existe uma tela de arquivados ou lixeira para restaurar depois do aviso.
  • A confirmação de ação irreversível nomeia o objeto e diz a consequência.
  • O botão de confirmar repete o verbo, e o foco inicial fica em cancelar.
  • Ações em massa mostram a quantidade antes de executar.
  • A limpeza definitiva roda com prazo explícito e foi testada numa cópia do banco.

Escolha a ação mais destrutiva do seu sistema e execute numa conta de teste. Se foi engano, a pessoa consegue voltar sozinha, sem chamar o suporte e sem você abrir o banco?

Avalie este artigo

Referência: https://www.nngroup.com/articles/confirmation-dialog/