feat(master): unificar painel de clientes com busca e cadastro retratil

This commit is contained in:
Tiago
2026-10-10 16:36:20 -03:00
parent d6976374d8
commit 602dd4cc5b
2 changed files with 110 additions and 2 deletions
+74 -1
View File
@@ -1 +1,74 @@
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Clientes</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Clientes</h1></div><section class="panel"><form id="empresa-form" class="grid cols"><div class="field"><label>Slug</label><input name="slug" required></div><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>CNPJ</label><input name="cnpj"></div><div class="field"><label>Plano</label><select name="plano_id" id="plano-select"><option value="">Sem plano</option></select></div><div class="field"><label>WhatsApp</label><input name="telefone" placeholder="5599999999999"></div><div class="field"><label>E-mail de login</label><input name="email" type="email" required></div><div class="field"><label>Senha inicial</label><input name="senha" type="password" required></div><button class="btn">Cadastrar</button><div class="field"><label>Busca</label><input id="busca" placeholder="Buscar por slug, nome, CNPJ, status ou plano"></div></form></section><section class="panel"><div class="table-wrap"><table><thead><tr><th>Slug</th><th>Nome</th><th>Status</th><th>Plano</th><th>Editar</th></tr></thead><tbody id="rows"></tbody></table></div></section></main><script>Layout.mount('master','clientes');</script><script src="/master/clientes.js"></script></body></html>
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Clientes</title>
<link rel="stylesheet" href="/css/app.css">
<script src="/js/auth.js"></script>
<script src="/js/util.js"></script>
<script src="/js/api.js"></script>
<script src="/js/layout.js"></script>
</head>
<body>
<main>
<div class="topbar">
<h1>Clientes</h1>
</div>
<section class="panel">
<div class="section-head">
<h2>Clientes</h2>
<div class="section-actions">
<button class="search-icon-btn" type="button" onclick="alternarBusca()" aria-label="Buscar clientes" title="Buscar">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m16.5 16.5 4 4"></path></svg>
</button>
<button class="header-add-btn" type="button" onclick="alternarCadastro()" title="Novo cadastro" aria-label="Novo cadastro">
<svg viewBox="0 0 24 24" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
</button>
</div>
</div>
<div id="busca-strip" class="search-strip hidden">
<input id="busca" placeholder="Buscar por slug, nome, CNPJ, status ou plano">
</div>
<div id="cadastro-wrap" class="hidden" style="margin-bottom: 20px; padding: 18px; background: var(--panel-muted); border: 1px solid var(--line); border-radius: var(--radius);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px;">
<h3 style="margin: 0; font-size: 16px; font-weight: 800;">Novo cadastro</h3>
<button class="btn secondary" type="button" onclick="alternarCadastro(false)" style="min-height: 32px; padding: 4px 10px; font-size: 13px;">Fechar</button>
</div>
<form id="empresa-form" class="grid cols">
<div class="field"><label>Slug</label><input name="slug" required></div>
<div class="field"><label>Nome</label><input name="nome" required></div>
<div class="field"><label>CNPJ</label><input name="cnpj"></div>
<div class="field"><label>Plano</label><select name="plano_id" id="plano-select"><option value="">Sem plano</option></select></div>
<div class="field"><label>WhatsApp</label><input name="telefone" placeholder="5599999999999"></div>
<div class="field"><label>E-mail de login</label><input name="email" type="email" required></div>
<div class="field"><label>Senha inicial</label><input name="senha" type="password" required></div>
<div class="row" style="align-self: end; gap: 8px;">
<button class="btn" type="submit">Cadastrar</button>
<button class="btn secondary" type="button" onclick="alternarCadastro(false)">Cancelar</button>
</div>
</form>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Slug</th>
<th>Nome</th>
<th>Status</th>
<th>Plano</th>
<th>Editar</th>
</tr>
</thead>
<tbody id="rows"></tbody>
</table>
</div>
</section>
</main>
<script>Layout.mount('master','clientes');</script>
<script src="/master/clientes.js"></script>
</body>
</html>
+36 -1
View File
@@ -8,7 +8,42 @@ function statusAction(e){const acao=e.status==='ativa'?'Bloquear':'Ativar';retur
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 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>${statusAction(e)}</td><td>${e.plano?`${e.plano} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'}</td><td><button class="btn secondary" onclick="editar(${e.id})">Editar</button> <button class="btn secondary" onclick="ver(${e.id})">Ver</button> <button class="btn danger" onclick="excluir(${e.id})">Excluir</button></td></tr>${detalheRow(e)}`).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)}}
empresaForm.onsubmit=async e=>{e.preventDefault();try{await Api.post('/api/master/empresas',Util.formData(empresaForm));empresaForm.reset();Util.toast('Cliente cadastrado');load()}catch(err){Util.toast(err.message,true)}};
window.alternarBusca=()=>{
const strip=document.querySelector('#busca-strip');
if(!strip)return;
strip.classList.toggle('hidden');
if(!strip.classList.contains('hidden')){
busca.focus();
}
};
window.alternarCadastro=(forcar)=>{
const wrap=document.querySelector('#cadastro-wrap');
if(!wrap)return;
if(typeof forcar==='boolean'){
wrap.classList.toggle('hidden',!forcar);
}else{
wrap.classList.toggle('hidden');
}
if(!wrap.classList.contains('hidden')){
empresaForm.querySelector('input[name="slug"]')?.focus();
}
};
empresaForm.onsubmit=async e=>{
e.preventDefault();
try{
await Api.post('/api/master/empresas',Util.formData(empresaForm));
empresaForm.reset();
alternarCadastro(false);
Util.toast('Cliente cadastrado');
load();
}catch(err){
Util.toast(err.message,true);
}
};
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()};