🌴 Tropical Clube

Sistema de gestão
🌴 Tropical Clube

Painel administrativo

Primeira versão conectada ao banco na nuvem.

🚪 PortariaEntrada, visitantes e sócios
Abrir Portaria
👥 SóciosCadastro e mensalidades
🍔 Bar / PDVProdutos, pedidos e caixa
👨‍🍳 CozinhaFila e preparo dos pedidos
🧑‍🍳 GarçonsMesas, pedidos e entregas
📦 EstoqueEntradas, saídas e alertas
🔥 Churrasqueiras4 unidades e reservas
🪑 LocaçõesMesas, cadeiras e entregas
🎉 EventosIngressos e consumo
👥 EquipeUsuários, funções e acesso
💰 FinanceiroCaixa, pagamentos e relatórios
⚙️ ConfiguraçõesPagamentos e preferências do clube
👤 UsuáriosEquipe e permissões
`); w.document.close(); w.focus(); setTimeout(()=>w.print(),700); } async function obterQrPng(){ const box=document.getElementById('qrIngresso'); const canvas=box?.querySelector('canvas'); if(canvas)return await new Promise(resolve=>canvas.toBlob(b=>resolve(b),'image/png')); const img=box?.querySelector('img'); if(img?.src){ const res=await fetch(img.src);return await res.blob(); } return null; } async function enviarIngressoWhatsApp(){ const numero=String(ingressoWhatsAppAtual||'').replace(/\D/g,''); const texto=`🎟️ Ingresso Tropical Clube\n\nEvento: ${ingressoEventoAtual}\nComprador: ${ingressoNomeAtual}\nValor: R$ ${Number(ingressoPrecoAtual||0).toFixed(2).replace('.',',')}\nPagamento: ${String(ingressoPagamentoAtual||'').toUpperCase()}\n\nCódigo do ingresso: ${ingressoCodigoAtual}\n\nApresente o QR Code na portaria. Este ingresso só pode ser utilizado uma vez.`; try{ const blob=await obterQrPng(); if(blob && navigator.share && navigator.canShare){ const file=new File([blob],'ingresso-tropical-clube.png',{type:'image/png'}); if(navigator.canShare({files:[file]})){ await navigator.share({title:'Ingresso Tropical Clube',text:texto,files:[file]}); return; } } }catch(e){if(e?.name==='AbortError')return;console.warn('Compartilhamento de imagem indisponível',e);} if(numero){ const url='https://wa.me/'+numero+'?text='+encodeURIComponent(texto); window.open(url,'_blank'); alert('O WhatsApp foi aberto com a mensagem. Neste modo o QR precisa ser anexado manualmente. Em uma próxima etapa podemos automatizar o envio da imagem pelo WhatsApp Business.'); }else{ const url='https://wa.me/?text='+encodeURIComponent(texto); window.open(url,'_blank'); alert('WhatsApp do comprador não foi informado. Selecione o contato do comprador e envie o ingresso.'); } } let ingressoVendaPendente=null; let ingressoClienteSelecionado=null; let ingressoBuscaTimer=null; function fecharClienteIngresso(){document.getElementById('clienteIngressoModal').style.display='none';ingressoVendaPendente=null;} function abrirClienteIngresso(opts){ingressoVendaPendente=opts;ingressoClienteSelecionado=null;const b=document.getElementById('ingressoClienteBusca');b.value='';document.getElementById('ingressoClienteResultados').innerHTML='
Digite pelo menos 2 caracteres para buscar.
';document.getElementById('clienteIngressoModal').style.display='flex';setTimeout(()=>b.focus(),100);} function escHtml(v){return String(v??'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));} function buscarClientesIngresso(){clearTimeout(ingressoBuscaTimer);ingressoBuscaTimer=setTimeout(async()=>{const q=(document.getElementById('ingressoClienteBusca').value||'').trim();const box=document.getElementById('ingressoClienteResultados');if(q.length<2){box.innerHTML='
Digite pelo menos 2 caracteres para buscar.
';return;}const digits=q.replace(/\D/g,'');let query=client.from('customers').select('id,full_name,cpf,whatsapp,birth_date').order('full_name').limit(15);if(digits.length>=5) query=query.or(`cpf.ilike.%${digits}%,whatsapp.ilike.%${digits}%`);else query=query.ilike('full_name','%'+q+'%');const {data,error}=await query;if(error){box.innerHTML='
Erro ao buscar cliente.
';return;}const rows=data||[];box.innerHTML=rows.length?rows.map(c=>``).join(''):'
Cliente não encontrado.
';},300);} async function selecionarClienteIngresso(id){const {data,error}=await client.from('customers').select('id,full_name,cpf,whatsapp,birth_date').eq('id',id).single();if(error||!data){alert('Não foi possível carregar o cliente.');return;}ingressoClienteSelecionado=data;fecharClienteIngresso();await concluirVendaIngresso(ingressoVendaPendente,data);} async function novoClienteIngresso(){const nome=prompt('Nome completo do cliente:','');if(!nome?.trim())return;const cpf=(prompt('CPF (obrigatório):','')||'').replace(/\D/g,'');if(cpf.length!==11){alert('CPF deve ter 11 números.');return;}const whatsapp=(prompt('WhatsApp (opcional):','')||'').replace(/\D/g,'');const r=await client.from('customers').select('id,full_name,cpf,whatsapp').eq('cpf',cpf).maybeSingle();if(r.data){alert('CPF já cadastrado. O cliente existente será selecionado.');selecionarClienteIngresso(r.data.id);return;}const n=await client.from('customers').insert({full_name:nome.trim(),cpf,whatsapp:whatsapp||null,customer_type:'evento'}).select('id,full_name,cpf,whatsapp').single();if(n.error){alert('Erro ao cadastrar cliente: '+n.error.message);return;}fecharClienteIngresso();await concluirVendaIngresso(ingressoVendaPendente,n.data);} async function concluirVendaIngresso(opts,cliente){const {eventId,eventName,lotId,lotName,lotPrice,metodo}=opts;const nome=cliente.full_name.trim(),cpf=(cliente.cpf||'').replace(/\D/g,'');const whatsapp=(cliente.whatsapp||'').replace(/\D/g,'');try{const u=await client.from('customers').update({customer_type:'evento'}).eq('id',cliente.id);if(u.error)throw u.error;if(metodo==='pix'){await carregarConfiguracoes();if(configuracoesClube.pix_mode==='manual'){await abrirPixManualIngresso({eventId,eventName,lotId,lotName,lotPrice,customer:cliente,title:'Ingresso '+eventName+' • '+lotName,amount:Number(lotPrice)});return;}}if(metodo==='pix'||metodo==='credito'||metodo==='debito'){const authUser=(await client.auth.getUser()).data.user;const email=(authUser?.email||pdvUser?.email||document.getElementById('email')?.value||'tropicalclubedeaguaquente@gmail.com').trim();await iniciarPagamentoReal({action:metodo==='pix'?'pix':'card',payment_method:metodo,amount:Number(lotPrice),title:'Ingresso '+eventName+' • '+lotName,context_type:'ingresso',context_id:lotId,customer_id:cliente.id,customer_name:nome,cpf,whatsapp:whatsapp||null,email,metadata:{event_id:eventId,lot_id:lotId,event_name:eventName,lot_name:lotName}});return;}const {data,error}=await client.rpc('sell_advance_ticket',{p_event_id:eventId,p_lot_id:lotId,p_customer_id:cliente.id,p_customer_name:nome,p_cpf:cpf,p_payment_method:metodo});if(error)throw error;await mostrarIngressoVendido(data,eventName,lotName,nome,cpf,whatsapp,metodo,Number(data.price||lotPrice));}catch(e){console.error(e);alert('Não foi possível vender o ingresso: '+(e.message||e));}} async function venderIngressoAntecipado(eventId,eventName,lotId,lotName,lotPrice){const pagamento=(prompt('Forma de pagamento: PIX, Dinheiro, Débito ou Crédito','PIX')||'PIX').trim().toLowerCase();const mapa={pix:'pix',dinheiro:'dinheiro',debito:'debito','débito':'debito',credito:'credito','crédito':'credito'};const metodo=mapa[pagamento];if(!metodo){alert('Forma de pagamento inválida.');return;}abrirClienteIngresso({eventId,eventName,lotId,lotName,lotPrice,metodo});} async function mostrarIngressoVendido(data,eventName,lotName,nome,cpf,whatsapp,metodo,precoIngresso){ const qr=data?.qr_code;if(!qr)throw new Error('Venda criada, mas o código QR não foi gerado.'); ingressoWhatsAppAtual=whatsapp||'';ingressoNomeAtual=nome;ingressoEventoAtual=eventName+' • '+lotName;ingressoCodigoAtual=qr;ingressoPrecoAtual=precoIngresso;ingressoPagamentoAtual=metodo; document.getElementById('qrEventoNome').textContent=eventName+' • '+lotName; document.getElementById('qrClienteNome').textContent=nome+' • CPF '+cpf+(whatsapp?' • WhatsApp '+whatsapp:''); document.getElementById('qrCodigo').textContent=qr; document.getElementById('qrInfoIngresso').innerHTML='Valor: R$ '+precoIngresso.toFixed(2).replace('.',',')+' • Pagamento: '+metodo.toUpperCase()+'
Pagamento confirmado. Apresente este QR Code na portaria. O mesmo ingresso não poderá ser utilizado duas vezes.'; const q=document.getElementById('qrIngresso');q.innerHTML='';new QRCode(q,{text:qr,width:240,height:240,correctLevel:QRCode.CorrectLevel.M}); document.getElementById('qrIngressoModal').style.display='flex'; await carregarEventos(); }