Files
App-Estoque-LiberiKids/server-supabase.js
2025-11-29 21:31:52 -03:00

4898 lines
150 KiB
JavaScript
Raw Permalink Blame History

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