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