Initial project import
This commit is contained in:
@@ -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>Detalhe do Condominio</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>Detalhe do Condominio</h1></div><section class="panel" id="header"></section><div class="nav-tabs"><button data-tab="contatos" class="active">Contatos</button><button data-tab="espacos">Espacos</button><button data-tab="assembleias">Assembleias</button><button data-tab="documentos">Documentos</button><button data-tab="avisos">Avisos</button></div><section class="panel tab" id="contatos"></section><section class="panel tab hidden" id="espacos"></section><section class="panel tab hidden" id="assembleias"></section><section class="panel tab hidden" id="documentos"></section><section class="panel tab hidden" id="avisos"></section></main><script>Layout.mount('empresa','condominios');</script><script src="/cliente/condominio-detalhe.js"></script></body></html>
|
||||
@@ -0,0 +1,30 @@
|
||||
const id=new URLSearchParams(location.search).get('id');const sections=['contatos','espacos','assembleias','documentos','avisos'];document.querySelectorAll('.nav-tabs button').forEach(b=>b.onclick=()=>{document.querySelectorAll('.nav-tabs button').forEach(x=>x.classList.remove('active'));b.classList.add('active');sections.forEach(s=>document.querySelector('#'+s).classList.toggle('hidden',s!==b.dataset.tab))});let contatos=[],docs=[],avisos=[],funcionariosVinculados=[],extras=[],assembleiasCalendario=[],calMonth=new Date();function recLabel(v){return {1:'Mensal',3:'Trimestral',12:'Anual'}[Number(v)]||'Sem recorrencia'}
|
||||
|
||||
function dateKey(v){const d=new Date(v);if(Number.isNaN(d.getTime()))return'';const pad=n=>String(n).padStart(2,'0');return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())}
|
||||
function monthLabel(d){return d.toLocaleDateString('pt-BR',{month:'long',year:'numeric'})}
|
||||
function ensureCalendarModal(){let m=document.querySelector('#assembleia-calendar-modal');if(m)return m;document.body.insertAdjacentHTML('beforeend','<div id="assembleia-calendar-modal" class="modal hidden"><div class="modal-backdrop" onclick="fecharCalendarioAssembleias()"></div><section class="modal-panel calendar-modal"><div class="modal-head"><h2>Calendário de assembleias</h2><button class="btn secondary" type="button" onclick="fecharCalendarioAssembleias()">Fechar</button></div><div class="calendar-layout"><div><div class="calendar-toolbar"><button class="btn secondary" type="button" onclick="mudarMesCalendario(-1)">Anterior</button><strong id="calendar-title"></strong><button class="btn secondary" type="button" onclick="mudarMesCalendario(1)">Próximo</button></div><div id="calendar-grid" class="calendar-grid"></div></div><aside class="calendar-side"><h3 id="calendar-day-title">Selecione um dia</h3><div id="calendar-day-list" class="calendar-day-list"><p class="muted">Clique em um dia para ver as assembleias e notificados.</p></div></aside></div></section></div>');return document.querySelector('#assembleia-calendar-modal')}
|
||||
function renderCalendarioAssembleias(selected){ensureCalendarModal();const title=document.querySelector('#calendar-title'),grid=document.querySelector('#calendar-grid');title.textContent=monthLabel(calMonth);const y=calMonth.getFullYear(),m=calMonth.getMonth(),first=new Date(y,m,1),start=new Date(y,m,1-first.getDay()),todayKey=dateKey(new Date());let html=['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'].map(x=>'<div class="calendar-weekday">'+x+'</div>').join('');for(let i=0;i<42;i++){const d=new Date(start);d.setDate(start.getDate()+i);const key=dateKey(d),items=assembleiasCalendario.filter(a=>dateKey(a.data_hora)===key),other=d.getMonth()!==m,classes=['calendar-day'];if(other)classes.push('other');if(items.length)classes.push('busy');if(key===todayKey)classes.push('today');html+='<button type="button" class="'+classes.join(' ')+'" data-date="'+key+'"><span>'+d.getDate()+'</span>'+(items.length?'<b>'+items.length+'</b>':'')+'</button>'}grid.innerHTML=html;grid.querySelectorAll('.calendar-day').forEach(btn=>btn.onclick=()=>selecionarDiaCalendario(btn.dataset.date));if(selected)selecionarDiaCalendario(selected,false)}
|
||||
window.abrirCalendarioAssembleias=async()=>{try{const d=await Api.get('/api/assembleias-calendario');assembleiasCalendario=d.assembleias||[];calMonth=new Date();ensureCalendarModal().classList.remove('hidden');renderCalendarioAssembleias()}catch(e){Util.toast(e.message,true)}};
|
||||
window.fecharCalendarioAssembleias=()=>document.querySelector('#assembleia-calendar-modal')?.classList.add('hidden');
|
||||
window.mudarMesCalendario=delta=>{calMonth=new Date(calMonth.getFullYear(),calMonth.getMonth()+delta,1);renderCalendarioAssembleias()};
|
||||
window.selecionarDiaCalendario=(key,mark=true)=>{if(mark)document.querySelectorAll('.calendar-day').forEach(x=>x.classList.toggle('selected',x.dataset.date===key));const list=document.querySelector('#calendar-day-list'),title=document.querySelector('#calendar-day-title'),items=assembleiasCalendario.filter(a=>dateKey(a.data_hora)===key);const [yy,mm,dd]=key.split('-');title.textContent=dd+'/'+mm+'/'+yy;if(!items.length){list.innerHTML='<p class="muted">Nenhuma assembleia agendada neste dia.</p>';return;}list.innerHTML=items.map(a=>{const dest=(a.destinatarios||[]).map(x=>x.nome+' ('+x.tipo+')').join(', ')||'Nenhum destinatário registrado';return '<div class="calendar-event"><strong>'+a.condominio_nome+'</strong><span>'+Util.date(a.data_hora)+'</span><span>'+a.tipo+' - '+(a.local||'-')+'</span><p>'+dest+'</p><div class="row compact"><button class="btn secondary" type="button" onclick="reenviarAvisoAssembleia('+a.condominio_id+','+a.id+')">Reenviar notificação</button><button class="btn danger" type="button" onclick="cancelarAssembleiaCalendario('+a.condominio_id+','+a.id+')">Cancelar assembleia</button></div></div>'}).join('')};
|
||||
window.reenviarAvisoAssembleia=async(cond,aid)=>{try{const a=assembleiasCalendario.find(x=>Number(x.id)===Number(aid));if(!a||!a.aviso_id)throw new Error('Aviso da assembleia não encontrado.');await Api.post('/api/condominios/'+cond+'/avisos/'+a.aviso_id+'/reenviar',{});Util.toast('Notificação reenviada')}catch(e){Util.toast(e.message,true)}};
|
||||
window.cancelarAssembleiaCalendario=async(cond,aid)=>{if(!confirm('Cancelar esta assembleia?'))return;try{await Api.put('/api/condominios/'+cond+'/assembleias/'+aid,{status:'cancelada'});Util.toast('Assembleia cancelada');await abrirCalendarioAssembleias();await loadAssembleias();await load()}catch(e){Util.toast(e.message,true)}};
|
||||
function ensureAvisoNotifModal(){let m=document.querySelector('#aviso-notif-modal');if(m)return m;document.body.insertAdjacentHTML('beforeend','<div id="aviso-notif-modal" class="modal hidden"><div class="modal-backdrop" onclick="fecharAvisoNotificacao()"></div><section class="modal-panel"><div class="modal-head"><h2>Notificação</h2><button class="btn secondary" type="button" onclick="fecharAvisoNotificacao()">Fechar</button></div><div id="aviso-notif-body"></div><div class="row"><button class="btn" type="button" onclick="salvarAvisoNotificacao()">Salvar</button><button class="btn secondary" type="button" onclick="fecharAvisoNotificacao()">Cancelar</button></div></section></div>');return document.querySelector('#aviso-notif-modal')}
|
||||
function avisoDestInputs(){return [...document.querySelectorAll('#aviso-dests-hidden input[name=dest]')]}
|
||||
function updateAvisoNotifSummary(){const n=avisoDestInputs().filter(x=>x.checked).length;const el=document.querySelector('#aviso-notif-summary');if(el)el.textContent=n?(n+' selecionado'+(n>1?'s':'')):'Nenhuma pessoa selecionada'}
|
||||
window.abrirAvisoNotificacao=()=>{const m=ensureAvisoNotifModal(),ids=new Set(avisoDestInputs().filter(x=>x.checked).map(x=>x.value));const pessoas=[...contatos.map(c=>({value:'contato:'+c.contato_id,nome:c.contato_nome,tipo:c.tipo})),...funcionariosVinculados.map(f=>({value:'funcionario:'+(f.funcionario_id||f.id),nome:f.nome,tipo:'funcionario'})),...extras.map(x=>({value:'extra:'+x.id,nome:x.nome,tipo:'extra'}))];document.querySelector('#aviso-notif-body').innerHTML=pessoas.map(p=>'<label><input type="checkbox" value="'+p.value+'" '+(ids.has(p.value)?'checked':'')+'> '+p.nome+' ('+p.tipo+')</label>').join('')||'<p class="muted">Nenhuma pessoa cadastrada.</p>';m.classList.remove('hidden')};
|
||||
window.fecharAvisoNotificacao=()=>document.querySelector('#aviso-notif-modal')?.classList.add('hidden');
|
||||
window.salvarAvisoNotificacao=()=>{const ids=new Set([...document.querySelectorAll('#aviso-notif-body input:checked')].map(x=>x.value));avisoDestInputs().forEach(x=>x.checked=ids.has(x.value));updateAvisoNotifSummary();fecharAvisoNotificacao()};
|
||||
async function safeLoad(section,fn){try{await fn()}catch(e){document.querySelector('#'+section).innerHTML=`<p>${e.message}</p>`;Util.toast(e.message,true)}}
|
||||
async function load(){try{const d=await Api.get(`/api/condominios/${id}`);document.querySelector('#header').innerHTML=`<h2>${d.condominio.nome}</h2><p>${d.condominio.endereco||''}</p><div class="cards"><div class="stat">Sindico<strong>${d.sindico_atual?.contato_nome||'-'}</strong></div><div class="stat">Assembleias futuras<strong>${d.assembleias_futuras||0}</strong></div><div class="stat">Avisos ativos<strong>${d.avisos_ativos||0}</strong></div></div>`;await safeLoad('contatos',loadContatos);await safeLoad('espacos',loadEspacos);await safeLoad('assembleias',loadAssembleias);await safeLoad('documentos',loadDocs);await safeLoad('avisos',loadFuncionariosVinculados);await safeLoad('avisos',loadExtras);await safeLoad('avisos',loadAvisos)}catch(e){Util.toast(e.message,true)}}
|
||||
async function loadContatos(){const d=await Api.get(`/api/condominios/${id}/contatos`);contatos=d.contatos||[];document.querySelector('#contatos').innerHTML=`<h2>Contatos</h2><form id="contato-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>Telefone</label><input name="telefone"></div><div class="field"><label>E-mail</label><input name="email"></div><div class="field"><label>Tipo</label><select name="tipo"><option value="sindico">Sindico</option><option value="subsindico">Subsindico</option><option value="conselheiro">Conselheiro</option></select></div><button class="btn">Cadastrar</button></form><div class="table-wrap"><table><thead><tr><th>Nome</th><th>Tipo</th><th>Telefone</th><th>Email</th></tr></thead><tbody>${contatos.map(c=>`<tr><td>${c.contato_nome}</td><td>${c.tipo}</td><td>${c.contato_telefone||''}</td><td>${c.contato_email||''}</td></tr>`).join('')}</tbody></table></div>`;document.querySelector('#contato-form').onsubmit=async e=>{e.preventDefault();try{await Api.post(`/api/condominios/${id}/contatos/vincular`,Util.formData(e.target));Util.toast('Contato salvo');loadContatos()}catch(err){Util.toast(err.message,true)}}}
|
||||
async function loadEspacos(){const d=await Api.get(`/api/condominios/${id}/espacos-comuns`);document.querySelector('#espacos').innerHTML=`<h2>Espacos comuns</h2><form id="espaco-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>Valor centavos</label><input name="valor_utilizacao_centavos" type="number"></div><div class="field"><label>Indice</label><input name="indice_correcao"></div><div class="field"><label>Descricao</label><input name="descricao"></div><button class="btn">Cadastrar</button></form><div class="table-wrap"><table><thead><tr><th>Nome</th><th>Valor</th><th>Indice</th><th>Descricao</th></tr></thead><tbody>${(d.espacos||[]).map(x=>`<tr><td>${x.nome}</td><td>${Util.money(x.valor_utilizacao_centavos)}</td><td>${x.indice_correcao||''}</td><td>${x.descricao||''}</td></tr>`).join('')}</tbody></table></div>`;document.querySelector('#espaco-form').onsubmit=async e=>{e.preventDefault();try{await Api.post(`/api/condominios/${id}/espacos-comuns`,Util.formData(e.target));Util.toast('Espaco salvo');loadEspacos()}catch(err){Util.toast(err.message,true)}}}
|
||||
async function loadAssembleias(){const d=await Api.get(`/api/condominios/${id}/assembleias`);document.querySelector('#assembleias').innerHTML=`<div class="section-head"><h2>Assembleias</h2><button class="icon-btn calendar-open" type="button" onclick="abrirCalendarioAssembleias()" title="Calendário de assembleias" aria-label="Calendário de assembleias"></button></div><form id="ass-form" class="grid cols"><div class="field"><label>Tipo</label><input name="tipo" required></div><div class="field"><label>Data e hora</label><input name="data_hora" type="datetime-local" required></div><div class="field"><label>Local</label><input name="local"></div><div class="field"><label>Pauta</label><input name="pauta"></div><button class="btn">Cadastrar</button></form><div class="table-wrap"><table><thead><tr><th>Tipo</th><th>Data</th><th>Local</th><th>Status</th></tr></thead><tbody>${(d.assembleias||[]).map(a=>`<tr><td>${a.tipo}</td><td>${Util.date(a.data_hora)}</td><td>${a.local||''}</td><td>${Util.badge(a.status)}</td></tr>`).join('')}</tbody></table></div>`;document.querySelector('#ass-form').onsubmit=async e=>{e.preventDefault();try{await Api.post(`/api/condominios/${id}/assembleias`,Util.formData(e.target));Util.toast('Assembleia salva');loadAssembleias()}catch(err){Util.toast(err.message,true)}}}
|
||||
async function loadFuncionariosVinculados(){const d=await Api.get(`/api/condominios/${id}/funcionarios-vinculados`);funcionariosVinculados=d.funcionarios||[]}
|
||||
async function loadExtras(){const d=await Api.get(`/api/extras`);extras=(d.extras||[]).filter(x=>x.status==='ativo')}
|
||||
async function loadDocs(){const d=await Api.get(`/api/condominios/${id}/documentos`);docs=d.documentos||[];document.querySelector('#documentos').innerHTML=`<h2>Documentos</h2><form id="doc-form" class="row"><div class="field"><label>Tipo</label><input name="tipo"></div><div class="field"><label>Arquivo</label><input name="arquivo" type="file" required></div><button class="btn">Enviar</button></form><div class="table-wrap"><table><thead><tr><th>ID</th><th>Arquivo</th><th>Tipo</th><th>Download</th></tr></thead><tbody>${docs.map(d=>`<tr><td>${d.id}</td><td>${d.nome_arquivo}</td><td>${d.tipo||''}</td><td><a href="/api/documentos/${d.id}/download">Baixar</a></td></tr>`).join('')}</tbody></table></div>`;document.querySelector('#doc-form').onsubmit=async e=>{e.preventDefault();const fd=new FormData(e.target);try{await Api.post(`/api/condominios/${id}/documentos`,fd);Util.toast('Documento enviado');loadDocs()}catch(err){Util.toast(err.message,true)}}}
|
||||
async function loadAvisos(){const d=await Api.get(`/api/condominios/${id}/avisos`);avisos=d.avisos||[];const destContatos=contatos.map(c=>`<label><input type="checkbox" name="dest" value="contato:${c.contato_id}"> ${c.contato_nome} (${c.tipo})</label>`).join('');const destFuncionarios=funcionariosVinculados.map(f=>`<label><input type="checkbox" name="dest" value="funcionario:${f.funcionario_id||f.id}"> ${f.nome} (funcionario)</label>`).join('');const destExtras=extras.map(x=>`<label><input type="checkbox" name="dest" value="extra:${x.id}"> ${x.nome} (extra)</label>`).join('');const dest=[destContatos,destFuncionarios,destExtras].filter(Boolean).join('');const docChecks=docs.map(d=>`<label><input type="checkbox" name="doc" value="${d.id}"> ${d.nome_arquivo}</label>`).join('');document.querySelector('#avisos').innerHTML=`<h2>Avisos</h2><form id="aviso-form" class="grid cols"><div class="field"><label>Titulo</label><input name="titulo" required></div><div class="field"><label>Tipo</label><input name="tipo" value="aviso"></div><div class="field"><label>Prioridade</label><select name="prioridade"><option>normal</option><option>alta</option><option>urgente</option></select></div><div class="field"><label>Mensagem</label><textarea name="mensagem" required></textarea></div><div class="field"><label>Data/hora de envio</label><input name="data_evento" type="datetime-local"></div><div class="field"><label>Recorrencia</label><select name="recorrencia_meses"><option value="0">Sem recorrencia</option><option value="1">1 vez por mes</option><option value="3">A cada 3 meses</option><option value="12">1 vez por ano</option></select></div><div class="field"><label>Destinatarios</label><button class="btn secondary" type="button" onclick="abrirAvisoNotificacao()">Notificação</button> <span id="aviso-notif-summary" class="muted">Nenhuma pessoa selecionada</span><div id="aviso-dests-hidden" class="hidden">${dest}</div></div><div class="field"><label>Documentos</label>${docChecks||'<p>Nenhum documento.</p>'}</div><label><input type="checkbox" name="enviar_whatsapp" value="true"> Enviar por WhatsApp</label><label><input type="checkbox" name="enviar_email" value="true"> Enviar por e-mail</label><button class="btn">Criar aviso</button></form><div class="table-wrap"><table><thead><tr><th>Titulo</th><th>Status</th><th>Envio</th><th>Publicado em</th><th>Recorrencia</th><th>Acoes</th></tr></thead><tbody>${avisos.map(a=>`<tr><td>${a.titulo}</td><td>${Util.badge(a.status_envio)}</td><td>${Util.date(a.data_evento)}</td><td>${Util.date(a.data_publicacao)}</td><td>${recLabel(a.recorrencia_meses)}</td><td><button class="btn secondary" onclick="reenviarAviso(${a.id})">Reenviar</button> <button class="btn danger" onclick="excluirAviso(${a.id})">Excluir</button></td></tr>`).join('')}</tbody></table></div>`;updateAvisoNotifSummary();document.querySelector('#aviso-form').onsubmit=async e=>{e.preventDefault();const f=e.target,b=Util.formData(f);b.enviar_whatsapp=!!f.enviar_whatsapp.checked;b.enviar_email=!!f.enviar_email.checked;b.destinatarios=[...f.querySelectorAll('input[name=dest]:checked')].map(x=>{const [tipo,did]=x.value.split(':');return{tipo_destinatario:tipo,id:Number(did)}});b.documentos=[...f.querySelectorAll('input[name=doc]:checked')].map(x=>Number(x.value));try{await Api.post(`/api/condominios/${id}/avisos`,b);Util.toast('Aviso criado');loadAvisos();load()}catch(err){Util.toast(err.message,true)}}}
|
||||
window.reenviarAviso=async avisoId=>{try{await Api.post(`/api/condominios/${id}/avisos/${avisoId}/reenviar`,{});Util.toast('Aviso reenviado');loadAvisos()}catch(err){Util.toast(err.message,true)}};
|
||||
window.excluirAviso=async avisoId=>{try{await Api.request(`/api/condominios/${id}/avisos/${avisoId}`,{method:'DELETE'});Util.toast('Aviso excluido');loadAvisos();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>Cadastro</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>Cadastro</h1></div><section class="panel"><h2>Novo condominio</h2><form id="cond-form" class="row"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>CNPJ</label><input name="cnpj" required></div><div class="field"><label>Endereco</label><input name="endereco"></div><button class="btn">Criar</button></form><div class="panel-block"><h2>Condominios</h2><div class="table-wrap"><table><thead><tr><th>ID</th><th>Nome</th><th>CNPJ</th><th>Endereco</th><th></th></tr></thead><tbody id="rows"></tbody></table></div></div></section><section id="funcionarios-panel" class="panel hidden"><h2>Funcionarios</h2><form id="func-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>E-mail de login</label><input name="email" type="email" required></div><div class="field"><label>Telefone/WhatsApp</label><input name="telefone"></div><div class="field"><label>Senha inicial</label><input name="senha" type="password" required></div><button class="btn">Cadastrar funcionario</button></form><div class="table-wrap"><table><thead><tr><th>Nome</th><th>E-mail</th><th>Telefone</th><th>Cargo</th><th>Status</th><th>Condominios</th><th>Opcoes</th></tr></thead><tbody id="func-rows"></tbody></table></div></section><section id="extras-panel" class="panel hidden"><h2>Extra</h2><form id="extra-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>E-mail</label><input name="email" type="email"></div><div class="field"><label>WhatsApp</label><input name="telefone"></div><button class="btn">Cadastrar extra</button></form><div class="table-wrap"><table><thead><tr><th>Nome</th><th>E-mail</th><th>WhatsApp</th><th>Status</th><th>Opcoes</th></tr></thead><tbody id="extra-rows"></tbody></table></div></section><div id="cond-modal" class="modal hidden"><div class="modal-backdrop" onclick="fecharModalCondominios()"></div><section class="modal-panel"><div class="modal-head"><h2 id="cond-modal-title">Condominios</h2><button class="btn secondary" type="button" onclick="fecharModalCondominios()">Fechar</button></div><div id="cond-modal-body"></div></section></div></main><script>Layout.mount('empresa','condominios');</script><script src="/cliente/condominios.js"></script></body></html>
|
||||
@@ -0,0 +1,29 @@
|
||||
const form=document.querySelector('#cond-form'),rows=document.querySelector('#rows');
|
||||
let condominios=[],funcionarios=[],extras=[],funcLinks=new Map(),funcAtual=null,funcEditId=null;
|
||||
async function load(){try{const d=await Api.get('/api/condominios');condominios=d.condominios||[];rows.innerHTML=condominios.map(c=>`<tr><td>${c.id}</td><td>${c.nome}</td><td>${c.cnpj||''}</td><td>${c.endereco||''}</td><td><a class="btn secondary" href="/cliente/condominio-detalhe.html?id=${c.id}">Abrir</a></td></tr>`).join('')}catch(e){Util.toast(e.message,true)}}
|
||||
form.onsubmit=async e=>{e.preventDefault();try{await Api.post('/api/condominios',Util.formData(form));form.reset();Util.toast('Condominio criado');await load();await loadFuncionarios()}catch(err){Util.toast(err.message,true)}};
|
||||
load();
|
||||
|
||||
const funcPanel=document.querySelector('#funcionarios-panel'),funcForm=document.querySelector('#func-form'),funcRows=document.querySelector('#func-rows'),extraPanel=document.querySelector('#extras-panel'),extraForm=document.querySelector('#extra-form'),extraRows=document.querySelector('#extra-rows'),modal=document.querySelector('#cond-modal'),modalTitle=document.querySelector('#cond-modal-title'),modalBody=document.querySelector('#cond-modal-body');
|
||||
function esc(v){return String(v??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]))}
|
||||
async function linksPorFuncionario(){const mapa=new Map();await Promise.all(condominios.map(async c=>{try{const d=await Api.get(`/api/condominios/${c.id}/funcionarios-vinculados`);(d.funcionarios||[]).forEach(f=>{const fid=Number(f.funcionario_id||f.id);if(!mapa.has(fid))mapa.set(fid,[]);mapa.get(fid).push({id:Number(c.id),nome:c.nome});});}catch(e){}}));return mapa;}
|
||||
async function refreshLinks(){funcLinks=await linksPorFuncionario()}
|
||||
async function loadFuncionarios(){if(!Auth.isAdminEmpresa())return;funcPanel.classList.remove('hidden');try{if(!condominios.length)await load();const d=await Api.get('/api/funcionarios');funcionarios=d.funcionarios||[];await refreshLinks();funcRows.innerHTML=funcionarios.map(f=>{const acao=f.cargo==='admin_empresa'?'<span class="muted">Acesso total</span>':'<button class="btn secondary" type="button" onclick="abrirModalCondominios('+f.id+')">Condominios</button>';const opcoes=f.cargo==='admin_empresa'?'<span class="muted">-</span>':'<button class="btn secondary" type="button" onclick="abrirEditarFuncionario('+f.id+')">Editar</button> <button class="btn danger" type="button" onclick="abrirExcluirFuncionario('+f.id+')">Excluir</button>';return '<tr><td>'+esc(f.nome)+'</td><td>'+esc(f.email)+'</td><td>'+esc(f.telefone||'')+'</td><td>'+esc(f.cargo||'')+'</td><td>'+Util.badge(f.status)+'</td><td>'+acao+'</td><td>'+opcoes+'</td></tr>'}).join('')||'<tr><td colspan="7">Nenhum funcionario cadastrado.</td></tr>'}catch(e){Util.toast(e.message,true)}}
|
||||
if(funcForm){funcForm.onsubmit=async e=>{e.preventDefault();try{await Api.post('/api/funcionarios',Util.formData(funcForm));funcForm.reset();Util.toast('Funcionario cadastrado');loadFuncionarios()}catch(err){Util.toast(err.message,true)}}}
|
||||
function funcionarioPorId(funcId){return funcionarios.find(x=>Number(x.id)===Number(funcId))}
|
||||
window.abrirEditarFuncionario=funcId=>{const f=funcionarioPorId(funcId);if(!f)return;modalTitle.textContent='Editar funcionario';modalBody.innerHTML='<form id="func-edit-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required value="'+esc(f.nome||'')+'"></div><div class="field"><label>E-mail de login</label><input name="email" type="email" required value="'+esc(f.email||'')+'"></div><div class="field"><label>Telefone/WhatsApp</label><input name="telefone" value="'+esc(f.telefone||'')+'"></div><div class="field"><label>Status</label><select name="status"><option value="ativo" '+(f.status==='ativo'?'selected':'')+'>Ativo</option><option value="inativo" '+(f.status==='inativo'?'selected':'')+'>Inativo</option></select></div><div class="field"><label>Nova senha</label><input name="senha" type="password"></div><div class="row"><button class="btn">Salvar</button><button class="btn secondary" type="button" onclick="fecharModalCondominios()">Cancelar</button></div></form>';modal.classList.remove('hidden');document.querySelector('#func-edit-form').onsubmit=async e=>{e.preventDefault();try{await Api.put('/api/funcionarios/'+funcId,Util.formData(e.target));Util.toast('Funcionario atualizado');fecharModalCondominios();loadFuncionarios()}catch(err){Util.toast(err.message,true)}}};
|
||||
window.abrirExcluirFuncionario=funcId=>{const f=funcionarioPorId(funcId);if(!f)return;modalTitle.textContent='Excluir funcionario';modalBody.innerHTML='<p>Deseja realmente excluir o acesso de <strong>'+esc(f.nome||'')+'</strong>?</p><p class="muted">O funcionario sera inativado e perdera acesso ao sistema.</p><div class="row"><button class="btn danger" type="button" onclick="confirmarExcluirFuncionario('+funcId+')">Excluir</button><button class="btn secondary" type="button" onclick="fecharModalCondominios()">Cancelar</button></div>';modal.classList.remove('hidden')};
|
||||
window.confirmarExcluirFuncionario=async funcId=>{try{await Api.request('/api/funcionarios/'+funcId,{method:'DELETE'});Util.toast('Funcionario inativado');fecharModalCondominios();loadFuncionarios()}catch(err){Util.toast(err.message,true)}};
|
||||
function renderModalCondominios(){const f=funcionarios.find(x=>Number(x.id)===Number(funcAtual));if(!f)return;const vinculados=funcLinks.get(Number(funcAtual))||[];const vinculadosIds=new Set(vinculados.map(c=>Number(c.id)));const disponiveis=condominios.filter(c=>!vinculadosIds.has(Number(c.id)));modalTitle.textContent=`Condominios de ${f.nome}`;const listaVinculados=vinculados.map(c=>`<div class="cond-item"><span>${esc(c.nome)}</span><button class="btn danger" type="button" onclick="desvincularFuncionarioCondominio(${funcAtual},${c.id})">Desvincular</button></div>`).join('')||'<p class="muted">Nenhum condominio vinculado.</p>';const listaDisponiveis=disponiveis.map(c=>`<div class="cond-item"><span>${esc(c.nome)}</span><button class="btn secondary" type="button" onclick="vincularFuncionarioCondominio(${funcAtual},${c.id})">Vincular</button></div>`).join('')||'<p class="muted">Todos os condominios ja estao vinculados.</p>';modalBody.innerHTML=`<div class="modal-grid"><section><h3>Vinculados</h3>${listaVinculados}</section><section><h3>Nao vinculados</h3>${listaDisponiveis}</section></div>`}
|
||||
window.abrirModalCondominios=async funcId=>{funcAtual=Number(funcId);try{await refreshLinks();renderModalCondominios();modal.classList.remove('hidden')}catch(err){Util.toast(err.message,true)}};
|
||||
window.fecharModalCondominios=()=>{modal.classList.add('hidden');funcAtual=null};
|
||||
window.vincularFuncionarioCondominio=async (funcId,condId)=>{try{await Api.post(`/api/condominios/${condId}/funcionarios-vinculados`,{funcionario_id:funcId});Util.toast('Funcionario vinculado');await refreshLinks();renderModalCondominios()}catch(err){Util.toast(err.message,true)}};
|
||||
window.desvincularFuncionarioCondominio=async (funcId,condId)=>{try{await Api.request(`/api/condominios/${condId}/funcionarios-vinculados/${funcId}`,{method:'DELETE'});Util.toast('Funcionario desvinculado');await refreshLinks();renderModalCondominios()}catch(err){Util.toast(err.message,true)}};
|
||||
loadFuncionarios();
|
||||
|
||||
async function loadExtras(){if(!Auth.isAdminEmpresa())return;extraPanel.classList.remove('hidden');try{const d=await Api.get('/api/extras');extras=d.extras||[];extraRows.innerHTML=extras.map(x=>'<tr><td>'+esc(x.nome)+'</td><td>'+esc(x.email||'')+'</td><td>'+esc(x.telefone||'')+'</td><td>'+Util.badge(x.status)+'</td><td><button class="btn secondary" type="button" onclick="abrirEditarExtra('+x.id+')">Editar</button> <button class="btn danger" type="button" onclick="excluirExtra('+x.id+')">Excluir</button></td></tr>').join('')||'<tr><td colspan="5">Nenhum destinatario extra cadastrado.</td></tr>'}catch(e){Util.toast(e.message,true)}}
|
||||
if(extraForm){extraForm.onsubmit=async e=>{e.preventDefault();try{await Api.post('/api/extras',Util.formData(extraForm));extraForm.reset();Util.toast('Extra cadastrado');loadExtras()}catch(err){Util.toast(err.message,true)}}}
|
||||
function extraPorId(id){return extras.find(x=>Number(x.id)===Number(id))}
|
||||
window.abrirEditarExtra=id=>{const x=extraPorId(id);if(!x)return;modalTitle.textContent='Editar extra';modalBody.innerHTML='<form id="extra-edit-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required value="'+esc(x.nome||'')+'"></div><div class="field"><label>E-mail</label><input name="email" type="email" value="'+esc(x.email||'')+'"></div><div class="field"><label>WhatsApp</label><input name="telefone" value="'+esc(x.telefone||'')+'"></div><div class="field"><label>Status</label><select name="status"><option value="ativo" '+(x.status==='ativo'?'selected':'')+'>Ativo</option><option value="inativo" '+(x.status==='inativo'?'selected':'')+'>Inativo</option></select></div><div class="row"><button class="btn">Salvar</button><button class="btn secondary" type="button" onclick="fecharModalCondominios()">Cancelar</button></div></form>';modal.classList.remove('hidden');document.querySelector('#extra-edit-form').onsubmit=async e=>{e.preventDefault();try{await Api.put('/api/extras/'+id,Util.formData(e.target));Util.toast('Extra atualizado');fecharModalCondominios();loadExtras()}catch(err){Util.toast(err.message,true)}}};
|
||||
window.excluirExtra=async id=>{try{await Api.request('/api/extras/'+id,{method:'DELETE'});Util.toast('Extra excluido');loadExtras()}catch(err){Util.toast(err.message,true)}};
|
||||
loadExtras();
|
||||
@@ -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>WhatsApp</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>WhatsApp</h1></div><section class="panel"><h2>WhatsApp</h2><form id="wa-form" class="row"><div class="field"><label>Numero WhatsApp</label><input name="numero_whatsapp" required placeholder="5511999999999"></div><button class="btn">Cadastrar</button></form></section><section class="panel"><h2>Status</h2><div id="status"></div><div class="row"><button id="btn-qr" class="btn secondary" type="button">Conectar via QR Code</button><button id="btn-reconnect" class="btn secondary" type="button">Reconectar</button><button id="btn-disconnect" class="btn danger" type="button">Desconectar</button></div><div id="qr-box" class="hidden"><h2>QR Code</h2><img id="qr" class="qr hidden" alt="QR Code Evolution"><div class="field"><label>Codigo de pareamento</label><input id="pairing" readonly></div></div></section></main><script>Layout.mount('empresa','configuracoes');</script><script src="/cliente/configuracoes.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
const form=document.querySelector('#wa-form'),statusEl=document.querySelector('#status'),qr=document.querySelector('#qr'),qrBox=document.querySelector('#qr-box'),pairing=document.querySelector('#pairing'),btnQr=document.querySelector('#btn-qr'),btnReconnect=document.querySelector('#btn-reconnect'),btnDisconnect=document.querySelector('#btn-disconnect');async function load(){try{const d=await Api.get('/api/config/evolution-instancia/status');const i=d.instancia;const evo=d.evolution||{};statusEl.innerHTML=i?`<p>Status aprovacao: ${Util.badge(i.status)}</p><p>Numero: ${i.numero_whatsapp||'-'}</p><p>Conexao: ${Util.badge(evo.state||'-')}</p>`:`<p>Status: ${Util.badge(d.status)}</p>`;const has=i!=null;btnQr.disabled=!has;btnReconnect.disabled=!has;btnDisconnect.disabled=!has}catch(e){Util.toast(e.message,true)}}function showQr(d){qrBox.classList.remove('hidden');const base64=d.qrcode_base64||d.qr_code_base64;if(base64){qr.src=base64.startsWith('data:image/')?base64:'data:image/png;base64,'+base64;qr.classList.remove('hidden')}else{qr.classList.add('hidden');Util.toast('Evolution nao retornou QR Code nesta tentativa',true)}pairing.value=d.pairing_code||''}form.onsubmit=async e=>{e.preventDefault();try{const d=await Api.post('/api/config/evolution-instancia/conectar',Util.formData(form));statusEl.innerHTML=`<p class="success-message">WhatsApp cadastrado com sucesso. Aguarde a liberacao no painel Master.</p><p>Status aprovacao: ${Util.badge(d.status)}</p><p>Instancia: ${d.instance_name}</p>`;showQr(d);Util.toast('WhatsApp cadastrado com sucesso');setTimeout(load,1600)}catch(err){Util.toast(err.message,true)}};btnQr.onclick=async()=>{try{const d=await Api.post('/api/config/evolution-instancia/qrcode',{});showQr(d);Util.toast('QR Code atualizado')}catch(e){Util.toast(e.message,true)}};btnReconnect.onclick=async()=>{try{await Api.post('/api/config/evolution-instancia/reconectar',{});Util.toast('Reconexao solicitada');setTimeout(load,1200)}catch(e){Util.toast(e.message,true)}};btnDisconnect.onclick=async()=>{if(!confirm('Desconectar esta sessao do WhatsApp?'))return;try{await Api.post('/api/config/evolution-instancia/desconectar',{});Util.toast('Sessao desconectada');setTimeout(load,1200)}catch(e){Util.toast(e.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>Financeiro</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</h1></div><section class="panel"><h2>Periodo atual</h2><div id="status" class="cards"></div><button id="gerar" class="btn">Gerar cobranca PIX</button></section><section id="pix" class="panel hidden"><h2>PIX</h2><img id="qr" class="qr" alt="QR Code PIX"><div class="field"><label>Copia e cola</label><textarea id="payload" readonly></textarea></div><button id="copy" class="btn secondary">Copiar</button></section></main><script>Layout.mount('empresa','financeiro');</script><script src="/cliente/financeiro.js"></script></body></html>
|
||||
@@ -0,0 +1,7 @@
|
||||
let pagamento=null,timer=null;const statusEl=document.querySelector('#status'),pix=document.querySelector('#pix'),qr=document.querySelector('#qr'),payload=document.querySelector('#payload');
|
||||
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')}
|
||||
async function load(){try{const d=await Api.get('/api/financeiro/periodo-atual');pagamento=d.pagamento;const p=d.periodo||{},a=d.assinatura||{};statusEl.innerHTML='<div class="stat">Periodo de utilizacao<strong>'+data(p.data_inicio)+' a '+data(p.data_fim)+'</strong><span class="muted">Mes fechado</span></div><div class="stat">Vencimento<strong>'+data(p.data_vencimento)+'</strong><span class="muted">Dia '+(a.dia_vencimento||7)+' de cada mes</span></div><div class="stat">Mensalidade<strong>'+Util.money(a.valor_mensal_centavos)+'</strong><span>'+Util.badge(p.status)+'</span></div><div class="stat">Pagamento<strong>'+Util.badge(pagamento?.status)+'</strong></div>';if(pagamento&&pagamento.qr_code_base64)showPix(pagamento)}catch(e){Util.toast(e.message,true)}}
|
||||
function showPix(p){pix.classList.remove('hidden');qr.src='data:image/png;base64,'+p.qr_code_base64;payload.value=p.qr_code_payload||'';if(timer)clearInterval(timer);timer=setInterval(async()=>{const d=await Api.get('/api/financeiro/pagamentos/'+p.id);if(d.pagamento?.status==='pago'){clearInterval(timer);Util.toast('Pagamento confirmado');load()}},5000)}
|
||||
document.querySelector('#gerar').onclick=async()=>{try{const d=await Api.post('/api/financeiro/pagamentos',{});pagamento=d.pagamento;showPix(pagamento);load()}catch(e){Util.toast(e.message,true)}};
|
||||
document.querySelector('#copy').onclick=()=>navigator.clipboard.writeText(payload.value).then(()=>Util.toast('Codigo copiado'));
|
||||
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>Lembrete</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>Lembrete</h1></div><section class="panel"><h2>Novo lembrete</h2><form id="lembrete-form" class="grid cols"><input type="hidden" name="id"><div class="field"><label>Titulo</label><input name="titulo" required></div><div class="field"><label>Data/hora</label><input name="data_hora" type="datetime-local"></div><div class="field"><label>Status</label><select name="status"><option value="ativo">Ativo</option><option value="concluido">Concluido</option><option value="cancelado">Cancelado</option></select></div><div class="field"><label>Recorrencia</label><select name="recorrencia_meses"><option value="0">Sem recorrencia</option><option value="1">1 vez por mes</option><option value="3">A cada 3 meses</option><option value="12">1 vez por ano</option></select></div><div class="field"><label>Mensagem</label><textarea name="mensagem" required></textarea></div><div class="field"><label>Pessoas que receberao</label><div id="pessoas"></div></div><div class="row"><button class="btn" id="submit-btn">Cadastrar lembrete</button><button class="btn secondary hidden" id="cancel-edit" type="button">Cancelar edicao</button></div></form></section><section class="panel"><h2>Lembretes</h2><div class="table-wrap"><table><thead><tr><th>Titulo</th><th>Data/hora</th><th>Status</th><th>Destinatarios</th><th>Acoes</th></tr></thead><tbody id="rows"></tbody></table></div></section></main><script>Layout.mount('empresa','lembretes');</script><script src="/cliente/lembretes.js"></script></body></html>
|
||||
@@ -0,0 +1,20 @@
|
||||
const form=document.querySelector('#lembrete-form'),rows=document.querySelector('#rows'),pessoasEl=document.querySelector('#pessoas'),submitBtn=document.querySelector('#submit-btn'),cancelBtn=document.querySelector('#cancel-edit');
|
||||
let pessoas=[],lembretes=[];
|
||||
function esc(v){return String(v??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]))}
|
||||
function toInputDate(v){if(!v)return'';const d=new Date(v);if(Number.isNaN(d.getTime()))return'';const pad=n=>String(n).padStart(2,'0');return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`}
|
||||
function recLabel(v){return {1:"Mensal",3:"Trimestral",12:"Anual"}[Number(v)]||"Sem recorrencia"}
|
||||
function selectedDestIds(){return new Set([...form.querySelectorAll('input[name=dest]:checked')].map(x=>String(x.value)))}
|
||||
function updateNotifSummary(){const n=selectedDestIds().size;const el=document.querySelector('#notif-summary');if(el)el.textContent=n?(n+' selecionado'+(n>1?'s':'')):'Nenhuma pessoa selecionada'}
|
||||
function ensureNotifModal(){let m=document.querySelector('#notif-modal');if(m)return m;document.body.insertAdjacentHTML('beforeend','<div id="notif-modal" class="modal hidden"><div class="modal-backdrop" onclick="fecharNotificacao()"></div><section class="modal-panel"><div class="modal-head"><h2>Notificação</h2><button class="btn secondary" type="button" onclick="fecharNotificacao()">Fechar</button></div><div id="notif-modal-body"></div><div class="row"><button class="btn" type="button" onclick="salvarNotificacao()">Salvar</button><button class="btn secondary" type="button" onclick="fecharNotificacao()">Cancelar</button></div></section></div>');return document.querySelector('#notif-modal')}
|
||||
window.abrirNotificacao=()=>{const m=ensureNotifModal(),ids=selectedDestIds();document.querySelector('#notif-modal-body').innerHTML=pessoas.map(p=>'<label><input type="checkbox" value="'+p.id+'" '+(ids.has(String(p.id))?'checked':'')+'> '+esc(p.nome)+' ('+esc(p.cargo||'')+')</label>').join('')||'<p class="muted">Nenhum funcionario ativo.</p>';m.classList.remove('hidden')};
|
||||
window.fecharNotificacao=()=>document.querySelector('#notif-modal')?.classList.add('hidden');
|
||||
window.salvarNotificacao=()=>{const ids=new Set([...document.querySelectorAll('#notif-modal-body input:checked')].map(x=>String(x.value)));form.querySelectorAll('input[name=dest]').forEach(x=>x.checked=ids.has(String(x.value)));updateNotifSummary();fecharNotificacao()};
|
||||
async function loadPessoas(){const d=await Api.get('/api/lembrete-pessoas');pessoas=d.pessoas||[];pessoasEl.innerHTML='<button class="btn secondary" type="button" onclick="abrirNotificacao()">Notificação</button> <span id="notif-summary" class="muted">Nenhuma pessoa selecionada</span><div id="pessoas-hidden" class="hidden">'+pessoas.map(p=>'<label><input type="checkbox" name="dest" value="'+p.id+'"> '+esc(p.nome)+' ('+esc(p.cargo||'')+')</label>').join('')+'</div>';updateNotifSummary()}
|
||||
async function load(){try{const d=await Api.get('/api/lembretes');lembretes=d.lembretes||[];rows.innerHTML=lembretes.map(l=>{const dest=(l.destinatarios||[]).map(x=>x.nome).join(', ')||'-';const acoes=l.pode_alterar?`<button class="btn" onclick="enviar(${l.id})">Enviar</button> ${l.pode_editar?`<button class="btn secondary" onclick="editar(${l.id})">Editar</button>`:''} <button class="btn danger" onclick="excluir(${l.id})">Excluir</button>`:'-';return `<tr><td><strong>${esc(l.titulo)}</strong><br><span class="muted">${esc(l.mensagem)}</span></td><td>${Util.date(l.data_hora)}<br><span class="muted">${recLabel(l.recorrencia_meses)}</span></td><td>${Util.badge(l.status)}</td><td>${esc(dest)}</td><td>${acoes}</td></tr>`}).join('')||'<tr><td colspan="5">Nenhum lembrete cadastrado.</td></tr>'}catch(e){Util.toast(e.message,true)}}
|
||||
function resetForm(){form.reset();form.id.value='';form.recorrencia_meses.value='0';submitBtn.textContent='Cadastrar lembrete';cancelBtn.classList.add('hidden');form.querySelectorAll('input[name=dest]').forEach(x=>x.checked=false);updateNotifSummary()}
|
||||
window.editar=id=>{const l=lembretes.find(x=>Number(x.id)===Number(id));if(!l)return;if(!l.pode_editar){Util.toast('Este lembrete ja foi enviado e nao pode mais ser editado.',true);return;}form.id.value=l.id;form.titulo.value=l.titulo||'';form.mensagem.value=l.mensagem||'';form.data_hora.value=toInputDate(l.data_hora);form.status.value='ativo';form.recorrencia_meses.value=String(l.recorrencia_meses||0);const ids=new Set((l.destinatarios||[]).map(x=>Number(x.id)));form.querySelectorAll('input[name=dest]').forEach(x=>x.checked=ids.has(Number(x.value)));updateNotifSummary();submitBtn.textContent='Salvar alteracao';cancelBtn.classList.remove('hidden');scrollTo({top:0,behavior:'smooth'})};
|
||||
window.enviar=async id=>{try{const r=await Api.post(`/api/lembretes/${id}/enviar`,{});const en=r.envio||{};Util.toast(en.falhas?("Enviados: "+(en.enviados||0)+". Falhas: "+en.falhas):("Lembrete enviado para "+(en.enviados||0)));await load()}catch(e){Util.toast(e.message,true)}};
|
||||
window.excluir=async id=>{try{await Api.request(`/api/lembretes/${id}`,{method:'DELETE'});Util.toast('Lembrete excluido');await load();resetForm()}catch(e){Util.toast(e.message,true)}};
|
||||
cancelBtn.onclick=resetForm;
|
||||
form.onsubmit=async e=>{e.preventDefault();const b=Util.formData(form);b.destinatarios=[...form.querySelectorAll('input[name=dest]:checked')].map(x=>Number(x.value));try{if(form.id.value){await Api.put(`/api/lembretes/${form.id.value}`,b);Util.toast('Lembrete atualizado')}else{const r=await Api.post('/api/lembretes',b);const en=r.envio||{};Util.toast(en.enfileirado?'Lembrete enfileirado para envio':(en.falhas?('Lembrete salvo. Enviados: '+(en.enviados||0)+'. Falhas: '+en.falhas):'Lembrete cadastrado'))}resetForm();await load()}catch(err){Util.toast(err.message,true)}};
|
||||
(async()=>{try{await loadPessoas();await load();setInterval(load,5000)}catch(e){Util.toast(e.message,true)}})();
|
||||
@@ -0,0 +1,4 @@
|
||||
:root{--bg:#f6f7f9;--panel:#fff;--line:#d9dee7;--text:#1c2430;--muted:#667085;--brand:#176b87;--brand-strong:#0f5268;--ok:#1f8a5b;--warn:#b7791f;--danger:#b42318}*{box-sizing:border-box}body{margin:0;min-height:100vh;font-family:Arial,Helvetica,sans-serif;background:var(--bg);color:var(--text)}a{color:inherit}button,input,select,textarea{font:inherit}.login-page{min-height:100vh;display:grid;place-items:center;padding:32px}.login-box,.panel{background:var(--panel);border:1px solid var(--line);border-radius:8px;box-shadow:0 16px 40px rgba(16,24,40,.08)}.login-box{width:min(440px,100%);padding:28px}.tabs,.nav-tabs{display:flex;gap:8px;border-bottom:1px solid var(--line);margin-bottom:20px}.tabs button,.nav-tabs button{border:0;background:transparent;padding:10px 12px;cursor:pointer;color:var(--muted)}.tabs button.active,.nav-tabs button.active{color:var(--brand);border-bottom:3px solid var(--brand)}.field{display:grid;gap:6px;margin-bottom:12px}label{color:var(--muted);font-size:13px}input,select,textarea{width:100%;border:1px solid var(--line);border-radius:6px;padding:10px 12px;background:#fff}textarea{min-height:88px;resize:vertical}.btn{border:0;border-radius:6px;padding:10px 14px;color:#fff;background:var(--brand);cursor:pointer;display:inline-flex;align-items:center;gap:8px;justify-content:center}.btn:hover{background:var(--brand-strong)}.btn.secondary{color:var(--text);background:#eef2f6}.btn.danger{background:var(--danger)}.app{display:grid;grid-template-columns:250px minmax(0,1fr);min-height:100vh}.sidebar{background:#102a38;color:#fff;padding:22px 16px;display:flex;flex-direction:column;gap:20px}.brand{font-size:20px;font-weight:700}.user{color:#c9d6df;font-size:13px}.menu{display:grid;gap:6px}.menu a{text-decoration:none;padding:10px 12px;border-radius:6px;color:#dbe6ec}.menu a.active,.menu a:hover{background:rgba(255,255,255,.12);color:#fff}.sidebar .btn{margin-top:auto;width:100%;background:rgba(255,255,255,.14)}.content{padding:28px;min-width:0}.topbar{display:flex;justify-content:space-between;gap:16px;align-items:center;margin-bottom:18px}h1{margin:0;font-size:26px}h2{margin:0 0 14px;font-size:18px}.grid{display:grid;gap:16px}.cols{grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}.panel{padding:18px;box-shadow:none}.panel-block{margin-top:16px}.row{display:flex;gap:10px;align-items:end;flex-wrap:wrap}.table-wrap{overflow:auto}table{width:100%;border-collapse:collapse;min-width:680px}th,td{padding:10px 8px;border-bottom:1px solid var(--line);text-align:left;font-size:14px}th{color:var(--muted);font-size:12px;text-transform:uppercase}.badge{display:inline-block;padding:4px 8px;border-radius:999px;background:#eef2f6;font-size:12px}.badge.ok{background:#dcfce7;color:var(--ok)}.badge.warn{background:#fef3c7;color:var(--warn)}.toast,.finance-banner{position:fixed;left:50%;transform:translateX(-50%);z-index:20;max-width:min(720px,calc(100vw - 24px));border-radius:8px;padding:12px 14px}.toast{bottom:18px;background:#17202a;color:#fff}.toast.error{background:var(--danger)}.finance-banner{top:12px;background:#fff7ed;border:1px solid #fed7aa;color:#9a3412}.hidden{display:none!important}.qr{max-width:260px;border:1px solid var(--line);border-radius:8px;padding:8px;background:#fff}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.stat{border:1px solid var(--line);border-radius:8px;padding:14px;background:#fff}.stat strong{display:block;font-size:22px;margin-top:6px}@media(max-width:820px){.app{grid-template-columns:1fr}.sidebar{position:static}.content{padding:18px}}.status-action{border:0;background:transparent;padding:0;display:inline-flex;align-items:center;gap:8px;cursor:pointer}.status-action span{font-size:12px;color:var(--brand);text-decoration:underline}.status-action:hover span{color:var(--brand-strong)}.inline-panel{background:#f8fafc;border:1px solid var(--line);border-radius:8px;padding:14px}.inline-panel h2{margin-bottom:10px}.inline-panel p{margin:6px 0}.row.compact{align-items:center;gap:8px}.row.compact select{min-width:180px}.link-button{border:0;background:transparent;color:var(--brand);cursor:pointer;font-size:12px;text-decoration:underline;padding:0 0 0 6px}.link-button:hover{color:var(--brand-strong)}
|
||||
.modal{position:fixed;inset:0;z-index:30;display:grid;place-items:center;padding:24px}.modal-backdrop{position:absolute;inset:0;background:rgba(15,23,42,.45)}.modal-panel{position:relative;width:min(860px,100%);max-height:calc(100vh - 48px);overflow:auto;background:#fff;border:1px solid var(--line);border-radius:8px;padding:18px;box-shadow:0 24px 80px rgba(15,23,42,.24)}.modal-head{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:16px}.modal-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.modal-grid h3{margin:0 0 10px;font-size:15px}.cond-item{display:flex;justify-content:space-between;gap:12px;align-items:center;border:1px solid var(--line);border-radius:8px;padding:10px;margin-bottom:8px}.muted{color:var(--muted)}@media(max-width:720px){.modal-grid{grid-template-columns:1fr}.modal{padding:12px}}
|
||||
.success-message{display:inline-block;margin:0 0 10px;padding:10px 12px;border:1px solid #bbf7d0;border-radius:8px;background:#dcfce7;color:var(--ok);font-weight:700}
|
||||
.section-head{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:14px}.section-head h2{margin:0}.icon-btn{position:relative;width:42px;height:42px;border:1px solid var(--line);border-radius:6px;background:#fff;cursor:pointer;display:inline-grid;place-items:center}.icon-btn:hover{border-color:var(--brand);background:#eef8fb}.calendar-open::before{content:"";width:20px;height:18px;border:2px solid var(--brand);border-radius:3px;box-shadow:inset 0 5px 0 #e8f3f7;position:relative}.calendar-open::after{content:"";position:absolute;width:12px;height:2px;background:var(--brand);transform:translateY(-7px)}.calendar-modal{width:min(1120px,100%)}.calendar-layout{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px}.calendar-toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px}.calendar-toolbar strong{text-transform:capitalize}.calendar-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));border:1px solid var(--line);border-radius:8px;overflow:hidden;background:#fff}.calendar-weekday{padding:10px;background:#f8fafc;color:var(--muted);font-size:12px;font-weight:700;text-align:center;border-bottom:1px solid var(--line)}.calendar-day{min-height:86px;border:0;border-right:1px solid var(--line);border-bottom:1px solid var(--line);background:#fff;padding:8px;text-align:left;color:var(--text);cursor:pointer;position:relative}.calendar-day:nth-child(7n+7){border-right:0}.calendar-day.other{background:#f8fafc;color:#98a2b3}.calendar-day.busy{background:#fff7ed}.calendar-day.busy span{font-weight:700;color:var(--warn)}.calendar-day.today{box-shadow:inset 0 0 0 2px var(--brand)}.calendar-day.selected{background:#e8f3f7}.calendar-day b{position:absolute;right:8px;bottom:8px;background:var(--brand);color:#fff;border-radius:999px;padding:2px 7px;font-size:12px}.calendar-side{border:1px solid var(--line);border-radius:8px;padding:14px;background:#fff;min-height:420px}.calendar-side h3{margin:0 0 12px}.calendar-event{border:1px solid var(--line);border-radius:8px;padding:10px;margin-bottom:10px;display:grid;gap:6px}.calendar-event span{font-size:13px}.calendar-event p{margin:0;color:var(--muted);font-size:13px}@media(max-width:900px){.calendar-layout{grid-template-columns:1fr}.calendar-day{min-height:64px}.calendar-side{min-height:auto}}
|
||||
@@ -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>SeConectai</title><script src="/js/auth.js"></script></head><body><script>const session=Auth.session();if(!session) location.href='/login.html';else location.href=session.scope==='master'?'/master/clientes.html':'/cliente/condominios.html';</script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
window.Api={async request(path,options={}){const headers=options.headers||{};const token=Auth.token();if(token)headers.Authorization=`Bearer ${token}`;if(options.body&&!(options.body instanceof FormData))headers['Content-Type']='application/json';const res=await fetch(path,{...options,headers});const text=await res.text();const data=text?JSON.parse(text):{};if(res.status===401){Auth.logout();return}if(res.status===403&&data.erro==='pendencia_financeira'){this.financeBanner(data.mensagem);throw new Error(data.mensagem||'Pendencia financeira')}if(!res.ok)throw new Error(data.mensagem||data.erro||`Erro HTTP ${res.status}`);return data},get(path){return this.request(path)},post(path,body){return this.request(path,{method:'POST',body:body instanceof FormData?body:JSON.stringify(body||{})})},put(path,body){return this.request(path,{method:'PUT',body:JSON.stringify(body||{})})},financeBanner(message){if(document.querySelector('.finance-banner'))return;const banner=document.createElement('div');banner.className='finance-banner';banner.innerHTML=`${message||'Seu acesso esta bloqueado por pendencia de pagamento.'} <a href="/cliente/financeiro.html">Regularizar em Financeiro</a>`;document.body.appendChild(banner)}};
|
||||
@@ -0,0 +1 @@
|
||||
window.Auth={claims(){const t=this.token();if(!t)return{};try{return JSON.parse(atob(t.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')))}catch(_){return{}}},isAdminEmpresa(){return this.claims().cargo==='admin_empresa'},session(){const scope=localStorage.getItem('auth_scope');const token=scope==='master'?localStorage.getItem('master_token'):localStorage.getItem('funcionario_token');return token?{scope,token}:null},token(){const s=this.session();return s?s.token:null},save(scope,token,label){localStorage.setItem('auth_scope',scope);localStorage.setItem(scope==='master'?'master_token':'funcionario_token',token);localStorage.setItem('auth_label',label||(scope==='master'?'Master':'Escritorio'))},logout(){localStorage.removeItem('auth_scope');localStorage.removeItem('master_token');localStorage.removeItem('funcionario_token');localStorage.removeItem('auth_label');location.href='/login.html'},require(scope){const s=this.session();if(!s||(scope&&s.scope!==scope))location.href='/login.html';return s}};
|
||||
@@ -0,0 +1 @@
|
||||
window.Layout={mount(scope,active){Auth.require(scope);const label=localStorage.getItem('auth_label')||(scope==='master'?'Master':'Escritorio');const master=[['Clientes','/master/clientes.html','clientes'],['Planos','/master/planos.html','planos'],['Financeiro','/master/financeiro.html','financeiro'],['Configuracoes','/master/configuracoes.html','configuracoes']];let cliente=[['Cadastro','/cliente/condominios.html','condominios'],['Lembrete','/cliente/lembretes.html','lembretes'],['Financeiro','/cliente/financeiro.html','financeiro'],['WhatsApp','/cliente/configuracoes.html','configuracoes']];if(scope==='empresa'&&!Auth.isAdminEmpresa())cliente=cliente.filter(x=>x[2]!=='configuracoes');const menu=(scope==='master'?master:cliente).map(([n,h,k])=>`<a class="${k===active?'active':''}" href="${h}">${n}</a>`).join('');document.body.insertAdjacentHTML('afterbegin',`<aside class="sidebar"><div><div class="brand">SeConectai</div><div class="user">${label}</div></div><nav class="menu">${menu}</nav><button class="btn" onclick="Auth.logout()">Sair</button></aside>`);document.body.classList.add('app');document.querySelector('main')?.classList.add('content')}};
|
||||
@@ -0,0 +1 @@
|
||||
window.Util={money(cents){return(Number(cents||0)/100).toLocaleString('pt-BR',{style:'currency',currency:'BRL'})},date(value){return value?new Date(value).toLocaleString('pt-BR'):'-'},toast(message,error){const el=document.createElement('div');el.className='toast'+(error?' error':'');el.textContent=message;document.body.appendChild(el);setTimeout(()=>el.remove(),3600)},formData(form){return Object.fromEntries(new FormData(form).entries())},badge(status){const s=status||'-';const klass=['ativa','ativo','pago','enviado','aprovada'].includes(s)?'ok':(['pendente','bloqueada','falhou'].includes(s)?'warn':'');return `<span class="badge ${klass}">${s}</span>`}};
|
||||
@@ -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>Login - SeConectai</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script></head><body class="login-page"><main class="login-box"><h1>SeConectai</h1><p class="user">Acesse o painel administrativo ou o escritorio.</p><div class="tabs"><button id="tab-master" class="active" type="button">Master</button><button id="tab-cliente" type="button">Escritorio</button></div><form id="master-form"><div class="field"><label>Senha master</label><input name="senha" type="password" required></div><button class="btn" type="submit">Entrar</button></form><form id="cliente-form" class="hidden"><div class="field"><label>E-mail</label><input name="email" type="email" required></div><div class="field"><label>Senha</label><input name="senha" type="password" required></div><button class="btn" type="submit">Entrar</button></form></main><script>const masterTab=document.querySelector('#tab-master'),clienteTab=document.querySelector('#tab-cliente'),masterForm=document.querySelector('#master-form'),clienteForm=document.querySelector('#cliente-form');function tab(scope){masterTab.classList.toggle('active',scope==='master');clienteTab.classList.toggle('active',scope==='cliente');masterForm.classList.toggle('hidden',scope!=='master');clienteForm.classList.toggle('hidden',scope!=='cliente')}masterTab.onclick=()=>tab('master');clienteTab.onclick=()=>tab('cliente');async function login(url,body,scope,label){try{const res=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});const data=await res.json();if(!res.ok)throw new Error(data.mensagem||data.erro||'Falha no login');Auth.save(scope,data.token,label);location.href=scope==='master'?'/master/clientes.html':'/cliente/condominios.html'}catch(e){Util.toast(e.message,true)}}masterForm.onsubmit=e=>{e.preventDefault();login('/api/master/login',Util.formData(masterForm),'master','Master')};clienteForm.onsubmit=e=>{e.preventDefault();const b=Util.formData(clienteForm);login('/api/auth/login',b,'empresa',b.email)};</script></body></html>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user