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'); const googleSheetsService = require('./config/google-sheets'); // Versão de produção ativada const mercadoPagoService = require('./config/mercadopago'); 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'))); // Configuração do Multer para upload de arquivos const storage = multer.diskStorage({ destination: function (req, file, cb) { const uploadDir = 'uploads/'; if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } cb(null, uploadDir); }, filename: function (req, file, cb) { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)); } }); const upload = multer({ storage: storage }); // ===================================================== // ROTAS DA API // ===================================================== // === 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(razao_social), produto_variacoes( id, tamanho, cor, quantidade, foto_url ) `) .order('created_at', { ascending: false }); if (error) throw error; // Formatar dados para o catálogo const produtosCatalogo = data.map(produto => ({ 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: produto.foto_principal_url ? `/uploads/${produto.foto_principal_url}` : null, fornecedor: produto.fornecedores?.razao_social || null, variacoes: produto.produto_variacoes || [], estoque_total: produto.produto_variacoes?.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; res.json(data); } 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 { razao_social, telefone, whatsapp, endereco, email } = req.body; const { data, error } = await supabase .from('fornecedores') .insert([{ razao_social, telefone, whatsapp, endereco, email }]) .select(); if (error) throw error; res.json({ id: data[0].id, message: 'Fornecedor criado 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 { razao_social, telefone, whatsapp, endereco, email } = req.body; const { error } = await supabase .from('fornecedores') .update({ razao_social, telefone, whatsapp, endereco, email }) .eq('id', id); if (error) throw error; res.json({ 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(razao_social), produto_variacoes(id, tamanho, cor, quantidade, foto_url) `) .order('created_at', { ascending: false }); if (error) throw error; // Processar dados para compatibilidade const produtos = data.map(produto => ({ ...produto, fornecedor_nome: produto.fornecedores?.razao_social || null, total_variacoes: produto.produto_variacoes?.length || 0, estoque_total: produto.produto_variacoes?.reduce((total, v) => total + (v.quantidade || 0), 0) || 0 })); 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, use_google_drive } = 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; } // 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...'); // Inicializar Google Drive se necessário let googleDrive = null; const useGoogleDrive = use_google_drive === 'true'; if (useGoogleDrive) { try { const GoogleDriveService = require('./config/google-drive'); googleDrive = new GoogleDriveService(); // Carregar credenciais e tokens const [credentialsRes, tokensRes] = await Promise.all([ supabase.from('configuracoes').select('configuracao').eq('tipo', 'google_drive_credentials').single(), supabase.from('configuracoes').select('configuracao').eq('tipo', 'google_drive_tokens').single() ]); if (!credentialsRes.error && !tokensRes.error) { await googleDrive.initializeAuth(credentialsRes.data.configuracao); // Carregar tokens const fs = require('fs'); const path = require('path'); const tokensPath = path.join(__dirname, 'config', 'google-tokens.json'); fs.writeFileSync(tokensPath, JSON.stringify(tokensRes.data.configuracao, null, 2)); await googleDrive.refreshTokenIfNeeded(); console.log('✅ Google Drive inicializado para upload'); } else { console.log('⚠️ Google Drive não configurado, usando armazenamento local'); useGoogleDrive = false; } } catch (error) { console.error('❌ Erro ao inicializar Google Drive:', error); useGoogleDrive = false; } } // 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 foto_url = null; if (fotosVariacao.length > 0) { const primeiraFoto = fotosVariacao[0]; if (useGoogleDrive && googleDrive) { try { // Criar pasta específica do produto se não existir const folderId = await googleDrive.createFolder('Liberi Kids - Fotos Produtos'); // Nome do arquivo com informações do produto const nomeArquivo = `${marca}_${nome}_${variacao.tamanho}_${variacao.cor}_${Date.now()}.${primeiraFoto.originalname.split('.').pop()}`; // Upload para Google Drive const uploadResult = await googleDrive.uploadFile( primeiraFoto.path, nomeArquivo, folderId, primeiraFoto.mimetype ); foto_url = uploadResult.publicUrl; console.log(`📤 Foto enviada para Google Drive: ${uploadResult.name}`); // Remover arquivo local após upload const fs = require('fs'); if (fs.existsSync(primeiraFoto.path)) { fs.unlinkSync(primeiraFoto.path); } } catch (error) { console.error('❌ Erro no upload para Google Drive:', error); // Fallback para armazenamento local foto_url = `/uploads/${primeiraFoto.filename}`; } } else { // Usar armazenamento local foto_url = `/uploads/${primeiraFoto.filename}`; } } // 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), foto_url }]); if (variacaoError) throw variacaoError; } 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; } 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); } catch (error) { console.error('Erro ao listar variações:', 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_despesas') .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_despesas') .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 { const { data, error } = await supabase .from('despesas') .select(` *, tipos_despesas(nome), fornecedores(razao_social) `) .order('data_despesa', { ascending: false }); if (error) throw error; // Processar dados para compatibilidade const despesas = data.map(despesa => ({ ...despesa, tipo_nome: despesa.tipos_despesas?.nome || null, fornecedor_nome: despesa.fornecedores?.razao_social || null })); res.json(despesas); } 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_despesas') .select('id') .eq('nome', tipo_despesa) .single(); if (tipoExistente) { tipoId = tipoExistente.id; } else { const { data: novoTipo } = await supabase .from('tipos_despesas') .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('razao_social', fornecedor) .single(); if (fornecedorExistente) { fornecedorId = fornecedorExistente.id; } else { const { data: novoFornecedor } = await supabase .from('fornecedores') .insert([{ razao_social: 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_despesas') .select('id') .eq('nome', tipo_despesa) .single(); if (tipoExistente) { tipoId = tipoExistente.id; } else { const { data: novoTipo } = await supabase .from('tipos_despesas') .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('razao_social', fornecedor) .single(); if (fornecedorExistente) { fornecedorId = fornecedorExistente.id; } else { const { data: novoFornecedor } = await supabase .from('fornecedores') .insert([{ razao_social: 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_url, 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(item => ({ ...item, produto_nome: `${item.produtos?.marca} - ${item.produtos?.nome}`, produto_codigo: item.produtos?.id_produto, produto_foto: item.produtos?.foto_principal_url, variacao_info: `${item.produto_variacoes?.tamanho} - ${item.produto_variacoes?.cor}` })) || [] })); 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_url, 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 devolucaoItem = devolucoesPorItem[item.id]; let foiDevolvido = !!devolucaoItem; // LÓGICA ALTERNATIVA: Se quantidade é 0, considerar como devolvido/trocado if (!foiDevolvido && parseInt(item.quantidade) === 0) { foiDevolvido = true; } let statusItem = 'vendido'; let dataEvento = null; let tipoEvento = null; if (foiDevolvido) { if (devolucaoItem) { // Pegar a devolução mais recente 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 { // Lógica inteligente baseada no padrão da venda if (provavelmenteDevolucao) { statusItem = 'devolvido'; tipoEvento = 'devolucao'; } else if (provavelmenteTroca) { statusItem = 'trocado'; tipoEvento = 'troca'; } else { // Fallback: se não conseguir determinar, assumir como trocado statusItem = 'trocado'; tipoEvento = 'troca'; } dataEvento = item.updated_at || data.data_venda; } } return { ...item, produto_nome: `${item.produtos?.marca} - ${item.produtos?.nome}`, produto_codigo: item.produtos?.id_produto, produto_foto: item.produtos?.foto_principal_url, variacao_info: `${item.produto_variacoes?.tamanho} - ${item.produto_variacoes?.cor}`, 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, observacoes, itens, id_venda } = req.body; // Validar estoque antes de processar a venda if (itens && itens.length > 0) { for (const item of itens) { if (item.produto_variacao_id) { const { data: variacao, error: variacaoError } = await supabase .from('produto_variacoes') .select('quantidade, tamanho, cor, produtos(nome, marca)') .eq('id', item.produto_variacao_id) .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: parseFloat(valor_total), desconto: parseFloat(desconto) || 0, parcelas: parseInt(parcelas) || 1, valor_parcela: parseFloat(valor_parcela) || parseFloat(valor_total), data_venda: data_venda || new Date().toISOString().split('T')[0], data_primeiro_vencimento: data_primeiro_vencimento || new Date().toISOString().split('T')[0], observacoes: observacoes || null }]) .select(); if (vendaError) throw vendaError; const vendaId = vendaData[0].id; // Inserir itens da venda if (itens && itens.length > 0) { const itensVenda = itens.map(item => ({ venda_id: vendaId, produto_id: item.produto_id, variacao_id: item.produto_variacao_id || null, 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; // Atualizar estoque das variações for (const item of itens) { if (item.produto_variacao_id) { try { // Tentar usar RPC primeiro, se não existir, usar UPDATE direto const { error: estoqueError } = await supabase.rpc('update_estoque_variacao', { variacao_id: item.produto_variacao_id, quantidade_vendida: parseInt(item.quantidade) }); if (estoqueError && estoqueError.code === '42883') { // Função não existe, usar UPDATE direto // Buscar estoque atual const { data: variacaoAtual, error: buscaError } = await supabase .from('produto_variacoes') .select('quantidade') .eq('id', item.produto_variacao_id) .single(); if (!buscaError && variacaoAtual) { const novoEstoque = (variacaoAtual.quantidade || 0) - parseInt(item.quantidade); await supabase .from('produto_variacoes') .update({ quantidade: Math.max(0, novoEstoque) }) .eq('id', item.produto_variacao_id); } } else if (estoqueError) { console.error('Erro ao atualizar estoque:', estoqueError); } } catch (err) { console.error('Erro ao atualizar estoque:', err); } } } } res.json({ id: vendaId, message: 'Venda registrada com sucesso!' }); } 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 { error } = await supabase .from('vendas') .update({ 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_venda, data_primeiro_vencimento, observacoes }) .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 }); } }); // === CONFIGURAÇÕES === app.get('/api/configuracoes/evolution', async (req, res) => { try { const { data, error } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'evolution_api') .single(); if (error && error.code !== 'PGRST116') { // PGRST116 = no rows returned throw error; } // Se não existe configuração, retorna valores padrão const config = data ? data.configuracao : { 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; // Validações básicas if (config.enabled && (!config.baseUrl || !config.apiKey || !config.instanceName)) { return res.status(400).json({ error: 'URL Base, API Key e Nome da Instância são obrigatórios quando a integração está ativada' }); } // Usar upsert para inserir ou atualizar const { data, error } = await supabase .from('configuracoes') .upsert({ tipo: 'evolution_api', configuracao: config, updated_at: new Date().toISOString() }, { onConflict: 'tipo' }); if (error) throw error; 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: dataVencimento.toISOString().split('T')[0], 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 === 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('configuracao') .eq('tipo', 'evolution_api') .single(); if (!configData || !JSON.parse(configData.configuracao).enabled) { return res.status(400).json({ error: 'Evolution API não configurada ou desabilitada' }); } const config = JSON.parse(configData.configuracao); // Buscar template de mensagem personalizada const { data: templateData } = await supabase .from('configuracoes') .select('configuracao') .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.'; if (templateData) { mensagem = JSON.parse(templateData.configuracao).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('configuracao') .eq('tipo', 'whatsapp_alertas') .single(); if (error && error.code !== 'PGRST116') { throw error; } const config = data ? data.configuracao : { alertaVencimento: false, diasAntecedencia: 1, mensagemPersonalizada: 'Olá {cliente}! Lembramos que sua parcela no valor de R$ {valor} vence {quando}. Entre em contato conosco para mais informações. 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; // Usar upsert para inserir ou atualizar const { data, error } = await supabase .from('configuracoes') .upsert({ tipo: 'whatsapp_alertas', configuracao: config, updated_at: new Date().toISOString() }, { onConflict: 'tipo' }); if (error) throw error; res.json({ message: 'Configurações de alertas salvas com sucesso!' }); } catch (error) { console.error('Erro ao salvar configurações WhatsApp alertas:', error); res.status(500).json({ error: error.message }); } }); // === EMPRÉSTIMOS === app.get('/api/emprestimos', async (req, res) => { try { const { data, error } = await supabase .from('emprestimos') .select(` *, emprestimo_devolucoes( id, valor_devolvido, data_devolucao, 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 || new Date().toISOString().split('T')[0] }]) .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: new Date().toISOString() }) .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_devolucoes') .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_devolucoes') .insert([{ emprestimo_id: id, valor_devolvido: parseFloat(valor_devolvido), data_devolucao: data_devolucao || new Date().toISOString().split('T')[0], 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_devolucoes') .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 inicioMes = new Date(hoje.getFullYear(), hoje.getMonth(), 1).toISOString().split('T')[0]; const fimMes = new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0).toISOString().split('T')[0]; // 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 { 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', hoje.toISOString().split('T')[0]); // 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 { data, error } = await supabase .from('vendas') .select(` *, clientes(nome_completo, whatsapp, telefone), venda_itens( *, produtos(nome, marca, foto_principal_url, id_produto), produto_variacoes(tamanho, cor) ) `) .gte('data_venda', dataLimite.toISOString().split('T')[0]) .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_url, 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) if (itemOriginal.variacao_id) { const { data: variacaoAtual, error: buscaError } = await supabase .from('produto_variacoes') .select('quantidade') .eq('id', itemOriginal.variacao_id) .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', itemOriginal.variacao_id); 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: new Date().toISOString(), 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 { variacao_id, quantidade } = itemTroca; if (variacao_id) { const { data: variacao, error: variacaoError } = await supabase .from('produto_variacoes') .select('quantidade, tamanho, cor, produtos(nome, marca)') .eq('id', variacao_id) .single(); if (variacaoError) { throw new Error(`Erro ao verificar estoque para troca: ${variacaoError.message}`); } if (!variacao || variacao.quantidade < parseInt(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 { produto_id, variacao_id, quantidade, valor_unitario } = itemTroca; // Calcular valor da troca const valorTroca = parseFloat(valor_unitario) * parseInt(quantidade); valorTotalTroca += valorTroca; // Reduzir estoque do produto de troca (produto que está saindo) if (variacao_id) { // Buscar estoque atual const { data: variacaoAtual, error: buscaError } = await supabase .from('produto_variacoes') .select('quantidade') .eq('id', variacao_id) .single(); if (buscaError) { console.error('Erro ao buscar estoque atual da troca:', buscaError); } else { // Atualizar com novo estoque (reduzir) const novoEstoque = (variacaoAtual.quantidade || 0) - parseInt(quantidade); const { error: estoqueError } = await supabase .from('produto_variacoes') .update({ quantidade: Math.max(0, novoEstoque) }) // Garantir que não fique negativo .eq('id', variacao_id); if (estoqueError) { console.error('Erro ao atualizar estoque da troca:', estoqueError); } else { console.log(`✅ Estoque reduzido na troca: -${quantidade} unidades (novo total: ${novoEstoque})`); } } } // Registrar a troca como um novo item na venda const { error: itemError } = await supabase .from('venda_itens') .insert({ venda_id: venda_id, produto_id: produto_id, variacao_id: variacao_id, quantidade: parseInt(quantidade), 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: ${quantidade} 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); // 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 GOOGLE SHEETS // ===================================================== // Salvar credenciais Google app.post('/api/google/credentials', async (req, res) => { try { const { client_id, client_secret, redirect_uris } = req.body; if (!client_id || !client_secret || !redirect_uris) { return res.status(400).json({ error: 'Todos os campos são obrigatórios' }); } const credentials = { client_id, client_secret, redirect_uris: Array.isArray(redirect_uris) ? redirect_uris : [redirect_uris] }; // Salvar no Supabase usando a estrutura correta da tabela const { data, error } = await supabase .from('configuracoes') .upsert({ tipo: 'google_credentials', configuracao: credentials, updated_at: new Date().toISOString() }, { onConflict: 'tipo' }); if (error) throw error; res.json({ success: true, message: 'Credenciais salvas com sucesso!' }); } catch (error) { console.error('Erro ao salvar credenciais Google:', error); res.status(500).json({ error: error.message }); } }); // Carregar credenciais Google app.get('/api/google/credentials', async (req, res) => { try { const { data, error } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'google_credentials') .single(); if (error && error.code !== 'PGRST116') throw error; if (data) { // Retornar apenas se as credenciais existem (sem expor os valores) res.json({ configured: true, hasClientId: !!data.configuracao.client_id, hasClientSecret: !!data.configuracao.client_secret }); } else { res.json({ configured: false }); } } catch (error) { console.error('Erro ao carregar credenciais Google:', error); res.status(500).json({ error: error.message }); } }); // Inicializar autenticação Google app.get('/api/google/init', async (req, res) => { try { // Carregar credenciais do banco const { data: configData, error: configError } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'google_credentials') .single(); if (configError && configError.code !== 'PGRST116') throw configError; if (!configData) { return res.status(400).json({ error: 'Credenciais do Google não configuradas. Configure na página de Configurações.' }); } const initialized = await googleSheetsService.initializeAuth(configData.configuracao); if (!initialized) { return res.status(500).json({ error: 'Erro ao inicializar Google Sheets. Verifique as credenciais.' }); } // Tentar carregar tokens salvos const hasTokens = await googleSheetsService.loadSavedTokens(); if (hasTokens && googleSheetsService.isAuthenticated()) { return res.json({ authenticated: true, message: 'Já autenticado com Google' }); } // Gerar URL de autorização const authUrl = googleSheetsService.getAuthUrl(); res.json({ authenticated: false, authUrl, message: 'Acesse a URL para autorizar o acesso ao Google Sheets' }); } catch (error) { console.error('Erro ao inicializar Google Sheets:', error); res.status(500).json({ error: error.message }); } }); // Callback de autorização Google app.get('/auth/google/callback', async (req, res) => { try { const { code } = req.query; if (!code) { return res.status(400).send('Código de autorização não fornecido'); } await googleSheetsService.handleAuthCallback(code); // Redirecionar para o frontend com sucesso res.redirect('http://localhost:3000/configuracoes?google_auth=success'); } catch (error) { console.error('Erro no callback Google:', error); res.redirect('http://localhost:3000/configuracoes?google_auth=error'); } }); // Verificar status da autenticação app.get('/api/google/status', async (req, res) => { try { // Carregar credenciais do banco const { data: configData, error: configError } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'google_credentials') .single(); if (configError && configError.code !== 'PGRST116') throw configError; if (!configData) { return res.json({ authenticated: false, configured: false, error: 'Credenciais não configuradas' }); } await googleSheetsService.initializeAuth(configData.configuracao); await googleSheetsService.loadSavedTokens(); const authenticated = googleSheetsService.isAuthenticated(); res.json({ authenticated, configured: true }); } catch (error) { res.json({ authenticated: false, configured: false, error: error.message }); } }); // Criar nova planilha app.post('/api/google/create-spreadsheet', async (req, res) => { try { if (!googleSheetsService.isAuthenticated()) { return res.status(401).json({ error: 'Não autenticado com Google' }); } // Renovar token se necessário await googleSheetsService.refreshTokenIfNeeded(); const { title } = req.body; const spreadsheetTitle = title || `Liberi Kids - ${new Date().toLocaleDateString('pt-BR')}`; // Verificar credenciais const { data: credentials } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'google_credentials') .single(); if (!credentials || !credentials.configuracao) { return res.status(400).json({ error: 'Credenciais do Google não configuradas' }); } // Inicializar Google Sheets const googleSheets = require('./config/google-sheets'); const authSuccess = await googleSheets.initializeAuth(credentials.configuracao); if (!authSuccess) { return res.status(400).json({ error: 'Falha na autenticação com Google' }); } await googleSheets.loadSavedTokens(); if (!googleSheets.isAuthenticated()) { return res.status(401).json({ error: 'Não autenticado com Google. Faça a autorização primeiro.' }); } // Renovar token se necessário await googleSheets.refreshTokenIfNeeded(); const result = await googleSheets.createSpreadsheet(spreadsheetTitle); res.json(result); } catch (error) { console.error('Erro ao criar planilha:', error); res.status(500).json({ error: error.message }); } }); // Exportar produtos para Google Sheets app.post('/api/google/export-produtos', async (req, res) => { try { if (!googleSheetsService.isAuthenticated()) { return res.status(401).json({ error: 'Não autenticado com Google' }); } // Renovar token se necessário await googleSheetsService.refreshTokenIfNeeded(); const { spreadsheetId } = req.body; if (!spreadsheetId) { return res.status(400).json({ error: 'ID da planilha é obrigatório' }); } // Buscar produtos com informações completas const { data: produtos, error } = await supabase .from('produtos') .select(` *, fornecedores(nome), produto_variacoes(*) `); if (error) throw error; // Processar dados dos produtos const produtosProcessados = []; for (const produto of produtos) { if (produto.produto_variacoes && produto.produto_variacoes.length > 0) { // Criar uma linha para cada variação for (const variacao of produto.produto_variacoes) { produtosProcessados.push({ id: produto.id, nome: produto.nome, fornecedor_nome: produto.fornecedores?.nome || '', tamanho: variacao.tamanho, estacao: produto.estacao, genero: produto.genero, valor_compra: produto.valor_compra, valor_revenda: produto.valor_revenda, created_at: produto.created_at, quantidade_total: variacao.quantidade, marca: produto.marca }); } } else { // Produto sem variações produtosProcessados.push({ id: produto.id, nome: produto.nome, fornecedor_nome: produto.fornecedores?.nome || '', tamanho: '', estacao: produto.estacao, genero: produto.genero, valor_compra: produto.valor_compra, valor_revenda: produto.valor_revenda, created_at: produto.created_at, quantidade_total: 0, marca: produto.marca }); } } const result = await googleSheetsService.exportProdutos(spreadsheetId, produtosProcessados); res.json(result); } catch (error) { console.error('Erro ao exportar produtos:', error); res.status(500).json({ error: error.message }); } }); // Exportar vendas para Google Sheets app.post('/api/google/export-vendas', async (req, res) => { try { if (!googleSheetsService.isAuthenticated()) { return res.status(401).json({ error: 'Não autenticado com Google' }); } // Renovar token se necessário await googleSheetsService.refreshTokenIfNeeded(); const { spreadsheetId } = req.body; if (!spreadsheetId) { return res.status(400).json({ error: 'ID da planilha é obrigatório' }); } // Buscar vendas com informações completas const { data: vendas, error } = await supabase .from('vendas') .select(` *, clientes(nome), venda_itens( quantidade, valor_unitario, produtos(nome) ) `) .order('data_venda', { ascending: false }); if (error) throw error; // Processar dados das vendas const vendasProcessadas = vendas.map(venda => ({ id: venda.id, cliente_nome: venda.clientes?.nome || 'Cliente não informado', data_venda: venda.data_venda, tipo_pagamento: venda.tipo_pagamento, valor_total: venda.valor_total, desconto: venda.desconto || 0, valor_final: venda.valor_final, status: 'Concluída', observacoes: venda.observacoes || '', produtos: venda.venda_itens?.map(item => ({ nome: item.produtos?.nome || 'Produto não encontrado', quantidade: item.quantidade })) || [] })); const result = await googleSheetsService.exportVendas(spreadsheetId, vendasProcessadas); res.json(result); } catch (error) { console.error('Erro ao exportar vendas:', error); res.status(500).json({ error: error.message }); } }); // Criar ou atualizar planilha persistente do sistema app.post('/api/google/create-system-spreadsheet', async (req, res) => { try { // Verificar se já existe uma planilha do sistema salva const { data: configData } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'google_system_spreadsheet') .single(); let existingSpreadsheetId = null; if (configData && configData.configuracao && configData.configuracao.spreadsheetId) { existingSpreadsheetId = configData.configuracao.spreadsheetId; } // Verificar credenciais const { data: credentials } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'google_credentials') .single(); if (!credentials || !credentials.configuracao) { return res.status(400).json({ error: 'Credenciais do Google não configuradas' }); } // Inicializar Google Sheets const googleSheets = require('./config/google-sheets'); const authSuccess = await googleSheets.initializeAuth(credentials.configuracao); if (!authSuccess) { return res.status(400).json({ error: 'Falha na autenticação com Google' }); } await googleSheets.loadSavedTokens(); if (!googleSheets.isAuthenticated()) { return res.status(401).json({ error: 'Não autenticado com Google. Faça a autorização primeiro.' }); } // Renovar token se necessário await googleSheets.refreshTokenIfNeeded(); // Criar ou atualizar planilha const title = req.body.title || 'Liberi Kids - Sistema de Estoque'; const result = await googleSheets.createOrUpdatePersistentSpreadsheet( existingSpreadsheetId, title ); // Salvar ID da planilha no banco await supabase .from('configuracoes') .upsert({ tipo: 'google_system_spreadsheet', configuracao: { spreadsheetId: result.spreadsheetId, url: result.url, title: title, created_at: new Date().toISOString() } }, { onConflict: 'tipo' }); res.json({ success: true, spreadsheet: result, message: existingSpreadsheetId ? 'Planilha do sistema atualizada' : 'Nova planilha do sistema criada' }); } catch (error) { console.error('Erro ao criar/atualizar planilha do sistema:', error); res.status(500).json({ error: error.message }); } }); // Verificar status da planilha do sistema app.get('/api/google/system-spreadsheet-status', async (req, res) => { try { const { data: configData } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'google_system_spreadsheet') .single(); if (configData && configData.configuracao && configData.configuracao.spreadsheetId) { res.json({ exists: true, spreadsheetId: configData.configuracao.spreadsheetId, url: configData.configuracao.url, title: configData.configuracao.title || 'Planilha do Sistema', created_at: configData.configuracao.created_at }); } else { res.json({ exists: false }); } } catch (error) { console.error('Erro ao verificar status da planilha:', error); res.json({ exists: false }); } }); // Atualizar dados na planilha do sistema app.post('/api/google/update-system-data', async (req, res) => { try { // Buscar planilha do sistema const { data: configData } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'google_system_spreadsheet') .single(); if (!configData || !configData.configuracao || !configData.configuracao.spreadsheetId) { return res.status(400).json({ error: 'Planilha do sistema não configurada. Crie uma planilha primeiro.' }); } const spreadsheetId = configData.configuracao.spreadsheetId; // Verificar credenciais const { data: credentials } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'google_credentials') .single(); if (!credentials || !credentials.configuracao) { return res.status(400).json({ error: 'Credenciais do Google não configuradas' }); } // Inicializar Google Sheets const googleSheets = require('./config/google-sheets'); const authSuccess = await googleSheets.initializeAuth(credentials.configuracao); if (!authSuccess) { return res.status(400).json({ error: 'Falha na autenticação com Google' }); } await googleSheets.loadSavedTokens(); if (!googleSheets.isAuthenticated()) { return res.status(401).json({ error: 'Não autenticado com Google. Faça a autorização primeiro.' }); } // Renovar token se necessário await googleSheets.refreshTokenIfNeeded(); // Buscar dados do sistema const [produtosRes, vendasRes] = await Promise.all([ supabase.from('produtos').select(` *, fornecedores(razao_social), produto_variacoes(tamanho, cor, quantidade) `), supabase.from('vendas').select(` *, clientes(nome), venda_produtos(quantidade, preco_unitario, produtos(nome)) `) ]); // Processar dados dos produtos const produtos = (produtosRes.data || []).map(produto => ({ ...produto, fornecedor_nome: produto.fornecedores?.razao_social || '', quantidade_total: produto.produto_variacoes?.reduce((total, v) => total + (v.quantidade || 0), 0) || 0 })); // Processar dados das vendas const vendas = (vendasRes.data || []).map(venda => ({ ...venda, cliente_nome: venda.clientes?.nome || 'Cliente não informado', produtos: venda.venda_produtos || [] })); // Atualizar dados na planilha const [produtosResult, vendasResult] = await Promise.all([ googleSheets.exportProdutos(spreadsheetId, produtos), googleSheets.exportVendas(spreadsheetId, vendas) ]); res.json({ success: true, spreadsheet: { spreadsheetId, url: configData.configuracao.url }, results: { produtos: produtosResult, vendas: vendasResult }, message: 'Dados atualizados na planilha do sistema com sucesso!' }); } catch (error) { console.error('Erro ao atualizar dados na planilha:', error); res.status(500).json({ error: error.message }); } }); // Exportar tudo (produtos + vendas) app.post('/api/google/export-all', async (req, res) => { try { // Verificar credenciais const { data: credentials } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'google_credentials') .single(); if (!credentials || !credentials.configuracao) { return res.status(400).json({ error: 'Credenciais do Google não configuradas' }); } // Inicializar Google Sheets const googleSheets = require('./config/google-sheets'); const authSuccess = await googleSheets.initializeAuth(credentials.configuracao); if (!authSuccess) { return res.status(400).json({ error: 'Falha na autenticação com Google' }); } await googleSheets.loadSavedTokens(); if (!googleSheets.isAuthenticated()) { return res.status(401).json({ error: 'Não autenticado com Google. Faça a autorização primeiro.' }); } // Renovar token se necessário await googleSheets.refreshTokenIfNeeded(); const { title } = req.body; const spreadsheetTitle = title || `Liberi Kids - Exportação Completa - ${new Date().toLocaleDateString('pt-BR')}`; // Criar nova planilha const spreadsheet = await googleSheets.createSpreadsheet(spreadsheetTitle); // Exportar produtos const produtosResponse = await fetch(`http://localhost:${PORT}/api/google/export-produtos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ spreadsheetId: spreadsheet.spreadsheetId }) }); // Exportar vendas const vendasResponse = await fetch(`http://localhost:${PORT}/api/google/export-vendas`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ spreadsheetId: spreadsheet.spreadsheetId }) }); res.json({ success: true, spreadsheet, message: 'Dados exportados com sucesso para Google Sheets' }); } catch (error) { console.error('Erro ao exportar todos os dados:', error); res.status(500).json({ error: error.message }); } }); // Desconectar do Google Sheets app.post('/api/google/disconnect', async (req, res) => { try { // Remover credenciais do banco const { error: credentialsError } = await supabase .from('configuracoes') .delete() .eq('tipo', 'google_credentials'); // Remover planilha do sistema do banco const { error: spreadsheetError } = await supabase .from('configuracoes') .delete() .eq('tipo', 'google_system_spreadsheet'); // Remover arquivo de tokens local se existir const tokensPath = path.join(__dirname, 'google-tokens.json'); if (fs.existsSync(tokensPath)) { fs.unlinkSync(tokensPath); } // Resetar o serviço Google Sheets googleSheetsService.reset(); res.json({ message: 'Desconectado do Google Sheets com sucesso', credentialsRemoved: !credentialsError, spreadsheetRemoved: !spreadsheetError }); } catch (error) { console.error('Erro ao desconectar do Google:', 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('configuracao') .eq('tipo', 'chatgpt_config') .single(); if (error && error.code !== 'PGRST116') { throw error; } const config = data?.configuracao || { 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({ tipo: 'chatgpt_config', configuracao: config }, { onConflict: 'tipo' }); 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('configuracao') .eq('tipo', 'chatgpt_config') .single(); if (configError || !configData?.configuracao?.apiKey) { return res.status(400).json({ error: 'ChatGPT não configurado. Configure a API Key nas configurações.' }); } const config = configData.configuracao; 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_despesas(nome), fornecedores(razao_social) `); 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; } res.json({ data: data || [] }); } 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' }); } // Buscar configurações da Evolution API const { data: configData } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'evolution_api') .single(); let config; if (!configData) { // Usar configurações padrão se não estiver configurado config = { baseUrl: 'https://criadordigital-evolution.jesdfs.easypanel.host', apiKey: 'DBDF609168B1-48A3-8A4A-5E50D0300F2C', instanceName: 'Tiago', enabled: true }; } else { try { config = typeof configData.configuracao === 'string' ? JSON.parse(configData.configuracao) : configData.configuracao; } catch (parseError) { console.error('Erro ao fazer parse da configuração:', parseError); return res.status(400).json({ error: 'Configuração da Evolution API inválida' }); } } if (!config.enabled) { return res.status(400).json({ error: 'Evolution API desabilitada' }); } // Formatar número 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) { throw new Error(`Erro ao enviar mensagem: ${response.status}`); } const evolutionResponse = await response.json(); // Tentar salvar mensagem no histórico let mensagemSalva = null; try { const { data: savedData, error: saveError } = await supabase .from('mensagens_whatsapp') .insert({ telefone_cliente: telefone, cliente_nome: clienteNome || 'Cliente', mensagem: mensagem, tipo: 'enviada', status: 'enviada', evolution_message_id: evolutionResponse.key?.id, created_at: new Date().toISOString() }) .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); // Criar dados simulados para resposta mensagemSalva = { id: Date.now(), telefone_cliente: telefone, cliente_nome: clienteNome || 'Cliente', mensagem: mensagem, tipo: 'enviada', status: 'enviada', created_at: new Date().toISOString() }; } res.json({ success: true, message: 'Mensagem enviada com sucesso!', data: mensagemSalva }); } 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: new Date().toISOString() }); } 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: new Date().toISOString() }) .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: new Date().toISOString() }) .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 }); } }); // ===== ROTAS GOOGLE DRIVE ===== const GoogleDriveService = require('./config/google-drive'); // Salvar credenciais Google Drive app.post('/api/google-drive/credentials', async (req, res) => { try { const { client_id, client_secret, redirect_uris } = req.body; if (!client_id || !client_secret) { return res.status(400).json({ error: 'Client ID e Client Secret são obrigatórios' }); } const credentials = { client_id, client_secret, redirect_uris: redirect_uris || [`http://localhost:${PORT}/auth/google-drive/callback`] }; // Salvar no Supabase const { error } = await supabase .from('configuracoes') .upsert({ tipo: 'google_drive_credentials', configuracao: credentials }, { onConflict: 'tipo' }); if (error) throw error; res.json({ success: true, message: 'Credenciais Google Drive salvas com sucesso' }); } catch (error) { console.error('Erro ao salvar credenciais Google Drive:', error); res.status(500).json({ error: error.message }); } }); // Carregar credenciais Google Drive app.get('/api/google-drive/credentials', async (req, res) => { try { const { data, error } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'google_drive_credentials') .single(); if (error && error.code !== 'PGRST116') throw error; res.json({ success: true, hasCredentials: !!data, redirectUri: `http://localhost:${PORT}/auth/google-drive/callback` }); } catch (error) { console.error('Erro ao carregar credenciais Google Drive:', error); res.status(500).json({ error: error.message }); } }); // Inicializar autenticação Google Drive app.get('/api/google-drive/init', async (req, res) => { try { // Carregar credenciais do banco const { data: configData, error: configError } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'google_drive_credentials') .single(); if (configError || !configData) { return res.status(400).json({ error: 'Credenciais do Google Drive não configuradas' }); } // Inicializar Google Drive const googleDrive = new GoogleDriveService(); const authSuccess = await googleDrive.initializeAuth(configData.configuracao); if (!authSuccess) { return res.status(400).json({ error: 'Falha na inicialização do Google Drive' }); } // Gerar URL de autorização const authUrl = googleDrive.getAuthUrl(); res.json({ success: true, authUrl: authUrl, message: 'Clique no link para autorizar o acesso ao Google Drive' }); } catch (error) { console.error('Erro ao inicializar Google Drive:', error); res.status(500).json({ error: error.message }); } }); // Callback de autenticação Google Drive app.get('/auth/google-drive/callback', async (req, res) => { try { const { code } = req.query; if (!code) { return res.status(400).send('Código de autorização não fornecido'); } // Carregar credenciais do banco const { data: configData, error: configError } = await supabase .from('configuracoes') .select('configuracao') .eq('tipo', 'google_drive_credentials') .single(); if (configError || !configData) { return res.status(400).send('Credenciais do Google Drive não configuradas'); } // Processar callback const googleDrive = new GoogleDriveService(); await googleDrive.initializeAuth(configData.configuracao); const tokens = await googleDrive.handleAuthCallback(code); // Salvar tokens no banco const { error: tokenError } = await supabase .from('configuracoes') .upsert({ tipo: 'google_drive_tokens', configuracao: tokens }, { onConflict: 'tipo' }); if (tokenError) throw tokenError; res.send(`

