Files
App-Estoque-LiberiKids/server-supabase.js
2025-10-14 14:04:17 -03:00

4031 lines
126 KiB
JavaScript

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(`
<html>
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
<h2 style="color: #4CAF50;">✅ Autorização Google Drive Concluída!</h2>
<p>Você pode fechar esta janela e voltar ao sistema.</p>
<script>
setTimeout(() => {
window.close();
}, 3000);
</script>
</body>
</html>
`);
} 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;