4031 lines
126 KiB
JavaScript
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;
|