503 lines
15 KiB
HTML
503 lines
15 KiB
HTML
|
|
{% extends "index.html" %}
|
||
|
|
{% block title %}Clientes{% endblock %}
|
||
|
|
{% block content %}
|
||
|
|
<h1>🧾 Clientes</h1>
|
||
|
|
|
||
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin:16px 0;">
|
||
|
|
<input id="busca" type="text" placeholder="Pesquisar por nome/CNPJ…"
|
||
|
|
style="padding:.6rem;border:1px solid #ddd;border-radius:10px;min-width:280px;">
|
||
|
|
<button id="btnNovo" class="btn btn-primary" type="button">Novo Cliente</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Tabela -->
|
||
|
|
<div class="tbl-wrap">
|
||
|
|
<table class="tbl">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th style="width:45%;">Cliente</th>
|
||
|
|
<th style="width:25%;">CNPJ</th>
|
||
|
|
<th style="width:15%;">Status</th>
|
||
|
|
<th style="width:15%; text-align:right;">Ações</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="tbody-clientes">
|
||
|
|
<tr><td colspan="4" class="muted">Nenhum cliente encontrado.</td></tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Modal -->
|
||
|
|
<div id="modal" class="modal hidden" aria-hidden="true">
|
||
|
|
<div class="modal-backdrop" onclick="fecharModal()"></div>
|
||
|
|
<div class="modal-card">
|
||
|
|
<div id="status_bar" class="status-bar on" aria-hidden="true"></div>
|
||
|
|
<div class="modal-header">
|
||
|
|
<h3 id="modal-titulo">Novo Cliente</h3>
|
||
|
|
<button type="button" class="btn btn-secondary" onclick="fecharModal()">✕</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<form id="form-modal" onsubmit="return salvarModal(event)">
|
||
|
|
<input type="hidden" id="cli_id">
|
||
|
|
|
||
|
|
<div class="modal-body form-grid">
|
||
|
|
<div class="form-group">
|
||
|
|
<label>Nome fantasia *</label>
|
||
|
|
<input id="cli_nome" required>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="form-group">
|
||
|
|
<label>CNPJ</label>
|
||
|
|
<input id="cli_cnpj"
|
||
|
|
inputmode="numeric"
|
||
|
|
autocomplete="off"
|
||
|
|
placeholder="00.000.000/0000-00">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="form-group status-inline" id="grp_status">
|
||
|
|
<div style="flex:1">
|
||
|
|
<label>Status</label>
|
||
|
|
<select id="cli_ativo" onchange="setStatusUI(this.value === 'true')">
|
||
|
|
<option value="true">Ativo</option>
|
||
|
|
<option value="false">Inativo</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="modal-footer">
|
||
|
|
<button type="button" class="btn btn-secondary" onclick="fecharModal()">Cancelar</button>
|
||
|
|
<button type="submit" class="btn btn-primary">💾 Salvar</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.tbl-wrap{ background:#fff;border-radius:12px;overflow:hidden;border:1px solid #e5e7eb; }
|
||
|
|
.tbl{ width:100%; border-collapse:separate; border-spacing:0; }
|
||
|
|
.tbl thead th{
|
||
|
|
background:#2563eb; color:#fff; padding:12px; text-align:left; font-weight:700;
|
||
|
|
}
|
||
|
|
.tbl thead th:first-child{ border-top-left-radius:12px; }
|
||
|
|
.tbl thead th:last-child{ border-top-right-radius:12px; }
|
||
|
|
.tbl tbody td{ padding:12px; border-top:1px solid #eef2f7; vertical-align:middle; }
|
||
|
|
.tbl tbody tr:nth-child(even){ background:#f8fafc; }
|
||
|
|
.muted{ color:#6b7280; text-align:center; padding:16px; }
|
||
|
|
|
||
|
|
.badge{ display:inline-block; padding:.2rem .6rem; border-radius:999px; font-weight:700; font-size:.78rem; color:#fff; }
|
||
|
|
.on{ background:#16a34a; } .off{ background:#9ca3af; }
|
||
|
|
|
||
|
|
/* Modal */
|
||
|
|
.hidden{ display:none; }
|
||
|
|
.modal{
|
||
|
|
position: fixed;
|
||
|
|
inset: 0;
|
||
|
|
z-index: 1000;
|
||
|
|
display: flex; /* centraliza */
|
||
|
|
align-items: center; /* <-- centraliza vertical */
|
||
|
|
justify-content: center;
|
||
|
|
padding: 6vh 16px; /* respiro e evita colar nas bordas */
|
||
|
|
}
|
||
|
|
.modal-backdrop{
|
||
|
|
position: absolute;
|
||
|
|
inset: 0;
|
||
|
|
background: rgba(15,23,42,.45);
|
||
|
|
z-index: 0;
|
||
|
|
}
|
||
|
|
.modal-card{
|
||
|
|
position: relative;
|
||
|
|
z-index: 1; /* acima do backdrop */
|
||
|
|
width: min(760px, 92vw); /* largura consistente */
|
||
|
|
background: #fff;
|
||
|
|
border-radius: 20px;
|
||
|
|
overflow: hidden; /* barra acompanha cantos */
|
||
|
|
box-shadow: 0 18px 40px rgba(0,0,0,.18);
|
||
|
|
padding: 18px; /* respiro interno */
|
||
|
|
}
|
||
|
|
.modal-header{
|
||
|
|
display:flex; justify-content:space-between; align-items:center;
|
||
|
|
margin-bottom: 12px; position: relative; z-index: 1;
|
||
|
|
}
|
||
|
|
.modal-header h3{ margin:0; font-size:1.4rem; }
|
||
|
|
|
||
|
|
.modal-body{
|
||
|
|
margin-top: 6px; position: relative; z-index: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.modal-footer{
|
||
|
|
display:flex; justify-content:flex-end; gap:.6rem; margin-top:16px;
|
||
|
|
position: relative; z-index: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.form-grid{ display:grid; grid-template-columns:1fr; gap:14px; }
|
||
|
|
@media (min-width: 900px){ .form-grid{ grid-template-columns:1fr 1fr; } }
|
||
|
|
.form-group label{ display:block; margin-bottom:6px; color:#374151; }
|
||
|
|
.form-group input, .form-group select{
|
||
|
|
width:100%; padding:.65rem .8rem; border:1px solid #e5e7eb;
|
||
|
|
border-radius:12px; background:#fff;
|
||
|
|
}
|
||
|
|
.form-group input:focus, .form-group select:focus{
|
||
|
|
outline:none; border-color:#2563eb; box-shadow:0 0 0 3px rgba(37,99,235,.15);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-inline{ display:flex; align-items:flex-end; gap:12px; }
|
||
|
|
.badge{ display:inline-block; padding:.35rem .7rem; border-radius:999px; font-weight:700; color:#fff; }
|
||
|
|
.badge.on{ background:#16a34a; } /* ativo */
|
||
|
|
.badge.off{ background:#dc2626; } /* inativo */
|
||
|
|
|
||
|
|
.form-group label{ display:block; font-size:.9rem; color:#374151; margin-bottom:4px; }
|
||
|
|
.form-group input, .form-group select{
|
||
|
|
width:100%; padding:.55rem .7rem; border:1px solid #e5e7eb; border-radius:10px; background:#fff;
|
||
|
|
}
|
||
|
|
.hint{ font-size:.78rem; color:#6b7280; margin-top:4px; }
|
||
|
|
|
||
|
|
.btn {
|
||
|
|
padding: .5rem 1rem;
|
||
|
|
border: none;
|
||
|
|
border-radius: 8px;
|
||
|
|
cursor: pointer;
|
||
|
|
font-weight: 600;
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: .4rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-primary {
|
||
|
|
background-color: #2563eb;
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
.btn-primary:hover {
|
||
|
|
background-color: #1d4ed8;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-secondary {
|
||
|
|
background-color: #e5e7eb;
|
||
|
|
color: #374151;
|
||
|
|
}
|
||
|
|
.btn-secondary:hover {
|
||
|
|
background-color: #d1d5db;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-danger {
|
||
|
|
background-color: #dc2626;
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
.btn-danger:hover {
|
||
|
|
background-color: #b91c1c;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-bar{
|
||
|
|
position:absolute;
|
||
|
|
top:0; left:0;
|
||
|
|
width: 10px; /* espessura da barra */
|
||
|
|
height:100%;
|
||
|
|
background:#16a34a; /* default: ativo */
|
||
|
|
pointer-events:none; /* não intercepta cliques */
|
||
|
|
z-index: 0; /* fica por trás do conteúdo */
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Cores por estado */
|
||
|
|
.status-bar.on { background:#16a34a; } /* ativo (verde) */
|
||
|
|
.status-bar.off { background:#ef4444; } /* inativo (vermelho) */
|
||
|
|
|
||
|
|
.status-ativo {
|
||
|
|
background-color: #16a34a; /* verde */
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-inativo {
|
||
|
|
background-color: #ef4444; /* vermelho */
|
||
|
|
}
|
||
|
|
|
||
|
|
.modal.hidden {
|
||
|
|
display: none !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
td.acoes { text-align: right; white-space: nowrap; }
|
||
|
|
|
||
|
|
.btn-icon{
|
||
|
|
width: 36px;
|
||
|
|
height: 36px;
|
||
|
|
border: none;
|
||
|
|
border-radius: 10px;
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
font-size: 18px;
|
||
|
|
background: #e5e7eb; /* cinza claro */
|
||
|
|
color: #374151;
|
||
|
|
cursor: pointer;
|
||
|
|
margin-left: 6px;
|
||
|
|
}
|
||
|
|
.btn-icon:hover{ background:#d1d5db; }
|
||
|
|
.btn-icon.danger{ background:#dc2626; color:#fff; }
|
||
|
|
.btn-icon.danger:hover{ background:#b91c1c; }
|
||
|
|
</style>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
const onlyDigits = s => (s||'').replace(/\D/g,'');
|
||
|
|
const elBody = document.getElementById('tbody-clientes');
|
||
|
|
const elBusca = document.getElementById('busca');
|
||
|
|
|
||
|
|
function setStatusUI(isActive){
|
||
|
|
const bar = document.getElementById('status_bar');
|
||
|
|
if(!bar) return;
|
||
|
|
bar.classList.toggle('on', isActive);
|
||
|
|
bar.classList.toggle('off', !isActive);
|
||
|
|
}
|
||
|
|
|
||
|
|
// monta uma linha da tabela
|
||
|
|
function linha(c){
|
||
|
|
return `
|
||
|
|
<tr>
|
||
|
|
<td>${c.nome_fantasia || '-'}</td>
|
||
|
|
<td>${formatCNPJ(c.cnpj || '')}</td>
|
||
|
|
<td>
|
||
|
|
<span class="badge ${c.ativo ? 'on' : 'off'}">
|
||
|
|
${c.ativo ? 'Ativo' : 'Inativo'}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td class="acoes">
|
||
|
|
<button class="btn-icon" title="Editar" aria-label="Editar"
|
||
|
|
onclick='abrirModalEditar(${JSON.stringify(c)})'>
|
||
|
|
✏️
|
||
|
|
</button>
|
||
|
|
<button class="btn-icon danger" title="Excluir" aria-label="Excluir"
|
||
|
|
onclick="removerCliente('${c.id}')">
|
||
|
|
🗑️
|
||
|
|
</button>
|
||
|
|
</td>
|
||
|
|
</tr>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// renderiza a lista no tbody (com filtro da busca)
|
||
|
|
function render(lista){
|
||
|
|
const termo = (elBusca.value || '').toLowerCase();
|
||
|
|
const filtrada = lista.filter(c =>
|
||
|
|
(c.nome_fantasia || '').toLowerCase().includes(termo) ||
|
||
|
|
(c.cnpj || '').includes(onlyDigits(termo))
|
||
|
|
);
|
||
|
|
elBody.innerHTML = filtrada.length
|
||
|
|
? filtrada.map(linha).join('')
|
||
|
|
: `<tr><td colspan="4" class="muted">Nenhum cliente encontrado.</td></tr>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// carrega clientes do backend e renderiza
|
||
|
|
async function carregar(busca = "") {
|
||
|
|
const r = await fetch('/api/clientes');
|
||
|
|
if (!r.ok){ console.error('Falha ao carregar clientes'); return; }
|
||
|
|
const dados = await r.json();
|
||
|
|
window.__clientes = dados; // guarda em memória para o filtro
|
||
|
|
render(dados);
|
||
|
|
}
|
||
|
|
|
||
|
|
// excluir cliente
|
||
|
|
async function carregar(busca = "") {
|
||
|
|
const r = await fetch(`/api/clientes?busca=${encodeURIComponent(busca)}`);
|
||
|
|
if (!r.ok) { console.error('Falha ao carregar clientes'); return; }
|
||
|
|
const dados = await r.json();
|
||
|
|
window.__clientes = dados; // mantém em memória, se quiser
|
||
|
|
render(dados);
|
||
|
|
}
|
||
|
|
|
||
|
|
function abrirModalNovo(){
|
||
|
|
const modal = document.getElementById('modal');
|
||
|
|
const grpStatus = document.getElementById('grp_status');
|
||
|
|
const inpId = document.getElementById('cli_id');
|
||
|
|
const inpNome = document.getElementById('cli_nome');
|
||
|
|
const inpCnpj = document.getElementById('cli_cnpj');
|
||
|
|
const selAtv = document.getElementById('cli_ativo');
|
||
|
|
|
||
|
|
document.getElementById('modal-titulo').textContent = 'Novo Cliente';
|
||
|
|
|
||
|
|
inpId.value = '';
|
||
|
|
inpNome.value = '';
|
||
|
|
inpCnpj.value = ''; // <<< nada de mask.textContent
|
||
|
|
|
||
|
|
selAtv.value = 'true';
|
||
|
|
setStatusUI(true);
|
||
|
|
|
||
|
|
// novo: não mostra o select de status
|
||
|
|
grpStatus.style.display = 'none';
|
||
|
|
|
||
|
|
modal.classList.remove('hidden');
|
||
|
|
modal.setAttribute('aria-hidden', 'false');
|
||
|
|
setTimeout(()=> inpNome.focus(), 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
function abrirModalEditar(c){
|
||
|
|
const modal = document.getElementById('modal');
|
||
|
|
const grpStatus = document.getElementById('grp_status');
|
||
|
|
const inpId = document.getElementById('cli_id');
|
||
|
|
const inpNome = document.getElementById('cli_nome');
|
||
|
|
const inpCnpj = document.getElementById('cli_cnpj');
|
||
|
|
const selAtv = document.getElementById('cli_ativo');
|
||
|
|
|
||
|
|
document.getElementById('modal-titulo').textContent = 'Editar Cliente';
|
||
|
|
|
||
|
|
inpId.value = c.id || '';
|
||
|
|
inpNome.value = c.nome_fantasia || '';
|
||
|
|
// Preenche já mascarado no próprio input
|
||
|
|
inpCnpj.value = formatCNPJ(c.cnpj || ''); // <<< em vez de mask.textContent
|
||
|
|
|
||
|
|
grpStatus.style.display = ''; // mostra no editar
|
||
|
|
const ativo = !!c.ativo;
|
||
|
|
selAtv.value = ativo ? 'true' : 'false';
|
||
|
|
setStatusUI(ativo);
|
||
|
|
|
||
|
|
modal.classList.remove('hidden');
|
||
|
|
modal.setAttribute('aria-hidden', 'false');
|
||
|
|
setTimeout(()=> inpNome.focus(), 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
function fecharModal(){
|
||
|
|
const modal = document.getElementById('modal');
|
||
|
|
modal.classList.add('hidden');
|
||
|
|
modal.setAttribute('aria-hidden', 'true');
|
||
|
|
}
|
||
|
|
|
||
|
|
// sincroniza barra quando trocar o select (só visível no modo edição)
|
||
|
|
document.getElementById('cli_ativo').addEventListener('change', (e)=>{
|
||
|
|
setStatusUI(e.target.value === 'true');
|
||
|
|
});
|
||
|
|
|
||
|
|
// LIGA o botão "Novo Cliente"
|
||
|
|
document.addEventListener('DOMContentLoaded', ()=>{
|
||
|
|
const btnNovo = document.getElementById('btnNovo');
|
||
|
|
if (btnNovo) btnNovo.addEventListener('click', abrirModalNovo);
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
function formatCNPJ(d){ // 14 dígitos -> 00.000.000/0000-00
|
||
|
|
d = onlyDigits(d).slice(0,14);
|
||
|
|
let out = '';
|
||
|
|
if (d.length > 0) out += d.substring(0,2);
|
||
|
|
if (d.length > 2) out += '.' + d.substring(2,5);
|
||
|
|
if (d.length > 5) out += '.' + d.substring(5,8);
|
||
|
|
if (d.length > 8) out += '/' + d.substring(8,12);
|
||
|
|
if (d.length > 12) out += '-' + d.substring(12,14);
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
function maskCNPJ(ev){
|
||
|
|
const el = ev.target;
|
||
|
|
const caret = el.selectionStart;
|
||
|
|
const before = el.value;
|
||
|
|
el.value = formatCNPJ(el.value);
|
||
|
|
// caret simples (bom o suficiente aqui)
|
||
|
|
const diff = el.value.length - before.length;
|
||
|
|
el.selectionStart = el.selectionEnd = Math.max(0, (caret||0) + diff);
|
||
|
|
}
|
||
|
|
|
||
|
|
// valida CNPJ com dígitos verificadores
|
||
|
|
function isValidCNPJ(v){
|
||
|
|
const c = onlyDigits(v);
|
||
|
|
if (c.length !== 14) return false;
|
||
|
|
if (/^(\d)\1{13}$/.test(c)) return false; // todos iguais
|
||
|
|
|
||
|
|
const calc = (base) => {
|
||
|
|
const nums = base.split('').map(n=>parseInt(n,10));
|
||
|
|
const pesos = [];
|
||
|
|
for (let i=0;i<nums.length;i++){
|
||
|
|
pesos.push( (nums.length+1-i) > 9 ? (nums.length+1-i)-8 : (nums.length+1-i) );
|
||
|
|
}
|
||
|
|
let soma = 0;
|
||
|
|
for (let i=0;i<nums.length;i++) soma += nums[i] * pesos[i];
|
||
|
|
const r = soma % 11;
|
||
|
|
return (r < 2) ? 0 : (11 - r);
|
||
|
|
};
|
||
|
|
|
||
|
|
const d1 = calc(c.substring(0,12));
|
||
|
|
const d2 = calc(c.substring(0,12) + d1);
|
||
|
|
return c.endsWith(`${d1}${d2}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ligar máscara
|
||
|
|
document.addEventListener('DOMContentLoaded', ()=>{
|
||
|
|
const cnpjEl = document.getElementById('cli_cnpj');
|
||
|
|
if (cnpjEl){
|
||
|
|
cnpjEl.addEventListener('input', maskCNPJ);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
async function salvarModal(e){
|
||
|
|
e.preventDefault();
|
||
|
|
|
||
|
|
const btnSalvar = document.querySelector('#form-modal .btn.btn-primary[type="submit"]');
|
||
|
|
if (btnSalvar) btnSalvar.disabled = true;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const nome = document.getElementById('cli_nome').value.trim();
|
||
|
|
const cnpjEl = document.getElementById('cli_cnpj');
|
||
|
|
const ativo = document.getElementById('cli_ativo').value === 'true';
|
||
|
|
const id = document.getElementById('cli_id').value || null;
|
||
|
|
|
||
|
|
const cnpjDigits = onlyDigits(cnpjEl.value);
|
||
|
|
|
||
|
|
if (!nome){
|
||
|
|
alert('Informe o nome fantasia.');
|
||
|
|
document.getElementById('cli_nome').focus();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (cnpjDigits && !isValidCNPJ(cnpjEl.value)){
|
||
|
|
alert('CNPJ inválido.');
|
||
|
|
cnpjEl.focus();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const payload = { nome_fantasia: nome, cnpj: cnpjDigits || null, ativo };
|
||
|
|
const url = id ? `/api/clientes/${id}` : '/api/clientes';
|
||
|
|
const method = id ? 'PUT' : 'POST';
|
||
|
|
|
||
|
|
const r = await fetch(url, {
|
||
|
|
method,
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(payload)
|
||
|
|
});
|
||
|
|
|
||
|
|
if (r.status === 409) {
|
||
|
|
const { detail } = await r.json().catch(() => ({ detail: 'CNPJ já cadastrado.' }));
|
||
|
|
alert(detail || 'CNPJ já cadastrado.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!r.ok){
|
||
|
|
alert('Erro ao salvar.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
fecharModal();
|
||
|
|
carregar();
|
||
|
|
} finally {
|
||
|
|
if (btnSalvar) btnSalvar.disabled = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
|
// Botão Novo Cliente
|
||
|
|
const btnNovo = document.getElementById('btnNovo');
|
||
|
|
if (btnNovo) btnNovo.addEventListener('click', abrirModalNovo);
|
||
|
|
|
||
|
|
// Campo de busca
|
||
|
|
const busca = document.getElementById('busca');
|
||
|
|
const debounce = (fn, wait=250) => {
|
||
|
|
let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
|
||
|
|
};
|
||
|
|
|
||
|
|
if (busca) {
|
||
|
|
busca.addEventListener('input', debounce(() => {
|
||
|
|
carregar(busca.value.trim()); // <-- agora consulta o backend a cada digitação (com debounce)
|
||
|
|
}, 250));
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
// Máscara no CNPJ do modal
|
||
|
|
const cnpjEl = document.getElementById('cli_cnpj');
|
||
|
|
if (cnpjEl) cnpjEl.addEventListener('input', maskCNPJ);
|
||
|
|
|
||
|
|
// Carregar clientes na tabela ao abrir a página
|
||
|
|
carregar();
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
</script>
|
||
|
|
{% endblock %}
|