Endereca 5 achados da revisao final de branch: - Important #1: normalizar_telefone_com_ddi agora e idempotente (nao duplica 55 independente do tamanho) - Important #2: mascara de telefone detecta DDI colado por tamanho total (12/13 digitos), nao mais so pelo campo especifico - Minor #3: buscarCnpj pula lookup na Receita quando DV do CNPJ e invalido, preservando a mensagem de erro - Minor #4: detectarEValidarDocumento roda uma vez no carregamento da pagina - Minor #5: autofill da Receita dispara evento input para acionar as mascaras de UF/CEP/TELEFONE Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
220 lines
8.7 KiB
HTML
220 lines
8.7 KiB
HTML
<!-- app/templates/cliente_form.html -->
|
||
{% extends "base.html" %}
|
||
{% block titulo %}{{ "Editar" if cliente else "Novo" }} cliente{% endblock %}
|
||
{% block conteudo %}
|
||
<h1>{{ "Editar" if cliente else "Novo" }} cliente</h1>
|
||
{% if erro_codigo_duplicado %}
|
||
<div class="mensagem danger">
|
||
O código {{ dados.CODIGO }} já está em uso por outro cliente — escolha outro código antes de salvar.
|
||
</div>
|
||
{% endif %}
|
||
{% if aviso_dv_invalido %}
|
||
<div class="mensagem warning">
|
||
O dígito verificador do documento ({{ dados.CNPJ }}) não confere — confirme se digitou certo antes de salvar.
|
||
<form method="post">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||
{% for campo, valor in dados.items() %}<input type="hidden" name="{{ campo }}" value="{{ valor }}">{% endfor %}
|
||
<input type="hidden" name="confirmar_dv_invalido" value="1">
|
||
<button type="submit">Cadastrar mesmo assim</button>
|
||
</form>
|
||
</div>
|
||
{% endif %}
|
||
{% if erro_cnpj_invalido %}
|
||
<div class="mensagem danger">
|
||
O dígito verificador do CNPJ ({{ dados.CNPJ }}) não confere — corrija antes de salvar.
|
||
</div>
|
||
{% endif %}
|
||
{% if erro_gravacao %}
|
||
<div class="mensagem danger">
|
||
Não foi possível salvar: {{ erro_gravacao }}
|
||
</div>
|
||
{% endif %}
|
||
<div class="cartao">
|
||
<form method="post">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||
<div class="grid-form">
|
||
{% for campo in campos %}
|
||
{% set largura_total = campo in ["OBS"] %}
|
||
<div class="{{ 'grid-form-largura-total' if largura_total }}">
|
||
<label for="{{ campo }}">{{ campo }}{% if campo in campos_obrigatorios %} *{% endif %}</label>
|
||
{% if campo in ["DATA_CONTRATO", "VENCIMENTO_CONTRATO"] %}
|
||
<input type="date" id="{{ campo }}" name="{{ campo }}" value="{{ dados[campo] if dados else '' }}" {% if campo in campos_obrigatorios %}required{% endif %}>
|
||
{% elif campo in ["IMPLANTACAO", "MENSALIDADE", "JUROS_MORA", "JUROS_MULTA"] %}
|
||
<input type="number" step="0.01" id="{{ campo }}" name="{{ campo }}" value="{{ dados[campo] if dados else '' }}" {% if campo in campos_obrigatorios %}required{% endif %}>
|
||
{% else %}
|
||
<input type="text" id="{{ campo }}" name="{{ campo }}" value="{{ dados[campo] if dados else '' }}" {% if campo in campos_obrigatorios %}required{% endif %}>
|
||
{% if campo == "CNPJ" %}
|
||
<small id="cnpj-status" style="display:block;color:var(--cor-texto-secundario);margin-top:4px"></small>
|
||
{% endif %}
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
<p><small>* campos obrigatórios. Os demais podem ficar em branco — implantação/mensalidade/juros ficam 0,00 e as datas de contrato ficam com a data de hoje até serem ajustadas.</small></p>
|
||
<button type="submit" id="botao-salvar">Salvar</button>
|
||
</form>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
// Ao sair do campo CNPJ, busca os dados na Receita (via BrasilAPI, ver
|
||
// app/cnpj_lookup.py) e preenche os campos abaixo — só os que estiverem
|
||
// vazios, nunca sobrescreve o que já foi digitado. Não faz nada para CPF
|
||
// (a rota /clientes/consulta-cnpj só responde para CNPJ).
|
||
var $cnpj = document.getElementById('CNPJ');
|
||
var $status = document.getElementById('cnpj-status');
|
||
if (!$cnpj) return;
|
||
|
||
var $botaoSalvar = document.getElementById('botao-salvar');
|
||
|
||
// Mesma fórmula de dígito verificador de app/normalize.py
|
||
// (_digito_verificador + validar_dv_cnpj) — módulo 11 sobre valor ASCII
|
||
// − 48 de cada caractere, suporta CNPJ numérico e o alfanumérico
|
||
// vigente desde 31/07/2026.
|
||
var PESOS_CNPJ_DV1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
|
||
var PESOS_CNPJ_DV2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
|
||
|
||
function digitoVerificador(valores, pesos) {
|
||
var soma = 0;
|
||
for (var i = 0; i < valores.length; i++) soma += valores[i] * pesos[i];
|
||
var resto = soma % 11;
|
||
return resto < 2 ? 0 : 11 - resto;
|
||
}
|
||
|
||
function valorCaractere(c) {
|
||
return c.charCodeAt(0) - 48;
|
||
}
|
||
|
||
function validarDvCnpj(cnpj14) {
|
||
var base = [];
|
||
for (var i = 0; i < 12; i++) base.push(valorCaractere(cnpj14[i]));
|
||
var dvInformado = [parseInt(cnpj14[12], 10), parseInt(cnpj14[13], 10)];
|
||
var dv1 = digitoVerificador(base, PESOS_CNPJ_DV1);
|
||
var dv2 = digitoVerificador(base.concat([dv1]), PESOS_CNPJ_DV2);
|
||
return dvInformado[0] === dv1 && dvInformado[1] === dv2;
|
||
}
|
||
|
||
function detectarEValidarDocumento() {
|
||
var limpo = normalizarCnpj($cnpj.value);
|
||
if ($botaoSalvar) $botaoSalvar.disabled = false;
|
||
|
||
if (/^[0-9]{11}$/.test(limpo)) {
|
||
if ($status) {
|
||
$status.textContent = 'CPF detectado.';
|
||
$status.style.color = 'var(--cor-texto-secundario)';
|
||
}
|
||
return;
|
||
}
|
||
if (/^[0-9A-Z]{12}[0-9]{2}$/.test(limpo)) {
|
||
if (validarDvCnpj(limpo)) {
|
||
if ($status) {
|
||
$status.textContent = 'CNPJ detectado.';
|
||
$status.style.color = 'var(--cor-texto-secundario)';
|
||
}
|
||
} else {
|
||
if ($status) {
|
||
$status.textContent = 'CNPJ inválido (dígito verificador não confere).';
|
||
$status.style.color = 'var(--cor-perigo)';
|
||
}
|
||
if ($botaoSalvar) $botaoSalvar.disabled = true;
|
||
}
|
||
return;
|
||
}
|
||
// Tamanho ainda incompleto/indeterminado: não bloqueia, não rotula.
|
||
if ($status) $status.textContent = '';
|
||
}
|
||
|
||
$cnpj.addEventListener('input', detectarEValidarDocumento);
|
||
detectarEValidarDocumento();
|
||
|
||
function aplicarMascaraTelefone(elemento, digitosAposDdd) {
|
||
elemento.addEventListener('input', function () {
|
||
var digitos = elemento.value.replace(/\D/g, '');
|
||
// DDI 55 colado: detecta por tamanho total (12 ou 13 dígitos), não
|
||
// pelo tamanho esperado do campo específico — um número de 12
|
||
// dígitos colado no campo de celular ainda deve ter o 55 removido.
|
||
if ((digitos.length === 12 || digitos.length === 13) && digitos.slice(0, 2) === '55') {
|
||
digitos = digitos.slice(2);
|
||
}
|
||
digitos = digitos.slice(0, digitosAposDdd);
|
||
|
||
var ddd = digitos.slice(0, 2);
|
||
var resto = digitos.slice(2);
|
||
var meio = resto.slice(0, digitosAposDdd - 6);
|
||
var fim = resto.slice(digitosAposDdd - 6);
|
||
|
||
var formatado = '';
|
||
if (ddd) formatado += '(' + ddd;
|
||
if (ddd.length === 2) formatado += ') ';
|
||
formatado += meio;
|
||
if (fim) formatado += '-' + fim;
|
||
elemento.value = formatado;
|
||
});
|
||
}
|
||
|
||
var $telefone = document.getElementById('TELEFONE');
|
||
var $celular = document.getElementById('CELULAR');
|
||
// TELEFONE: DDD(2) + 8 dígitos = 10 dígitos após o DDI.
|
||
if ($telefone) aplicarMascaraTelefone($telefone, 10);
|
||
// CELULAR: DDD(2) + 9 dígitos (com o 9 na frente) = 11 dígitos após o DDI.
|
||
if ($celular) aplicarMascaraTelefone($celular, 11);
|
||
|
||
var $uf = document.getElementById('UF');
|
||
if ($uf) {
|
||
$uf.addEventListener('input', function () {
|
||
$uf.value = $uf.value.toUpperCase().replace(/[^A-Z]/g, '').slice(0, 2);
|
||
});
|
||
}
|
||
|
||
var $cep = document.getElementById('CEP');
|
||
if ($cep) {
|
||
$cep.addEventListener('input', function () {
|
||
var digitos = $cep.value.replace(/\D/g, '').slice(0, 8);
|
||
$cep.value = digitos.length > 5 ? digitos.slice(0, 5) + '-' + digitos.slice(5) : digitos;
|
||
});
|
||
}
|
||
|
||
var CAMPOS_PARA_PREENCHER = ['RAZAO', 'FANTASIA', 'ENDERECO', 'CIDADE', 'BAIRRO', 'UF', 'CEP', 'TELEFONE', 'EMAIL'];
|
||
|
||
function normalizarCnpj(valor) {
|
||
return (valor || '').toUpperCase().replace(/[^0-9A-Z]/g, '');
|
||
}
|
||
|
||
async function buscarCnpj() {
|
||
var limpo = normalizarCnpj($cnpj.value);
|
||
if (limpo.length !== 14) return;
|
||
if (limpo.length === 14 && !validarDvCnpj(limpo)) return;
|
||
|
||
if ($status) $status.textContent = 'Buscando dados do CNPJ...';
|
||
|
||
try {
|
||
var resposta = await fetch('/clientes/consulta-cnpj/' + encodeURIComponent(limpo));
|
||
var corpo = await resposta.json();
|
||
if (!resposta.ok || !corpo.encontrado) {
|
||
if ($status) $status.textContent = 'CNPJ não encontrado na Receita — preencha manualmente.';
|
||
return;
|
||
}
|
||
var preencheu = false;
|
||
CAMPOS_PARA_PREENCHER.forEach(function (campo) {
|
||
var $input = document.getElementById(campo);
|
||
if ($input && !$input.value && corpo.dados[campo]) {
|
||
$input.value = corpo.dados[campo];
|
||
$input.dispatchEvent(new Event('input'));
|
||
preencheu = true;
|
||
}
|
||
});
|
||
if ($status) {
|
||
$status.textContent = preencheu
|
||
? 'Dados preenchidos automaticamente a partir do CNPJ — confira antes de salvar.'
|
||
: 'CNPJ encontrado (campos já preenchidos não foram alterados).';
|
||
}
|
||
} catch (erro) {
|
||
if ($status) $status.textContent = '';
|
||
}
|
||
}
|
||
|
||
$cnpj.addEventListener('blur', buscarCnpj);
|
||
})();
|
||
</script>
|
||
{% endblock %}
|