✅ Autorização Google Drive Concluída!

Você pode fechar esta janela e voltar ao sistema.

`); } catch (error) { console.error('Erro no callback Google Drive:', error); res.status(500).send(`Erro na autorização: ${error.message}`); } }); // Verificar status da autenticação Google Drive app.get('/api/google-drive/status', async (req, res) => { try { // Carregar credenciais e tokens do banco const [credentialsRes, tokensRes] = await Promise.all([ supabase.from('configuracoes').select('configuracao').eq('tipo', 'google_drive_credentials').single(), supabase.from('configuracoes').select('configuracao').eq('tipo', 'google_drive_tokens').single() ]); const hasCredentials = !credentialsRes.error && !!credentialsRes.data; const hasTokens = !tokensRes.error && !!tokensRes.data; if (!hasCredentials) { return res.json({ status: 'not_configured', message: 'Credenciais do Google Drive não configuradas' }); } if (!hasTokens) { return res.json({ status: 'not_authorized', message: 'Autorização do Google Drive pendente' }); } // Testar conexão const googleDrive = new GoogleDriveService(); await googleDrive.initializeAuth(credentialsRes.data.configuracao); // Carregar tokens salvos const fs = require('fs'); const path = require('path'); const tokensPath = path.join(__dirname, 'config', 'google-tokens.json'); if (tokensRes.data && tokensRes.data.configuracao) { fs.writeFileSync(tokensPath, JSON.stringify(tokensRes.data.configuracao, null, 2)); } if (googleDrive.isAuthenticated()) { // Obter informações de armazenamento const storageInfo = await googleDrive.getStorageInfo(); res.json({ status: 'connected', message: 'Conectado ao Google Drive', storage: storageInfo }); } else { res.json({ status: 'not_authorized', message: 'Autorização do Google Drive expirada' }); } } catch (error) { console.error('Erro ao verificar status Google Drive:', error); res.json({ status: 'error', message: error.message }); } }); // Upload de arquivo para Google Drive app.post('/api/google-drive/upload', upload.single('file'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'Nenhum arquivo enviado' }); } // Carregar credenciais e tokens const [credentialsRes, tokensRes] = await Promise.all([ supabase.from('configuracoes').select('configuracao').eq('tipo', 'google_drive_credentials').single(), supabase.from('configuracoes').select('configuracao').eq('tipo', 'google_drive_tokens').single() ]); if (credentialsRes.error || tokensRes.error) { return res.status(400).json({ error: 'Google Drive não configurado' }); } // Inicializar Google Drive const googleDrive = new GoogleDriveService(); await googleDrive.initializeAuth(credentialsRes.data.configuracao); // Carregar tokens const fs = require('fs'); const path = require('path'); const tokensPath = path.join(__dirname, 'config', 'google-tokens.json'); fs.writeFileSync(tokensPath, JSON.stringify(tokensRes.data.configuracao, null, 2)); // Renovar token se necessário await googleDrive.refreshTokenIfNeeded(); // Criar pasta Liberi Kids se não existir const folderId = await googleDrive.createFolder('Liberi Kids - Fotos Produtos'); // Fazer upload do arquivo const uploadResult = await googleDrive.uploadFile( req.file.path, req.file.originalname, folderId, req.file.mimetype ); // Remover arquivo local após upload fs.unlinkSync(req.file.path); res.json({ success: true, file: uploadResult, message: 'Arquivo enviado para Google Drive com sucesso' }); } catch (error) { console.error('Erro no upload para Google Drive:', error); res.status(500).json({ error: error.message }); } }); // Upload múltiplo para Google Drive app.post('/api/google-drive/upload-multiple', upload.array('files'), async (req, res) => { try { if (!req.files || req.files.length === 0) { return res.status(400).json({ error: 'Nenhum arquivo enviado' }); } // Carregar credenciais e tokens const [credentialsRes, tokensRes] = await Promise.all([ supabase.from('configuracoes').select('configuracao').eq('tipo', 'google_drive_credentials').single(), supabase.from('configuracoes').select('configuracao').eq('tipo', 'google_drive_tokens').single() ]); if (credentialsRes.error || tokensRes.error) { return res.status(400).json({ error: 'Google Drive não configurado' }); } // Inicializar Google Drive const googleDrive = new GoogleDriveService(); await googleDrive.initializeAuth(credentialsRes.data.configuracao); // Carregar tokens const fs = require('fs'); const path = require('path'); const tokensPath = path.join(__dirname, 'config', 'google-tokens.json'); fs.writeFileSync(tokensPath, JSON.stringify(tokensRes.data.configuracao, null, 2)); // Renovar token se necessário await googleDrive.refreshTokenIfNeeded(); // Criar pasta Liberi Kids se não existir const folderId = await googleDrive.createFolder('Liberi Kids - Fotos Produtos'); // Preparar arquivos para upload const files = req.files.map(file => ({ path: file.path, name: file.originalname, mimeType: file.mimetype })); // Fazer upload múltiplo const uploadResults = await googleDrive.uploadMultipleFiles(files, folderId); // Remover arquivos locais após upload req.files.forEach(file => { if (fs.existsSync(file.path)) { fs.unlinkSync(file.path); } }); res.json({ success: true, files: uploadResults, message: `${uploadResults.length} arquivos enviados para Google Drive com sucesso` }); } catch (error) { console.error('Erro no upload múltiplo para Google Drive:', error); res.status(500).json({ error: error.message }); } }); // Listar arquivos do Google Drive app.get('/api/google-drive/files', async (req, res) => { try { // Carregar credenciais e tokens const [credentialsRes, tokensRes] = await Promise.all([ supabase.from('configuracoes').select('configuracao').eq('tipo', 'google_drive_credentials').single(), supabase.from('configuracoes').select('configuracao').eq('tipo', 'google_drive_tokens').single() ]); if (credentialsRes.error || tokensRes.error) { return res.status(400).json({ error: 'Google Drive não configurado' }); } // Inicializar Google Drive const googleDrive = new GoogleDriveService(); await googleDrive.initializeAuth(credentialsRes.data.configuracao); // Carregar tokens const fs = require('fs'); const path = require('path'); const tokensPath = path.join(__dirname, 'config', 'google-tokens.json'); fs.writeFileSync(tokensPath, JSON.stringify(tokensRes.data.configuracao, null, 2)); // Renovar token se necessário await googleDrive.refreshTokenIfNeeded(); // Buscar pasta Liberi Kids const folderId = await googleDrive.createFolder('Liberi Kids - Fotos Produtos'); // Listar arquivos da pasta const files = await googleDrive.listFiles(folderId, 50); res.json({ success: true, files: files, message: 'Arquivos listados com sucesso' }); } catch (error) { console.error('Erro ao listar arquivos Google Drive:', error); res.status(500).json({ error: error.message }); } }); // Deletar arquivo do Google Drive app.delete('/api/google-drive/files/:fileId', async (req, res) => { try { const { fileId } = req.params; // Carregar credenciais e tokens const [credentialsRes, tokensRes] = await Promise.all([ supabase.from('configuracoes').select('configuracao').eq('tipo', 'google_drive_credentials').single(), supabase.from('configuracoes').select('configuracao').eq('tipo', 'google_drive_tokens').single() ]); if (credentialsRes.error || tokensRes.error) { return res.status(400).json({ error: 'Google Drive não configurado' }); } // Inicializar Google Drive const googleDrive = new GoogleDriveService(); await googleDrive.initializeAuth(credentialsRes.data.configuracao); // Carregar tokens const fs = require('fs'); const path = require('path'); const tokensPath = path.join(__dirname, 'config', 'google-tokens.json'); fs.writeFileSync(tokensPath, JSON.stringify(tokensRes.data.configuracao, null, 2)); // Renovar token se necessário await googleDrive.refreshTokenIfNeeded(); // Deletar arquivo await googleDrive.deleteFile(fileId); res.json({ success: true, message: 'Arquivo deletado com sucesso' }); } catch (error) { console.error('Erro ao deletar arquivo Google Drive:', 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;