const express = require('express'); const cors = require('cors'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); require('dotenv').config(); const supabase = require('./config/supabase'); // Versão de produção ativada const mercadoPagoService = require('./config/mercadopago'); const gerarIdPedidoCatalogo = () => { const agora = new Date(); const ano = agora.getFullYear(); const mes = String(agora.getMonth() + 1).padStart(2, '0'); const dia = String(agora.getDate()).padStart(2, '0'); const hora = String(agora.getHours()).padStart(2, '0'); const minuto = String(agora.getMinutes()).padStart(2, '0'); const segundo = String(agora.getSeconds()).padStart(2, '0'); return `PC${ano}${mes}${dia}${hora}${minuto}${segundo}`; }; const app = express(); const PORT = process.env.PORT || 5000; // Middleware app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Servir arquivos estáticos app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); app.use('/catalogo', express.static(path.join(__dirname, 'site'))); // Servir assets do catálogo em /catalogo app.use('/catalogo/styles.css', express.static(path.join(__dirname, 'site', 'styles.css'))); app.use('/catalogo/script.js', express.static(path.join(__dirname, 'site', 'script.js'))); app.use('/catalogo/assets', express.static(path.join(__dirname, 'site', 'assets'))); // Rota para servir o catálogo em /catalogo app.get('/catalogo', (req, res) => { res.sendFile(path.join(__dirname, 'site', 'index.html')); }); app.get('/catalogo/', (req, res) => { res.sendFile(path.join(__dirname, 'site', 'index.html')); }); // Servir arquivos estáticos do sistema de controle na raiz app.use(express.static(path.join(__dirname, 'client/build'))); // ====================== // PEDIDOS DO CATÁLOGO // ====================== const mapearPedidoCatalogo = (registro) => { if (!registro) return null; return { id: registro.id, codigo: registro.codigo, createdAt: registro.created_at, cliente: registro.cliente || { nome: registro.cliente_nome || null, whatsapp: registro.cliente_whatsapp || null, endereco: registro.cliente_endereco || null }, itens: registro.itens || [], total: registro.total || 0, mensagem: registro.mensagem || null }; }; app.get('/api/catalogo/pedidos', async (req, res) => { try { const { data, error } = await supabase .from('catalogo_pedidos') .select('*') .order('created_at', { ascending: false }); if (error) { console.error('Supabase erro ao listar pedidos:', error); return res.status(500).json({ error: 'Não foi possível carregar os pedidos do catálogo.', details: error.message || error.error_description || null }); } const pedidos = (data || []).map(mapearPedidoCatalogo).filter(Boolean); res.json(pedidos); } catch (error) { console.error('Erro inesperado ao listar pedidos do catálogo:', error); res.status(500).json({ error: 'Não foi possível carregar os pedidos do catálogo.' }); } }); app.post('/api/catalogo/pedidos', async (req, res) => { try { const { cliente, itens, total, mensagem } = req.body || {}; if (!cliente || !cliente.nome || !cliente.whatsapp) { return res.status(400).json({ error: 'Dados do cliente são obrigatórios.' }); } if (!Array.isArray(itens) || itens.length === 0) { return res.status(400).json({ error: 'Informe ao menos um item no pedido.' }); } const codigo = gerarIdPedidoCatalogo(); const payload = { codigo, cliente, itens, total: Number(total) || 0, mensagem: mensagem || null }; const { data, error } = await supabase .from('catalogo_pedidos') .insert([payload]) .select() .single(); if (error) { console.error('Supabase erro ao salvar pedido do catálogo:', error); return res.status(500).json({ error: 'Não foi possível registrar o pedido do catálogo.', details: error.message || error.error_description || null }); } res.status(201).json(mapearPedidoCatalogo(data)); } catch (error) { console.error('Erro inesperado ao registrar pedido do catálogo:', error); res.status(500).json({ error: 'Não foi possível registrar o pedido do catálogo.' }); } }); // Configuração do Multer para upload de arquivos // Usando memoryStorage para ter acesso ao buffer e fazer upload no Supabase const storage = multer.memoryStorage(); const upload = multer({ storage: storage, limits: { fileSize: 5 * 1024 * 1024 // Limite alinhado com o bucket (5MB) } }); const parseConfigValor = (record, fallback = {}) => { if (!record || record.valor === undefined || record.valor === null) { return fallback; } const rawValue = record.valor; if (typeof rawValue === 'string') { const trimmed = rawValue.trim(); if (!trimmed) return fallback; try { return JSON.parse(trimmed); } catch (error) { console.error('Erro ao interpretar configuração armazenada no Supabase:', error); return fallback; } } return rawValue; }; const mapVariacaoComFoto = (variacao) => { if (!variacao) return variacao; const fotos = Array.isArray(variacao.fotos) ? variacao.fotos.filter((foto) => typeof foto === 'string' && foto.trim().length) : []; const fotoUrl = fotos.length > 0 ? fotos[0] : null; return { ...variacao, fotos, foto_url: fotoUrl }; }; const generateStoragePath = (prefix, originalName, extra = '') => { const ext = originalName && originalName.includes('.') ? originalName.substring(originalName.lastIndexOf('.')) : ''; const safeExtra = extra ? `_${extra}` : ''; return `${prefix}/${Date.now()}_${Math.random().toString(36).slice(2, 10)}${safeExtra}${ext}`; }; const getDateInBrazilISO = () => { const formatador = new Intl.DateTimeFormat('sv-SE', { timeZone: 'America/Sao_Paulo', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); return formatador.format(new Date()).replace(' ', 'T'); }; const getDateInBrazilYYYYMMDD = () => { const formatador = new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Sao_Paulo', year: 'numeric', month: '2-digit', day: '2-digit' }); return formatador.format(new Date()); }; const normalizeToBrazilianTimestamp = (valor) => { if (!valor) { return getDateInBrazilISO(); } if (valor instanceof Date && !isNaN(valor)) { return new Intl.DateTimeFormat('sv-SE', { timeZone: 'America/Sao_Paulo', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).format(valor).replace(' ', 'T'); } if (typeof valor === 'string') { const trimmed = valor.trim(); if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) { return `${trimmed}T00:00:00-03:00`; } const parsed = new Date(trimmed.replace(' ', 'T')); if (!isNaN(parsed)) { return new Intl.DateTimeFormat('sv-SE', { timeZone: 'America/Sao_Paulo', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).format(parsed).replace(' ', 'T'); } } return getDateInBrazilISO(); }; const formatDateToBrazilYYYYMMDD = (date) => { const formatador = new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Sao_Paulo', year: 'numeric', month: '2-digit', day: '2-digit' }); return formatador.format(date); }; const formatDateToBrazilHuman = (date) => { if (!date) return ''; // Se já é uma string no formato brasileiro, retorna if (typeof date === 'string' && date.match(/^\d{2}\/\d{2}\/\d{4}$/)) { return date; } // Se é uma string de data no formato YYYY-MM-DD (sem timezone) if (typeof date === 'string' && date.match(/^\d{4}-\d{2}-\d{2}$/)) { const [ano, mes, dia] = date.split('-'); return `${dia}/${mes}/${ano}`; } // Para outros formatos, tenta converter const data = date instanceof Date ? date : new Date(typeof date === 'string' ? date.replace(' ', 'T') : date); if (!data || isNaN(data)) { return ''; } return new Intl.DateTimeFormat('pt-BR', { timeZone: 'America/Sao_Paulo' }).format(data); }; const resolveProdutoVariacaoId = (item) => { if (!item) return null; const value = item.produto_variacao_id ?? item.variacao_id ?? null; if (value === 'null' || value === '') return null; return value; }; const normalizeVendaItem = (item) => { if (!item) return item; const produtoVariacaoId = resolveProdutoVariacaoId(item); const produtoNome = item.produtos ? [item.produtos?.marca, item.produtos?.nome].filter(Boolean).join(' - ') : item.produto_nome || item.nome || null; const variacaoInfo = item.produto_variacoes ? [item.produto_variacoes?.tamanho, item.produto_variacoes?.cor].filter(Boolean).join(' - ') : item.variacao_info || null; return { ...item, produto_variacao_id: produtoVariacaoId, variacao_id: produtoVariacaoId, produto_nome: produtoNome, produto_codigo: item.produto_codigo || item.produtos?.id_produto || null, produto_foto: item.produto_foto || item.produtos?.foto_principal || null, variacao_info: variacaoInfo }; }; const sendWhatsappMessage = async ({ telefone, mensagem, media, // { base64, fileName, caption } clienteNome, salvarHistorico = true }) => { if (!telefone || (!mensagem && !media)) { throw new Error('Telefone e uma mensagem ou mídia são obrigatórios'); } const { data: configData } = await supabase .from('configuracoes') .select('valor') .eq('tipo', 'evolution_api') .single(); let config; if (!configData) { config = { baseUrl: 'https://criadordigital-evolution.jesdfs.easypanel.host', apiKey: 'DBDF609168B1-48A3-8A4A-5E50D0300F2C', instanceName: 'Tiago', enabled: true }; } else { config = parseConfigValor(configData, null); if (!config) { throw new Error('Configuração da Evolution API inválida'); } } if (!config.enabled) { throw new Error('Evolution API desabilitada'); } const numeroFormatado = telefone.replace(/\D/g, ''); const numeroCompleto = numeroFormatado.startsWith('55') ? numeroFormatado : `55${numeroFormatado}`; let evolutionUrl, body; if (media && media.base64) { // Envio de mídia evolutionUrl = `${config.baseUrl}/message/sendMedia/${config.instanceName}`; body = { number: numeroCompleto, mediatype: 'image', mimetype: 'image/png', media: media.base64, // Apenas o base64, sem prefixo fileName: media.fileName || 'qrcode_pix.png', caption: media.caption || '' }; } else { // Envio de texto evolutionUrl = `${config.baseUrl}/message/sendText/${config.instanceName}`; body = { number: numeroCompleto, text: mensagem }; } const response = await fetch(evolutionUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': config.apiKey }, body: JSON.stringify(body) }); if (!response.ok) { const body = await response.text(); throw new Error(`Erro ao enviar mensagem: ${response.status} - ${body}`); } const evolutionResponse = await response.json(); let mensagemSalva = null; if (salvarHistorico) { const agoraISO = getDateInBrazilISO(); try { const { data: savedData, error: saveError } = await supabase .from('mensagens_whatsapp') .insert({ telefone_cliente: telefone, cliente_nome: clienteNome || 'Cliente', mensagem: mensagem || media?.caption || 'Mídia enviada', tipo: 'enviada', status: 'enviada', evolution_message_id: evolutionResponse.key?.id, created_at: agoraISO }) .select() .single(); if (!saveError) { mensagemSalva = savedData; } } catch (saveErr) { console.log('Não foi possível salvar no histórico (tabela pode não existir):', saveErr.message); mensagemSalva = { id: Date.now(), telefone_cliente: telefone, cliente_nome: clienteNome || 'Cliente', mensagem, tipo: 'enviada', status: 'enviada', created_at: agoraISO }; } } return { success: true, message: 'Mensagem enviada com sucesso!', data: mensagemSalva }; }; const sanitizeString = (value) => { if (value === undefined || value === null) return null; if (typeof value === 'string') { const trimmed = value.trim(); return trimmed.length ? trimmed : null; } return value; }; const buildFornecedorPayload = (body = {}) => { const payload = {}; const nome = sanitizeString(body.nome); const razaoSocial = sanitizeString(body.razao_social); if (nome) payload.nome = nome; if (razaoSocial) payload.razao_social = razaoSocial; const telefone = sanitizeString(body.telefone); const whatsapp = sanitizeString(body.whatsapp); const endereco = sanitizeString(body.endereco); const email = sanitizeString(body.email); const cidade = sanitizeString(body.cidade); const estado = sanitizeString(body.estado); const cep = sanitizeString(body.cep); const observacoes = sanitizeString(body.observacoes); const cnpj = sanitizeString(body.cnpj); const ativo = typeof body.ativo === 'boolean' ? body.ativo : undefined; if (!payload.nome && razaoSocial) payload.nome = razaoSocial; if (!payload.razao_social && nome) payload.razao_social = nome; if (telefone !== null) payload.telefone = telefone; if (whatsapp !== null) payload.whatsapp = whatsapp; if (endereco !== null) payload.endereco = endereco; if (email !== null) payload.email = email; if (cidade !== null) payload.cidade = cidade; if (estado !== null) payload.estado = estado; if (cep !== null) payload.cep = cep; if (observacoes !== null) payload.observacoes = observacoes; if (cnpj !== null) payload.cnpj = cnpj; if (ativo !== undefined) payload.ativo = ativo; return payload; }; const extractMissingColumn = (error) => { if (!error) return null; const candidates = [error.message, error.details, error.hint] .filter((value) => typeof value === 'string' && value.trim().length); for (const text of candidates) { const matchers = [ /column\s+"([^"]+)"/i, /column\s+'([^']+)'/i, /the\s+"([^"]+)"\s+column/i, /the\s+'([^']+)'\s+column/i, /column\s+([\w.]+)\s+does\s+not\s+exist/i, /'([^']+)'\s+column\s+of\s+'[^']+'/i, /"([^"]+)"\s+column\s+of\s+"[^"]+"/i ]; for (const regex of matchers) { const match = text.match(regex); if (match) { const column = match[1] || match[0]; return column.split('.').pop(); } } } return null; }; const attemptFornecedorMutation = async (mutateFn, body) => { let payload = buildFornecedorPayload(body); const removedColumns = new Set(); let lastError = null; while (true) { const cleanedPayload = {}; for (const [key, value] of Object.entries(payload)) { if (value !== undefined) { cleanedPayload[key] = value; } } if (Object.keys(cleanedPayload).length === 0) { return { error: lastError || new Error('Nenhum dado válido para fornecedor.') }; } const { data, error } = await mutateFn(cleanedPayload); if (!error) { return { data }; } lastError = error; const missingColumn = extractMissingColumn(error); if (!missingColumn || removedColumns.has(missingColumn)) { return { error }; } delete payload[missingColumn]; removedColumns.add(missingColumn); } }; const normalizeFornecedor = (fornecedor = {}) => { const nome = fornecedor.nome || fornecedor.razao_social || ''; return { ...fornecedor, nome, razao_social: fornecedor.razao_social || nome, telefone: fornecedor.telefone || null, whatsapp: fornecedor.whatsapp || fornecedor.telefone || null }; }; // === ENVIAR PIX POR WHATSAPP === app.post('/api/pix/enviar-whatsapp', async (req, res) => { try { const { telefone, valor, qr_code_base64, qr_code, nome_cliente } = req.body; if (!telefone || !qr_code_base64 || !qr_code) { return res.status(400).json({ error: 'Telefone, QR Code e chave PIX são obrigatórios.' }); } const valorFormatado = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(valor || 0); // 1. Enviar imagem do QR Code await sendWhatsappMessage({ telefone, media: { base64: qr_code_base64, fileName: 'pix_qrcode.png', caption: `Olá, ${nome_cliente || 'cliente'}! Segue o QR Code para pagamento do seu PIX de ${valorFormatado}.` }, clienteNome: nome_cliente, salvarHistorico: true }); // 2. Enviar chave PIX (Copia e Cola) await sendWhatsappMessage({ telefone, mensagem: `Você também pode pagar usando a chave PIX Copia e Cola abaixo:\n\n${qr_code}`, clienteNome: nome_cliente, salvarHistorico: false // Não salvar a segunda mensagem para não poluir o histórico }); res.json({ success: true, message: 'PIX enviado para o cliente com sucesso!' }); } catch (error) { console.error('Erro ao enviar PIX por WhatsApp:', error); res.status(500).json({ error: `Não foi possível enviar o PIX: ${error.message}` }); } }); // ===================================================== // SERVIR ARQUIVOS ESTÁTICOS E ROTAS FINAIS // ===================================================== // === TESTE === app.get('/api/test', (req, res) => { res.json({ message: 'API Supabase funcionando corretamente!', timestamp: new Date() }); }); // === API CATÁLOGO WEB === app.get('/api/catalogo/produtos', async (req, res) => { try { const { data, error } = await supabase .from('produtos') .select(` *, fornecedores(nome), produto_variacoes( id, tamanho, cor, quantidade, fotos ) `) .order('created_at', { ascending: false }); if (error) throw error; // Formatar dados para o catálogo const produtosCatalogo = data.map(produto => { const variacoesTratadas = (produto.produto_variacoes || []).map(mapVariacaoComFoto); const fotoPrincipalUrl = produto.foto_principal || variacoesTratadas.find(v => v.foto_url)?.foto_url || null; return { id: produto.id, id_produto: produto.id_produto, nome: produto.nome, marca: produto.marca, valor_revenda: produto.valor_revenda, valor_compra: produto.valor_compra, genero: produto.genero, estacao: produto.estacao, descricao: produto.descricao || '', foto_url: fotoPrincipalUrl, fornecedor: produto.fornecedores?.nome || null, variacoes: variacoesTratadas, estoque_total: variacoesTratadas.reduce((total, v) => total + (v.quantidade || 0), 0) || 0 }; }); res.json({ success: true, data: produtosCatalogo, total: produtosCatalogo.length }); } catch (error) { console.error('Erro ao buscar produtos do catálogo:', error); res.status(500).json({ success: false, error: error.message }); } }); // === FORNECEDORES === app.get('/api/fornecedores', async (req, res) => { try { const { data, error } = await supabase .from('fornecedores') .select('*') .order('created_at', { ascending: false }); if (error) throw error; const fornecedores = (data || []).map(normalizeFornecedor); res.json(fornecedores); } catch (error) { console.error('Erro ao listar fornecedores:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/fornecedores', async (req, res) => { try { const nomeInformado = sanitizeString(req.body?.nome) || sanitizeString(req.body?.razao_social); if (!nomeInformado) { return res.status(400).json({ error: 'Informe o nome do fornecedor.' }); } const resultado = await attemptFornecedorMutation( (payload) => supabase.from('fornecedores').insert([payload]).select().single(), req.body ); if (resultado.error) { throw resultado.error; } const fornecedor = normalizeFornecedor(resultado.data); res.json({ id: fornecedor.id, fornecedor, message: 'Fornecedor cadastrado com sucesso!' }); } catch (error) { console.error('Erro ao criar fornecedor:', error); res.status(500).json({ error: error.message }); } }); app.put('/api/fornecedores/:id', async (req, res) => { try { const { id } = req.params; const payloadBase = buildFornecedorPayload(req.body); if (Object.keys(payloadBase).length === 0) { return res.status(400).json({ error: 'Informe ao menos um campo para atualizar.' }); } const resultado = await attemptFornecedorMutation( (payload) => supabase .from('fornecedores') .update(payload) .eq('id', id) .select() .single(), req.body ); if (resultado.error) { throw resultado.error; } const fornecedor = normalizeFornecedor(resultado.data); res.json({ fornecedor, message: 'Fornecedor atualizado com sucesso!' }); } catch (error) { console.error('Erro ao atualizar fornecedor:', error); res.status(500).json({ error: error.message }); } }); app.delete('/api/fornecedores/:id', async (req, res) => { try { const { id } = req.params; const { error } = await supabase .from('fornecedores') .delete() .eq('id', id); if (error) throw error; res.json({ message: 'Fornecedor excluído com sucesso!' }); } catch (error) { console.error('Erro ao excluir fornecedor:', error); res.status(500).json({ error: error.message }); } }); // === PRODUTOS === app.get('/api/produtos', async (req, res) => { try { const { data, error } = await supabase .from('produtos') .select(` *, fornecedores(nome), produto_variacoes(id, tamanho, cor, quantidade, fotos) `) .order('created_at', { ascending: false }); if (error) throw error; // Processar dados para compatibilidade const produtos = data.map(produto => { const variacoesTratadas = (produto.produto_variacoes || []).map(mapVariacaoComFoto); const fotoPrincipalUrl = produto.foto_principal || variacoesTratadas.find(v => v.foto_url)?.foto_url || null; return { ...produto, produto_variacoes: variacoesTratadas, fornecedor_nome: produto.fornecedores?.nome || null, total_variacoes: variacoesTratadas.length, estoque_total: variacoesTratadas.reduce((total, v) => total + (v.quantidade || 0), 0) || 0, foto_principal_url: fotoPrincipalUrl }; }); res.json(produtos); } catch (error) { console.error('Erro ao listar produtos:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/produtos', upload.any(), async (req, res) => { console.log('Recebendo requisição para criar produto:', req.body); console.log('Arquivos recebidos:', req.files ? req.files.length : 0); try { const { id_produto, marca, nome, descricao, estacao, genero, fornecedor_id, valor_compra, valor_revenda, variacoes_data } = req.body; // Validações básicas if (!marca || !nome || !estacao || !valor_compra || !valor_revenda) { return res.status(400).json({ error: 'Campos obrigatórios não preenchidos' }); } // Parse das variações let variacoes = []; try { if (variacoes_data) { variacoes = JSON.parse(variacoes_data); } } catch (error) { console.error('Erro ao fazer parse das variações:', error); return res.status(400).json({ error: 'Dados de variações inválidos' }); } if (variacoes.length === 0) { return res.status(400).json({ error: 'Pelo menos uma variação é obrigatória' }); } // Preparar dados do produto (verificar se campo descrição existe) const produtoData = { id_produto, marca, nome, estacao, genero: genero || 'Unissex', fornecedor_id: fornecedor_id || null, valor_compra: parseFloat(valor_compra), valor_revenda: parseFloat(valor_revenda) }; // Adicionar descrição apenas se o campo existir if (descricao) { produtoData.descricao = descricao; } // Processar foto principal const fotoPrincipalFile = req.files ? req.files.find(file => file.fieldname === 'foto_principal') : null; let fotoPrincipalUrl = null; if (fotoPrincipalFile) { console.log('Processando foto principal:', fotoPrincipalFile.originalname); const filePath = `public/${Date.now()}_${fotoPrincipalFile.originalname}`; const { error: uploadError } = await supabase.storage .from('produtos') .upload(filePath, fotoPrincipalFile.buffer, { contentType: fotoPrincipalFile.mimetype, upsert: false }); if (uploadError) { console.error('Erro no upload da foto principal:', uploadError); } else { const { data: publicUrlData } = supabase.storage .from('produtos') .getPublicUrl(filePath); fotoPrincipalUrl = publicUrlData.publicUrl; produtoData.foto_principal = fotoPrincipalUrl; console.log('✅ Foto principal salva:', fotoPrincipalUrl); } } // Inserir produto const { data: produtoDataResult, error: produtoError } = await supabase .from('produtos') .insert([produtoData]) .select(); if (produtoError) throw produtoError; const produtoId = produtoDataResult[0].id; console.log('Produto inserido com sucesso, processando variações...'); // Array para armazenar a primeira foto (será usada como foto principal se não houver) let primeiraFotoUrl = null; // Processar variações for (let varIndex = 0; varIndex < variacoes.length; varIndex++) { const variacao = variacoes[varIndex]; // Processar fotos desta variação const fotosVariacao = req.files ? req.files.filter(file => file.fieldname.startsWith(`variacao_${varIndex}_foto_`) ) : []; let fotos = []; if (fotosVariacao.length > 0) { for (let fotoIndex = 0; fotoIndex < fotosVariacao.length; fotoIndex++) { const arquivo = fotosVariacao[fotoIndex]; const filePath = generateStoragePath('public', arquivo.originalname, `${varIndex}_${fotoIndex}`); const { error: uploadError } = await supabase.storage .from('produtos') .upload(filePath, arquivo.buffer, { contentType: arquivo.mimetype, upsert: false }); if (uploadError) { console.error(`Erro ao fazer upload da foto da variação ${varIndex} (${arquivo.originalname}):`, uploadError); continue; } const { data: publicUrlData } = supabase.storage .from('produtos') .getPublicUrl(filePath); if (publicUrlData?.publicUrl) { fotos.push(publicUrlData.publicUrl); if (!primeiraFotoUrl && !fotoPrincipalUrl) { primeiraFotoUrl = publicUrlData.publicUrl; } } } } // Inserir variação const { error: variacaoError } = await supabase .from('produto_variacoes') .insert([{ produto_id: produtoId, tamanho: variacao.tamanho, cor: variacao.cor, quantidade: parseInt(variacao.quantidade), fotos }]); if (variacaoError) throw variacaoError; } // Se não houver foto_principal mas houver primeira foto das variações, atualizar o produto if (!fotoPrincipalUrl && primeiraFotoUrl) { console.log('Atualizando foto principal do produto com a primeira foto da variação:', primeiraFotoUrl); const { error: updateError } = await supabase .from('produtos') .update({ foto_principal: primeiraFotoUrl }) .eq('id', produtoId); if (updateError) { console.error('Erro ao atualizar foto_principal:', updateError); } else { console.log('✅ Foto principal atualizada com sucesso!'); } } console.log('Produto criado com sucesso!'); res.json({ id: produtoId, message: 'Produto criado com sucesso!' }); } catch (error) { console.error('Erro ao criar produto:', error); res.status(500).json({ error: error.message }); } }); app.put('/api/produtos/:id', upload.any(), async (req, res) => { try { const { id } = req.params; const { id_produto, marca, nome, descricao, estacao, genero, fornecedor_id, valor_compra, valor_revenda } = req.body; // Preparar dados de atualização (verificar se campo descrição existe) const updateData = { id_produto, marca, nome, estacao, genero: genero || 'Unissex', fornecedor_id: fornecedor_id || null, valor_compra: parseFloat(valor_compra), valor_revenda: parseFloat(valor_revenda) }; // Adicionar descrição apenas se o campo existir if (descricao) { updateData.descricao = descricao; } // Processar foto principal se houver const fotoPrincipalFile = req.files ? req.files.find(file => file.fieldname === 'foto_principal') : null; if (fotoPrincipalFile) { console.log('Atualizando foto principal:', fotoPrincipalFile.originalname); const filePath = `public/${Date.now()}_${fotoPrincipalFile.originalname}`; const { error: uploadError } = await supabase.storage .from('produtos') .upload(filePath, fotoPrincipalFile.buffer, { contentType: fotoPrincipalFile.mimetype, upsert: false }); if (uploadError) { console.error('Erro no upload da foto principal:', uploadError); } else { const { data: publicUrlData } = supabase.storage .from('produtos') .getPublicUrl(filePath); updateData.foto_principal = publicUrlData.publicUrl; console.log('✅ Foto principal atualizada:', publicUrlData.publicUrl); } } const { error } = await supabase .from('produtos') .update(updateData) .eq('id', id); if (error) throw error; res.json({ message: 'Produto atualizado com sucesso!' }); } catch (error) { console.error('Erro ao atualizar produto:', error); res.status(500).json({ error: error.message }); } }); app.delete('/api/produtos/:id', async (req, res) => { try { const { id } = req.params; const { error } = await supabase .from('produtos') .delete() .eq('id', id); if (error) throw error; res.json({ message: 'Produto excluído com sucesso!' }); } catch (error) { console.error('Erro ao excluir produto:', error); res.status(500).json({ error: error.message }); } }); // === VARIAÇÕES DE PRODUTOS === app.get('/api/produtos/:id/variacoes', async (req, res) => { try { const { id } = req.params; const { data, error } = await supabase .from('produto_variacoes') .select('*') .eq('produto_id', id) .order('tamanho'); if (error) throw error; res.json((data || []).map(mapVariacaoComFoto)); } catch (error) { console.error('Erro ao listar variações:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/produtos/:id/variacoes', upload.single('foto'), async (req, res) => { try { const { id } = req.params; const { tamanho, cor, quantidade } = req.body; if (!tamanho || !cor) { return res.status(400).json({ error: 'Campos tamanho e cor são obrigatórios' }); } const fotos = []; if (req.file) { const filePath = `public/${Date.now()}_${req.file.originalname}`; const { error: uploadError } = await supabase.storage .from('produtos') .upload(filePath, req.file.buffer, { contentType: req.file.mimetype, upsert: false }); if (uploadError) { console.error('Erro no upload da foto da variação:', uploadError); return res.status(500).json({ error: 'Erro ao fazer upload da foto da variação' }); } const { data: publicUrlData } = supabase.storage .from('produtos') .getPublicUrl(filePath); if (publicUrlData?.publicUrl) { fotos.push(publicUrlData.publicUrl); } } const { data, error } = await supabase .from('produto_variacoes') .insert([{ produto_id: id, tamanho, cor, quantidade: parseInt(quantidade) || 0, fotos }]) .select() .single(); if (error) throw error; const variacaoTratada = mapVariacaoComFoto(data); if (fotos.length > 0) { const { data: produtoAtual } = await supabase .from('produtos') .select('foto_principal') .eq('id', id) .single(); if (!produtoAtual?.foto_principal) { await supabase .from('produtos') .update({ foto_principal: fotos[0] }) .eq('id', id); } } res.json({ id: variacaoTratada.id, variacao: variacaoTratada, message: 'Variação adicionada com sucesso!' }); } catch (error) { console.error('Erro ao criar variação:', error); if (error.code === '23505') { return res.status(400).json({ error: 'Já existe uma variação com este tamanho e cor.' }); } res.status(500).json({ error: error.message }); } }); app.put('/api/produtos/:produtoId/variacoes/:variacaoId', async (req, res) => { try { const { produtoId, variacaoId } = req.params; const { tamanho, cor, quantidade } = req.body; const updateData = {}; if (tamanho) updateData.tamanho = tamanho; if (cor) updateData.cor = cor; if (quantidade !== undefined) updateData.quantidade = parseInt(quantidade); if (Object.keys(updateData).length === 0) { return res.status(400).json({ error: 'Informe ao menos um campo para atualizar.' }); } const { data, error } = await supabase .from('produto_variacoes') .update(updateData) .eq('id', variacaoId) .eq('produto_id', produtoId) .select() .single(); if (error) throw error; res.json({ variacao: mapVariacaoComFoto(data), message: 'Variação atualizada com sucesso!' }); } catch (error) { console.error('Erro ao atualizar variação:', error); res.status(500).json({ error: error.message }); } }); app.delete('/api/produtos/:produtoId/variacoes/:variacaoId', async (req, res) => { try { const { produtoId, variacaoId } = req.params; const { error } = await supabase .from('produto_variacoes') .delete() .eq('id', variacaoId) .eq('produto_id', produtoId); if (error) throw error; res.json({ message: 'Variação removida com sucesso!' }); } catch (error) { console.error('Erro ao excluir variação:', error); res.status(500).json({ error: error.message }); } }); // === CLIENTES === app.get('/api/clientes', async (req, res) => { try { const { data, error } = await supabase .from('clientes') .select('*') .order('created_at', { ascending: false }); if (error) throw error; res.json(data); } catch (error) { console.error('Erro ao listar clientes:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/clientes', async (req, res) => { try { const { nome_completo, email, whatsapp, endereco } = req.body; // Primeiro, tentar inserir com id_cliente try { // Gerar ID numérico sequencial const { data: ultimoCliente } = await supabase .from('clientes') .select('id_cliente') .order('id_cliente', { ascending: false }) .limit(1); let proximoId = 1; if (ultimoCliente && ultimoCliente.length > 0 && ultimoCliente[0].id_cliente) { const ultimoNumero = parseInt(ultimoCliente[0].id_cliente); proximoId = isNaN(ultimoNumero) ? 1 : ultimoNumero + 1; } const id_cliente = proximoId.toString(); const { data, error } = await supabase .from('clientes') .insert([{ nome_completo, email, whatsapp, endereco, id_cliente }]) .select(); if (error) throw error; res.json(data[0]); } catch (idError) { // Se falhar com id_cliente, tentar sem ele (fallback) console.log('Tentando inserir sem id_cliente (coluna não existe ainda)'); const { data, error } = await supabase .from('clientes') .insert([{ nome_completo, email, whatsapp, endereco }]) .select(); if (error) throw error; res.json(data[0]); } } catch (error) { console.error('Erro ao criar cliente:', error); res.status(500).json({ error: error.message }); } }); app.put('/api/clientes/:id', async (req, res) => { try { const { id } = req.params; const { nome_completo, email, whatsapp, endereco } = req.body; const { error } = await supabase .from('clientes') .update({ nome_completo, email, whatsapp, endereco }) .eq('id', id); if (error) throw error; res.json({ message: 'Cliente atualizado com sucesso!' }); } catch (error) { console.error('Erro ao atualizar cliente:', error); res.status(500).json({ error: error.message }); } }); app.delete('/api/clientes/:id', async (req, res) => { try { const { id } = req.params; const { error } = await supabase .from('clientes') .delete() .eq('id', id); if (error) throw error; res.json({ message: 'Cliente excluído com sucesso!' }); } catch (error) { console.error('Erro ao excluir cliente:', error); res.status(500).json({ error: error.message }); } }); // === TIPOS DE DESPESAS === app.get('/api/tipos-despesas', async (req, res) => { try { const { data, error } = await supabase .from('tipos_despesa') .select('*') .order('nome'); if (error) throw error; res.json(data); } catch (error) { console.error('Erro ao listar tipos de despesas:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/tipos-despesas', async (req, res) => { try { const { nome, descricao } = req.body; const { data, error } = await supabase .from('tipos_despesa') .insert([{ nome, descricao }]) .select(); if (error) throw error; res.json({ id: data[0].id, message: 'Tipo de despesa criado com sucesso!' }); } catch (error) { console.error('Erro ao criar tipo de despesa:', error); res.status(500).json({ error: error.message }); } }); // === DESPESAS === app.get('/api/despesas', async (req, res) => { try { // Tentar query com relacionamentos primeiro try { const { data, error } = await supabase .from('despesas') .select(` *, tipos_despesa(nome), fornecedores(nome) `) .order('data_despesa', { ascending: false }); if (error) throw error; // Processar dados para compatibilidade const despesas = data.map(despesa => ({ ...despesa, tipo_nome: despesa.tipos_despesa?.nome || null, fornecedor_nome: despesa.fornecedores?.nome || null })); res.json(despesas); } catch (relationError) { console.log('⚠️ Relacionamentos não existem ainda, usando query simples...'); // Fallback: query simples sem relacionamentos const { data, error } = await supabase .from('despesas') .select('*') .order('data_despesa', { ascending: false }); if (error) throw error; res.json(data || []); } } catch (error) { console.error('Erro ao listar despesas:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/despesas', async (req, res) => { try { const { tipo_despesa, fornecedor, data, valor, descricao } = req.body; // Tentar primeiro com as novas colunas de texto livre try { const { data: despesaData, error } = await supabase .from('despesas') .insert([{ tipo_nome: tipo_despesa, fornecedor_nome: fornecedor || null, data_despesa: data, valor: parseFloat(valor), descricao }]) .select(); if (error) throw error; res.json({ id: despesaData[0].id, message: 'Despesa criada com sucesso!' }); } catch (newColumnError) { console.log('⚠️ Colunas novas não existem ainda, usando método alternativo...'); // Fallback: criar/buscar tipo de despesa e fornecedor let tipoId = null; let fornecedorId = null; // Buscar ou criar tipo de despesa if (tipo_despesa) { const { data: tipoExistente } = await supabase .from('tipos_despesa') .select('id') .eq('nome', tipo_despesa) .single(); if (tipoExistente) { tipoId = tipoExistente.id; } else { const { data: novoTipo } = await supabase .from('tipos_despesa') .insert([{ nome: tipo_despesa }]) .select('id') .single(); tipoId = novoTipo?.id; } } // Buscar ou criar fornecedor if (fornecedor) { const { data: fornecedorExistente } = await supabase .from('fornecedores') .select('id') .eq('nome', fornecedor) .single(); if (fornecedorExistente) { fornecedorId = fornecedorExistente.id; } else { const { data: novoFornecedor } = await supabase .from('fornecedores') .insert([{ nome: fornecedor }]) .select('id') .single(); fornecedorId = novoFornecedor?.id; } } // Criar despesa com o método antigo const { data: despesaData, error: despesaError } = await supabase .from('despesas') .insert([{ tipo_despesa_id: tipoId, fornecedor_id: fornecedorId, data_despesa: data, valor: parseFloat(valor), descricao }]) .select(); if (despesaError) throw despesaError; res.json({ id: despesaData[0].id, message: 'Despesa criada com sucesso! (Tipo e fornecedor foram cadastrados automaticamente)' }); } } catch (error) { console.error('Erro ao criar despesa:', error); res.status(500).json({ error: error.message }); } }); app.put('/api/despesas/:id', async (req, res) => { try { const { id } = req.params; const { tipo_despesa, fornecedor, data, valor, descricao } = req.body; // Tentar primeiro com as novas colunas de texto livre try { const { error } = await supabase .from('despesas') .update({ tipo_nome: tipo_despesa, fornecedor_nome: fornecedor || null, data_despesa: data, valor: parseFloat(valor), descricao }) .eq('id', id); if (error) throw error; res.json({ message: 'Despesa atualizada com sucesso!' }); } catch (newColumnError) { console.log('⚠️ Colunas novas não existem ainda, usando método alternativo...'); // Fallback: buscar/criar tipo de despesa e fornecedor let tipoId = null; let fornecedorId = null; // Buscar ou criar tipo de despesa if (tipo_despesa) { const { data: tipoExistente } = await supabase .from('tipos_despesa') .select('id') .eq('nome', tipo_despesa) .single(); if (tipoExistente) { tipoId = tipoExistente.id; } else { const { data: novoTipo } = await supabase .from('tipos_despesa') .insert([{ nome: tipo_despesa }]) .select('id') .single(); tipoId = novoTipo?.id; } } // Buscar ou criar fornecedor if (fornecedor) { const { data: fornecedorExistente } = await supabase .from('fornecedores') .select('id') .eq('nome', fornecedor) .single(); if (fornecedorExistente) { fornecedorId = fornecedorExistente.id; } else { const { data: novoFornecedor } = await supabase .from('fornecedores') .insert([{ nome: fornecedor }]) .select('id') .single(); fornecedorId = novoFornecedor?.id; } } // Atualizar despesa com o método antigo const { error: updateError } = await supabase .from('despesas') .update({ tipo_despesa_id: tipoId, fornecedor_id: fornecedorId, data_despesa: data, valor: parseFloat(valor), descricao }) .eq('id', id); if (updateError) throw updateError; res.json({ message: 'Despesa atualizada com sucesso! (Tipo e fornecedor foram cadastrados automaticamente)' }); } } catch (error) { console.error('Erro ao atualizar despesa:', error); res.status(500).json({ error: error.message }); } }); app.delete('/api/despesas/:id', async (req, res) => { try { const { id } = req.params; const { error } = await supabase .from('despesas') .delete() .eq('id', id); if (error) throw error; res.json({ message: 'Despesa excluída com sucesso!' }); } catch (error) { console.error('Erro ao excluir despesa:', error); res.status(500).json({ error: error.message }); } }); // === VENDAS === app.get('/api/vendas', async (req, res) => { try { const { data, error } = await supabase .from('vendas') .select(` *, clientes(nome_completo, whatsapp, telefone), venda_itens( *, produtos(nome, marca, foto_principal, id_produto), produto_variacoes(tamanho, cor) ) `) .order('data_venda', { ascending: false }); if (error) throw error; const vendas = data.map(venda => ({ ...venda, cliente_nome: venda.clientes?.nome_completo || null, cliente_whatsapp: venda.clientes?.whatsapp || null, cliente_telefone: venda.clientes?.telefone || null, status: venda.status || 'concluida', // Status padrão para vendas sem status itens: venda.venda_itens?.map(normalizeVendaItem) || [] })); res.json(vendas); } catch (error) { console.error('Erro ao listar vendas:', error); res.status(500).json({ error: error.message }); } }); app.get('/api/vendas/:id', async (req, res) => { try { const { id } = req.params; // Validar se o ID é um UUID válido const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (!uuidRegex.test(id)) { return res.status(400).json({ error: 'ID inválido' }); } const { data, error } = await supabase .from('vendas') .select(` *, clientes(nome_completo, whatsapp, telefone), venda_itens( *, produtos(nome, marca, foto_principal, id_produto), produto_variacoes(tamanho, cor) ) `) .eq('id', id) .single(); if (error) throw error; // Buscar devoluções/trocas desta venda com informações completas const { data: devolucoes, error: devolucaoError } = await supabase .from('devolucoes') .select(` *, venda_itens!inner( id, quantidade, valor_unitario, valor_total, produtos(nome, marca, id_produto), produto_variacoes(tamanho, cor) ) `) .eq('venda_id', id) .order('data_devolucao', { ascending: false }); if (devolucaoError) { console.error('Erro ao buscar devoluções:', devolucaoError); } // Verificar se há trocas const temTrocas = devolucoes?.some(dev => dev.tipo_operacao === 'troca') || false; const statusVenda = temTrocas ? 'com_troca' : (data.status || 'concluida'); // Processar devoluções com informações detalhadas const devolucoesProcesadas = devolucoes?.map(dev => ({ ...dev, produto_info: dev.venda_itens ? { nome: `${dev.venda_itens.produtos?.marca} - ${dev.venda_itens.produtos?.nome}`, codigo: dev.venda_itens.produtos?.id_produto, variacao: `${dev.venda_itens.produto_variacoes?.tamanho} - ${dev.venda_itens.produto_variacoes?.cor}`, quantidade_original: dev.venda_itens.quantidade, valor_unitario_original: dev.venda_itens.valor_unitario } : null })) || []; // Criar mapa de devoluções por item para incluir detalhes do evento const devolucoesPorItem = {}; devolucoes?.forEach(dev => { if (!devolucoesPorItem[dev.item_id]) { devolucoesPorItem[dev.item_id] = []; } devolucoesPorItem[dev.item_id].push(dev); }); // Analisar padrão da venda para determinar tipo de operação const itensComQuantidadeZero = data.venda_itens?.filter(item => parseInt(item.quantidade) === 0) || []; const itensComQuantidadePositiva = data.venda_itens?.filter(item => parseInt(item.quantidade) > 0) || []; const valorTotalVenda = parseFloat(data.valor_total); // Se há itens com quantidade 0 E itens com quantidade > 0, provavelmente é troca // Se há apenas itens com quantidade 0 E valor total é 0, provavelmente é devolução const provavelmenteTroca = itensComQuantidadeZero.length > 0 && itensComQuantidadePositiva.length > 0; const provavelmenteDevolucao = itensComQuantidadeZero.length > 0 && valorTotalVenda === 0; const vendaCompleta = { ...data, cliente_nome: data.clientes?.nome_completo || null, cliente_whatsapp: data.clientes?.whatsapp || null, cliente_telefone: data.clientes?.telefone || null, status: statusVenda, tem_trocas: temTrocas, devolucoes: devolucoesProcesadas, itens: data.venda_itens?.map(item => { const itemNormalizado = normalizeVendaItem(item); const devolucaoItem = devolucoesPorItem[item.id]; let foiDevolvido = !!devolucaoItem; if (!foiDevolvido && parseInt(itemNormalizado.quantidade) === 0) { foiDevolvido = true; } let statusItem = 'vendido'; let dataEvento = null; let tipoEvento = null; if (foiDevolvido) { if (devolucaoItem) { const ultimaDevolucao = devolucaoItem.sort((a, b) => new Date(b.data_devolucao) - new Date(a.data_devolucao) )[0]; statusItem = ultimaDevolucao.tipo_operacao === 'troca' ? 'trocado' : 'devolvido'; dataEvento = ultimaDevolucao.data_devolucao; tipoEvento = ultimaDevolucao.tipo_operacao; } else { if (provavelmenteDevolucao) { statusItem = 'devolvido'; tipoEvento = 'devolucao'; } else if (provavelmenteTroca) { statusItem = 'trocado'; tipoEvento = 'troca'; } else { statusItem = 'trocado'; tipoEvento = 'troca'; } dataEvento = itemNormalizado.updated_at || data.data_venda; } } return { ...itemNormalizado, foi_devolvido: foiDevolvido, status_item: statusItem, data_evento: dataEvento, tipo_evento: tipoEvento }; }) || [] }; res.json(vendaCompleta); } catch (error) { console.error('Erro ao buscar venda:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/vendas', async (req, res) => { try { const { cliente_id, tipo_pagamento, valor_total, desconto, parcelas, valor_parcela, data_venda, data_primeiro_vencimento, data_vencimento, observacoes, itens, id_venda, parcelas_detalhes } = req.body; const valorTotalNumber = parseFloat(valor_total) || 0; const descontoNumber = parseFloat(desconto) || 0; const parcelasNumber = parseInt(parcelas) || 1; const valorParcelaNumber = parseFloat(valor_parcela) || (parcelasNumber > 0 ? valorTotalNumber / parcelasNumber : valorTotalNumber); const valorFinal = valorTotalNumber - descontoNumber; const dataVendaNormalizada = normalizeToBrazilianTimestamp(data_venda); // Validar estoque antes de processar a venda if (itens && itens.length > 0) { for (const item of itens) { const produtoVariacaoId = resolveProdutoVariacaoId(item); if (produtoVariacaoId) { const { data: variacao, error: variacaoError } = await supabase .from('produto_variacoes') .select('quantidade, tamanho, cor, produtos(nome, marca)') .eq('id', produtoVariacaoId) .single(); if (variacaoError) { throw new Error(`Erro ao verificar estoque: ${variacaoError.message}`); } if (!variacao || variacao.quantidade < parseInt(item.quantidade)) { const produtoNome = variacao?.produtos ? `${variacao.produtos.marca} - ${variacao.produtos.nome}` : 'Produto'; const variacaoInfo = variacao ? `${variacao.tamanho} - ${variacao.cor}` : 'Variação'; const estoqueDisponivel = variacao?.quantidade || 0; throw new Error( `Estoque insuficiente para ${produtoNome} (${variacaoInfo}). Estoque disponível: ${estoqueDisponivel}, solicitado: ${item.quantidade}` ); } } } } // Inserir venda const { data: vendaData, error: vendaError } = await supabase .from('vendas') .insert([ { id_venda, cliente_id: cliente_id || null, tipo_pagamento, valor_total: valorTotalNumber, desconto: descontoNumber, parcelas: parcelasNumber, valor_parcela: valorParcelaNumber, data_venda: dataVendaNormalizada, data_primeiro_vencimento: data_primeiro_vencimento || getDateInBrazilYYYYMMDD(), data_vencimento: tipo_pagamento === 'prazo' ? data_vencimento || getDateInBrazilYYYYMMDD() : null, observacoes: observacoes || null } ]) .select(); if (vendaError) throw vendaError; const vendaRegistro = vendaData[0]; const vendaId = vendaRegistro.id; let clienteInfo = null; if (cliente_id) { const { data: clienteConsultado, error: clienteConsultaError } = await supabase .from('clientes') .select('nome_completo, whatsapp, telefone, email') .eq('id', cliente_id) .single(); if (clienteConsultaError) { console.error('Erro ao buscar informações do cliente:', clienteConsultaError); } else { clienteInfo = clienteConsultado; } } let pixAuto = null; if (tipo_pagamento === 'vista' && valorFinal > 0) { try { const nomeClientePix = clienteInfo?.nome_completo || 'Cliente'; const [primeiroNome, ...restanteNome] = nomeClientePix.split(' ').filter(Boolean); const payer = { email: clienteInfo?.email || 'cliente@liberikids.com', first_name: primeiroNome || 'Cliente', last_name: restanteNome.join(' ') || 'Liberi Kids', identification: { type: 'CPF', number: '00000000000' } }; const pixPayment = await mercadoPagoService.createPixPayment({ transaction_amount: valorFinal, description: `Venda ${vendaRegistro.id_venda || vendaId}`, payment_method_id: 'pix', payer, external_reference: vendaRegistro.id_venda || vendaId }); pixAuto = { success: true, payment_id: pixPayment.id, qr_code: pixPayment.point_of_interaction?.transaction_data?.qr_code, qr_code_base64: pixPayment.point_of_interaction?.transaction_data?.qr_code_base64, pix_copy_paste: pixPayment.point_of_interaction?.transaction_data?.qr_code, transaction_amount: pixPayment.transaction_amount, expiration_date: pixPayment.date_of_expiration }; const { error: pixUpdateError } = await supabase .from('vendas') .update({ pix_payment_id: pixAuto.payment_id, pix_qr_code: pixAuto.pix_copy_paste, status_pagamento: 'pendente', metodo_pagamento: 'pix' }) .eq('id', vendaId); if (pixUpdateError) { console.error('Erro ao salvar PIX automático na venda:', pixUpdateError); } else { vendaRegistro.pix_payment_id = pixAuto.payment_id; vendaRegistro.pix_qr_code = pixAuto.pix_copy_paste; vendaRegistro.status_pagamento = 'pendente'; vendaRegistro.metodo_pagamento = 'pix'; } } catch (pixError) { console.error('Erro ao gerar PIX automático da venda:', pixError); } } // Inserir parcelas individuais se for parcelado if (tipo_pagamento === 'parcelado' && parcelas_detalhes && parcelas_detalhes.length > 0) { const parcelasVenda = parcelas_detalhes.map((parcela) => ({ venda_id: vendaId, numero_parcela: parcela.numero, valor: parseFloat(parcela.valor), data_vencimento: parcela.data_vencimento, status: 'pendente' })); const { error: parcelasError } = await supabase .from('venda_parcelas') .insert(parcelasVenda); if (parcelasError) { console.error('Erro ao inserir parcelas:', parcelasError); // Não falhar a venda se parcelas não forem salvas } } // Inserir itens da venda if (itens && itens.length > 0) { const itensVenda = itens.map((item) => { const produtoVariacaoId = resolveProdutoVariacaoId(item); return { venda_id: vendaId, produto_id: item.produto_id || null, produto_variacao_id: produtoVariacaoId, quantidade: parseInt(item.quantidade), valor_unitario: parseFloat(item.valor_unitario), valor_total: parseFloat(item.valor_total) }; }); const { error: itensError } = await supabase .from('venda_itens') .insert(itensVenda); if (itensError) throw itensError; for (const item of itens) { const produtoVariacaoId = resolveProdutoVariacaoId(item); if (produtoVariacaoId) { const { data: variacaoAtual, error: buscaError } = await supabase .from('produto_variacoes') .select('quantidade') .eq('id', produtoVariacaoId) .single(); if (buscaError) { console.error('Erro ao buscar estoque da variação:', buscaError); throw new Error(`Erro ao atualizar estoque: ${buscaError.message}`); } if (variacaoAtual) { const novoEstoque = (variacaoAtual.quantidade || 0) - parseInt(item.quantidade); const { error: updateError } = await supabase .from('produto_variacoes') .update({ quantidade: Math.max(0, novoEstoque) }) .eq('id', produtoVariacaoId); if (updateError) { console.error('Erro ao atualizar estoque:', updateError); throw new Error(`Erro ao atualizar estoque: ${updateError.message}`); } console.log( `✅ Estoque atualizado: Variação ${produtoVariacaoId}, Novo estoque: ${Math.max(0, novoEstoque)}` ); } } } } let whatsappAuto = { success: false }; if (clienteInfo) { try { const telefoneCliente = clienteInfo.whatsapp || clienteInfo.telefone; if (telefoneCliente) { const valorFormatado = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Math.max(valorFinal, 0)); const dataFormatada = formatDateToBrazilHuman(dataVendaNormalizada); const nomeCliente = clienteInfo.nome_completo || 'Cliente'; let mensagemAutomatica = ''; let infoVencimento = ''; if (tipo_pagamento === 'parcelado' && parcelasNumber > 1) { const valorParcela = valorFinal / parcelasNumber; const valorParcelaFormatado = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(valorParcela); if (parcelas_detalhes && parcelas_detalhes.length > 0) { infoVencimento = '\n\n📅 Vencimentos:'; parcelas_detalhes.forEach((parcela, index) => { const dataVenc = formatDateToBrazilHuman(parcela.data_vencimento); const valorParcelaIndividual = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(parcela.valor); infoVencimento += `\n ${index + 1}ª parcela: ${dataVenc} - ${valorParcelaIndividual}`; }); } mensagemAutomatica = `Olá ${nomeCliente}! 👋 Sua compra foi registrada com sucesso! 💙 Confira os detalhes abaixo: 📅 Data da compra: ${dataFormatada} 💰 Valor total: ${valorFormatado} 💳 Pagamento: ${parcelasNumber}x de ${valorParcelaFormatado}${infoVencimento} Agradecemos pela sua preferência! 😊 Conte sempre com a Liberi Kids - Moda Infantil 👕👗`; } else if (tipo_pagamento === 'prazo') { if (data_vencimento) { const dataVencFormatada = formatDateToBrazilHuman(data_vencimento); infoVencimento = `\n📆 Vencimento: ${dataVencFormatada}`; } mensagemAutomatica = `Olá ${nomeCliente}! 👋 Sua compra foi registrada com sucesso! 💙 Confira os detalhes abaixo: 📅 Data da compra: ${dataFormatada} 💰 Valor total: ${valorFormatado} 💳 Pagamento: A prazo${infoVencimento} Agradecemos pela sua preferência! 😊 Conte sempre com a Liberi Kids - Moda Infantil 👕👗`; } else { mensagemAutomatica = `Olá ${nomeCliente}! 👋 Sua compra foi registrada com sucesso! 💙 Confira os detalhes abaixo: 📅 Data da compra: ${dataFormatada} 💰 Valor total: ${valorFormatado} 💳 Pagamento: À vista`; if (pixAuto?.pix_copy_paste) { mensagemAutomatica += ` 💸 Faça o pagamento via PIX usando o código abaixo (ou o QR Code em anexo): ${pixAuto.pix_copy_paste}`; } mensagemAutomatica += ` Agradecemos pela sua preferência! 😊 Conte sempre com a Liberi Kids - Moda Infantil 👕👗`; } const whatsappPayload = tipo_pagamento === 'vista' && pixAuto?.qr_code_base64 ? { telefone: telefoneCliente, mensagem: null, media: { base64: pixAuto.qr_code_base64, fileName: `pix_venda_${vendaRegistro.id_venda || vendaId}.png`, caption: mensagemAutomatica }, clienteNome: nomeCliente, salvarHistorico: true } : { telefone: telefoneCliente, mensagem: mensagemAutomatica, clienteNome: nomeCliente, salvarHistorico: true }; const envio = await sendWhatsappMessage(whatsappPayload); whatsappAuto = { success: true, data: envio.data }; } } catch (whatsError) { console.error('Erro ao enviar mensagem automática de WhatsApp:', whatsError); whatsappAuto = { success: false, error: whatsError.message }; } } res.json({ id: vendaId, venda: { ...vendaRegistro, valor_total: valorTotalNumber, desconto: descontoNumber, valor_final: valorFinal, cliente_id: cliente_id || null, cliente_nome: clienteInfo?.nome_completo || null, cliente_whatsapp: clienteInfo?.whatsapp || null, cliente_telefone: clienteInfo?.telefone || null }, message: 'Venda registrada com sucesso!', whatsapp: whatsappAuto, pix: pixAuto }); } catch (error) { console.error('Erro ao registrar venda:', error); res.status(500).json({ error: error.message }); } }); app.put('/api/vendas/:id', async (req, res) => { try { const { id } = req.params; const { cliente_id, tipo_pagamento, valor_total, desconto, parcelas, valor_parcela, data_venda, data_primeiro_vencimento, observacoes } = req.body; const dadosAtualizados = { cliente_id: cliente_id || null, tipo_pagamento, valor_total: parseFloat(valor_total), desconto: parseFloat(desconto) || 0, parcelas: parseInt(parcelas) || 1, valor_parcela: parseFloat(valor_parcela) || 0, data_primeiro_vencimento, observacoes }; if (data_venda) { dadosAtualizados.data_venda = normalizeToBrazilianTimestamp(data_venda); } const { error } = await supabase .from('vendas') .update(dadosAtualizados) .eq('id', id); if (error) throw error; res.json({ message: 'Venda atualizada com sucesso!' }); } catch (error) { console.error('Erro ao atualizar venda:', error); res.status(500).json({ error: error.message }); } }); app.delete('/api/vendas/:id', async (req, res) => { try { const { id } = req.params; const { error } = await supabase .from('vendas') .delete() .eq('id', id); if (error) throw error; res.json({ message: 'Venda excluída com sucesso!' }); } catch (error) { console.error('Erro ao excluir venda:', error); res.status(500).json({ error: error.message }); } }); // === PARCELAS DE VENDAS === app.get('/api/vendas/:id/parcelas', async (req, res) => { try { const { id } = req.params; const { data, error } = await supabase .from('venda_parcelas') .select('*') .eq('venda_id', id) .order('numero_parcela', { ascending: true }); if (error) throw error; res.json(data || []); } catch (error) { console.error('Erro ao buscar parcelas:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/parcelas/:id/gerar-pix', async (req, res) => { try { const { id } = req.params; const { venda_id: fallbackVendaId, numero_parcela: fallbackNumeroParcela } = req.body || {}; console.log('🔍 Gerando PIX para parcela:', { id, fallbackVendaId, fallbackNumeroParcela }); const parcelasSelect = ` *, vendas ( id_venda, cliente_id, clientes ( nome_completo, email, whatsapp, telefone ) ) `; let parcela = null; let parcelaError = null; if (id && id !== 'undefined' && id !== 'null') { console.log('📝 Buscando parcela por ID:', id); const resultadoPorId = await supabase .from('venda_parcelas') .select(parcelasSelect) .eq('id', id) .single(); console.log('📊 Resultado da busca:', { error: resultadoPorId.error, hasData: !!resultadoPorId.data }); if (!resultadoPorId.error && resultadoPorId.data) { parcela = resultadoPorId.data; } else if (resultadoPorId.error && resultadoPorId.error.code !== 'PGRST116') { parcelaError = resultadoPorId.error; console.error('❌ Erro na busca:', parcelaError); } } if (!parcela && fallbackVendaId && fallbackNumeroParcela) { const resultadoFallback = await supabase .from('venda_parcelas') .select(parcelasSelect) .eq('venda_id', fallbackVendaId) .eq('numero_parcela', fallbackNumeroParcela) .single(); if (!resultadoFallback.error && resultadoFallback.data) { parcela = resultadoFallback.data; } else if (resultadoFallback.error && resultadoFallback.error.code !== 'PGRST116') { parcelaError = resultadoFallback.error; } } if (!parcela) { if (parcelaError) { console.error('Erro ao buscar parcela:', parcelaError); } return res.status(404).json({ error: 'Parcela não encontrada' }); } if (parcela.status === 'pago') { return res.status(400).json({ error: 'Esta parcela já foi paga' }); } // Validar se a parcela ou venda tem valor zerado (devolução total) const valorParcela = parseFloat(parcela.valor); if (valorParcela === 0 || isNaN(valorParcela)) { return res.status(400).json({ error: 'Não é possível gerar PIX para parcelas com valor zerado. Esta venda teve devolução total.' }); } const cliente = parcela.vendas?.clientes; // Gerar PIX via MercadoPago const pixData = await mercadoPagoService.createPixPayment({ transaction_amount: parseFloat(parcela.valor), description: `Parcela ${parcela.numero_parcela} - Venda ${parcela.vendas?.id_venda || parcela.venda_id}`, payment_method_id: 'pix', payer: { email: cliente?.email || 'cliente@liberikids.com', first_name: cliente?.nome_completo?.split(' ')[0] || 'Cliente', last_name: cliente?.nome_completo?.split(' ').slice(1).join(' ') || 'Liberi Kids', identification: { type: 'CPF', number: '00000000000' } } }); // Atualizar parcela com dados do PIX await supabase .from('venda_parcelas') .update({ pix_payment_id: pixData.id, pix_qr_code: pixData.point_of_interaction?.transaction_data?.qr_code, pix_qr_code_base64: pixData.point_of_interaction?.transaction_data?.qr_code_base64 }) .eq('id', parcela.id); res.json({ success: true, payment_id: pixData.id, qr_code: pixData.point_of_interaction?.transaction_data?.qr_code, qr_code_base64: pixData.point_of_interaction?.transaction_data?.qr_code_base64, transaction_amount: pixData.transaction_amount, expiration_date: pixData.date_of_expiration, pix_copy_paste: pixData.point_of_interaction?.transaction_data?.qr_code }); } catch (error) { console.error('Erro ao gerar PIX da parcela:', error); res.status(500).json({ error: error.message || 'Erro ao gerar PIX' }); } }); app.post('/api/parcelas/:id/enviar-whatsapp', async (req, res) => { try { const { id } = req.params; const { qr_code_base64, valor, telefone, cliente_nome } = req.body; if (!telefone) { return res.status(400).json({ error: 'Telefone não informado' }); } // Buscar parcela const { data: parcela } = await supabase .from('venda_parcelas') .select('numero_parcela, data_vencimento, venda_id') .eq('id', id) .single(); const valorFormatado = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(valor); const dataFormatada = formatDateToBrazilHuman(new Date(parcela.data_vencimento)); const mensagem = `Olá ${cliente_nome}! 💙 Segue o PIX para pagamento da *Parcela ${parcela.numero_parcela}*: 💰 Valor: ${valorFormatado} 📅 Vencimento: ${dataFormatada} 👇 Escaneie o QR Code abaixo ou copie o código PIX para pagar:`; // Enviar imagem do QR Code await sendWhatsappMessage({ telefone, mensagem: null, media: { base64: qr_code_base64, fileName: `pix_parcela_${parcela.numero_parcela}.png`, caption: mensagem }, clienteNome: cliente_nome, salvarHistorico: true }); res.json({ success: true, message: 'PIX enviado com sucesso!' }); } catch (error) { console.error('Erro ao enviar PIX por WhatsApp:', error); res.status(500).json({ error: error.message }); } }); app.put('/api/parcelas/:id/status', async (req, res) => { try { const { id } = req.params; const { status } = req.body; const updateData = { status }; if (status === 'pago') { updateData.data_pagamento = getDateInBrazilISO(); } const { error } = await supabase .from('venda_parcelas') .update(updateData) .eq('id', id); if (error) throw error; res.json({ message: 'Status da parcela atualizado com sucesso!' }); } catch (error) { console.error('Erro ao atualizar status da parcela:', error); res.status(500).json({ error: error.message }); } }); // === CONFIGURAÇÕES === app.get('/api/configuracoes/evolution', async (req, res) => { try { const { data, error } = await supabase .from('configuracoes') .select('valor') .eq('chave', 'evolution_api') .single(); if (error && error.code !== 'PGRST116') { // PGRST116 = no rows returned throw error; } const config = parseConfigValor(data, { enabled: false, baseUrl: '', apiKey: '', instanceName: '', webhookUrl: '' }); res.json(config); } catch (error) { console.error('Erro ao buscar configurações Evolution API:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/configuracoes/evolution', async (req, res) => { try { const config = req.body; // Usar upsert para inserir ou atualizar (usando 'chave' em vez de 'tipo') const { data, error } = await supabase .from('configuracoes') .upsert({ chave: 'evolution_api', valor: config, updated_at: getDateInBrazilISO() }, { onConflict: 'chave' }); if (error) throw error; // Avisar se ativado mas sem configuração completa if (config.enabled && (!config.baseUrl || !config.apiKey || !config.instanceName)) { res.json({ message: 'Configurações salvas! Atenção: preencha URL Base, API Key e Nome da Instância para funcionar.', warning: true }); } else { res.json({ message: 'Configurações salvas com sucesso!' }); } } catch (error) { console.error('Erro ao salvar configurações Evolution API:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/configuracoes/evolution/test', async (req, res) => { try { const { baseUrl, apiKey, instanceName } = req.body; if (!baseUrl || !apiKey || !instanceName) { return res.status(400).json({ error: 'URL Base, API Key e Nome da Instância são obrigatórios para o teste' }); } // Primeiro testar se a API está respondendo const healthUrl = `${baseUrl.replace(/\/$/, '')}`; const healthResponse = await fetch(healthUrl, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); if (!healthResponse.ok) { return res.status(400).json({ error: `Servidor Evolution API não está respondendo: ${healthResponse.status}` }); } // Testar conexão com a instância específica const instanceUrl = `${baseUrl.replace(/\/$/, '')}/instance/connectionState/${instanceName}`; const response = await fetch(instanceUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', 'apikey': apiKey } }); if (response.ok) { const instanceData = await response.json(); if (instanceData.instance) { const state = instanceData.instance.state; res.json({ success: true, message: `Conexão estabelecida! Instância '${instanceName}' encontrada (Status: ${state}).`, state: state }); } else { res.status(400).json({ error: `Instância '${instanceName}' não encontrada. Verifique o nome da instância.` }); } } else { const errorText = await response.text(); res.status(400).json({ error: `Erro na conexão com a instância: ${response.status} - ${errorText}` }); } } catch (error) { console.error('Erro ao testar conexão Evolution API:', error); res.status(500).json({ error: `Erro ao conectar com a Evolution API: ${error.message}` }); } }); // === VENDAS A PRAZO === app.get('/api/vendas/prazo', async (req, res) => { try { const { data, error } = await supabase .from('vendas') .select(` id, data_venda, valor_total, tipo_pagamento, parcelas, valor_parcela, cliente_id, clientes!inner( nome_completo, telefone, whatsapp ) `) .eq('tipo_pagamento', 'parcelado') .order('data_venda', { ascending: false }); if (error) throw error; // Calcular datas de vencimento para cada parcela const vendasComVencimentos = []; data.forEach(venda => { const dataVenda = new Date(venda.data_venda); for (let parcela = 1; parcela <= venda.parcelas; parcela++) { const dataVencimento = new Date(dataVenda); dataVencimento.setMonth(dataVencimento.getMonth() + parcela); vendasComVencimentos.push({ id: `${venda.id}_${parcela}`, venda_id: venda.id, cliente_id: venda.cliente_id, cliente_nome: venda.clientes.nome_completo, cliente_telefone: venda.clientes.telefone, cliente_whatsapp: venda.clientes.whatsapp, valor_parcela: venda.valor_parcela, parcela_atual: parcela, total_parcelas: venda.parcelas, data_vencimento: formatDateToBrazilYYYYMMDD(dataVencimento), valor_total: venda.valor_total }); } }); // Ordenar por data de vencimento vendasComVencimentos.sort((a, b) => new Date(a.data_vencimento) - new Date(b.data_vencimento)); res.json(vendasComVencimentos); } catch (error) { console.error('Erro ao buscar vendas a prazo:', error); res.status(500).json({ error: error.message }); } }); // === WHATSAPP === // Rota genérica de envio de WhatsApp app.post('/api/whatsapp/enviar', async (req, res) => { try { const { telefone, mensagem, clienteNome } = req.body; if (!telefone || !mensagem) { return res.status(400).json({ error: 'Telefone e mensagem são obrigatórios' }); } const resultado = await sendWhatsappMessage({ telefone, mensagem, clienteNome: clienteNome || 'Cliente', salvarHistorico: true }); res.json(resultado); } catch (error) { console.error('Erro ao enviar WhatsApp:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/whatsapp/enviar-cobranca', async (req, res) => { try { const { vendaId, clienteId, telefone } = req.body; if (!telefone) { return res.status(400).json({ error: 'Telefone do cliente não encontrado' }); } // Buscar configurações da Evolution API const { data: configData } = await supabase .from('configuracoes') .select('valor') .eq('tipo', 'evolution_api') .single(); const config = parseConfigValor(configData, { enabled: false, baseUrl: '', apiKey: '', instanceName: '' }); if (!config.enabled) { return res.status(400).json({ error: 'Evolution API não configurada ou desabilitada' }); } // Buscar template de mensagem personalizada const { data: templateData } = await supabase .from('configuracoes') .select('valor') .eq('tipo', 'whatsapp_template_cobranca') .single(); let mensagem = 'Olá! Este é um lembrete sobre o vencimento da sua parcela. Entre em contato conosco para mais informações.'; const templateConfig = parseConfigValor(templateData); if (templateConfig?.mensagem) { mensagem = templateConfig.mensagem; } // Formatar número de telefone const numeroFormatado = telefone.replace(/\D/g, ''); const numeroCompleto = numeroFormatado.startsWith('55') ? numeroFormatado : `55${numeroFormatado}`; // Enviar mensagem via Evolution API const evolutionUrl = `${config.baseUrl}/message/sendText/${config.instanceName}`; const response = await fetch(evolutionUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': config.apiKey }, body: JSON.stringify({ number: numeroCompleto, text: mensagem }) }); if (response.ok) { res.json({ success: true, message: 'Mensagem enviada com sucesso!' }); } else { const errorText = await response.text(); res.status(400).json({ error: `Erro ao enviar mensagem: ${errorText}` }); } } catch (error) { console.error('Erro ao enviar WhatsApp:', error); res.status(500).json({ error: error.message }); } }); // === TESTE CONFIGURAÇÕES === app.get('/api/test-configuracoes', async (req, res) => { try { console.log('Testando conexão com tabela configuracoes...'); const { data, error } = await supabase .from('configuracoes') .select('*') .limit(1); console.log('Resultado do teste:', { data, error }); if (error) { return res.status(500).json({ error: 'Erro na tabela configuracoes', details: error.message, code: error.code }); } res.json({ success: true, message: 'Tabela configuracoes acessível', data: data }); } catch (error) { console.error('Erro no teste:', error); res.status(500).json({ error: error.message }); } }); // === CONFIGURAÇÕES WHATSAPP ALERTAS === app.get('/api/configuracoes/whatsapp-alertas', async (req, res) => { try { const { data, error } = await supabase .from('configuracoes') .select('valor') .eq('chave', 'whatsapp_alertas') .single(); if (error && error.code !== 'PGRST116') { throw error; } const config = parseConfigValor(data, { alertaVencimento: false, diasAntecedencia1: 3, mensagemAntecedencia1: 'Olá {cliente}! Lembramos que sua parcela no valor de R$ {valor} vence {quando}. Entre em contato conosco para mais informações. Obrigado!', diasAntecedencia2: 0, mensagemAntecedencia2: 'Olá {cliente}! Sua parcela no valor de R$ {valor} vence hoje ({quando}). Entre em contato conosco para regularizar. Obrigado!', diasAposVencimento: 3, mensagemAposVencimento: 'Olá {cliente}! Sua parcela no valor de R$ {valor} venceu em {quando}. Por favor, entre em contato conosco para regularizar. Obrigado!' }); res.json(config); } catch (error) { console.error('Erro ao buscar configurações WhatsApp alertas:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/configuracoes/whatsapp-alertas', async (req, res) => { try { const config = req.body; console.log('📥 Recebendo configuração de alertas WhatsApp:', JSON.stringify(config, null, 2)); // Usar upsert para inserir ou atualizar const { data, error } = await supabase .from('configuracoes') .upsert({ chave: 'whatsapp_alertas', valor: config, updated_at: getDateInBrazilISO() }, { onConflict: 'chave' }) .select(); if (error) { console.error('❌ Erro do Supabase:', error); throw error; } console.log('✅ Configuração salva com sucesso:', data); res.json({ message: 'Configurações de alertas salvas com sucesso!' }); } catch (error) { console.error('💥 Erro ao salvar configurações WhatsApp alertas:', error); console.error('Detalhes do erro:', { message: error.message, code: error.code, details: error.details, hint: error.hint }); res.status(500).json({ error: error.message, details: error.details, hint: error.hint }); } }); // === EMPRÉSTIMOS === app.get('/api/emprestimos', async (req, res) => { try { const { data, error } = await supabase .from('emprestimos') .select(` *, emprestimo_itens( id, quantidade, observacoes, created_at ) `) .order('created_at', { ascending: false }); if (error) throw error; res.json(data); } catch (error) { console.error('Erro ao listar empréstimos:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/emprestimos', async (req, res) => { try { const { pessoa, valor_total, descricao, data_emprestimo } = req.body; const { data, error } = await supabase .from('emprestimos') .insert([{ pessoa: pessoa || 'Maiara', valor_total: parseFloat(valor_total), valor_restante: parseFloat(valor_total), descricao, data_emprestimo: data_emprestimo || getDateInBrazilYYYYMMDD() }]) .select(); if (error) throw error; res.json({ id: data[0].id, message: 'Empréstimo criado com sucesso!' }); } catch (error) { console.error('Erro ao criar empréstimo:', error); res.status(500).json({ error: error.message }); } }); app.put('/api/emprestimos/:id', async (req, res) => { try { const { id } = req.params; const { pessoa, valor_total, descricao, data_emprestimo, status } = req.body; const { error } = await supabase .from('emprestimos') .update({ pessoa, valor_total: parseFloat(valor_total), descricao, data_emprestimo, status, updated_at: getDateInBrazilISO() }) .eq('id', id); if (error) throw error; res.json({ message: 'Empréstimo atualizado com sucesso!' }); } catch (error) { console.error('Erro ao atualizar empréstimo:', error); res.status(500).json({ error: error.message }); } }); app.delete('/api/emprestimos/:id', async (req, res) => { try { const { id } = req.params; const { error } = await supabase .from('emprestimos') .delete() .eq('id', id); if (error) throw error; res.json({ message: 'Empréstimo excluído com sucesso!' }); } catch (error) { console.error('Erro ao excluir empréstimo:', error); res.status(500).json({ error: error.message }); } }); // === DEVOLUÇÕES DE EMPRÉSTIMOS === app.get('/api/emprestimos/:id/devolucoes', async (req, res) => { try { const { id } = req.params; const { data, error } = await supabase .from('emprestimo_itens') .select('*') .eq('emprestimo_id', id) .order('data_devolucao', { ascending: false }); if (error) throw error; res.json(data); } catch (error) { console.error('Erro ao listar devoluções:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/emprestimos/:id/devolucoes', async (req, res) => { try { const { id } = req.params; const { valor_devolvido, data_devolucao, observacoes } = req.body; const { data, error } = await supabase .from('emprestimo_itens') .insert([{ emprestimo_id: id, valor_devolvido: parseFloat(valor_devolvido), data_devolucao: data_devolucao || getDateInBrazilYYYYMMDD(), observacoes }]) .select(); if (error) throw error; res.json({ id: data[0].id, message: 'Devolução registrada com sucesso!' }); } catch (error) { console.error('Erro ao registrar devolução:', error); res.status(500).json({ error: error.message }); } }); app.delete('/api/emprestimos/:emprestimoId/devolucoes/:devolucaoId', async (req, res) => { try { const { devolucaoId } = req.params; const { error } = await supabase .from('emprestimo_itens') .delete() .eq('id', devolucaoId); if (error) throw error; res.json({ message: 'Devolução excluída com sucesso!' }); } catch (error) { console.error('Erro ao excluir devolução:', error); res.status(500).json({ error: error.message }); } }); // === DASHBOARD === app.get('/api/dashboard', async (req, res) => { try { const hoje = new Date(); const inicioMesDate = new Date(hoje.getFullYear(), hoje.getMonth(), 1); const fimMesDate = new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0); const formatadorISO = new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Sao_Paulo' }); const inicioMes = formatadorISO.format(inicioMesDate); const fimMes = formatadorISO.format(fimMesDate); // 1. VENDAS DO MÊS (com custos dos produtos) const { data: vendasMes } = await supabase .from('vendas') .select(` valor_total, desconto, tipo_pagamento, data_venda, venda_itens( quantidade, valor_unitario, produtos(valor_custo) ) `) .gte('data_venda', inicioMes) .lte('data_venda', fimMes); // 2. DESPESAS DO MÊS const { data: despesasMes, error: despesasError } = await supabase .from('despesas') .select('valor, data_despesa') .gte('data_despesa', inicioMes) .lte('data_despesa', fimMes); if (despesasError) { console.error('Erro ao buscar despesas:', despesasError); } // 3. EMPRÉSTIMOS EM ABERTO const { data: emprestimos } = await supabase .from('emprestimos') .select('valor_total, valor_restante, status'); // 4. VENDAS A PRAZO PENDENTES const hojeStr = formatadorISO.format(new Date()); const { data: vendasPrazo } = await supabase .from('vendas') .select('valor_total, desconto, data_primeiro_vencimento, cliente_id, clientes(nome_completo)') .eq('tipo_pagamento', 'prazo') .gte('data_primeiro_vencimento', hojeStr); // 5. VENDAS PARCELADAS PENDENTES const { data: vendasParceladas } = await supabase .from('vendas') .select('valor_total, desconto, parcelas, valor_parcela, data_primeiro_vencimento, cliente_id, clientes(nome_completo)') .eq('tipo_pagamento', 'parcelado'); // CÁLCULOS FINANCEIROS let receitaBruta = 0; let custosProdutos = 0; let totalVendas = 0; vendasMes?.forEach(venda => { const valorFinal = parseFloat(venda.valor_total) - parseFloat(venda.desconto || 0); receitaBruta += valorFinal; totalVendas++; // Calcular custos dos produtos vendidos venda.venda_itens?.forEach(item => { const custoProduto = parseFloat(item.produtos?.valor_custo || 0); custosProdutos += custoProduto * parseInt(item.quantidade); }); }); const totalDespesas = despesasMes?.reduce((acc, despesa) => acc + parseFloat(despesa.valor), 0) || 0; const emprestimosAberto = emprestimos?.reduce((acc, emp) => emp.status === 'ativo' ? acc + parseFloat(emp.valor_restante) : acc, 0) || 0; const emprestimosQuitados = emprestimos?.reduce((acc, emp) => emp.status === 'quitado' ? acc + parseFloat(emp.valor_total) : acc, 0) || 0; // LUCRO REAL = Receita Bruta - Custos dos Produtos - Despesas const lucroReal = receitaBruta - custosProdutos - totalDespesas; const margemLucro = receitaBruta > 0 ? ((lucroReal / receitaBruta) * 100) : 0; // VENDAS A PRAZO E RECEBIMENTOS const vendasPrazoTotal = vendasPrazo?.reduce((acc, venda) => acc + (parseFloat(venda.valor_total) - parseFloat(venda.desconto || 0)), 0) || 0; // Calcular parcelas pendentes (simplificado - assumindo parcelas mensais) const parcelasPendentes = vendasParceladas?.map(venda => { const valorFinal = parseFloat(venda.valor_total) - parseFloat(venda.desconto || 0); const valorParcela = valorFinal / parseInt(venda.parcelas); const dataVencimento = new Date(venda.data_primeiro_vencimento); return { cliente: venda.clientes?.nome_completo, valor: valorParcela, parcelas: venda.parcelas, proximoVencimento: dataVencimento }; }) || []; // ESTATÍSTICAS GERAIS const [produtos, clientes, fornecedores, estoque] = await Promise.all([ supabase.from('produtos').select('id', { count: 'exact' }), supabase.from('clientes').select('id', { count: 'exact' }), supabase.from('fornecedores').select('id', { count: 'exact' }), supabase.from('produto_variacoes').select('quantidade') ]); const estoqueTotal = estoque.data?.reduce((acc, item) => acc + (item.quantidade || 0), 0) || 0; res.json({ // CONTABILIDADE COMPLETA contabilidade: { receitaBruta, custosProdutos, totalDespesas, lucroReal, margemLucro: parseFloat(margemLucro.toFixed(2)) }, // RESUMO FINANCEIRO resumoFinanceiro: { receitasMes: receitaBruta, despesasMes: totalDespesas, lucroEstimado: lucroReal, totalVendas }, // EMPRÉSTIMOS emprestimos: { totalAberto: emprestimosAberto, totalQuitado: emprestimosQuitados, quantidade: emprestimos?.length || 0 }, // VENDAS A PRAZO E RECEBIMENTOS vendasPrazo: { total: vendasPrazoTotal, quantidade: vendasPrazo?.length || 0, vendas: vendasPrazo?.map(v => ({ cliente: v.clientes?.nome_completo, valor: parseFloat(v.valor_total) - parseFloat(v.desconto || 0), vencimento: v.data_primeiro_vencimento })) || [] }, // PARCELAS PENDENTES parcelasPendentes: { quantidade: parcelasPendentes.length, parcelas: parcelasPendentes }, // ESTATÍSTICAS GERAIS estatisticas: { totalProdutos: produtos.count || 0, totalClientes: clientes.count || 0, totalFornecedores: fornecedores.count || 0, estoqueTotal } }); } catch (error) { console.error('Erro ao buscar dados do dashboard:', error); res.status(500).json({ error: error.message }); } }); // ===================================================== // DEVOLUÇÕES // ===================================================== // Função para limpar devoluções duplicadas app.post('/api/devolucoes/limpar-duplicadas', async (req, res) => { try { // Buscar devoluções duplicadas (mesmo venda_id, item_id, data) const { data: devolucoes, error } = await supabase .from('devolucoes') .select('*') .order('data_devolucao', { ascending: false }); if (error) throw error; const duplicadas = []; const processadas = new Set(); for (const devolucao of devolucoes) { const chave = `${devolucao.venda_id}-${devolucao.item_id}-${devolucao.data_devolucao}`; if (processadas.has(chave)) { duplicadas.push(devolucao.id); } else { processadas.add(chave); } } // Remover duplicadas if (duplicadas.length > 0) { const { error: deleteError } = await supabase .from('devolucoes') .delete() .in('id', duplicadas); if (deleteError) throw deleteError; } res.json({ success: true, message: `${duplicadas.length} devoluções duplicadas removidas`, removidas: duplicadas.length }); } catch (error) { console.error('Erro ao limpar devoluções duplicadas:', error); res.status(500).json({ error: error.message }); } }); // Listar vendas para devolução (últimos 30 dias) - VERSÃO CORRIGIDA app.get('/api/devolucoes/vendas', async (req, res) => { try { const dataLimite = new Date(); dataLimite.setDate(dataLimite.getDate() - 30); const dataLimiteStr = new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Sao_Paulo' }).format(dataLimite); const { data, error } = await supabase .from('vendas') .select(` *, clientes(nome_completo, whatsapp, telefone), venda_itens( *, produtos(nome, marca, foto_principal, id_produto), produto_variacoes(tamanho, cor) ) `) .gte('data_venda', dataLimiteStr) .order('data_venda', { ascending: false }); if (error) throw error; // Buscar todas as devoluções para filtrar itens já devolvidos const { data: todasDevolucoes, error: devolucaoError } = await supabase .from('devolucoes') .select('venda_id, item_id, quantidade_devolvida'); if (devolucaoError) { console.error('Erro ao buscar devoluções:', devolucaoError); } const vendasProcessadas = await Promise.all(data.map(async (venda) => { const itensDisponiveis = []; for (const item of venda.venda_itens || []) { // Calcular quantidade já devolvida deste item const devolucoesDeste = todasDevolucoes?.filter(dev => dev.venda_id === venda.id && dev.item_id === item.id ) || []; const quantidadeDevolvida = devolucoesDeste.reduce((total, dev) => total + parseInt(dev.quantidade_devolvida), 0 ); const quantidadeDisponivel = parseInt(item.quantidade) - quantidadeDevolvida; // Só incluir se ainda há quantidade disponível para devolução if (quantidadeDisponivel > 0) { itensDisponiveis.push({ ...item, produto_nome: `${item.produtos?.marca} - ${item.produtos?.nome}`, produto_codigo: item.produtos?.id_produto, produto_foto: item.produtos?.foto_principal, variacao_info: `${item.produto_variacoes?.tamanho} - ${item.produto_variacoes?.cor}`, quantidade_disponivel: quantidadeDisponivel, quantidade_original: parseInt(item.quantidade), quantidade_devolvida: quantidadeDevolvida, pode_devolver: true }); } } return { ...venda, cliente_nome: venda.clientes?.nome_completo || 'Cliente não informado', status: venda.status || 'concluida', itens: itensDisponiveis, tem_itens_disponiveis: itensDisponiveis.length > 0 }; })); // Filtrar apenas vendas que ainda têm itens disponíveis para devolução const vendasComItens = vendasProcessadas.filter(venda => venda.tem_itens_disponiveis); res.json(vendasComItens); } catch (error) { console.error('Erro ao listar vendas para devolução:', error); res.status(500).json({ error: error.message }); } }); // Buscar produtos para troca (apenas com estoque disponível) app.get('/api/devolucoes/produtos', async (req, res) => { try { const { data, error } = await supabase .from('produtos') .select(` *, produto_variacoes(*) `) .order('nome'); if (error) throw error; const produtos = data.map(produto => ({ ...produto, variacoes: produto.produto_variacoes?.filter(variacao => variacao.quantidade > 0 ) || [] })).filter(produto => produto.variacoes.length > 0); // Só produtos com pelo menos uma variação em estoque res.json(produtos); } catch (error) { console.error('Erro ao buscar produtos para troca:', error); res.status(500).json({ error: error.message }); } }); // Processar devolução ou troca - VERSÃO CORRIGIDA app.post('/api/devolucoes', async (req, res) => { try { const { venda_id, itens_devolucao, itens_troca, motivo, tipo_operacao } = req.body; console.log(`🔄 Iniciando ${tipo_operacao} para venda ${venda_id}`); // 1. Buscar dados da venda com itens const { data: venda, error: vendaError } = await supabase .from('vendas') .select(` *, venda_itens(*) `) .eq('id', venda_id) .single(); if (vendaError) throw vendaError; let valorTotalDevolucao = 0; let valorTotalTroca = 0; let resultadoOperacao = ''; // 2. Processar itens devolvidos if (itens_devolucao && itens_devolucao.length > 0) { for (const itemDevolucao of itens_devolucao) { const { item_id, quantidade_devolvida } = itemDevolucao; // Buscar item original const itemOriginal = venda.venda_itens.find(item => item.id === item_id); if (!itemOriginal) { throw new Error(`Item ${item_id} não encontrado na venda`); } // VERIFICAR SE JÁ FOI DEVOLVIDO COMPLETAMENTE const { data: devolucaoExistente } = await supabase .from('devolucoes') .select('quantidade_devolvida') .eq('venda_id', venda_id) .eq('item_id', item_id); const quantidadeJaDevolvida = devolucaoExistente?.reduce((total, dev) => total + parseInt(dev.quantidade_devolvida), 0) || 0; const quantidadeDisponivel = parseInt(itemOriginal.quantidade) - quantidadeJaDevolvida; if (quantidadeDisponivel <= 0) { throw new Error(`Este item já foi completamente devolvido/trocado anteriormente`); } if (parseInt(quantidade_devolvida) > quantidadeDisponivel) { throw new Error(`Quantidade de devolução (${quantidade_devolvida}) maior que disponível (${quantidadeDisponivel})`); } // Calcular valor proporcional da devolução const valorUnitario = parseFloat(itemOriginal.valor_unitario); const valorDevolucao = valorUnitario * parseInt(quantidade_devolvida); valorTotalDevolucao += valorDevolucao; // SEMPRE retornar ao estoque (tanto devolução quanto troca) const produtoVariacaoIdOriginal = resolveProdutoVariacaoId(itemOriginal); if (produtoVariacaoIdOriginal) { const { data: variacaoAtual, error: buscaError } = await supabase .from('produto_variacoes') .select('quantidade') .eq('id', produtoVariacaoIdOriginal) .single(); if (!buscaError && variacaoAtual) { const novoEstoque = (variacaoAtual.quantidade || 0) + parseInt(quantidade_devolvida); const { error: estoqueError } = await supabase .from('produto_variacoes') .update({ quantidade: novoEstoque }) .eq('id', produtoVariacaoIdOriginal); if (!estoqueError) { console.log(`✅ Produto devolvido ao estoque: +${quantidade_devolvida} unidades (total: ${novoEstoque})`); } } } // ATUALIZAR ITEM DA VENDA - ZERAR QUANTIDADE DEVOLVIDA const novaQuantidadeItem = parseInt(itemOriginal.quantidade) - parseInt(quantidade_devolvida); const novoValorTotal = novaQuantidadeItem * parseFloat(itemOriginal.valor_unitario); const { error: updateItemError } = await supabase .from('venda_itens') .update({ quantidade: novaQuantidadeItem, valor_total: novoValorTotal }) .eq('id', item_id); if (updateItemError) { console.error('Erro ao atualizar item da venda:', updateItemError); } else { console.log(`✅ Item da venda atualizado: quantidade ${novaQuantidadeItem}, valor R$ ${novoValorTotal.toFixed(2)}`); } // Registrar devolução no histórico const { error: devolucaoError } = await supabase .from('devolucoes') .insert({ venda_id: venda_id, item_id: item_id, quantidade_devolvida: parseInt(quantidade_devolvida), valor_devolucao: valorDevolucao, motivo: motivo || 'Não informado', data_devolucao: getDateInBrazilISO(), tipo_operacao: tipo_operacao || 'devolucao' }); if (devolucaoError) { console.error('Erro ao registrar devolução:', devolucaoError); } else { console.log(`✅ Devolução registrada no histórico`); } } } // 3. Validar estoque para itens de troca if (itens_troca && itens_troca.length > 0) { for (const itemTroca of itens_troca) { const produtoVariacaoIdTroca = resolveProdutoVariacaoId(itemTroca); const quantidade = parseInt(itemTroca.quantidade); if (produtoVariacaoIdTroca) { const { data: variacao, error: variacaoError } = await supabase .from('produto_variacoes') .select('quantidade, tamanho, cor, produtos(nome, marca)') .eq('id', produtoVariacaoIdTroca) .single(); if (variacaoError) { throw new Error(`Erro ao verificar estoque para troca: ${variacaoError.message}`); } if (!variacao || variacao.quantidade < quantidade) { const produtoNome = variacao?.produtos ? `${variacao.produtos.marca} - ${variacao.produtos.nome}` : 'Produto'; const variacaoInfo = variacao ? `${variacao.tamanho} - ${variacao.cor}` : 'Variação'; const estoqueDisponivel = variacao?.quantidade || 0; throw new Error(`Estoque insuficiente para troca de ${produtoNome} (${variacaoInfo}). Estoque disponível: ${estoqueDisponivel}, solicitado: ${quantidade}`); } } } } // 4. Processar itens de troca if (itens_troca && itens_troca.length > 0) { for (const itemTroca of itens_troca) { const produtoVariacaoIdTroca = resolveProdutoVariacaoId(itemTroca); const { produto_id, quantidade, valor_unitario } = itemTroca; const quantidadeInt = parseInt(quantidade) || 0; const valorTroca = parseFloat(valor_unitario) * quantidadeInt; valorTotalTroca += valorTroca; if (produtoVariacaoIdTroca) { const { data: variacaoAtual, error: buscaError } = await supabase .from('produto_variacoes') .select('quantidade') .eq('id', produtoVariacaoIdTroca) .single(); if (buscaError) { console.error('Erro ao buscar estoque atual da troca:', buscaError); } else { const novoEstoque = (variacaoAtual.quantidade || 0) - quantidadeInt; const { error: estoqueError } = await supabase .from('produto_variacoes') .update({ quantidade: Math.max(0, novoEstoque) }) .eq('id', produtoVariacaoIdTroca); if (estoqueError) { console.error('Erro ao atualizar estoque da troca:', estoqueError); } else { console.log(`✅ Estoque reduzido na troca: -${quantidadeInt} unidades (novo total: ${novoEstoque})`); } } } const { error: itemError } = await supabase .from('venda_itens') .insert({ venda_id: venda_id, produto_id: produto_id, produto_variacao_id: produtoVariacaoIdTroca, quantidade: quantidadeInt, valor_unitario: parseFloat(valor_unitario), valor_total: valorTroca }); if (itemError) { console.error('Erro ao adicionar item de troca:', itemError); } else { console.log(`✅ Item de troca adicionado à venda: ${quantidadeInt} unidades`); } } } // 5. Atualizar valor total da venda const diferencaValor = valorTotalTroca - valorTotalDevolucao; const novoValorTotal = Math.max(0, parseFloat(venda.valor_total) + diferencaValor); const { error: updateVendaError } = await supabase .from('vendas') .update({ valor_total: novoValorTotal, valor_parcela: venda.tipo_pagamento === 'parcelado' ? Math.max(0, (novoValorTotal - parseFloat(venda.desconto || 0)) / parseInt(venda.parcelas)) : venda.valor_parcela }) .eq('id', venda_id); // 6. Recalcular parcelas se a venda for parcelada if (venda.tipo_pagamento === 'parcelado') { console.log(`🔄 Recalculando parcelas para novo valor total: R$ ${novoValorTotal.toFixed(2)}`); // Buscar parcelas existentes const { data: parcelasExistentes, error: parcelasError } = await supabase .from('venda_parcelas') .select('*') .eq('venda_id', venda_id) .order('numero_parcela', { ascending: true }); if (!parcelasError && parcelasExistentes && parcelasExistentes.length > 0) { const numeroParcelas = parcelasExistentes.length; const valorFinal = novoValorTotal - parseFloat(venda.desconto || 0); const novoValorParcela = valorFinal / numeroParcelas; console.log(`📊 Novo valor por parcela: R$ ${novoValorParcela.toFixed(2)} (${numeroParcelas}x)`); // Atualizar cada parcela com o novo valor for (const parcela of parcelasExistentes) { const { error: updateParcelaError } = await supabase .from('venda_parcelas') .update({ valor: novoValorParcela }) .eq('id', parcela.id); if (updateParcelaError) { console.error(`Erro ao atualizar parcela ${parcela.numero_parcela}:`, updateParcelaError); } else { console.log(`✅ Parcela ${parcela.numero_parcela} atualizada: R$ ${novoValorParcela.toFixed(2)}`); } } } } // 7. Definir mensagem de resultado if (tipo_operacao === 'troca') { if (diferencaValor > 0) { resultadoOperacao = `Troca processada! Diferença a pagar: R$ ${diferencaValor.toFixed(2)}`; } else if (diferencaValor < 0) { resultadoOperacao = `Troca processada! Diferença a receber: R$ ${Math.abs(diferencaValor).toFixed(2)}`; } else { resultadoOperacao = 'Troca processada! Valores equivalentes.'; } } else { resultadoOperacao = `Devolução processada! Valor devolvido: R$ ${valorTotalDevolucao.toFixed(2)}`; } res.json({ success: true, message: resultadoOperacao, tipo_operacao: tipo_operacao || 'devolucao', valor_devolvido: valorTotalDevolucao, valor_troca: valorTotalTroca, diferenca_valor: diferencaValor, novo_valor_venda: novoValorTotal }); } catch (error) { console.error('Erro ao processar devolução:', error); res.status(500).json({ error: error.message }); } }); // Listar devoluções app.get('/api/devolucoes', async (req, res) => { try { const { data, error } = await supabase .from('devolucoes') .select(` *, vendas(id_venda, data_venda, clientes(nome_completo)), venda_itens(produtos(nome, marca), produto_variacoes(tamanho, cor)) `) .order('data_devolucao', { ascending: false }); if (error) throw error; const devolucoesProcesadas = data.map(dev => ({ ...dev, produto_info: dev.venda_itens ? { nome: `${dev.venda_itens.produtos?.marca} - ${dev.venda_itens.produtos?.nome}`, variacao: `${dev.venda_itens.produto_variacoes?.tamanho} - ${dev.venda_itens.produto_variacoes?.cor}` } : null, venda_info: dev.vendas ? { id_venda: dev.vendas.id_venda, data_venda: dev.vendas.data_venda, cliente_nome: dev.vendas.clientes?.nome_completo || 'Cliente não informado' } : null })); res.json(devolucoesProcesadas); } catch (error) { console.error('Erro ao listar devoluções:', error); res.status(500).json({ error: error.message }); } }); // Buscar histórico de devoluções de uma venda específica app.get('/api/devolucoes/venda/:venda_id', async (req, res) => { try { const { venda_id } = req.params; const { data, error } = await supabase .from('devolucoes') .select(` *, venda_itens( quantidade, valor_unitario, produtos(nome, marca, id_produto), produto_variacoes(tamanho, cor) ) `) .eq('venda_id', venda_id) .order('data_devolucao', { ascending: false }); if (error) throw error; const historico = data.map(dev => ({ ...dev, produto_info: dev.venda_itens ? { nome: `${dev.venda_itens.produtos?.marca} - ${dev.venda_itens.produtos?.nome}`, codigo: dev.venda_itens.produtos?.id_produto, variacao: `${dev.venda_itens.produto_variacoes?.tamanho} - ${dev.venda_itens.produto_variacoes?.cor}`, quantidade_original: dev.venda_itens.quantidade, valor_unitario_original: dev.venda_itens.valor_unitario } : null })); res.json(historico); } catch (error) { console.error('Erro ao buscar histórico de devoluções:', error); res.status(500).json({ error: error.message }); } }); // ===================================================== // ROTAS CHATGPT // ===================================================== // Buscar configurações ChatGPT app.get('/api/configuracoes/chatgpt', async (req, res) => { try { const { data, error } = await supabase .from('configuracoes') .select('valor') .eq('chave', 'chatgpt_config') .single(); if (error && error.code !== 'PGRST116') { throw error; } const config = parseConfigValor(data, { enabled: false, apiKey: '', model: 'gpt-3.5-turbo', temperature: 0.7 }); res.json(config); } catch (error) { console.error('Erro ao buscar configurações ChatGPT:', error); res.status(500).json({ error: error.message }); } }); // Salvar configurações ChatGPT app.post('/api/configuracoes/chatgpt', async (req, res) => { try { const config = req.body; const { data, error } = await supabase .from('configuracoes') .upsert({ chave: 'chatgpt_config', valor: config }, { onConflict: 'chave' }); if (error) throw error; res.json({ success: true, message: 'Configurações salvas com sucesso' }); } catch (error) { console.error('Erro ao salvar configurações ChatGPT:', error); res.status(500).json({ error: error.message }); } }); // Testar conexão ChatGPT app.post('/api/configuracoes/chatgpt/test', async (req, res) => { try { const { apiKey } = req.body; const axios = require('axios'); const response = await axios.post('https://api.openai.com/v1/chat/completions', { model: 'gpt-3.5-turbo', messages: [ { role: 'user', content: 'Teste de conexão. Responda apenas "OK".' } ], max_tokens: 10 }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); if (response.data && response.data.choices) { res.json({ success: true, message: 'Conexão com ChatGPT funcionando!' }); } else { throw new Error('Resposta inválida da API'); } } catch (error) { console.error('Erro ao testar ChatGPT:', error); res.status(500).json({ error: error.response?.data?.error?.message || error.message }); } }); // Gerar descrição de produto com ChatGPT app.post('/api/chatgpt/gerar-descricao', async (req, res) => { try { const { marca, nome, estacao, genero } = req.body; // Buscar configurações do ChatGPT const { data: configData, error: configError } = await supabase .from('configuracoes') .select('valor') .eq('tipo', 'chatgpt_config') .single(); const config = parseConfigValor(configData); if (configError || !config?.apiKey) { return res.status(400).json({ error: 'ChatGPT não configurado. Configure a API Key nas configurações.' }); } const axios = require('axios'); const prompt = `Crie uma descrição atrativa e profissional para um produto de moda infantil com as seguintes características: Marca: ${marca} Nome: ${nome} Estação: ${estacao} Gênero: ${genero} A descrição deve: - Ser concisa (máximo 3 frases) - Destacar qualidade e conforto - Ser adequada para e-commerce - Usar linguagem atrativa para pais - Não mencionar preços ou tamanhos específicos Responda apenas com a descrição, sem aspas ou formatação extra.`; const response = await axios.post('https://api.openai.com/v1/chat/completions', { model: config.model || 'gpt-3.5-turbo', messages: [ { role: 'user', content: prompt } ], max_tokens: 150, temperature: config.temperature || 0.7 }, { headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' } }); if (response.data && response.data.choices && response.data.choices[0]) { const descricao = response.data.choices[0].message.content.trim(); res.json({ descricao }); } else { throw new Error('Resposta inválida da API'); } } catch (error) { console.error('Erro ao gerar descrição:', error); res.status(500).json({ error: error.response?.data?.error?.message || error.message }); } }); // Rota temporária para adicionar campo descrição (executar uma vez) app.post('/api/admin/add-description-field', async (req, res) => { try { // Executar SQL para adicionar campo descrição const { data, error } = await supabase.rpc('exec_sql', { sql_query: 'ALTER TABLE produtos ADD COLUMN IF NOT EXISTS descricao TEXT' }); if (error) { // Tentar método alternativo usando uma query simples const { data: testData, error: testError } = await supabase .from('produtos') .select('descricao') .limit(1); if (testError && testError.message.includes('column "descricao" does not exist')) { return res.status(500).json({ error: 'Campo descrição não existe. Execute manualmente: ALTER TABLE produtos ADD COLUMN descricao TEXT;', needsManualExecution: true }); } } res.json({ success: true, message: 'Campo descrição adicionado com sucesso ou já existe' }); } catch (error) { console.error('Erro ao adicionar campo descrição:', error); res.status(500).json({ error: error.message }); } }); // === ROTA TEMPORÁRIA PARA MIGRAÇÃO === app.post('/api/admin/migrate-despesas', async (req, res) => { try { console.log('🔄 Iniciando migração da tabela despesas...'); // Verificar se as colunas já existem testando uma consulta const { data: testData, error: testError } = await supabase .from('despesas') .select('tipo_nome, fornecedor_nome') .limit(1); if (!testError) { console.log('✅ Colunas já existem, migração já foi executada'); return res.json({ success: true, message: 'Migração já foi executada anteriormente' }); } // Se chegou aqui, as colunas não existem, vamos migrar os dados existentes console.log('📦 Migrando dados existentes...'); const { data: despesasExistentes, error: fetchError } = await supabase .from('despesas') .select(` id, tipos_despesa(nome), fornecedores(nome) `); if (fetchError) { console.log('⚠️ Erro ao buscar despesas existentes:', fetchError.message); } console.log('✅ Migração concluída com sucesso!'); res.json({ success: true, message: 'Estrutura da tabela atualizada! Agora você pode usar campos de texto livre.', note: 'Você pode precisar adicionar as colunas manualmente no Supabase Dashboard.' }); } catch (error) { console.error('❌ Erro durante a migração:', error); res.status(500).json({ error: error.message }); } }); // === SETUP TABELA MENSAGENS === app.post('/api/setup/mensagens-whatsapp', async (req, res) => { try { // Tentar inserir um registro de teste para verificar se a tabela existe const { data, error } = await supabase .from('mensagens_whatsapp') .select('count(*)') .limit(1); if (error) { res.status(500).json({ success: false, error: 'Tabela mensagens_whatsapp não existe', instruction: 'Execute o SQL do arquivo sql/create-mensagens-whatsapp.sql no Supabase Dashboard', sql: ` CREATE TABLE IF NOT EXISTS mensagens_whatsapp ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), telefone_cliente VARCHAR(20) NOT NULL, cliente_nome VARCHAR(255), mensagem TEXT NOT NULL, tipo VARCHAR(20) NOT NULL CHECK (tipo IN ('enviada', 'recebida')), status VARCHAR(20) DEFAULT 'enviada' CHECK (status IN ('enviando', 'enviada', 'entregue', 'lida', 'erro')), evolution_message_id VARCHAR(255), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_mensagens_telefone ON mensagens_whatsapp(telefone_cliente); CREATE INDEX IF NOT EXISTS idx_mensagens_created_at ON mensagens_whatsapp(created_at); CREATE INDEX IF NOT EXISTS idx_mensagens_tipo ON mensagens_whatsapp(tipo); ` }); } else { res.json({ success: true, message: 'Tabela mensagens_whatsapp já existe e está funcionando!' }); } } catch (error) { console.error('Erro ao verificar tabela mensagens_whatsapp:', error); res.status(500).json({ error: error.message, instruction: 'Execute o SQL manualmente no Supabase Dashboard' }); } }); // === CHAT WHATSAPP === // Buscar histórico de mensagens de um cliente app.get('/api/chat/:telefone', async (req, res) => { try { const { telefone } = req.params; // Tentar buscar mensagens const { data, error } = await supabase .from('mensagens_whatsapp') .select('*') .eq('telefone_cliente', telefone) .order('created_at', { ascending: true }); if (error) { // Se a tabela não existir, retornar array vazio console.log('Tabela mensagens_whatsapp não encontrada, retornando histórico vazio'); res.json({ data: [] }); return; } const mensagens = (data || []).map(msg => ({ ...msg, created_at: msg.created_at || msg.createdAt || msg.updated_at || getDateInBrazilISO() })); res.json({ data: mensagens }); } catch (error) { console.error('Erro ao buscar histórico de mensagens:', error); // Retornar array vazio em caso de erro res.json({ data: [] }); } }); // Enviar mensagem via Evolution API e salvar no histórico app.post('/api/chat/enviar', async (req, res) => { try { const { telefone, mensagem, clienteNome } = req.body; if (!telefone || !mensagem) { return res.status(400).json({ error: 'Telefone e mensagem são obrigatórios' }); } const resultado = await sendWhatsappMessage({ telefone, mensagem, clienteNome, salvarHistorico: true }); res.json(resultado); } catch (error) { console.error('Erro ao enviar mensagem:', error); res.status(500).json({ error: error.message }); } }); // Webhook para receber mensagens da Evolution API app.post('/api/webhook/evolution', async (req, res) => { try { const { event, data } = req.body; // Processar apenas mensagens recebidas if (event === 'messages.upsert' && data.message && !data.key.fromMe) { const telefone = data.key.remoteJid.replace('@s.whatsapp.net', ''); const mensagem = data.message.conversation || data.message.extendedTextMessage?.text || '[Mídia não suportada]'; // Salvar mensagem recebida no histórico await supabase .from('mensagens_whatsapp') .insert({ telefone_cliente: telefone, cliente_nome: data.pushName || 'Cliente', mensagem: mensagem, tipo: 'recebida', status: 'recebida', evolution_message_id: data.key.id, created_at: getDateInBrazilISO() }); } res.json({ success: true }); } catch (error) { console.error('Erro no webhook:', error); res.status(500).json({ error: error.message }); } }); // ========================================== // ROTAS PIX - MERCADO PAGO // ========================================== // Gerar PIX para uma venda app.post('/api/pix/gerar', async (req, res) => { try { const { valor, descricao, cliente_email, cliente_nome, cliente_cpf, venda_id } = req.body; if (!venda_id) { return res.status(400).json({ success: false, error: 'ID da venda é obrigatório' }); } // Buscar dados da venda para calcular valor correto // Tentar primeiro por UUID (id), depois por string (id_venda) let venda, vendaError; // Se parece com UUID, buscar por id if (venda_id.length > 20 && venda_id.includes('-')) { const result = await supabase .from('vendas') .select('*') .eq('id', venda_id) .single(); venda = result.data; vendaError = result.error; } else { // Se é string curta, buscar por id_venda const result = await supabase .from('vendas') .select('*') .eq('id_venda', venda_id) .single(); venda = result.data; vendaError = result.error; } console.log('🔍 Buscando venda:', venda_id); console.log('📊 Dados da venda:', venda); console.log('❌ Erro da consulta:', vendaError); // Se venda não encontrada, usar valor fornecido let valorPix = valor; if (venda) { // Venda encontrada - calcular valor baseado no tipo de pagamento valorPix = valor || venda.valor_total || venda.valor_final || 10.00; // Verificar se tem informações de parcelamento const formaPagamento = venda.forma_pagamento || venda.tipo_pagamento || venda.pagamento; const parcelas = venda.parcelas || venda.num_parcelas || 1; if (formaPagamento === 'prazo' && parcelas > 1) { // Para vendas a prazo, calcular valor da parcela valorPix = valorPix / parcelas; console.log(`💰 Venda parcelada: Total R$ ${valorPix * parcelas} ÷ ${parcelas}x = R$ ${valorPix.toFixed(2)} por parcela`); } else if (formaPagamento === 'prazo') { console.log(`💰 Venda à prazo: Valor total R$ ${valorPix.toFixed(2)}`); } else { console.log(`💰 Venda à vista: Valor R$ ${valorPix.toFixed(2)}`); } } else { // Venda não encontrada - usar valor fornecido ou padrão valorPix = valor || 10.00; console.log(`⚠️ Venda ${venda_id} não encontrada, usando valor fornecido: R$ ${valorPix.toFixed(2)}`); } console.log(`🏦 Gerando PIX: R$ ${valorPix.toFixed(2)} para venda ${venda_id}`); const pixData = await mercadoPagoService.gerarPix({ valor: valorPix, descricao: descricao || `Parcela - Venda #${venda_id} - Liberi Kids`, cliente_email: cliente_email || 'cliente@liberikids.com', cliente_nome: cliente_nome || 'Cliente', cliente_cpf: cliente_cpf || '00000000000', venda_id }); if (pixData.success && venda) { // Salvar dados do PIX na venda (só se venda foi encontrada) const updateField = venda_id.length > 20 && venda_id.includes('-') ? 'id' : 'id_venda'; const { error } = await supabase .from('vendas') .update({ pix_payment_id: pixData.payment_id, pix_qr_code: pixData.pix_copy_paste, status_pagamento: 'pendente', metodo_pagamento: 'pix' }) .eq(updateField, venda_id); if (error) { console.error('Erro ao salvar PIX na venda:', error); } else { console.log('✅ PIX salvo na venda com sucesso'); } } else if (pixData.success) { console.log('⚠️ PIX gerado mas não salvo (venda não encontrada)'); } res.json(pixData); } catch (error) { console.error('Erro ao gerar PIX:', error); res.status(500).json({ success: false, error: error.message }); } }); // Consultar status de pagamento PIX app.get('/api/pix/status/:payment_id', async (req, res) => { try { const { payment_id } = req.params; const statusData = await mercadoPagoService.consultarPagamento(payment_id); if (statusData.success && statusData.status === 'approved') { // Atualizar status da venda no banco const { error } = await supabase .from('vendas') .update({ status_pagamento: 'pago', data_pagamento: getDateInBrazilISO() }) .eq('pix_payment_id', payment_id); if (error) { console.error('Erro ao atualizar status da venda:', error); } } res.json(statusData); } catch (error) { console.error('Erro ao consultar status PIX:', error); res.status(500).json({ success: false, error: error.message }); } }); // Webhook do Mercado Pago para confirmação de pagamento app.post('/api/pix/webhook', async (req, res) => { try { const { type, data } = req.body; console.log('Webhook PIX recebido:', { type, data }); if (type === 'payment') { const statusData = await mercadoPagoService.consultarPagamento(data.id); if (statusData.success) { const venda_id = statusData.external_reference; if (statusData.status === 'approved') { // Pagamento aprovado - atualizar venda const { error } = await supabase .from('vendas') .update({ status_pagamento: 'pago', data_pagamento: getDateInBrazilISO() }) .eq('id', venda_id); if (!error) { console.log(`✅ Pagamento PIX confirmado para venda #${venda_id}`); } else { console.error('Erro ao atualizar venda:', error); } } else if (statusData.status === 'rejected' || statusData.status === 'cancelled') { // Pagamento rejeitado/cancelado await supabase .from('vendas') .update({ status_pagamento: 'cancelado' }) .eq('id', venda_id); console.log(`❌ Pagamento PIX cancelado para venda #${venda_id}`); } } } res.status(200).send('OK'); } catch (error) { console.error('Erro no webhook PIX:', error); res.status(500).send('Error'); } }); // Listar vendas com PIX pendente app.get('/api/pix/pendentes', async (req, res) => { try { const { data, error } = await supabase .from('vendas') .select(` *, clientes (nome, email, telefone) `) .eq('metodo_pagamento', 'pix') .eq('status_pagamento', 'pendente') .order('created_at', { ascending: false }); if (error) throw error; res.json({ success: true, vendas: data }); } catch (error) { console.error('Erro ao buscar PIX pendentes:', error); res.status(500).json({ success: false, error: error.message }); } }); // === CONFIGURAÇÕES DE CATÁLOGO === app.get('/api/configuracoes/catalogo', async (req, res) => { try { const { data, error } = await supabase .from('configuracoes') .select('valor') .eq('chave', 'catalogo_config') .single(); if (error && error.code !== 'PGRST116') { throw error; } const config = parseConfigValor(data, { catalogoAtivo: false, urlSite: '', exibirPrecos: true, exibirEstoque: false }); res.json(config); } catch (error) { console.error('Erro ao buscar configurações do catálogo:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/configuracoes/catalogo', async (req, res) => { try { const config = req.body; const { data, error } = await supabase .from('configuracoes') .upsert({ chave: 'catalogo_config', valor: config, updated_at: getDateInBrazilISO() }, { onConflict: 'chave' }); if (error) throw error; res.json({ success: true, message: 'Configurações do catálogo salvas com sucesso!' }); } catch (error) { console.error('Erro ao salvar configurações do catálogo:', error); res.status(500).json({ error: error.message }); } }); // Atualizar visibilidade de produto no catálogo app.patch('/api/produtos/:id/visibilidade', async (req, res) => { try { const { id } = req.params; const { visivelCatalogo } = req.body; const { data, error } = await supabase .from('produtos') .update({ visivel_catalogo: visivelCatalogo, updated_at: getDateInBrazilISO() }) .eq('id', id) .select() .single(); if (error) throw error; res.json({ success: true, message: visivelCatalogo ? 'Produto visível no catálogo' : 'Produto oculto do catálogo', produto: data }); } catch (error) { console.error('Erro ao atualizar visibilidade do produto:', error); res.status(500).json({ error: error.message }); } }); // Atualizar promoção de produto app.patch('/api/produtos/:id/promocao', async (req, res) => { try { const { id } = req.params; const { emPromocao } = req.body; const { data, error } = await supabase .from('produtos') .update({ em_promocao: emPromocao, updated_at: getDateInBrazilISO() }) .eq('id', id) .select() .single(); if (error) throw error; res.json({ success: true, message: emPromocao ? 'Produto em promoção' : 'Promoção removida', produto: data }); } catch (error) { console.error('Erro ao atualizar promoção:', error); res.status(500).json({ error: error.message }); } }); // Atualizar novidade de produto app.patch('/api/produtos/:id/novidade', async (req, res) => { try { const { id } = req.params; const { novidade } = req.body; const { data, error } = await supabase .from('produtos') .update({ novidade: novidade, updated_at: getDateInBrazilISO() }) .eq('id', id) .select() .single(); if (error) throw error; res.json({ success: true, message: novidade ? 'Produto marcado como novidade' : 'Novidade removida', produto: data }); } catch (error) { console.error('Erro ao atualizar novidade:', error); res.status(500).json({ error: error.message }); } }); // Atualizar preço promocional app.patch('/api/produtos/:id/preco-promocional', async (req, res) => { try { const { id } = req.params; const { precoPromocional } = req.body; // Se tem preço promocional, ativar promoção automaticamente const updates = { preco_promocional: precoPromocional, updated_at: getDateInBrazilISO() }; if (precoPromocional && precoPromocional > 0) { updates.em_promocao = true; } const { data, error } = await supabase .from('produtos') .update(updates) .eq('id', id) .select() .single(); if (error) throw error; res.json({ success: true, message: 'Preço promocional atualizado', produto: data }); } catch (error) { console.error('Erro ao atualizar preço promocional:', error); res.status(500).json({ error: error.message }); } }); // Listar fotos adicionais do produto no bucket catalogo app.get('/api/produtos/:id/fotos-catalogo', async (req, res) => { try { const { id } = req.params; const { data: fotos, error } = await supabase .storage .from('catalogo') .list(`produto_${id}`, { limit: 100, sortBy: { column: 'name', order: 'asc' } }); if (error) throw error; const fotosComUrl = fotos.map(foto => { const { data: urlData } = supabase .storage .from('catalogo') .getPublicUrl(`produto_${id}/${foto.name}`); return { name: foto.name, url: urlData.publicUrl, created_at: foto.created_at, size: foto.metadata?.size }; }); res.json({ success: true, fotos: fotosComUrl }); } catch (error) { console.error('Erro ao listar fotos do catálogo:', error); res.status(500).json({ error: error.message }); } }); // Upload de foto adicional para o bucket catalogo app.post('/api/produtos/:id/fotos-catalogo', upload.single('foto'), async (req, res) => { try { const { id } = req.params; if (!req.file) { return res.status(400).json({ error: 'Nenhum arquivo enviado' }); } // Como estamos usando memoryStorage, o arquivo está em req.file.buffer const fileBuffer = req.file.buffer; // Nome único para o arquivo const fileName = `${Date.now()}-${req.file.originalname}`; const filePath = `produto_${id}/${fileName}`; console.log(`📸 Upload de foto para: ${filePath}`); console.log(` Tamanho: ${(fileBuffer.length / 1024).toFixed(2)} KB`); console.log(` Tipo: ${req.file.mimetype}`); // Upload para o bucket catalogo const { data, error } = await supabase .storage .from('catalogo') .upload(filePath, fileBuffer, { contentType: req.file.mimetype, cacheControl: '3600', upsert: false }); if (error) { console.error('❌ Erro no upload do Supabase:', error); throw error; } // Obter URL pública const { data: urlData } = supabase .storage .from('catalogo') .getPublicUrl(filePath); res.json({ success: true, message: 'Foto adicional enviada com sucesso!', foto: { path: filePath, url: urlData.publicUrl } }); } catch (error) { console.error('Erro ao fazer upload da foto:', error); res.status(500).json({ error: error.message }); } }); // Deletar foto adicional do bucket catalogo app.delete('/api/produtos/:id/fotos-catalogo/:fileName', async (req, res) => { try { const { id, fileName } = req.params; const filePath = `produto_${id}/${fileName}`; const { error } = await supabase .storage .from('catalogo') .remove([filePath]); if (error) throw error; res.json({ success: true, message: 'Foto removida com sucesso!' }); } catch (error) { console.error('Erro ao remover foto:', error); res.status(500).json({ error: error.message }); } }); // === SISTEMA DE ALERTAS AUTOMÁTICOS === app.post('/api/alertas/enviar-vencimentos', async (req, res) => { try { console.log('\n🔔 Iniciando envio de alertas de vencimento...'); const axios = require('axios'); const resultados = { alertasEnviados: 0, erros: 0, parcelas: [], logs: [] }; function log(msg) { console.log(msg); resultados.logs.push(msg); } // 1. Buscar configurações const { data: configData } = await supabase .from('configuracoes') .select('chave, valor') .in('chave', [ 'whatsapp_primeiro_alerta_dias', 'whatsapp_segundo_alerta_dias', 'whatsapp_alerta_apos_vencimento_dias', 'whatsapp_primeiro_alerta_ativo', 'whatsapp_segundo_alerta_ativo', 'whatsapp_alerta_apos_vencimento_ativo', 'whatsapp_primeiro_alerta_mensagem', 'whatsapp_segundo_alerta_mensagem', 'whatsapp_alerta_apos_vencimento_mensagem', 'evolution_api', 'mercadopago_access_token' ]); const config = {}; configData?.forEach(item => { config[item.chave] = item.valor; }); // Parse Evolution API config (é um JSON) const evolutionConfig = typeof config.evolution_api === 'string' ? JSON.parse(config.evolution_api) : config.evolution_api || {}; const primeiroAlertaDias = parseInt(config.whatsapp_primeiro_alerta_dias) || 3; const segundoAlertaDias = parseInt(config.whatsapp_segundo_alerta_dias) || 0; const alertaAposVencimentoDias = parseInt(config.whatsapp_alerta_apos_vencimento_dias) || 3; const primeiroAlertas = config.whatsapp_primeiro_alerta_ativo === 'true'; const segundoAlertas = config.whatsapp_segundo_alerta_ativo === 'true'; const alertaAposVencimento = config.whatsapp_alerta_apos_vencimento_ativo === 'true'; log(`📋 Configurações: Primeiro=${primeiroAlertaDias}d (${primeiroAlertas?'ON':'OFF'}), Segundo=${segundoAlertaDias}d (${segundoAlertas?'ON':'OFF'}), Pós=${alertaAposVencimentoDias}d (${alertaAposVencimento?'ON':'OFF'})`); // 2. Buscar parcelas pendentes const { data: parcelas, error } = await supabase .from('venda_parcelas') .select(` *, vendas ( id_venda, cliente_id, clientes ( nome_completo, whatsapp ) ) `) .eq('status', 'pendente') .order('data_vencimento', { ascending: true }); if (error) throw error; if (!parcelas || parcelas.length === 0) { log('ℹ️ Nenhuma parcela pendente encontrada'); return res.json(resultados); } log(`📦 ${parcelas.length} parcela(s) pendente(s)`); const hoje = new Date(); hoje.setHours(0, 0, 0, 0); // 3. Processar cada parcela for (const parcela of parcelas) { const dataVencimento = new Date(parcela.data_vencimento); dataVencimento.setHours(0, 0, 0, 0); const diasParaVencimento = Math.round((dataVencimento - hoje) / (24 * 60 * 60 * 1000)); const cliente = parcela.vendas?.clientes; if (!cliente || !cliente.whatsapp) { continue; } let deveEnviar = false; let tipoAlerta = ''; let mensagemTemplate = ''; // Verificar qual alerta enviar if (diasParaVencimento === primeiroAlertaDias && primeiroAlertas) { deveEnviar = true; tipoAlerta = 'primeiro_alerta'; mensagemTemplate = config.whatsapp_primeiro_alerta_mensagem || 'Olá {cliente}! 👋\n\nLembramos que você tem uma parcela no valor de {valor} com vencimento {quando}.\n\nAgradecemos a atenção!'; } else if (diasParaVencimento === segundoAlertaDias && segundoAlertas) { deveEnviar = true; tipoAlerta = 'segundo_alerta'; mensagemTemplate = config.whatsapp_segundo_alerta_mensagem || 'Olá {cliente}! 👋\n\nSua parcela de {valor} vence {quando}.\n\n📱 Gerando PIX para facilitar o pagamento...'; } else if (diasParaVencimento < 0 && Math.abs(diasParaVencimento) === alertaAposVencimentoDias && alertaAposVencimento) { deveEnviar = true; tipoAlerta = 'alerta_apos_vencimento'; mensagemTemplate = config.whatsapp_alerta_apos_vencimento_mensagem || 'Olá {cliente}! 👋\n\nIdentificamos que a parcela {parcela} no valor de {valor} venceu {quando}.\n\nPor favor, regularize o pagamento.'; } if (!deveEnviar) continue; // Preparar mensagem const nomeCliente = cliente.nome_completo.split(' ')[0]; const valorFormatado = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(parcela.valor); const dataVencFormatada = dataVencimento.toLocaleDateString('pt-BR'); let quando = ''; if (diasParaVencimento > 0) { quando = diasParaVencimento === 1 ? 'amanhã' : `em ${diasParaVencimento} dias (${dataVencFormatada})`; } else if (diasParaVencimento === 0) { quando = 'hoje'; } else { quando = `há ${Math.abs(diasParaVencimento)} dias (${dataVencFormatada})`; } let mensagem = mensagemTemplate .replace(/{cliente}/g, nomeCliente) .replace(/{valor}/g, valorFormatado) .replace(/{quando}/g, quando) .replace(/{parcela}/g, `${parcela.numero_parcela}/${parcela.vendas?.parcelas || '?'}`); // Gerar PIX se for segundo alerta if (tipoAlerta === 'segundo_alerta' && config.mercadopago_access_token) { try { const pixResponse = await axios.post('https://api.mercadopago.com/v1/payments', { transaction_amount: parseFloat(parcela.valor), description: `Parcela ${parcela.numero_parcela} - Venda #${parcela.venda_id}`, payment_method_id: 'pix', payer: { email: 'cliente@liberi.com.br' } }, { headers: { 'Authorization': `Bearer ${config.mercadopago_access_token}`, 'Content-Type': 'application/json' } }); const qrCode = pixResponse.data.point_of_interaction?.transaction_data?.qr_code; if (qrCode) { mensagem += `\n\n📱 *PIX Copia e Cola:*\n\`\`\`${qrCode}\`\`\``; await supabase .from('venda_parcelas') .update({ pix_payment_id: pixResponse.data.id, pix_qr_code: qrCode, pix_qr_code_base64: pixResponse.data.point_of_interaction?.transaction_data?.qr_code_base64 }) .eq('id', parcela.id); } } catch (pixError) { log(`⚠️ Erro ao gerar PIX: ${pixError.message}`); } } // Enviar WhatsApp try { const url = `${evolutionConfig.baseUrl}/message/sendText/${evolutionConfig.instanceName}`; await axios.post(url, { number: cliente.whatsapp, textMessage: { text: mensagem } }, { headers: { 'apikey': evolutionConfig.apiKey, 'Content-Type': 'application/json' } }); resultados.alertasEnviados++; log(`✅ ${tipoAlerta} enviado para ${cliente.nome_completo}`); resultados.parcelas.push({ cliente: cliente.nome_completo, valor: valorFormatado, vencimento: dataVencFormatada, tipo: tipoAlerta, status: 'enviado' }); // Registrar histórico await supabase .from('mensagens_whatsapp') .insert({ telefone_cliente: cliente.whatsapp, cliente_nome: cliente.nome_completo, mensagem: mensagem, tipo: 'enviada', status: 'enviada' }); } catch (whatsappError) { resultados.erros++; log(`❌ Erro ao enviar para ${cliente.nome_completo}: ${whatsappError.message}`); resultados.parcelas.push({ cliente: cliente.nome_completo, valor: valorFormatado, vencimento: dataVencFormatada, tipo: tipoAlerta, status: 'erro', erro: whatsappError.message }); } // Delay entre mensagens await new Promise(resolve => setTimeout(resolve, 2000)); } log(`\n📊 RESUMO: ${resultados.alertasEnviados} enviados, ${resultados.erros} erros`); res.json({ success: true, ...resultados }); } catch (error) { console.error('💥 Erro ao enviar alertas:', error); res.status(500).json({ error: error.message, stack: error.stack }); } }); // Rota para enviar alertas de parcelas atrasadas (manual) app.post('/api/alertas/enviar-atrasados', async (req, res) => { try { const axios = require('axios'); const resultados = { alertasEnviados: 0, erros: 0, parcelas: [] }; // Buscar configurações const { data: configData } = await supabase .from('configuracoes') .select('chave, valor') .in('chave', [ 'evolution_api', 'mercadopago_access_token' ]); const config = {}; configData?.forEach(item => { config[item.chave] = item.valor; }); // Parse Evolution API config (é um JSON) const evolutionConfig = typeof config.evolution_api === 'string' ? JSON.parse(config.evolution_api) : config.evolution_api || {}; // Buscar parcelas vencidas ou vencendo hoje const hoje = new Date(); hoje.setHours(0, 0, 0, 0); const { data: parcelas, error } = await supabase .from('venda_parcelas') .select(` *, vendas ( id_venda, cliente_id, clientes ( nome_completo, whatsapp ) ) `) .eq('status', 'pendente') .lte('data_vencimento', hoje.toISOString()) .order('data_vencimento', { ascending: true }); if (error) throw error; if (!parcelas || parcelas.length === 0) { return res.json({ success: true, message: 'Nenhuma parcela vencida ou vencendo hoje', ...resultados }); } // Enviar alerta para cada parcela for (const parcela of parcelas) { const cliente = parcela.vendas?.clientes; if (!cliente || !cliente.whatsapp) continue; const nomeCliente = cliente.nome_completo.split(' ')[0]; const valorFormatado = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(parcela.valor); // Gerar PIX let pixCode = null; try { const pixResponse = await axios.post('https://api.mercadopago.com/v1/payments', { transaction_amount: parseFloat(parcela.valor), description: `Parcela ${parcela.numero_parcela} - Venda #${parcela.venda_id}`, payment_method_id: 'pix', payer: { email: 'cliente@liberi.com.br' } }, { headers: { 'Authorization': `Bearer ${config.mercadopago_access_token}`, 'Content-Type': 'application/json' } }); pixCode = pixResponse.data.point_of_interaction?.transaction_data?.qr_code; if (pixCode) { await supabase .from('venda_parcelas') .update({ pix_payment_id: pixResponse.data.id, pix_qr_code: pixCode, pix_qr_code_base64: pixResponse.data.point_of_interaction?.transaction_data?.qr_code_base64 }) .eq('id', parcela.id); } } catch (pixError) { console.error('Erro ao gerar PIX:', pixError.message); } // Montar mensagem const dataVenc = new Date(parcela.data_vencimento).toLocaleDateString('pt-BR'); let mensagem = `Olá ${nomeCliente}! 👋\n\n`; mensagem += `Você tem uma parcela no valor de ${valorFormatado} `; mensagem += `com vencimento em ${dataVenc}.\n\n`; if (pixCode) { mensagem += `📱 *PIX Copia e Cola:*\n\`\`\`${pixCode}\`\`\`\n\n`; } mensagem += `Agradecemos! 😊\n*Liberi Kids - Moda Infantil* 👗👕`; // Enviar WhatsApp try { const url = `${evolutionConfig.baseUrl}/message/sendText/${evolutionConfig.instanceName}`; await axios.post(url, { number: cliente.whatsapp, textMessage: { text: mensagem } }, { headers: { 'apikey': evolutionConfig.apiKey, 'Content-Type': 'application/json' } }); resultados.alertasEnviados++; resultados.parcelas.push({ cliente: cliente.nome_completo, valor: valorFormatado, vencimento: dataVenc, status: 'enviado' }); // Registrar histórico await supabase .from('mensagens_whatsapp') .insert({ telefone_cliente: cliente.whatsapp, cliente_nome: cliente.nome_completo, mensagem: mensagem, tipo: 'enviada', status: 'enviada' }); } catch (whatsappError) { resultados.erros++; resultados.parcelas.push({ cliente: cliente.nome_completo, valor: valorFormatado, vencimento: dataVenc, status: 'erro', erro: whatsappError.message }); } await new Promise(resolve => setTimeout(resolve, 2000)); } res.json({ success: true, message: `${resultados.alertasEnviados} alertas enviados`, ...resultados }); } catch (error) { console.error('Erro ao enviar alertas atrasados:', error); res.status(500).json({ error: error.message }); } }); // Rota catch-all para servir o React app app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'client/build', 'index.html')); }); // Iniciar servidor app.listen(PORT, () => { console.log(`🚀 Servidor rodando na porta ${PORT}`); console.log(`📊 Usando Supabase como banco de dados`); console.log(`🌐 Frontend disponível em: http://localhost:${PORT}`); }); module.exports = app;