feat(master): abrir visualizacao e edicao de clientes em popup e confirmacao para exclusao

This commit is contained in:
Tiago
2026-10-10 18:06:04 -03:00
parent eeae68ec70
commit 258230f1c4
2 changed files with 229 additions and 16 deletions
+85
View File
@@ -78,6 +78,91 @@
</form>
</section>
</div>
<!-- Modal de Visualização (Popup) -->
<div id="ver-modal" class="modal hidden">
<div class="modal-backdrop" onclick="fecharModalVer()"></div>
<section class="modal-panel">
<div class="modal-head">
<h2 id="ver-modal-titulo">Detalhes do cliente</h2>
<button class="btn secondary" type="button" onclick="fecharModalVer()">Fechar</button>
</div>
<div class="grid cols">
<div class="field">
<label>Slug</label>
<input id="ver-slug" readonly style="background: var(--bg-soft, #f8fafc); cursor: default;">
</div>
<div class="field">
<label>Nome</label>
<input id="ver-nome" readonly style="background: var(--bg-soft, #f8fafc); cursor: default;">
</div>
<div class="field">
<label>CNPJ</label>
<input id="ver-cnpj" readonly style="background: var(--bg-soft, #f8fafc); cursor: default;">
</div>
<div class="field">
<label>Status</label>
<div id="ver-status" style="margin-top: 8px;"></div>
</div>
<div class="field" style="grid-column: 1 / -1;">
<label>Plano</label>
<input id="ver-plano" readonly style="background: var(--bg-soft, #f8fafc); cursor: default;">
</div>
<div class="field" style="grid-column: 1 / -1;">
<label>API Key de integração</label>
<div style="display: flex; gap: 8px;">
<input id="ver-api-key" readonly onclick="this.select()" style="flex: 1; background: var(--bg-soft, #f8fafc);">
<button class="btn secondary" type="button" onclick="copiarApiKeyModal()">Copiar</button>
</div>
</div>
<div style="grid-column: 1 / -1; display: flex; gap: 8px; flex-wrap: wrap;">
<button class="btn secondary" type="button" onclick="solicitarAlteracaoApiKeyModal()">Solicitar alteração de API Key</button>
</div>
<p style="grid-column: 1 / -1; font-size: 13px; color: var(--muted); margin: 4px 0 0;">
<strong>Conta principal:</strong> utilize "Editar" para alterar os dados da empresa. Funcionários e contatos são gerenciados no acesso do próprio cliente.
</p>
<div class="modal-actions" style="grid-column: 1 / -1;">
<button class="btn secondary" type="button" onclick="fecharModalVer()">Fechar</button>
<button class="btn" type="button" onclick="abrirEditarDeVer()">Editar cliente</button>
</div>
</div>
</section>
</div>
<!-- Modal de Edição (Popup) -->
<div id="editar-modal" class="modal hidden">
<div class="modal-backdrop" onclick="fecharModalEditar()"></div>
<section class="modal-panel">
<div class="modal-head">
<h2 id="editar-modal-titulo">Editar cliente</h2>
<button class="btn secondary" type="button" onclick="fecharModalEditar()">Fechar</button>
</div>
<form id="editar-form" class="grid cols" onsubmit="salvarEdicaoModal(event)">
<div id="editar-modal-error-msg" class="hidden" style="grid-column: 1 / -1; padding: 12px 16px; background: var(--danger-soft); color: var(--danger); border: 1px solid var(--danger); border-radius: var(--radius); font-size: 14px; font-weight: 600;"></div>
<input type="hidden" name="id" id="editar-id">
<div class="field">
<label>Slug</label>
<input id="editar-slug" readonly disabled style="background: var(--bg-soft, #f8fafc); opacity: 0.7; cursor: not-allowed;">
</div>
<div class="field">
<label>Nome</label>
<input name="nome" id="editar-nome" required>
</div>
<div class="field">
<label>CNPJ</label>
<input name="cnpj" id="editar-cnpj">
</div>
<div class="field">
<label>Plano</label>
<select name="plano_id" id="editar-plano-select"></select>
</div>
<div class="modal-actions" style="grid-column: 1 / -1;">
<button class="btn secondary" type="button" onclick="fecharModalEditar()">Cancelar</button>
<button class="btn" type="submit">Salvar alterações</button>
</div>
</form>
</section>
</div>
</main>
<script>Layout.mount('master','clientes');</script>
<script src="/master/clientes.js"></script>
+144 -16
View File
@@ -1,5 +1,17 @@
const empresaForm=document.querySelector('#empresa-form'),rows=document.querySelector('#rows'),busca=document.querySelector('#busca'),planoSelect=document.querySelector('#plano-select'),cadastroModal=document.querySelector('#cadastro-modal');let empresas=[],planos=[],modo=null,selecionado=null;
let modoSlugAuto='sequencial';
const empresaForm = document.querySelector('#empresa-form');
const rows = document.querySelector('#rows');
const busca = document.querySelector('#busca');
const planoSelect = document.querySelector('#plano-select');
const cadastroModal = document.querySelector('#cadastro-modal');
const verModal = document.querySelector('#ver-modal');
const editarModal = document.querySelector('#editar-modal');
const editarForm = document.querySelector('#editar-form');
let empresas = [];
let planos = [];
let clienteVisualizandoId = null;
let clienteEditandoId = null;
let modoSlugAuto = 'sequencial';
const Icons = {
edit: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>',
@@ -13,7 +25,6 @@ function planoOptions(valor){return '<option value="">Sem plano</option>'+planos
function renderPlanos(){planoSelect.innerHTML=planoOptions('')}
function match(e,q){if(!q)return true;return [e.slug,e.nome,e.cnpj,e.status,e.plano,e.api_key].join(' ').toLowerCase().includes(q.toLowerCase())}
function statusAction(e){const acao=e.status==='ativa'?'Bloquear':'Ativar';return `<button class="status-action" onclick="alterarStatus(${e.id},'${e.status==='ativa'?'bloqueada':'ativa'}')">${Util.badge(e.status)}<span>${acao}</span></button>`}
function detalheRow(e){if(selecionado!==e.id||!modo)return'';if(modo==='ver')return `<tr><td colspan="5"><div class="inline-panel"><h2>${esc(e.nome)}</h2><p><strong>Slug:</strong> ${esc(e.slug)}</p><p><strong>CNPJ:</strong> ${esc(e.cnpj||'-')}</p><p><strong>Status:</strong> ${esc(e.status)}</p><p><strong>Plano:</strong> ${e.plano?`${esc(e.plano)} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'}</p><div class="field"><label>API Key de integração</label><input readonly value="${esc(e.api_key||'')}" onclick="this.select()"></div><div class="row compact"><button class="btn secondary" type="button" onclick="copiarApiKey(${e.id})">Copiar API Key</button><button class="btn secondary" type="button" onclick="solicitarAlteracaoApiKey(${e.id})">Solicitar alteração</button></div><p><strong>Conta principal:</strong> abra Editar para alterar dados da empresa; funcionarios ficam no acesso da empresa.</p><button class="btn secondary" onclick="fecharDetalhe()">Fechar</button></div></td></tr>`;return `<tr><td colspan="5"><form class="inline-panel grid cols" onsubmit="salvarEdicao(event,${e.id})"><div class="field"><label>Nome</label><input name="nome" value="${esc(e.nome||'')}" required></div><div class="field"><label>CNPJ</label><input name="cnpj" value="${esc(e.cnpj||'')}"></div><div class="field"><label>Plano</label><select name="plano_id">${planoOptions(e.plano_id)}</select></div><div class="row"><button class="btn">Salvar</button><button type="button" class="btn secondary" onclick="fecharDetalhe()">Cancelar</button></div></form></td></tr>`}
function proximoSlugSequencial(){
const slugsExistentes = new Set(empresas.map(e => String(e.slug || '').trim().toLowerCase()));
@@ -67,10 +78,10 @@ function render(){
const q=busca.value.trim();
const lista=empresas.filter(e=>match(e,q));
rows.innerHTML=lista.length?lista.map(e=>`<tr>
<td>${e.slug}</td>
<td>${e.nome}</td>
<td>${esc(e.slug)}</td>
<td>${esc(e.nome)}</td>
<td>${statusAction(e)}</td>
<td>${e.plano?`${e.plano} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'}</td>
<td>${e.plano?`${esc(e.plano)} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'}</td>
<td style="text-align: right;">
<div class="table-actions">
<button class="table-icon-btn" type="button" onclick="editar(${e.id})" title="Editar cliente" aria-label="Editar cliente">${Icons.edit}</button>
@@ -78,7 +89,7 @@ function render(){
<button class="table-icon-btn danger" type="button" onclick="excluir(${e.id})" title="Excluir cliente" aria-label="Excluir cliente">${Icons.trash}</button>
</div>
</td>
</tr>${detalheRow(e)}`).join(''):'<tr><td colspan="5">Nenhum cliente encontrado.</td></tr>';
</tr>`).join(''):'<tr><td colspan="5">Nenhum cliente encontrado.</td></tr>';
}
async function load(){try{const [pe,de]=await Promise.all([Api.get('/api/master/planos'),Api.get('/api/master/empresas')]);planos=pe.planos||[];empresas=de.empresas||[];renderPlanos();render()}catch(e){Util.toast(e.message,true)}}
@@ -191,13 +202,130 @@ empresaForm.onsubmit=async e=>{
}
};
busca.oninput=render;
window.ver=id=>{selecionado=selecionado===id&&modo==='ver'?null:id;modo=selecionado?'ver':null;render()};
window.editar=id=>{selecionado=selecionado===id&&modo==='editar'?null:id;modo=selecionado?'editar':null;render()};
window.fecharDetalhe=()=>{selecionado=null;modo=null;render()};
window.copiarApiKey=async id=>{const e=empresas.find(x=>Number(x.id)===Number(id));if(!e?.api_key)return;try{await navigator.clipboard.writeText(e.api_key);Util.toast('API Key copiada')}catch(_){Util.toast('Selecione e copie a API Key manualmente',true)}};
window.solicitarAlteracaoApiKey=id=>{const e=empresas.find(x=>Number(x.id)===Number(id));Util.toast('Solicitacao de alteracao registrada para '+(e?.nome||'cliente'))};
window.salvarEdicao=async(ev,id)=>{ev.preventDefault();try{await Api.put(`/api/master/empresas/${id}`,Util.formData(ev.target));Util.toast('Cliente atualizado');selecionado=null;modo=null;load()}catch(err){Util.toast(err.message,true)}};
window.alterarStatus=async(id,status)=>{try{await Api.post(`/api/master/empresas/${id}/status`,{status});Util.toast(status==='ativa'?'Cliente ativado':'Cliente bloqueado');load()}catch(err){Util.toast(err.message,true)}};
window.excluir=async id=>{try{await Api.request(`/api/master/empresas/${id}`,{method:'DELETE'});Util.toast('Cliente excluido');selecionado=null;modo=null;load()}catch(err){Util.toast(err.message,true)}};
busca.oninput = render;
// Visualização em Popup Modal
window.ver = (id) => {
const emp = empresas.find(x => Number(x.id) === Number(id));
if (!emp) return;
clienteVisualizandoId = emp.id;
document.querySelector('#ver-modal-titulo').textContent = emp.nome || 'Detalhes do cliente';
document.querySelector('#ver-slug').value = emp.slug || '-';
document.querySelector('#ver-nome').value = emp.nome || '-';
document.querySelector('#ver-cnpj').value = emp.cnpj || '-';
document.querySelector('#ver-status').innerHTML = Util.badge(emp.status);
document.querySelector('#ver-plano').value = emp.plano ? `${emp.plano} ${emp.plano_valor_centavos ? '- ' + Util.money(emp.plano_valor_centavos) : ''}` : 'Sem plano';
document.querySelector('#ver-api-key').value = emp.api_key || '';
verModal?.classList.remove('hidden');
};
window.fecharModalVer = () => {
verModal?.classList.add('hidden');
clienteVisualizandoId = null;
};
window.copiarApiKeyModal = async () => {
const apiKey = document.querySelector('#ver-api-key')?.value;
if (!apiKey) return;
try {
await navigator.clipboard.writeText(apiKey);
Util.toast('API Key copiada com sucesso!');
} catch (_) {
Util.toast('Selecione e copie a API Key manualmente', true);
}
};
window.solicitarAlteracaoApiKeyModal = () => {
const emp = empresas.find(x => Number(x.id) === Number(clienteVisualizandoId));
Util.toast('Solicitação de alteração registrada para ' + (emp?.nome || 'cliente'));
};
window.abrirEditarDeVer = () => {
const id = clienteVisualizandoId;
fecharModalVer();
if (id) editar(id);
};
// Edição em Popup Modal
window.editar = (id) => {
const emp = empresas.find(x => Number(x.id) === Number(id));
if (!emp) return;
clienteEditandoId = emp.id;
editarForm.reset();
const errEl = document.querySelector('#editar-modal-error-msg');
if (errEl) { errEl.textContent = ''; errEl.classList.add('hidden'); }
document.querySelector('#editar-modal-titulo').textContent = `Editar: ${emp.nome}`;
document.querySelector('#editar-id').value = emp.id;
document.querySelector('#editar-slug').value = emp.slug || '';
document.querySelector('#editar-nome').value = emp.nome || '';
document.querySelector('#editar-cnpj').value = emp.cnpj || '';
document.querySelector('#editar-plano-select').innerHTML = planoOptions(emp.plano_id);
editarModal?.classList.remove('hidden');
document.querySelector('#editar-nome')?.focus();
};
window.fecharModalEditar = () => {
editarModal?.classList.add('hidden');
clienteEditandoId = null;
};
window.salvarEdicaoModal = async (ev) => {
ev.preventDefault();
if (!clienteEditandoId) return;
try {
const dados = Util.formData(editarForm);
await Api.put(`/api/master/empresas/${clienteEditandoId}`, dados);
Util.toast('Cliente atualizado com sucesso!');
fecharModalEditar();
load();
} catch (err) {
const errEl = document.querySelector('#editar-modal-error-msg');
if (errEl) {
errEl.textContent = err.message || 'Erro ao atualizar cliente';
errEl.classList.remove('hidden');
}
Util.toast(err.message, true);
}
};
// Exclusão com confirmação em Popup Modal
window.excluir = async (id) => {
const emp = empresas.find(x => Number(x.id) === Number(id));
const nome = emp ? ` "${emp.nome}"` : '';
if (!await Util.confirm(`Tem certeza que deseja excluir o cliente${nome}?`, 'Excluir cliente')) return;
try {
await Api.request(`/api/master/empresas/${id}`, { method: 'DELETE' });
Util.toast('Cliente excluído com sucesso');
load();
} catch (err) {
Util.toast(err.message, true);
}
};
window.copiarApiKey = async (id) => {
const e = empresas.find(x => Number(x.id) === Number(id));
if (!e?.api_key) return;
try {
await navigator.clipboard.writeText(e.api_key);
Util.toast('API Key copiada');
} catch (_) {
Util.toast('Selecione e copie a API Key manualmente', true);
}
};
window.solicitarAlteracaoApiKey = (id) => {
const e = empresas.find(x => Number(x.id) === Number(id));
Util.toast('Solicitação de alteração registrada para ' + (e?.nome || 'cliente'));
};
window.alterarStatus = async (id, status) => {
try {
await Api.post(`/api/master/empresas/${id}/status`, { status });
Util.toast(status === 'ativa' ? 'Cliente ativado' : 'Cliente bloqueado');
load();
} catch (err) {
Util.toast(err.message, true);
}
};
load();