Initial project import

This commit is contained in:
Tiago
2026-09-29 12:12:31 -03:00
commit c435bb5e23
120 changed files with 5371 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
<!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>Telefone/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>
+18
View File
@@ -0,0 +1,18 @@
const empresaForm=document.querySelector('#empresa-form'),rows=document.querySelector('#rows'),busca=document.querySelector('#busca'),planoSelect=document.querySelector('#plano-select');let empresas=[],planos=[],modo=null,selecionado=null;
function planoLabel(p){return `${p.nome} - ${Util.money(p.valor_mensal_centavos)}`}
function planoOptions(valor){return '<option value="">Sem plano</option>'+planos.map(p=>`<option value="${p.id}" ${String(valor||'')===String(p.id)?'selected':''}>${planoLabel(p)}</option>`).join('')}
function renderPlanos(){planoSelect.innerHTML=planoOptions('')}
function match(e,q){if(!q)return true;return [e.slug,e.nome,e.cnpj,e.status,e.plano].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>${e.nome}</h2><p><strong>Slug:</strong> ${e.slug}</p><p><strong>CNPJ:</strong> ${e.cnpj||'-'}</p><p><strong>Status:</strong> ${e.status}</p><p><strong>Plano:</strong> ${e.plano?`${e.plano} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'}</p><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="${e.nome||''}" required></div><div class="field"><label>CNPJ</label><input name="cnpj" value="${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)}};
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.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)}};
load();
@@ -0,0 +1 @@
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Configuracoes</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>Configuracoes</h1></div><section class="panel"><h2>E-mail</h2><form id="email-form" class="grid cols"><div class="field"><label>Host</label><input name="smtp_host"></div><div class="field"><label>Porta</label><input name="smtp_port" type="number"></div><div class="field"><label>Usuario</label><input name="smtp_user"></div><div class="field"><label>Senha</label><input name="smtp_password" type="password"></div><div class="field"><label>Remetente</label><input name="smtp_from"></div><div class="field"><label>StartTLS</label><select name="smtp_starttls"><option value="true">Sim</option><option value="false">Nao</option></select></div><button class="btn">Salvar e-mail</button></form><div id="email-rows"></div></section><section class="panel"><h2>AbacatePay</h2><form id="abacate-form" class="grid cols"><div class="field"><label>API Key</label><input name="abacatepay_api_key" type="password" placeholder="Cole a chave da AbacatePay"></div><div class="field"><label>Webhook Secret</label><input name="abacatepay_webhook_secret" type="password" placeholder="Segredo do webhook"></div><button class="btn">Salvar AbacatePay</button></form><div id="abacate-rows"></div></section><section class="panel"><h2>Acessos WhatsApp</h2><div class="table-wrap"><table><thead><tr><th>Empresa</th><th>Numero</th><th>Status</th><th>Solicitado em</th><th>Acoes</th></tr></thead><tbody id="evolution-rows"></tbody></table></div></section></main><script>Layout.mount('master','configuracoes');</script><script src="/master/configuracoes.js"></script></body></html>
+1
View File
@@ -0,0 +1 @@
const emailForm=document.querySelector('#email-form'),abacateForm=document.querySelector('#abacate-form'),emailRows=document.querySelector('#email-rows'),abacateRows=document.querySelector('#abacate-rows'),evolutionRows=document.querySelector('#evolution-rows');let empresas=[];const emailKeys=['smtp_from','smtp_host','smtp_password','smtp_port','smtp_starttls','smtp_user'];const abacateKeys=['abacatepay_api_key','abacatepay_webhook_secret'];function renderConfig(target,cfg,keys,labels){target.innerHTML=keys.map(k=>`<p><strong>${labels[k]||k}</strong>: ${cfg[k]||''}</p>`).join('')}async function load(){try{const d=await Api.get('/api/master/configuracoes');const cfg=d.configuracoes||{};for(const [k,v] of Object.entries(cfg)){if(emailForm.elements[k])emailForm.elements[k].value=v;if(abacateForm.elements[k])abacateForm.elements[k].value=v}renderConfig(emailRows,cfg,emailKeys,{smtp_from:'Remetente',smtp_host:'Host',smtp_password:'Senha',smtp_port:'Porta',smtp_starttls:'StartTLS',smtp_user:'Usuario'});renderConfig(abacateRows,cfg,abacateKeys,{abacatepay_api_key:'API Key',abacatepay_webhook_secret:'Webhook Secret'});await loadEvolution()}catch(e){Util.toast(e.message,true)}}async function loadEvolution(){const d=await Api.get('/api/master/empresas');empresas=d.empresas||[];const grupos=await Promise.all(empresas.map(async e=>{try{const r=await Api.get(`/api/master/empresas/${e.id}/evolution-instancias`);return (r.instancias||[]).map(i=>({...i,empresa_nome:e.nome}))}catch(_){return[]}}));const instancias=grupos.flat();evolutionRows.innerHTML=instancias.length?instancias.map(i=>`<tr><td>${i.empresa_nome}</td><td>${i.numero_whatsapp||'-'}</td><td>${Util.badge(i.status)}</td><td>${Util.date(i.solicitado_em)}</td><td><button class="btn secondary" onclick="aprovar(${i.empresa_id},${i.id})">Aprovar</button> <button class="btn danger" onclick="rejeitar(${i.empresa_id},${i.id})">Rejeitar</button> <button class="btn danger" onclick="excluir(${i.empresa_id},${i.id})">Excluir</button></td></tr>`).join(''):'<tr><td colspan="5">Nenhuma solicitacao encontrada.</td></tr>'}async function aprovar(emp,id){try{await Api.post(`/api/master/empresas/${emp}/evolution-instancias/${id}/aprovar`,{});Util.toast('Acesso liberado');loadEvolution()}catch(e){Util.toast(e.message,true)}}async function rejeitar(emp,id){try{await Api.post(`/api/master/empresas/${emp}/evolution-instancias/${id}/rejeitar`,{});Util.toast('Acesso rejeitado');loadEvolution()}catch(e){Util.toast(e.message,true)}}async function excluir(emp,id){if(!confirm('Excluir esta conexao de WhatsApp?'))return;try{await Api.request(`/api/master/empresas/${emp}/evolution-instancias/${id}`,{method:'DELETE'});Util.toast('Conexao excluida');loadEvolution()}catch(e){Util.toast(e.message,true)}}emailForm.onsubmit=async e=>{e.preventDefault();try{await Api.put('/api/master/configuracoes',Util.formData(emailForm));Util.toast('Configuracoes de e-mail salvas');load()}catch(err){Util.toast(err.message,true)}};abacateForm.onsubmit=async e=>{e.preventDefault();try{await Api.put('/api/master/configuracoes',Util.formData(abacateForm));Util.toast('Configuracoes AbacatePay salvas');load()}catch(err){Util.toast(err.message,true)}};load();
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Financeiro Master</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>Financeiro Master</h1></div><section class="panel"><h2>Situacao das empresas</h2><div class="cards" id="cards"></div><div class="table-wrap"><table><thead><tr><th>Empresa</th><th>Status</th><th>Periodo de utilizacao</th><th>Vencimento</th><th>Acao</th></tr></thead><tbody id="rows"></tbody></table></div></section></main><script>Layout.mount('master','financeiro');</script><script src="/master/financeiro.js"></script></body></html>
+8
View File
@@ -0,0 +1,8 @@
const rows=document.querySelector('#rows'),cards=document.querySelector('#cards');
function data(v){if(!v)return'-';const d=new Date(v+'T00:00:00');return Number.isNaN(d.getTime())?String(v):d.toLocaleDateString('pt-BR')}
function periodo(e){return '<strong>'+data(e.data_inicio)+' a '+data(e.data_fim)+'</strong><br><span class="muted">Vencimento: '+data(e.data_vencimento)+'</span><br>'+Util.badge(e.periodo_status||'pendente')}
async function load(){try{const d=await Api.get('/api/master/empresas');const es=d.empresas||[];cards.innerHTML='<div class="stat">Ativas<strong>'+es.filter(e=>e.status==='ativa').length+'</strong></div><div class="stat">Bloqueadas<strong>'+es.filter(e=>e.status==='bloqueada').length+'</strong></div><div class="stat">Pagas<strong>'+es.filter(e=>e.periodo_status==='pago').length+'</strong></div>';rows.innerHTML=es.map(e=>{const bloqueada=e.status==='bloqueada'||e.periodo_status==='bloqueado';const liberar=bloqueada?' <button class="btn" onclick="desbloquear('+e.id+')">Desbloquear</button>':'';return '<tr><td>'+e.nome+'</td><td>'+Util.badge(e.status)+'</td><td>'+periodo(e)+'</td><td><div class="field inline"><label>Dia vencimento</label><input type="number" min="1" max="28" value="'+(e.dia_vencimento||7)+'" onchange="salvarVencimento('+e.id+',this.value)"></div></td><td><button class="btn secondary" onclick="lembrar('+e.id+')">Lembrete</button>'+liberar+'</td></tr>'}).join('')}catch(e){Util.toast(e.message,true)}}
async function salvarVencimento(id,dia){try{await Api.put('/api/master/empresas/'+id+'/vencimento',{dia_vencimento:Number(dia)});Util.toast('Vencimento atualizado');load()}catch(e){Util.toast(e.message,true)}}
async function lembrar(id){try{await Api.post('/api/master/empresas/'+id+'/lembrete-cobranca',{canal:'email'});Util.toast('Lembrete registrado')}catch(e){Util.toast(e.message,true)}}
async function desbloquear(id){if(!confirm('Desbloquear manualmente esta empresa e liberar o periodo atual?'))return;try{await Api.post('/api/master/empresas/'+id+'/desbloquear',{});Util.toast('Empresa desbloqueada');load()}catch(e){Util.toast(e.message,true)}}
load();
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Planos</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>Planos</h1></div><section class="panel"><h2>Novo plano</h2><form id="plano-form" class="row"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>Valor em centavos</label><input name="valor_mensal_centavos" type="number" required></div><div class="field"><label>Descricao</label><input name="descricao"></div><div class="field"><label>Funcionalidades</label><input name="funcionalidades" placeholder="whatsapp_envio,email_envio"></div><button class="btn">Salvar</button></form></section><section class="panel"><h2>Planos cadastrados</h2><div class="table-wrap"><table><thead><tr><th>ID</th><th>Nome</th><th>Valor</th><th>Descricao</th><th>Status</th></tr></thead><tbody id="rows"></tbody></table></div></section></main><script>Layout.mount('master','planos');</script><script src="/master/planos.js"></script></body></html>
+1
View File
@@ -0,0 +1 @@
const form=document.querySelector('#plano-form'),rows=document.querySelector('#rows');async function load(){try{const d=await Api.get('/api/master/planos');rows.innerHTML=(d.planos||[]).map(p=>`<tr><td>${p.id}</td><td>${p.nome}</td><td>${Util.money(p.valor_mensal_centavos)}</td><td>${p.descricao||''}</td><td>${Util.badge(p.ativo?'ativo':'inativo')}</td></tr>`).join('')}catch(e){Util.toast(e.message,true)}}form.onsubmit=async e=>{e.preventDefault();const b=Util.formData(form);b.funcionalidades=(b.funcionalidades||'').split(',').map(x=>x.trim()).filter(Boolean);try{await Api.post('/api/master/planos',b);form.reset();Util.toast('Plano salvo');load()}catch(err){Util.toast(err.message,true)}};load();