/* ============================================================ APP ============================================================ */ function App(){ const [loggedIn,setLoggedIn] = useState(false); const [authChecked,setAuthChecked] = useState(false); const [authUser,setAuthUser] = useState(null); const [role,setRole] = useState('empresa'); const [page,setPage] = useState('financeiro'); // Lembra a última tela vista nesta aba — se o navegador descartar a aba em // segundo plano e recarregar ao voltar, a pessoa não cai mais na tela inicial. useEffect(()=>{ if(page) sessionStorage.setItem('nexus_last_page', page); }, [page]); const [expandedGroups,setExpandedGroups] = useState({financeiro:true, clientes:true}); const [clientName,setClientName] = useState(''); const [branding,setBranding] = useState({ name:'Souza Tecnologia', initial:'S', primary:'#FF6A2B', secondary:'#1E1F24' }); const [clientes,setClientes] = useState([]); const [servicos,setServicos] = useState([]); const [clienteEmail,setClienteEmail] = useState(''); const [equipe,setEquipe] = useState([]); const [currentStaffId,setCurrentStaffId] = useState(null); const [currentClienteId,setCurrentClienteId] = useState(null); const [tickets,setTickets] = useState([]); const [caixa,setCaixa] = useState([]); const [orcamentos,setOrcamentos] = useState([]); const [gastos,setGastos] = useState([]); const [terceirizados,setTerceirizados] = useState([]); const [notifs,setNotifs] = useState([]); const [auditLog,setAuditLog] = useState([]); const [notifOpen,setNotifOpen] = useState(false); const [modalTicketId,setModalTicketId] = useState(null); const [toastMsg,setToastMsg] = useState(null); const toastTimer = useRef(null); const [personalizacaoSelectedId,setPersonalizacaoSelectedId] = useState(null); const [pendingAccount,setPendingAccount] = useState(null); // ---------- Autenticação real (Supabase Auth) ---------- // Restaura a sessão ao carregar a página e escuta mudanças de login/logout // (inclusive se o usuário sair em outra aba, ou o token expirar). useEffect(()=>{ supabaseClient.auth.getSession().then(async ({ data })=>{ await handleAuthChange(data.session); setAuthChecked(true); }); const { data: listener } = supabaseClient.auth.onAuthStateChange((event, session)=>{ handleAuthChange(session, event); }); return ()=>{ listener.subscription.unsubscribe(); }; },[]); async function handleAuthChange(session, event){ if(!session){ setAuthUser(null); setLoggedIn(false); return; } // O Supabase renova o token automaticamente ao voltar pra aba, o que dispara // este mesmo listener — sem essa checagem, isso resetava a pessoa pra tela // inicial toda vez que ela trocava de aba e voltava. Numa renovação de token, // só atualizamos a sessão e não mexemos em página/estado nenhum. if(event==='TOKEN_REFRESHED'){ setAuthUser(session.user); return; } setAuthUser(session.user); const { data: profile, error: profileErr } = await supabaseClient .from('profiles').select('*').eq('id', session.user.id).single(); if(profileErr || !profile){ showToast('Não encontrei um perfil para este usuário. Peça para a empresa te cadastrar.'); await supabaseClient.auth.signOut(); return; } if(profile.must_change_password){ setPendingAccount({ id: session.user.id, nome: profile.nome }); return; } if(profile.role === 'empresa'){ setRole('empresa'); setCurrentStaffId(session.user.id); setLoggedIn(true); setPage(sessionStorage.getItem('nexus_last_page') || 'financeiro'); } else { const { data: cliente } = await supabaseClient .from('clientes').select('*').eq('auth_user_id', session.user.id).single(); if(cliente){ setClientName(cliente.nome); setCurrentClienteId(cliente.id); setClienteEmail(cliente.email); setBranding({ name: cliente.empresa, initial: cliente.sigla, primary: cliente.cor_primaria, secondary: cliente.cor_secundaria }); } setRole('cliente'); setLoggedIn(true); setPage(sessionStorage.getItem('nexus_last_page') || 'inicio'); } } async function attemptLogin(email, senha){ const { error } = await supabaseClient.auth.signInWithPassword({ email, password: senha }); if(error){ return { error: 'E-mail ou senha inválidos.' }; } return {}; } async function confirmNewPassword(newPass){ const { error } = await supabaseClient.auth.updateUser({ password: newPass }); if(error){ return { error: traduzErroSenha(error.message) }; } const { error: profileUpdateErr } = await supabaseClient.from('profiles').update({ must_change_password: false }).eq('id', pendingAccount.id); if(profileUpdateErr){ return { error: 'Senha alterada, mas houve um erro ao liberar o acesso: ' + profileUpdateErr.message }; } const { data } = await supabaseClient.auth.getSession(); setPendingAccount(null); await handleAuthChange(data.session); showToast('Senha definida com sucesso!'); return {}; } async function logout(){ await supabaseClient.auth.signOut(); setLoggedIn(false); setAuthUser(null); setNotifOpen(false); sessionStorage.removeItem('nexus_last_page'); } // ---------- Clientes / Equipe / Chamados (dados reais do Supabase) ---------- useEffect(()=>{ if(!loggedIn || role!=='empresa') return; loadClientes(); loadTickets(); loadCaixa(); loadServicos(); loadOrcamentos(); loadGastos(); loadTerceirizados(); loadEquipe().then(eq=>loadAuditLog(eq)); }, [loggedIn, role]); useEffect(()=>{ if(!loggedIn || !authUser) return; loadNotifs(); }, [loggedIn, authUser && authUser.id]); useEffect(()=>{ if(!loggedIn || role!=='cliente' || !currentClienteId) return; loadTickets(); }, [loggedIn, role, currentClienteId]); // ---------- Tempo real (Supabase Realtime) ---------- // Assim que algo muda no banco (novo chamado, resposta, notificação, pagamento), // o app atualiza sozinho — sem precisar recarregar a página. useEffect(()=>{ if(!loggedIn || !authUser) return; const channel = supabaseClient .channel('nexus-realtime-' + authUser.id) .on('postgres_changes', { event:'*', schema:'public', table:'tickets' }, ()=>{ loadTickets(); }) .on('postgres_changes', { event:'*', schema:'public', table:'ticket_historico' }, ()=>{ loadTickets(); }) .on('postgres_changes', { event:'*', schema:'public', table:'notificacoes', filter:`user_id=eq.${authUser.id}` }, ()=>{ loadNotifs(); }) .on('postgres_changes', { event:'*', schema:'public', table:'caixa_lancamentos' }, ()=>{ if(role==='empresa') loadCaixa(); }) .subscribe(); return ()=>{ supabaseClient.removeChannel(channel); }; }, [loggedIn, authUser && authUser.id, role]); async function loadClientes(){ const { data, error } = await supabaseClient.from('clientes').select('*').order('created_at', {ascending:true}); if(error){ showToast('Erro ao carregar clientes: ' + error.message); return; } const lista = data.map(clienteFromRow); setClientes(lista); if(lista.length>0 && !personalizacaoSelectedId) setPersonalizacaoSelectedId(lista[0].id); } async function loadEquipe(){ const { data, error } = await supabaseClient.from('equipe').select('*').eq('ativo', true).order('nome'); if(error){ showToast('Erro ao carregar equipe: ' + error.message); return []; } const lista = data.map(r=>({ id: r.id, nome: r.nome, cargo: r.cargo, avatar: r.nome.split(' ').filter(Boolean).slice(0,2).map(w=>w[0]).join('').toUpperCase() })); setEquipe(lista); return lista; } async function loadTickets(){ const { data, error } = await supabaseClient .from('tickets') .select('*, clientes(nome), ticket_historico(id, texto, created_at)') .order('criado_em', {ascending:false}); if(error){ showToast('Erro ao carregar chamados: ' + error.message); return; } setTickets(data.map(ticketFromRow)); } async function loadCaixa(){ const { data, error } = await supabaseClient .from('caixa_lancamentos') .select('*, clientes(nome)') .order('vencimento', {ascending:false}); if(error){ showToast('Erro ao carregar caixa: ' + error.message); return; } setCaixa(data.map(caixaFromRow)); } async function loadServicos(){ const { data, error } = await supabaseClient.from('servicos').select('*').order('nome'); if(error){ showToast('Erro ao carregar serviços: ' + error.message); return; } setServicos(data.map(servicoFromRow)); } async function loadOrcamentos(){ const { data, error } = await supabaseClient .from('orcamentos') .select('*, clientes(nome)') .order('created_at', {ascending:false}); if(error){ showToast('Erro ao carregar orçamentos: ' + error.message); return; } setOrcamentos(data.map(orcamentoFromRow)); } async function loadGastos(){ const { data, error } = await supabaseClient.from('gastos').select('*').order('vencimento', {ascending:false}); if(error){ showToast('Erro ao carregar gastos: ' + error.message); return; } setGastos(data.map(gastoFromRow)); } async function loadTerceirizados(){ const { data, error } = await supabaseClient.from('terceirizados').select('*').order('parceiro'); if(error){ showToast('Erro ao carregar terceirizados: ' + error.message); return; } setTerceirizados(data.map(terceirizadoFromRow)); } async function loadAuditLog(equipeList){ const lista = equipeList || equipe; const equipeById = {}; lista.forEach(e=>{ equipeById[e.id] = e; }); const { data, error } = await supabaseClient.from('audit_log').select('*').order('criado_em', {ascending:false}).limit(500); if(error){ showToast('Erro ao carregar auditoria: ' + error.message); return; } setAuditLog(data.map(r=>auditFromRow(r, equipeById))); } async function loadNotifs(){ const { data, error } = await supabaseClient.from('notificacoes').select('*').eq('user_id', authUser.id).order('created_at', {ascending:false}).limit(50); if(error){ showToast('Erro ao carregar notificações: ' + error.message); return; } setNotifs(data.map(notificacaoFromRow)); } // ---------- (bloco de persistência local removido — não há mais dados fictícios // sobrando no navegador; tudo relevante já vem do Supabase) ---------- useEffect(()=>{ function handler(e){ if(!e.target.closest('.bell-wrap')) setNotifOpen(false); } document.addEventListener('click', handler); return ()=>document.removeEventListener('click', handler); },[]); function showToast(msg){ setToastMsg(msg); clearTimeout(toastTimer.current); toastTimer.current = setTimeout(()=>setToastMsg(null), 3200); } function currentStaffName(){ const s = equipe.find(e=>e.id===currentStaffId); return s ? s.nome : 'Equipe'; } async function logAudit(acao, detalhe){ if(!authUser) return; const { error } = await supabaseClient.from('audit_log').insert({ usuario_id: authUser.id, acao, detalhe }); if(!error) loadAuditLog(); } function toggleGroup(id){ setExpandedGroups(prev=>({...prev,[id]:!prev[id]})); setPage(id); } function goPage(id){ setPage(id); if(id.startsWith('fin-')) setExpandedGroups(prev=>({...prev, financeiro:true})); if(id==='personalizacao'||id==='cli-acessos') setExpandedGroups(prev=>({...prev, clientes:true})); } async function handleBellClick(){ const opening = !notifOpen; setNotifOpen(opening); if(opening && authUser){ const temNaoLida = notifs.some(n=>!n.lido); if(temNaoLida){ setNotifs(prev=>prev.map(n=>({...n,lido:true}))); await supabaseClient.from('notificacoes').update({ lida:true }).eq('user_id', authUser.id).eq('lida', false); } } } async function notifyCliente(clienteAuthUserId, text){ if(!clienteAuthUserId) return; await supabaseClient.from('notificacoes').insert({ user_id: clienteAuthUserId, texto: text }); } async function notifyEmpresa(text){ await supabaseClient.rpc('notify_empresa', { texto: text }); } // ---------- Chamados ---------- async function updateTicketStatus(id, status){ const t = tickets.find(x=>x.id===id); if(!t) return; const patch = { status }; if(!t.primeiraRespostaEm) patch.primeira_resposta_em = new Date().toISOString(); if(status==='encerrado' && !t.encerradoEm) patch.encerrado_em = new Date().toISOString(); if(status!=='encerrado') patch.encerrado_em = null; const { error } = await supabaseClient.from('tickets').update(patch).eq('id', id); if(error){ showToast('Erro ao atualizar status: ' + error.message); return; } await loadTickets(); const cli = clientes.find(c=>c.id===t.clienteId); notifyCliente(cli?.authUserId, `Atualização no chamado #${t.numero}: status alterado para "${STATUS_LABELS[status]}"`); logAudit('Alterou status de chamado', `#${t.numero} → ${STATUS_LABELS[status]}`); showToast('Status do chamado #' + t.numero + ' atualizado'); } async function sendInfo(id, msg){ const t = tickets.find(x=>x.id===id); if(!t) return; const { error: histErr } = await supabaseClient.from('ticket_historico').insert({ ticket_id: id, texto: `Equipe solicitou: "${msg}"` }); if(histErr){ showToast('Erro ao enviar solicitação: ' + histErr.message); return; } const patch = { status:'aguardando_retorno' }; if(!t.primeiraRespostaEm) patch.primeira_resposta_em = new Date().toISOString(); const { error } = await supabaseClient.from('tickets').update(patch).eq('id', id); if(error){ showToast('Erro ao atualizar chamado: ' + error.message); return; } await loadTickets(); const cli = clientes.find(c=>c.id===t.clienteId); notifyCliente(cli?.authUserId, `O suporte pediu mais informações no chamado #${t.numero}`); showToast('Solicitação enviada ao cliente'); setModalTicketId(null); } async function addNotaInterna(id, texto){ const { error } = await supabaseClient.from('ticket_historico').insert({ ticket_id: id, texto, interno: true }); if(error){ showToast('Erro ao salvar nota interna: ' + error.message); return; } await loadTickets(); showToast('Nota interna adicionada.'); } async function assignResponsavel(id, responsavelId){ const { error } = await supabaseClient.from('tickets').update({ responsavel_id: responsavelId || null }).eq('id', id); if(error){ showToast('Erro ao atribuir responsável: ' + error.message); return; } await loadTickets(); const nome = equipe.find(e=>e.id===responsavelId); showToast('Chamado atribuído a ' + (nome?nome.nome:'—')); } async function submitNovaSolicitacao(data){ if(!currentClienteId){ showToast('Não foi possível identificar seu cadastro de cliente.'); return false; } const payload = { cliente_id: currentClienteId, titulo: data.assunto, status: 'em_atendimento', prioridade: data.prioridade, categoria: data.categoria, descricao: data.descricao, }; const { error } = await supabaseClient.from('tickets').insert(payload); if(error){ showToast('Erro ao enviar solicitação: ' + error.message); return false; } await loadTickets(); notifyEmpresa(`Novo chamado aberto por ${clientName} — "${data.assunto}"`); showToast('Solicitação enviada com sucesso!'); setPage('chamados'); return true; } async function avaliarChamado(id, nota){ const t = tickets.find(x=>x.id===id); const { error } = await supabaseClient.from('tickets').update({ avaliacao: nota }).eq('id', id); if(error){ showToast('Erro ao enviar avaliação: ' + error.message); return; } await loadTickets(); notifyEmpresa(`${clientName} avaliou o chamado #${t?t.numero:''} com nota ${nota}`); showToast('Obrigado pela avaliação!'); } // ---------- Clientes ---------- async function saveClient(data, editingId){ if(editingId){ const antes = clientes.find(c=>c.id===editingId); const { data: row, error } = await supabaseClient .from('clientes').update(clienteToRow(data)).eq('id', editingId).select().single(); if(error){ showToast('Erro ao salvar cliente: ' + error.message); return; } setClientes(prev=>prev.map(c=>c.id===editingId?clienteFromRow(row):c)); logAudit('Editou cliente', `${data.nome} — valor mensal ${fmtBRL(antes?antes.valorMensal:0)} → ${fmtBRL(data.valorMensal)}`); showToast('Cliente atualizado com sucesso!'); } else { const color = PALETTE[clientes.length % PALETTE.length]; const payload = clienteToRow({ ...data, empresa:data.nome, primary:color, secondary:'#1E1F24', initial:data.nome.charAt(0).toUpperCase() }); const { data: row, error } = await supabaseClient.from('clientes').insert(payload).select().single(); if(error){ showToast('Erro ao cadastrar cliente: ' + error.message); return; } setClientes(prev=>[...prev, clienteFromRow(row)]); logAudit('Cadastrou cliente', data.nome); showToast('Cliente cadastrado com sucesso!'); } } async function deleteClient(id){ if(!window.confirm('Excluir este cliente? Essa ação não pode ser desfeita.')) return; const c = clientes.find(x=>x.id===id); const { error } = await supabaseClient.from('clientes').delete().eq('id', id); if(error){ showToast('Erro ao excluir cliente: ' + error.message); return; } setClientes(prev=>prev.filter(c=>c.id!==id)); logAudit('Excluiu cliente', c?c.nome:id); showToast('Cliente excluído.'); } async function savePersonalizacao(id, patch){ const { data: row, error } = await supabaseClient .from('clientes').update(clienteToRow(patch)).eq('id', id).select().single(); if(error){ showToast('Erro ao salvar personalização: ' + error.message); return; } const atualizado = clienteFromRow(row); setClientes(prev=>prev.map(c=>c.id===id?atualizado:c)); if(atualizado.nome===clientName){ setBranding({ name:atualizado.empresa, initial:atualizado.initial, primary:atualizado.primary, secondary:atualizado.secondary }); } showToast('Personalização salva — o portal de ' + atualizado.nome + ' foi atualizado'); } // ---------- Acessos de Clientes (via Edge Function — precisa da service_role no servidor) ---------- async function criarAcessoCliente(clienteId, nome, email, senha){ const { data, error } = await supabaseClient.functions.invoke('manage-cliente-access', { body: { action:'create', clienteId, nome, email, senha } }); if(error || (data && data.error)){ showToast('Erro ao criar acesso: ' + (data?.error || error.message)); return; } await loadClientes(); logAudit('Criou acesso de cliente', `${nome} — ${email}`); showToast('Acesso criado! O cliente vai definir uma nova senha no primeiro login.'); } async function resetarSenhaCliente(authUserId, nome, senha){ const { data, error } = await supabaseClient.functions.invoke('manage-cliente-access', { body: { action:'reset_password', authUserId, senha } }); if(error || (data && data.error)){ showToast('Erro ao redefinir senha: ' + (data?.error || error.message)); return; } logAudit('Redefiniu senha de cliente', nome); showToast('Senha redefinida! O cliente vai definir uma nova senha no próximo login.'); } async function revogarAcessoCliente(authUserId, clienteId, nome){ if(!window.confirm('Remover o acesso deste cliente? Ele não vai mais conseguir fazer login.')) return; const { data, error } = await supabaseClient.functions.invoke('manage-cliente-access', { body: { action:'revoke', authUserId, clienteId } }); if(error || (data && data.error)){ showToast('Erro ao remover acesso: ' + (data?.error || error.message)); return; } await loadClientes(); logAudit('Removeu acesso de cliente', nome); showToast('Acesso removido.'); } // ---------- Caixa ---------- async function uploadAnexoCaixa(caixaId, tipo, file){ const path = `caixa/${caixaId}/${tipo}-${Date.now()}-${file.name}`; const { error } = await supabaseClient.storage.from('anexos').upload(path, file, { upsert:true }); if(error){ showToast(`Erro ao enviar ${tipo}: ` + error.message); return null; } return path; } async function baixarAnexoCaixa(path){ if(!path) return; const { data, error } = await supabaseClient.storage.from('anexos').createSignedUrl(path, 60); if(error){ showToast('Erro ao abrir anexo: ' + error.message); return; } window.open(data.signedUrl, '_blank'); } async function saveCaixa(data, editingId){ const cli = clientes.find(c=>c.id===data.clienteId); if(!cli){ showToast('Cliente não encontrado.'); return; } const { boletoFile, notaFile, anexosAtuais, ...campos } = data; if(editingId){ const anexos = { boleto: anexosAtuais?.boleto || null, nota: anexosAtuais?.nota || null }; if(boletoFile){ const p = await uploadAnexoCaixa(editingId, 'boleto', boletoFile); if(p) anexos.boleto = p; } if(notaFile){ const p = await uploadAnexoCaixa(editingId, 'nota', notaFile); if(p) anexos.nota = p; } const { error } = await supabaseClient.from('caixa_lancamentos').update(caixaToRow({ ...campos, anexos })).eq('id', editingId); if(error){ showToast('Erro ao salvar lançamento: ' + error.message); return; } await loadCaixa(); logAudit('Editou lançamento de caixa', `${cli.nome} — ${fmtBRL(campos.valor)} (venc. ${campos.vencimento})`); showToast('Lançamento atualizado com sucesso!'); } else { const { data: row, error } = await supabaseClient.from('caixa_lancamentos').insert(caixaToRow({ ...campos, anexos:{boleto:null,nota:null} })).select().single(); if(error){ showToast('Erro ao adicionar lançamento: ' + error.message); return; } const anexos = { boleto:null, nota:null }; if(boletoFile){ const p = await uploadAnexoCaixa(row.id, 'boleto', boletoFile); if(p) anexos.boleto = p; } if(notaFile){ const p = await uploadAnexoCaixa(row.id, 'nota', notaFile); if(p) anexos.nota = p; } if(anexos.boleto || anexos.nota){ await supabaseClient.from('caixa_lancamentos').update(caixaToRow({ anexos })).eq('id', row.id); } await loadCaixa(); const ref = mesReferencia(campos.vencimento); logAudit('Criou lançamento de caixa', `${cli.nome} — ${fmtBRL(campos.valor)} (venc. ${campos.vencimento})`); showToast('Lançamento adicionado — somado ao total de "' + ref.label + '"'); } } async function deleteCaixa(id){ if(!window.confirm('Excluir este lançamento?')) return; const e = caixa.find(x=>x.id===id); const { error } = await supabaseClient.from('caixa_lancamentos').delete().eq('id', id); if(error){ showToast('Erro ao excluir lançamento: ' + error.message); return; } setCaixa(prev=>prev.filter(x=>x.id!==id)); logAudit('Excluiu lançamento de caixa', e ? `${e.cliente} — ${fmtBRL(e.valor)}` : id); showToast('Lançamento excluído.'); } async function marcarPago(id, dataPagamento){ const dataFinal = dataPagamento || new Date().toISOString().slice(0,10); const { error } = await supabaseClient.from('caixa_lancamentos').update({ status:'pago', data_pagamento: dataFinal }).eq('id', id); if(error){ showToast('Erro ao marcar como pago: ' + error.message); return; } await loadCaixa(); const e = caixa.find(x=>x.id===id); logAudit('Marcou lançamento como pago', e ? `${e.cliente} — ${fmtBRL(e.valor)}` : id); showToast('Lançamento marcado como pago!'); } // Gera lançamentos futuros com base na periodicidade de cada cliente (mensal/ // trimestral/anual), evitando duplicar um mesmo período. Cobre os próximos 12 meses. async function gerarCobrancasFuturas(){ const hoje = new Date(); const novos = []; clientes.forEach(c=>{ const passo = PERIODICIDADE_MESES[c.periodicidade] || 1; for(let i=0; i<12; i+=passo){ const d = new Date(hoje.getFullYear(), hoje.getMonth()+i, Math.min(10,28)); const venc = d.toISOString().slice(0,10); const jaExiste = caixa.some(e=>e.clienteId===c.id && e.vencimento.slice(0,7)===venc.slice(0,7)); if(!jaExiste){ novos.push({ cliente_id:c.id, valor:c.valorMensal, vencimento:venc, status:'pendente', data_pagamento:null }); } } }); if(novos.length===0){ showToast('Nenhuma cobrança nova — já estava tudo gerado.'); return; } const { error } = await supabaseClient.from('caixa_lancamentos').insert(novos); if(error){ showToast('Erro ao gerar cobranças: ' + error.message); return; } await loadCaixa(); logAudit('Gerou cobranças futuras', `${novos.length} lançamento(s) criado(s) para os próximos 12 meses`); showToast(`${novos.length} cobrança(s) futura(s) gerada(s)!`); } // ---------- Orçamentos ---------- async function addOrcamento(data){ const cli = clientes.find(c=>c.id===data.clienteId); if(!cli){ showToast('Cliente não encontrado.'); return; } const srv = servicos.find(s=>s.nome===data.item); const payload = orcamentoToRow({ clienteId: cli.id, servicoId: srv?srv.id:null, item:data.item, valor:data.valor, comissao:data.comissao, status:'Em análise' }); const { error } = await supabaseClient.from('orcamentos').insert(payload); if(error){ showToast('Erro ao cadastrar orçamento: ' + error.message); return; } await loadOrcamentos(); showToast('Orçamento cadastrado!'); } async function updateOrcamentoStatus(id, status){ const { error } = await supabaseClient.from('orcamentos').update({ status }).eq('id', id); if(error){ showToast('Erro ao atualizar orçamento: ' + error.message); return; } setOrcamentos(prev=>prev.map(o=>o.id===id?{...o,status}:o)); showToast('Andamento atualizado para "' + status + '"'); } // ---------- Gastos ---------- async function saveGasto(data, editingId){ if(editingId){ const { error } = await supabaseClient.from('gastos').update(gastoToRow(data)).eq('id', editingId); if(error){ showToast('Erro ao salvar gasto: ' + error.message); return; } await loadGastos(); logAudit('Editou gasto', data.desc); showToast('Gasto atualizado!'); } else { const { error } = await supabaseClient.from('gastos').insert(gastoToRow(data)); if(error){ showToast('Erro ao cadastrar gasto: ' + error.message); return; } await loadGastos(); logAudit('Cadastrou gasto', data.desc); showToast('Gasto cadastrado!'); } } async function deleteGasto(id){ if(!window.confirm('Excluir este gasto?')) return; const g = gastos.find(x=>x.id===id); const { error } = await supabaseClient.from('gastos').delete().eq('id', id); if(error){ showToast('Erro ao excluir gasto: ' + error.message); return; } setGastos(prev=>prev.filter(x=>x.id!==id)); logAudit('Excluiu gasto', g?g.desc:id); showToast('Gasto excluído.'); } async function gerarGastosRecorrentes(){ const hoje = new Date(); const mesAtual = hoje.toISOString().slice(0,7); const fixos = gastos.filter(g=>g.recorrente); const novos = []; fixos.forEach(g=>{ const jaExiste = gastos.some(x=>x.desc===g.desc && x.vencimento.slice(0,7)===mesAtual); if(!jaExiste){ const d = new Date(hoje.getFullYear(), hoje.getMonth(), new Date(g.vencimento+'T00:00:00').getDate()); novos.push(gastoToRow({ desc:g.desc, valor:g.valor, vencimento:d.toISOString().slice(0,10), categoria:g.categoria, recorrente:true })); } }); if(novos.length===0){ showToast('Gastos fixos deste mês já estavam gerados.'); return; } const { error } = await supabaseClient.from('gastos').insert(novos); if(error){ showToast('Erro ao gerar gastos recorrentes: ' + error.message); return; } await loadGastos(); logAudit('Gerou gastos recorrentes do mês', `${novos.length} gasto(s) criado(s)`); showToast(`${novos.length} gasto(s) fixo(s) gerado(s) para este mês!`); } // ---------- Serviços ---------- async function saveServico(data, editingId){ if(editingId){ const { error } = await supabaseClient.from('servicos').update(servicoToRow(data)).eq('id', editingId); if(error){ showToast('Erro ao salvar serviço: ' + error.message); return; } await loadServicos(); showToast('Serviço atualizado com sucesso!'); } else { const { error } = await supabaseClient.from('servicos').insert(servicoToRow(data)); if(error){ showToast('Erro ao cadastrar serviço: ' + error.message); return; } await loadServicos(); showToast('Serviço cadastrado com sucesso!'); } } async function deleteServico(id){ if(!window.confirm('Excluir este serviço?')) return; const { error } = await supabaseClient.from('servicos').delete().eq('id', id); if(error){ showToast('Erro ao excluir serviço: ' + error.message); return; } setServicos(prev=>prev.filter(x=>x.id!==id)); showToast('Serviço excluído.'); } // ---------- Terceirizados ---------- async function saveTerceirizado(data){ const { error } = await supabaseClient.from('terceirizados').insert(terceirizadoToRow(data)); if(error){ showToast('Erro ao cadastrar terceirizado: ' + error.message); return; } await loadTerceirizados(); logAudit('Cadastrou terceirizado', `${data.parceiro} — ${data.servico}`); showToast('Terceirizado cadastrado!'); } async function deleteTerceirizado(id){ if(!window.confirm('Excluir este terceirizado?')) return; const t = terceirizados.find(x=>x.id===id); const { error } = await supabaseClient.from('terceirizados').delete().eq('id', id); if(error){ showToast('Erro ao excluir terceirizado: ' + error.message); return; } setTerceirizados(prev=>prev.filter(x=>x.id!==id)); logAudit('Excluiu terceirizado', t?t.parceiro:id); showToast('Terceirizado excluído.'); } if(!authChecked){ return