0b2748a579
- add: migration #25 cria tabela MENSAGENS_CLIENTES + generator - fix: insert agora usa db.nextId para gerar MSC_CODIGO_ID - fix: adiciona GEN_MENSAGENS_CLIENTES ao whitelist no database.js
1673 lines
72 KiB
JavaScript
1673 lines
72 KiB
JavaScript
const db = require('../database');
|
|
const ffmpegPath = require('ffmpeg-static');
|
|
const { execFile } = require('child_process');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
/**
|
|
* Converte buffer de áudio WebM para Ogg Opus usando ffmpeg
|
|
*/
|
|
function convertWebmToOgg(inputBuffer) {
|
|
return new Promise(function(resolve, reject) {
|
|
var tmpDir = path.join(__dirname, '../../uploads/audio');
|
|
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
|
|
|
var inputPath = path.join(tmpDir, 'input_' + Date.now() + '.webm');
|
|
var outputPath = path.join(tmpDir, 'output_' + Date.now() + '.ogg');
|
|
|
|
fs.writeFileSync(inputPath, inputBuffer);
|
|
|
|
execFile(ffmpegPath, [
|
|
'-y',
|
|
'-i', inputPath,
|
|
'-c:a', 'libopus',
|
|
'-b:a', '16k',
|
|
'-ar', '16000',
|
|
'-ac', '1',
|
|
outputPath
|
|
], { timeout: 30000 }, function(err) {
|
|
// Limpa arquivo temporário de entrada
|
|
try { fs.unlinkSync(inputPath); } catch(e) {}
|
|
|
|
if (err) {
|
|
try { fs.unlinkSync(outputPath); } catch(e) {}
|
|
return reject(err);
|
|
}
|
|
|
|
var outputBuffer = fs.readFileSync(outputPath);
|
|
try { fs.unlinkSync(outputPath); } catch(e) {}
|
|
|
|
resolve(outputBuffer);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Valida se um buffer tem cabeçalho de arquivo de mídia válido
|
|
* (defesa: rejeita JSON, HTML, ou dados criptografados salvos por engano)
|
|
* @param {Buffer} buf
|
|
* @param {string} mimeType
|
|
* @returns {boolean}
|
|
*/
|
|
function validarHeaderMidiaServida(buf, mimeType) {
|
|
if (!buf || buf.length < 4) return false;
|
|
var headerHex = buf.slice(0, 4).toString('hex');
|
|
// Valida headers de audio conhecidos
|
|
var validHeaders = ['4f676753', // OggS (ogg/opus)
|
|
'52494646', // RIFF (wav)
|
|
'66747970', // ftyp (mp4/m4a)
|
|
'494433', // ID3 (mp3)
|
|
'1a45dfa3', // WebM/Matroska EBML
|
|
'fffb', 'fff3', 'fffa', 'fff2']; // MP3 frames
|
|
for (var vh of validHeaders) {
|
|
if (headerHex.startsWith(vh)) return true;
|
|
}
|
|
// Para imagens: JPEG (FF D8 FF), PNG (89504e47), GIF (47494638), WebP (52494646 ja coberto)
|
|
if (mimeType.startsWith('image/')) {
|
|
if (headerHex.startsWith('ffd8ff') || headerHex.startsWith('89504e47') ||
|
|
headerHex.startsWith('47494638')) return true;
|
|
}
|
|
// Para documentos: ZIP/Office (504b)
|
|
if (mimeType.startsWith('application/')) {
|
|
if (headerHex.startsWith('504b')) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
class ChatController {
|
|
/**
|
|
* Página do chat
|
|
* GET /app/:alias/company/:empresaId/conversation/:conversaId
|
|
*/
|
|
static chatPage(req, res) {
|
|
res.sendFile(path.resolve(__dirname, '../public/chat.html'));
|
|
}
|
|
|
|
/**
|
|
* Listar conversas (sidebar esquerda)
|
|
* GET /api/:alias/conversations?empresaId=&status=A,E
|
|
*/
|
|
static async listConversations(req, res) {
|
|
try {
|
|
const { alias } = req.params;
|
|
const empresaId = parseInt(req.query.empresaId) || req.user?.empresas?.[0];
|
|
const statusFilter = req.query.status || 'A,E';
|
|
const filter = req.query.filter || ''; // 'mine', 'unassigned', 'all'
|
|
|
|
if (!empresaId || !req.user?.empresas?.some(function(e) { return Number(e) === Number(empresaId); }))
|
|
return res.status(403).json({ success: false, error: 'Sem permissão.' });
|
|
|
|
// Status via parâmetros (evita SQL injection). Aceita apenas A/E/F.
|
|
const statusList = statusFilter.split(',').map(s => s.trim().toUpperCase()).filter(s => /^[AEF]$/.test(s));
|
|
if (statusList.length === 0) statusList.push('A', 'E');
|
|
const statusPh = statusList.map(() => '?').join(',');
|
|
|
|
const params = [empresaId];
|
|
statusList.forEach(s => params.push(s));
|
|
|
|
let extraFilter = '';
|
|
if (filter === 'mine') {
|
|
extraFilter = ' AND c.CON_USUARIO_ID = ?';
|
|
params.push(req.user?.id || 0);
|
|
} else if (filter === 'unassigned') {
|
|
extraFilter = ' AND c.CON_USUARIO_ID IS NULL AND c.CON_EQUIPE_ID IS NULL';
|
|
} else if (filter === 'equipe') {
|
|
// Busca equipes do usuário logado
|
|
const minhasEquipes = await db.query(alias,
|
|
'SELECT EQU_EQUIPE_ID FROM CHATC2_USU_EQUIPES WHERE EQU_USUARIO_ID = ?', [req.user?.id]);
|
|
if (minhasEquipes.length > 0) {
|
|
const eqPh = minhasEquipes.map(() => '?').join(',');
|
|
extraFilter = ' AND c.CON_EQUIPE_ID IN (' + eqPh + ')';
|
|
minhasEquipes.forEach(e => params.push(e.EQU_EQUIPE_ID));
|
|
} else {
|
|
extraFilter = ' AND 1=0'; // Nenhuma equipe
|
|
}
|
|
}
|
|
|
|
const result = await db.query(alias, `
|
|
SELECT c.*,
|
|
(SELECT CME_TEXTO FROM CHATC2_CONVERSAS_MENSAGENS WHERE CME_CONVERSA_ID = c.CON_CODIGO_ID ORDER BY CME_DT_ENVIO DESC FETCH FIRST 1 ROWS ONLY) AS ULTIMA_MSG,
|
|
(SELECT CME_TIPO FROM CHATC2_CONVERSAS_MENSAGENS WHERE CME_CONVERSA_ID = c.CON_CODIGO_ID ORDER BY CME_DT_ENVIO DESC FETCH FIRST 1 ROWS ONLY) AS ULTIMA_TIPO,
|
|
(SELECT CME_DT_ENVIO FROM CHATC2_CONVERSAS_MENSAGENS WHERE CME_CONVERSA_ID = c.CON_CODIGO_ID ORDER BY CME_DT_ENVIO DESC FETCH FIRST 1 ROWS ONLY) AS DT_ULTIMA_MSG
|
|
FROM CHATC2_CONVERSAS c
|
|
WHERE c.CON_EMPRESA_ID = ? AND c.CON_STATUS IN (${statusPh}) AND c.CON_SITUACAO = 'A' ${extraFilter}
|
|
ORDER BY c.CON_DT_ULTIMA_MSG DESC
|
|
`, params);
|
|
|
|
// --- Busca em lote: etiquetas de todas as conversas (evita N+1 queries) ---
|
|
const conversaIds = result.map(r => r.CON_CODIGO_ID);
|
|
const labelsMap = {};
|
|
for (const cid of conversaIds) labelsMap[cid] = [];
|
|
|
|
if (conversaIds.length > 0) {
|
|
const idsPlaceholders = conversaIds.map(() => '?').join(',');
|
|
const todasEtiquetas = await db.query(alias, `
|
|
SELECT ce.CET_CONVERSA_ID, e.ETI_CODIGO_ID, e.ETI_NOME, e.ETI_COR
|
|
FROM CHATC2_ETIQUETAS e
|
|
INNER JOIN CHATC2_CONVERSAS_ETIQUETAS ce ON e.ETI_CODIGO_ID = ce.CET_ETIQUETA_ID
|
|
WHERE ce.CET_CONVERSA_ID IN (${idsPlaceholders}) AND e.ETI_SITUACAO = 'A'
|
|
`, conversaIds);
|
|
for (const et of todasEtiquetas) {
|
|
if (!labelsMap[et.CET_CONVERSA_ID]) labelsMap[et.CET_CONVERSA_ID] = [];
|
|
labelsMap[et.CET_CONVERSA_ID].push({
|
|
id: et.ETI_CODIGO_ID, nome: (et.ETI_NOME || '').trim(), cor: (et.ETI_COR || '#667eea').trim()
|
|
});
|
|
}
|
|
}
|
|
|
|
// --- Busca em lote: fotos dos clientes (se config ativo) ---
|
|
const fotosMap = {};
|
|
let fotoCelularAtivo = false;
|
|
{
|
|
const config = await db.query(alias,
|
|
'SELECT CFE_FOTO_CELULAR FROM CHATC2_CONFIGURACOES_EMPRESA WHERE CFE_EMPRESA_ID = ?', [empresaId]);
|
|
if (config.length > 0 && config[0].CFE_FOTO_CELULAR === 'S') {
|
|
fotoCelularAtivo = true;
|
|
const clienteIds = result.filter(r => r.CON_CLIENTE_ID).map(r => r.CON_CLIENTE_ID);
|
|
if (clienteIds.length > 0) {
|
|
const cliPlaceholders = clienteIds.map(() => '?').join(',');
|
|
const fotos = await db.query(alias,
|
|
'SELECT CLI_CODIGO_ID, CLI_FOTO, CLI_NOME_FOTO FROM CLIENTES WHERE CLI_CODIGO_ID IN (' + cliPlaceholders + ')',
|
|
clienteIds);
|
|
for (const f of fotos) {
|
|
const rawFoto = f.CLI_FOTO;
|
|
if (!rawFoto) continue;
|
|
let foto = null;
|
|
if (Buffer.isBuffer(rawFoto)) {
|
|
const str = rawFoto.toString('utf8');
|
|
if (/^[A-Za-z0-9+/]+=*$/.test(str.slice(0, 30))) {
|
|
foto = str;
|
|
} else {
|
|
let prefixo = 'data:image/jpeg;base64,';
|
|
if (rawFoto[0] === 0x89 && rawFoto[1] === 0x50) prefixo = 'data:image/png;base64,';
|
|
else if (rawFoto[0] === 0x47 && rawFoto[1] === 0x49) prefixo = 'data:image/gif;base64,';
|
|
else if (rawFoto[0] === 0x52 && rawFoto[1] === 0x49) prefixo = 'data:image/webp;base64,';
|
|
foto = prefixo + rawFoto.toString('base64');
|
|
}
|
|
} else {
|
|
foto = rawFoto;
|
|
}
|
|
fotosMap[f.CLI_CODIGO_ID] = foto;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Monta resultado final sem novas queries ---
|
|
const conversations = result.map((row) => ({
|
|
id: row.CON_CODIGO_ID,
|
|
empresaId: row.CON_EMPRESA_ID,
|
|
instanciaId: row.CON_INSTANCIA_ID,
|
|
clienteId: row.CON_CLIENTE_ID,
|
|
numero: (row.CON_NUMERO || '').trim(),
|
|
nomeContato: (row.CON_NOME_CONTATO || '').trim(),
|
|
status: (row.CON_STATUS || '').trim(),
|
|
usuarioId: row.CON_USUARIO_ID,
|
|
equipeId: row.CON_EQUIPE_ID,
|
|
dtInicio: row.CON_DT_INICIO,
|
|
dtFinal: row.CON_DT_FINAL,
|
|
ultimaMsg: row.ULTIMA_MSG || '',
|
|
ultimaTipo: (row.ULTIMA_TIPO || 'text').trim(),
|
|
dtUltimaMsg: row.DT_ULTIMA_MSG || row.CON_DT_ULTIMA_MSG,
|
|
saudacaoEnviada: (row.CON_SAUDACAO_ENVIADA || 'N').trim(),
|
|
csatEnviado: (row.CON_CSAT_ENVIADO || 'N').trim(),
|
|
primeiraMsg: row.CON_PRIMEIRA_MSG,
|
|
labels: labelsMap[row.CON_CODIGO_ID] || [],
|
|
foto: fotosMap[row.CON_CLIENTE_ID] || null,
|
|
}));
|
|
|
|
// Contagens para cada filtro
|
|
const userId = req.user?.id || 0;
|
|
var countMine = await db.query(alias,
|
|
`SELECT COUNT(*) AS CT FROM CHATC2_CONVERSAS WHERE CON_EMPRESA_ID = ? AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A' AND CON_USUARIO_ID = ?`,
|
|
[empresaId, userId]);
|
|
var countUnassigned = await db.query(alias,
|
|
`SELECT COUNT(*) AS CT FROM CHATC2_CONVERSAS WHERE CON_EMPRESA_ID = ? AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A' AND CON_USUARIO_ID IS NULL AND CON_EQUIPE_ID IS NULL`,
|
|
[empresaId]);
|
|
var countEquipe = 0;
|
|
if (filter === 'equipe' || filter === 'mine' || filter === 'unassigned' || filter === 'all') {
|
|
var minhasEquipes = await db.query(alias,
|
|
'SELECT EQU_EQUIPE_ID FROM CHATC2_USU_EQUIPES WHERE EQU_USUARIO_ID = ?', [userId]);
|
|
if (minhasEquipes.length > 0) {
|
|
var eqIds = minhasEquipes.map(function(e) { return "'" + e.EQU_EQUIPE_ID + "'"; }).join(',');
|
|
var countEq = await db.query(alias,
|
|
`SELECT COUNT(*) AS CT FROM CHATC2_CONVERSAS WHERE CON_EMPRESA_ID = ? AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A' AND CON_EQUIPE_ID IN (${eqIds})`,
|
|
[empresaId]);
|
|
countEquipe = countEq[0]?.CT || 0;
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: conversations,
|
|
contagens: {
|
|
mine: countMine[0]?.CT || 0,
|
|
unassigned: countUnassigned[0]?.CT || 0,
|
|
equipe: countEquipe,
|
|
},
|
|
});
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
|
|
/**
|
|
* Pegar/conversa específica
|
|
* GET /api/:alias/conversations/:id
|
|
*/
|
|
static async getConversation(req, res) {
|
|
try {
|
|
const { alias, id } = req.params;
|
|
|
|
const result = await db.query(alias,
|
|
'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [id]);
|
|
|
|
if (result.length === 0) return res.status(404).json({ success: false, error: 'Conversa não encontrada.' });
|
|
|
|
const row = result[0];
|
|
const empresaId = row.CON_EMPRESA_ID;
|
|
|
|
if (empresaId && !req.user?.empresas?.some(function(e) { return String(e) === String(empresaId); }))
|
|
return res.status(403).json({ success: false, error: 'Sem permissão.' });
|
|
|
|
// Verifica se o número pertence a um dependente
|
|
let dependenteInfo = null;
|
|
if (!row.CON_CLIENTE_ID && row.CON_NUMERO) {
|
|
const depNumero = row.CON_NUMERO.replace(/\D/g, '');
|
|
const depTermo = depNumero.slice(-8);
|
|
const dep = await db.query(alias,
|
|
`SELECT d.DEPC_NOME, d.DEPC_TELEFONE, d.DEPC_PARENTESCO,
|
|
c.CLI_CODIGO_ID AS TITULAR_ID, c.CLI_NOME AS TITULAR_NOME,
|
|
c.CLI_MATRICULA AS TITULAR_MATRICULA, c.CLI_PLANOS_ID AS TITULAR_PLANO_ID,
|
|
c.CLI_CIDADES_FAT_ID, c.CLI_BAIRROS_FAT_ID
|
|
FROM DEPENDENTES_CLI d
|
|
LEFT JOIN CLIENTES c ON d.DEPC_CLIENTE_ID = c.CLI_CODIGO_ID
|
|
WHERE d.DEPC_EMPRESA_ID = ? AND d.DEPC_SITUACAO = 'A'
|
|
AND REPLACE(REPLACE(REPLACE(COALESCE(d.DEPC_TELEFONE,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'`,
|
|
[empresaId, depTermo]
|
|
);
|
|
if (dep.length > 0) {
|
|
const d = dep[0];
|
|
// Busca dados adicionais do titular
|
|
let cidadeNome = null, bairroNome = null, planoNome = null;
|
|
let inadimplente = false;
|
|
if (d.TITULAR_ID) {
|
|
if (d.CLI_CIDADES_FAT_ID) {
|
|
const cid = await db.query(alias, 'SELECT CID_NOME FROM CIDADES WHERE CID_CODIGO_ID = ?', [d.CLI_CIDADES_FAT_ID]);
|
|
if (cid.length > 0) cidadeNome = cid[0].CID_NOME?.trim();
|
|
}
|
|
if (d.CLI_BAIRROS_FAT_ID) {
|
|
const bai = await db.query(alias, 'SELECT BAR_DESCRICAO FROM BAIRROS WHERE BAR_CODIGO_ID = ?', [d.CLI_BAIRROS_FAT_ID]);
|
|
if (bai.length > 0) bairroNome = bai[0].BAR_DESCRICAO?.trim();
|
|
}
|
|
if (d.TITULAR_PLANO_ID) {
|
|
const pla = await db.query(alias, 'SELECT PLA_NOME FROM PLANOS WHERE PLA_CODIGO_ID = ?', [d.TITULAR_PLANO_ID]);
|
|
if (pla.length > 0) planoNome = pla[0].PLA_NOME?.trim();
|
|
}
|
|
const tit = await db.query(alias,
|
|
'SELECT COUNT(*) AS T FROM CARNES WHERE CAR_CLIENTE_ID = ? AND CAR_SITUACAO = 0 AND CAR_DT_VENCIMENTO < CURRENT_DATE',
|
|
[d.TITULAR_ID]);
|
|
inadimplente = tit[0]?.T > 0;
|
|
}
|
|
dependenteInfo = {
|
|
nome: (d.DEPC_NOME || '').trim(),
|
|
telefone: (d.DEPC_TELEFONE || '').trim(),
|
|
parentesco: (d.DEPC_PARENTESCO || '').trim(),
|
|
titularId: d.TITULAR_ID || d.CLI_CODIGO_ID || null,
|
|
titularNome: (d.TITULAR_NOME || '').trim(),
|
|
titularMatricula: (d.TITULAR_MATRICULA || '').trim(),
|
|
titularCidade: cidadeNome,
|
|
titularBairro: bairroNome,
|
|
titularPlano: planoNome,
|
|
titularInadimplente: inadimplente,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Info do cliente se existir
|
|
let clienteInfo = null;
|
|
if (row.CON_CLIENTE_ID) {
|
|
const cli = await db.query(alias, `
|
|
SELECT CLI_CODIGO_ID, CLI_NOME, CLI_MATRICULA, CLI_SITUACAO, CLI_EMAIL, CLI_CELULAR,
|
|
CLI_FONE1, CLI_ENDERECO_FAT, CLI_NUMERO_FAT, CLI_CIDADES_FAT_ID, CLI_BAIRROS_FAT_ID,
|
|
CLI_COBRADOR_ID, CLI_DIA_COBRANCA, CLI_FOTO, CLI_NOME_FOTO, CLI_PLANOS_ID
|
|
FROM CLIENTES WHERE CLI_CODIGO_ID = ?
|
|
`, [row.CON_CLIENTE_ID]);
|
|
|
|
if (cli.length > 0) {
|
|
const c = cli[0];
|
|
|
|
// Busca plano
|
|
let planoNome = null;
|
|
if (c.CLI_PLANOS_ID) {
|
|
const plano = await db.query(alias, 'SELECT PLA_NOME FROM PLANOS WHERE PLA_CODIGO_ID = ?', [c.CLI_PLANOS_ID]);
|
|
if (plano.length > 0) planoNome = plano[0].PLA_NOME?.trim();
|
|
}
|
|
|
|
// Busca cidade/bairro
|
|
let cidadeNome = null, bairroNome = null;
|
|
if (c.CLI_CIDADES_FAT_ID) {
|
|
const cidade = await db.query(alias, 'SELECT CID_NOME FROM CIDADES WHERE CID_CODIGO_ID = ?', [c.CLI_CIDADES_FAT_ID]);
|
|
if (cidade.length > 0) cidadeNome = cidade[0].CID_NOME?.trim();
|
|
}
|
|
if (c.CLI_BAIRROS_FAT_ID) {
|
|
const bairro = await db.query(alias, 'SELECT BAR_DESCRICAO FROM BAIRROS WHERE BAR_CODIGO_ID = ?', [c.CLI_BAIRROS_FAT_ID]);
|
|
if (bairro.length > 0) bairroNome = bairro[0].BAR_DESCRICAO?.trim();
|
|
}
|
|
|
|
// Inadimplente? Verifica se tem títulos abertos vencidos
|
|
let inadimplente = false;
|
|
const titulos = await db.query(alias,
|
|
`SELECT COUNT(*) AS T FROM CARNES WHERE CAR_CLIENTE_ID = ? AND CAR_SITUACAO = 0 AND CAR_DT_VENCIMENTO < CURRENT_DATE`,
|
|
[c.CLI_CODIGO_ID]);
|
|
inadimplente = titulos[0].T > 0;
|
|
|
|
clienteInfo = {
|
|
id: c.CLI_CODIGO_ID,
|
|
nome: (c.CLI_NOME || '').trim(),
|
|
matricula: (c.CLI_MATRICULA || '').trim(),
|
|
situacao: (c.CLI_SITUACAO || '').trim(),
|
|
email: (c.CLI_EMAIL || '').trim(),
|
|
celular: (c.CLI_CELULAR || '').trim(),
|
|
telefone: (c.CLI_FONE1 || '').trim(),
|
|
endereco: (c.CLI_ENDERECO_FAT || '').trim(),
|
|
numero: (c.CLI_NUMERO_FAT || '').trim(),
|
|
cidade: cidadeNome,
|
|
bairro: bairroNome,
|
|
plano: planoNome,
|
|
inadimplente,
|
|
foto: (function(){
|
|
var f = c.CLI_FOTO;
|
|
if (!f) return null;
|
|
if (Buffer.isBuffer(f)) {
|
|
var s = f.toString('utf8');
|
|
if (/^[A-Za-z0-9+/]+=*$/.test(s.slice(0, 30))) return s;
|
|
var prefixo = 'data:image/jpeg;base64,';
|
|
if (f[0] === 0x89 && f[1] === 0x50) prefixo = 'data:image/png;base64,';
|
|
else if (f[0] === 0x47 && f[1] === 0x49) prefixo = 'data:image/gif;base64,';
|
|
else if (f[0] === 0x52 && f[1] === 0x49) prefixo = 'data:image/webp;base64,';
|
|
return prefixo + f.toString('base64');
|
|
}
|
|
return f;
|
|
})(),
|
|
};
|
|
}
|
|
}
|
|
|
|
// Etiquetas
|
|
const labels = await db.query(alias, `
|
|
SELECT e.* FROM CHATC2_ETIQUETAS e INNER JOIN CHATC2_CONVERSAS_ETIQUETAS ce ON e.ETI_CODIGO_ID = ce.CET_ETIQUETA_ID
|
|
WHERE ce.CET_CONVERSA_ID = ? AND e.ETI_SITUACAO = 'A'
|
|
`, [id]);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
id: row.CON_CODIGO_ID,
|
|
empresaId: row.CON_EMPRESA_ID,
|
|
instanciaId: row.CON_INSTANCIA_ID,
|
|
clienteId: row.CON_CLIENTE_ID,
|
|
numero: (row.CON_NUMERO || '').trim(),
|
|
nomeContato: (row.CON_NOME_CONTATO || '').trim(),
|
|
status: (row.CON_STATUS || '').trim(),
|
|
usuarioId: row.CON_USUARIO_ID,
|
|
equipeId: row.CON_EQUIPE_ID,
|
|
dtInicio: row.CON_DT_INICIO,
|
|
dtFinal: row.CON_DT_FINAL,
|
|
dtUltimaMsg: row.CON_DT_ULTIMA_MSG,
|
|
saudacaoEnviada: (row.CON_SAUDACAO_ENVIADA || 'N').trim(),
|
|
csatEnviado: (row.CON_CSAT_ENVIADO || 'N').trim(),
|
|
primeiraMsg: row.CON_PRIMEIRA_MSG,
|
|
motivoId: row.CON_MOTIVO_ID || null,
|
|
resolucao: row.CON_RESOLUCAO || null,
|
|
cliente: clienteInfo,
|
|
dependente: dependenteInfo,
|
|
labels: labels.map(l => ({ id: l.ETI_CODIGO_ID, nome: (l.ETI_NOME || '').trim(), cor: (l.ETI_COR || '#667eea').trim() })),
|
|
}
|
|
});
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
|
|
/**
|
|
* Pegar mensagens de uma conversa
|
|
* GET /api/:alias/conversations/:id/messages?page=&limit=
|
|
*/
|
|
/**
|
|
* Verifica se a conversa existe e pertence a uma empresa do usuário.
|
|
* Retorna { status: 404|403 } em caso de bloqueio, ou { ok:true, empresaId }.
|
|
*/
|
|
static async checarConversaEmpresa(alias, id, req) {
|
|
const r = await db.query(alias,
|
|
'SELECT CON_EMPRESA_ID FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [id]);
|
|
if (r.length === 0) return { status: 404 };
|
|
const emp = r[0].CON_EMPRESA_ID;
|
|
const minhas = (req.user && req.user.empresas) || [];
|
|
if (emp && !minhas.some(function(e) { return String(e) === String(emp); })) return { status: 403 };
|
|
return { ok: true, empresaId: emp };
|
|
}
|
|
|
|
static async getMessages(req, res) {
|
|
try {
|
|
const { alias, id } = req.params;
|
|
const page = parseInt(req.query.page) || 1;
|
|
const limit = parseInt(req.query.limit) || 50;
|
|
const offset = (page - 1) * limit;
|
|
|
|
const chk = await ChatController.checarConversaEmpresa(alias, id, req);
|
|
if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 404 ? 'Conversa não encontrada.' : 'Sem permissão.' });
|
|
|
|
const msgs = await db.query(alias, `
|
|
SELECT *
|
|
FROM CHATC2_CONVERSAS_MENSAGENS
|
|
WHERE CME_CONVERSA_ID = ? AND CME_SITUACAO = 'A'
|
|
ORDER BY CME_DT_ENVIO DESC
|
|
OFFSET ? ROWS FETCH NEXT ? ROWS ONLY
|
|
`, [id, offset, limit]);
|
|
|
|
var mapped = msgs.reverse().map(function(m) { return {
|
|
id: m.CME_CODIGO_ID,
|
|
conversaId: m.CME_CONVERSA_ID,
|
|
remetente: (m.CME_REMETENTE || '').trim(),
|
|
usuarioId: m.CME_USUARIO_ID,
|
|
mensagem: m.CME_TEXTO || m.CME_MENSAGEM || '',
|
|
tipo: (m.CME_TIPO || 'text').trim(),
|
|
dtEnvio: m.CME_DT_ENVIO,
|
|
privada: (m.CME_PRIVADA || 'N').trim(),
|
|
lida: (m.CME_LIDA || 'N').trim(),
|
|
midiaId: m.CME_MIDIA_ID,
|
|
};});
|
|
|
|
// Busca transcricao para mensagens com midia
|
|
for (var i = 0; i < mapped.length; i++) {
|
|
if (mapped[i].midiaId && (mapped[i].tipo === 'audio' || mapped[i].tipo === 'ptt')) {
|
|
try {
|
|
var trans = await db.query(alias,
|
|
'SELECT MAT_TRANSCRICAO FROM CHATC2_MENSAGENS_ATENDIMENTOS WHERE MAT_CODIGO_ID = ? AND MAT_TRANSCRICAO IS NOT NULL',
|
|
[mapped[i].midiaId]);
|
|
if (trans.length > 0 && trans[0].MAT_TRANSCRICAO) {
|
|
var txt = trans[0].MAT_TRANSCRICAO;
|
|
if (typeof txt === 'object' && txt.toString) txt = txt.toString();
|
|
mapped[i].transcricao = txt;
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: mapped,
|
|
});
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
|
|
/**
|
|
* Enviar mensagem
|
|
* POST /api/:alias/conversations/:id/messages
|
|
*/
|
|
static async sendMessage(req, res) {
|
|
try {
|
|
const { alias, id } = req.params;
|
|
const { mensagem, tipo, privada, nomeArquivo } = req.body;
|
|
let midiaBase64 = req.body.midiaBase64;
|
|
const usuarioId = req.user?.id;
|
|
|
|
const conv = await db.query(alias, 'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [id]);
|
|
if (conv.length === 0) return res.status(404).json({ success: false, error: 'Conversa não encontrada.' });
|
|
if (conv[0].CON_EMPRESA_ID && !((req.user && req.user.empresas) || []).some(function(e) { return String(e) === String(conv[0].CON_EMPRESA_ID); }))
|
|
return res.status(403).json({ success: false, error: 'Sem permissão.' });
|
|
|
|
// Se a conversa estiver finalizada, reabre
|
|
if (conv[0].CON_STATUS === 'F') {
|
|
await db.execute(alias,
|
|
"UPDATE CHATC2_CONVERSAS SET CON_STATUS = 'A', CON_DT_FINAL = NULL WHERE CON_CODIGO_ID = ?", [id]);
|
|
}
|
|
|
|
const newId = await db.nextId(alias, 'GEN_CONVERSAS_MENSAGENS');
|
|
let midiaId = null;
|
|
|
|
// Salva mídia se houver
|
|
if (midiaBase64) {
|
|
midiaId = await db.nextId(alias, 'GEN_MENSAGENS_ATENDIMENTOS');
|
|
|
|
// Para áudio: converte WebM → OGG antes de salvar no BD
|
|
// Assim o BD já tem o OGG e o envio para Evolution usa a mesma versão
|
|
var midiaFinal = midiaBase64;
|
|
var nomeFinal = nomeArquivo || 'arquivo';
|
|
var mimeType = tipo || 'application/octet-stream';
|
|
|
|
if (tipo === 'audio') {
|
|
mimeType = 'audio/webm';
|
|
try {
|
|
var inputBuf = Buffer.from(midiaBase64, 'base64');
|
|
if (inputBuf.length > 100) {
|
|
var oggBuf = await convertWebmToOgg(inputBuf);
|
|
if (oggBuf && oggBuf.length > 100) {
|
|
midiaFinal = oggBuf.toString('base64');
|
|
mimeType = 'audio/ogg';
|
|
nomeFinal = 'audio.ogg';
|
|
console.log('[SendMsg] Áudio convertido WebM→OGG antes de salvar:', inputBuf.length, '→', oggBuf.length);
|
|
}
|
|
}
|
|
} catch(convErr) {
|
|
console.error('[SendMsg] Conversão áudio falhou, usando WebM:', convErr.message.substring(0, 80));
|
|
}
|
|
} else if (tipo === 'image') {
|
|
var ext = (nomeArquivo || '').split('.').pop().toLowerCase();
|
|
if (ext === 'png') mimeType = 'image/png';
|
|
else if (ext === 'gif') mimeType = 'image/gif';
|
|
else if (ext === 'webp') mimeType = 'image/webp';
|
|
else mimeType = 'image/jpeg';
|
|
}
|
|
|
|
await db.execute(alias, `
|
|
INSERT INTO CHATC2_MENSAGENS_ATENDIMENTOS (MAT_CODIGO_ID, MAT_CONVERSA_ID, MAT_MENSAGEM_ID, MAT_NOME_ARQUIVO, MAT_TIPO_ARQUIVO, MAT_ARQUIVO)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`, [midiaId, id, newId, nomeFinal, mimeType, midiaFinal]);
|
|
|
|
// Usa a versão convertida para envio posterior à Evolution
|
|
midiaBase64 = midiaFinal;
|
|
}
|
|
|
|
// Verifica config para enviar nome do usuário
|
|
let textoFinal = mensagem || '';
|
|
try {
|
|
const config = await db.query(alias,
|
|
"SELECT CFE_ENVIAR_NOME_USUARIO FROM CHATC2_CONFIGURACOES_EMPRESA WHERE CFE_EMPRESA_ID = ?",
|
|
[conv[0].CON_EMPRESA_ID]);
|
|
if (config.length > 0 && config[0].CFE_ENVIAR_NOME_USUARIO === 'S') {
|
|
const user = await db.query(alias,
|
|
'SELECT USU_NOME FROM USUARIOS WHERE USU_CODIGO_ID = ?', [usuarioId]);
|
|
if (user.length > 0) {
|
|
const nomeUser = (user[0].USU_NOME || '').trim();
|
|
if (nomeUser && textoFinal) {
|
|
// Padrão WhatsApp: *Nome:* em negrito + quebra de linha + mensagem
|
|
textoFinal = '*' + nomeUser + ':*\n' + textoFinal;
|
|
}
|
|
}
|
|
}
|
|
} catch(e) {}
|
|
|
|
// Processa placeholders: [CLIENTE], [CONTATO], [EMPRESAR], [EMPRESAF]
|
|
textoFinal = await ChatController.processarPlaceholders(alias, id, textoFinal);
|
|
|
|
// Insere mensagem no banco
|
|
await db.execute(alias, `
|
|
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_PRIVADA, CME_SITUACAO, CME_DT_ENVIO, CME_MIDIA_ID)
|
|
VALUES (?, ?, 'U', ?, ?, ?, ?, 'A', CURRENT_TIMESTAMP, ?)
|
|
`, [newId, id, usuarioId, textoFinal, tipo || 'text', privada === 'S' ? 'S' : 'N', midiaId]);
|
|
|
|
// Atualiza data da última mensagem
|
|
await db.execute(alias,
|
|
'UPDATE CHATC2_CONVERSAS SET CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?', [id]);
|
|
|
|
// Se não for privada, envia via Evolution API
|
|
if (privada !== 'S' && conv[0].CON_NUMERO) {
|
|
try {
|
|
const instancia = await db.query(alias,
|
|
'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [conv[0].CON_INSTANCIA_ID]);
|
|
if (instancia.length > 0) {
|
|
const ins = instancia[0];
|
|
console.log('[SendMsg] Enviando mídia tipo:', tipo, 'tamanho:', (midiaBase64 || '').length, 'nome:', nomeArquivo, 'para:', conv[0].CON_NUMERO);
|
|
var evoResult = await sendEvolutionMessage(ins, conv[0].CON_NUMERO, textoFinal, tipo, midiaBase64, nomeArquivo);
|
|
console.log('[SendMsg] Evolution resposta:', (evoResult || '').substring(0, 200));
|
|
} else {
|
|
console.log('[SendMsg] Instância não encontrada para ID:', conv[0].CON_INSTANCIA_ID);
|
|
}
|
|
} catch (evoErr) {
|
|
console.error('[SendMsg] Erro ao enviar via Evolution:', evoErr.message);
|
|
}
|
|
}
|
|
|
|
res.json({ success: true, data: { id: newId } });
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
|
|
/**
|
|
* Finalizar conversa
|
|
* POST /api/:alias/conversations/:id/finalize
|
|
*/
|
|
static async finalizeConversation(req, res) {
|
|
try {
|
|
const { alias, id } = req.params;
|
|
|
|
const conv = await db.query(alias,
|
|
'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [id]);
|
|
if (conv.length === 0) return res.status(404).json({ success: false, error: 'Conversa não encontrada.' });
|
|
|
|
const c = conv[0];
|
|
if (c.CON_EMPRESA_ID && !((req.user && req.user.empresas) || []).some(function(e) { return String(e) === String(c.CON_EMPRESA_ID); }))
|
|
return res.status(403).json({ success: false, error: 'Sem permissão.' });
|
|
|
|
// ===== Fluxo de Resolução: valida motivo/resolução conforme config =====
|
|
const motivoId = (req.body && req.body.motivoId) ? parseInt(req.body.motivoId, 10) : null;
|
|
const resolucao = (req.body && req.body.resolucao != null) ? String(req.body.resolucao).trim() : '';
|
|
|
|
const flagsRows = await db.query(alias,
|
|
'SELECT CFE_MOTIVO_VISUALIZAR, CFE_MOTIVO_OBRIGATORIO, CFE_RESOLUCAO_VISUALIZAR, CFE_RESOLUCAO_OBRIGATORIO, CFE_RESOLUCAO_SALVAR_MSC FROM CHATC2_CONFIGURACOES_EMPRESA WHERE CFE_EMPRESA_ID = ?',
|
|
[c.CON_EMPRESA_ID]);
|
|
const fl = flagsRows[0] || {};
|
|
const isS = (v) => String(v || 'N').trim() === 'S';
|
|
if (isS(fl.CFE_MOTIVO_VISUALIZAR) && isS(fl.CFE_MOTIVO_OBRIGATORIO) && !motivoId) {
|
|
return res.status(400).json({ success: false, error: 'Selecione o motivo do atendimento para finalizar.' });
|
|
}
|
|
if (isS(fl.CFE_RESOLUCAO_VISUALIZAR) && isS(fl.CFE_RESOLUCAO_OBRIGATORIO) && !resolucao) {
|
|
return res.status(400).json({ success: false, error: 'Preencha a resolução do atendimento para finalizar.' });
|
|
}
|
|
|
|
// Busca nome do usuário que atendeu
|
|
let usuarioNome = null;
|
|
if (c.CON_USUARIO_ID) {
|
|
const user = await db.query(alias,
|
|
'SELECT USU_NOME FROM USUARIOS WHERE USU_CODIGO_ID = ?', [c.CON_USUARIO_ID]);
|
|
if (user.length > 0) usuarioNome = (user[0].USU_NOME || '').trim();
|
|
}
|
|
|
|
// Busca nome da equipe
|
|
let equipeNome = null;
|
|
if (c.CON_EQUIPE_ID) {
|
|
const equipe = await db.query(alias,
|
|
'SELECT EQU_NOME FROM CHATC2_EQUIPES WHERE EQU_CODIGO_ID = ?', [c.CON_EQUIPE_ID]);
|
|
if (equipe.length > 0) equipeNome = (equipe[0].EQU_NOME || '').trim();
|
|
}
|
|
|
|
// Busca etiquetas
|
|
const etiquetas = await db.query(alias, `
|
|
SELECT ETI_NOME FROM CHATC2_ETIQUETAS e
|
|
INNER JOIN CHATC2_CONVERSAS_ETIQUETAS ce ON e.ETI_CODIGO_ID = ce.CET_ETIQUETA_ID
|
|
WHERE ce.CET_CONVERSA_ID = ? AND e.ETI_SITUACAO = 'A'
|
|
`, [id]);
|
|
const etiquetasDesc = etiquetas.map(function(e) { return (e.ETI_NOME || '').trim(); }).filter(Boolean).join(', ');
|
|
|
|
await db.execute(alias, `
|
|
UPDATE CHATC2_CONVERSAS SET
|
|
CON_STATUS = 'F',
|
|
CON_DT_FINAL = CURRENT_TIMESTAMP,
|
|
CON_USUARIO_NOME = ?,
|
|
CON_EQUIPE_NOME = ?,
|
|
CON_ETIQUETAS_DESC = ?,
|
|
CON_MOTIVO_ID = ?,
|
|
CON_RESOLUCAO = ?
|
|
WHERE CON_CODIGO_ID = ?
|
|
`, [usuarioNome, equipeNome, etiquetasDesc, motivoId, resolucao || null, id]);
|
|
|
|
// CSAT — NÃO envia se a conversa for agendada (skipCsat = true)
|
|
const skipCsat = req.body && req.body.skipCsat === true;
|
|
if (!skipCsat) {
|
|
const config = await db.query(alias,
|
|
'SELECT CFE_CSAT_ATIVO, CFE_CSAT_MENSAGEM FROM CHATC2_CONFIGURACOES_EMPRESA WHERE CFE_EMPRESA_ID = ?',
|
|
[conv[0].CON_EMPRESA_ID]);
|
|
|
|
if (config.length > 0 && config[0].CFE_CSAT_ATIVO === 'S') {
|
|
var csatEnviadoSucesso = false;
|
|
try {
|
|
const empresaId = conv[0].CON_EMPRESA_ID;
|
|
var baseUrl = process.env.EXTERNAL_URL || process.env.NGROK_URL || ('https://' + (req.headers.host || 'localhost:3000'));
|
|
var csatLink = baseUrl.replace(/^https?:\/\//, '') + '/app/' + alias + '/csat?alias=' + alias + '&conversa=' + id + '&empresa=' + empresaId;
|
|
const msgCsat = 'Ol\u00e1! Gostar\u00edamos de saber como foi seu atendimento.\n\nAcesse o link abaixo para avaliar:\n' + csatLink;
|
|
|
|
const inst = await db.query(alias,
|
|
"SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ? AND INS_SITUACAO = 'A'",
|
|
[conv[0].CON_INSTANCIA_ID]);
|
|
if (inst.length > 0) {
|
|
var csatResult = await sendEvolutionMessage(inst[0], conv[0].CON_NUMERO, msgCsat, 'text', null);
|
|
console.log('[CSAT] Resultado envio:', JSON.stringify(csatResult));
|
|
if (csatResult) {
|
|
try {
|
|
var parsed = typeof csatResult === 'string' ? JSON.parse(csatResult) : csatResult;
|
|
if (parsed && parsed.key && parsed.key.remoteJid) csatEnviadoSucesso = true;
|
|
} catch(e) {
|
|
if (csatResult.status || (typeof csatResult === 'string' && csatResult.length > 20)) csatEnviadoSucesso = true;
|
|
}
|
|
}
|
|
} else {
|
|
console.log('[CSAT] Instancia nao encontrada ou inativa para ID:', conv[0].CON_INSTANCIA_ID);
|
|
}
|
|
} catch(e) { console.error('[CSAT] Erro ao enviar:', e.message); }
|
|
|
|
// So marca como enviado se realmente conseguiu enviar
|
|
if (csatEnviadoSucesso) {
|
|
await db.execute(alias,
|
|
"UPDATE CHATC2_CONVERSAS SET CON_CSAT_ENVIADO = 'S' WHERE CON_CODIGO_ID = ?", [id]);
|
|
console.log('[CSAT] Marcado como enviado para conversa', id);
|
|
} else {
|
|
console.log('[CSAT] NAO enviado - Evolution API pode estar offline');
|
|
}
|
|
}
|
|
} // fecha if (!skipCsat)
|
|
|
|
// Salva resolução na tabela MENSAGENS_CLIENTES como aviso (se configurado)
|
|
if (isS(fl.CFE_RESOLUCAO_SALVAR_MSC) && resolucao && c.CON_CLIENTE_ID) {
|
|
try {
|
|
const mscId = await db.nextId(alias, 'GEN_MENSAGENS_CLIENTES');
|
|
await db.execute(alias,
|
|
`INSERT INTO MENSAGENS_CLIENTES (MSC_CODIGO_ID, MSC_CLIENTE_ID, MSC_USUARIO_ID, MSC_DT_MENSAGEM, MSC_AVISO, MSC_MENSAGEM)
|
|
VALUES (?, ?, ?, CURRENT_TIMESTAMP, 'S', ?)`,
|
|
[mscId, c.CON_CLIENTE_ID, c.CON_USUARIO_ID || null, resolucao]);
|
|
console.log('[Resolucao] Aviso salvo em MENSAGENS_CLIENTES para cliente', c.CON_CLIENTE_ID);
|
|
} catch (mscErr) {
|
|
console.error('[Resolucao] Erro ao salvar em MENSAGENS_CLIENTES:', mscErr.message);
|
|
}
|
|
}
|
|
|
|
res.json({ success: true, message: 'Conversa finalizada.' });
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
|
|
/**
|
|
* Criar nova conversa e enviar primeira mensagem
|
|
* POST /api/:alias/conversations/create
|
|
* Body: { empresaId, numero, nomeContato, mensagem, instanciaId, clienteId }
|
|
*/
|
|
static async createConversation(req, res) {
|
|
try {
|
|
const { alias } = req.params;
|
|
const { empresaId, numero, mensagem, instanciaId } = req.body;
|
|
let clienteId = req.body.clienteId || null;
|
|
let nomeContato = req.body.nomeContato || '';
|
|
|
|
if (!empresaId || !numero || !mensagem) {
|
|
return res.status(400).json({ success: false, error: 'Campos obrigatórios: empresaId, numero, mensagem' });
|
|
}
|
|
|
|
// Verifica permissão
|
|
const userEmpresas = req.user?.empresas || [];
|
|
if (!userEmpresas.some(function(e) { return String(e) === String(empresaId); })) {
|
|
return res.status(403).json({ success: false, error: 'Sem permissão para esta empresa.' });
|
|
}
|
|
|
|
const numeroLimpo = numero.replace(/\D/g, '').substring(0, 15);
|
|
|
|
// Se não foi informado clienteId, tenta localizar pelo número de telefone
|
|
// Fluxo: cliente → dependente (titular) → número novo (sem vínculo)
|
|
let clienteEncontrado = null;
|
|
let nomeEncontrado = null;
|
|
if (!clienteId) {
|
|
// Variações do número para busca: completo, sem DDI (55), últimos 8 dígitos
|
|
const variantes = [numeroLimpo];
|
|
if (numeroLimpo.startsWith('55')) variantes.push(numeroLimpo.substring(2));
|
|
const ultimos8 = numeroLimpo.slice(-8);
|
|
if (!variantes.includes(ultimos8)) variantes.push(ultimos8);
|
|
|
|
// 1) Busca CLIENTES
|
|
for (const termo of variantes) {
|
|
const cli = await db.query(alias, `
|
|
SELECT CLI_CODIGO_ID, CLI_NOME
|
|
FROM CLIENTES
|
|
WHERE CLI_EMPRESA_ID = ?
|
|
AND (REPLACE(REPLACE(REPLACE(COALESCE(CLI_CELULAR,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'
|
|
OR REPLACE(REPLACE(REPLACE(COALESCE(CLI_FONE1,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'
|
|
OR REPLACE(REPLACE(REPLACE(COALESCE(CLI_FONE2,''),'-',''),'(',''),')','') LIKE '%' || ? || '%')
|
|
FETCH FIRST 1 ROWS ONLY
|
|
`, [empresaId, termo, termo, termo]);
|
|
if (cli.length > 0) {
|
|
clienteEncontrado = cli[0].CLI_CODIGO_ID;
|
|
nomeEncontrado = (cli[0].CLI_NOME || '').trim();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 2) Se não achou cliente, busca DEPENDENTES
|
|
if (!clienteEncontrado) {
|
|
for (const termo of variantes) {
|
|
const dep = await db.query(alias, `
|
|
SELECT d.DEPC_CLIENTE_ID AS TITULAR_ID, d.DEPC_NOME, c.CLI_NOME AS TITULAR_NOME
|
|
FROM DEPENDENTES_CLI d
|
|
LEFT JOIN CLIENTES c ON d.DEPC_CLIENTE_ID = c.CLI_CODIGO_ID
|
|
WHERE d.DEPC_EMPRESA_ID = ? AND d.DEPC_SITUACAO = 'A'
|
|
AND REPLACE(REPLACE(REPLACE(COALESCE(d.DEPC_TELEFONE,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'
|
|
FETCH FIRST 1 ROWS ONLY
|
|
`, [empresaId, termo]);
|
|
if (dep.length > 0) {
|
|
clienteEncontrado = dep[0].TITULAR_ID;
|
|
nomeEncontrado = (dep[0].TITULAR_NOME || dep[0].DEPC_NOME || '').trim();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (clienteEncontrado) {
|
|
clienteId = clienteEncontrado;
|
|
if (!nomeContato || nomeContato === numeroLimpo) nomeContato = nomeEncontrado;
|
|
}
|
|
}
|
|
|
|
// Verifica se já existe conversa ABERTA ou EM ESPERA para este número
|
|
// Tenta com o número exato, e também com os últimos 8 dígitos
|
|
let existente = await db.query(alias, `
|
|
SELECT CON_CODIGO_ID, CON_STATUS, CON_CLIENTE_ID, CON_NUMERO
|
|
FROM CHATC2_CONVERSAS
|
|
WHERE CON_NUMERO = ? AND CON_EMPRESA_ID = ?
|
|
AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A'
|
|
ORDER BY CON_CODIGO_ID ASC
|
|
FETCH FIRST 1 ROWS ONLY
|
|
`, [numeroLimpo, empresaId]);
|
|
|
|
// Se não achou pelo número exato, tenta pelos últimos 8 dígitos
|
|
if (existente.length === 0) {
|
|
const ultimos8 = numeroLimpo.slice(-8);
|
|
existente = await db.query(alias, `
|
|
SELECT CON_CODIGO_ID, CON_STATUS, CON_CLIENTE_ID, CON_NUMERO
|
|
FROM CHATC2_CONVERSAS
|
|
WHERE CON_NUMERO LIKE '%' || ? || '%' AND CON_EMPRESA_ID = ?
|
|
AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A'
|
|
ORDER BY CON_CODIGO_ID ASC
|
|
FETCH FIRST 1 ROWS ONLY
|
|
`, [ultimos8, empresaId]);
|
|
}
|
|
|
|
let conversaId;
|
|
if (existente.length > 0) {
|
|
// Reusa a conversa existente (prioriza a de menor ID)
|
|
conversaId = existente[0].CON_CODIGO_ID;
|
|
|
|
// Se tem clienteId e a conversa não tinha, atualiza
|
|
if (clienteId && !existente[0].CON_CLIENTE_ID) {
|
|
await db.execute(alias,
|
|
'UPDATE CHATC2_CONVERSAS SET CON_CLIENTE_ID = ?, CON_NOME_CONTATO = ? WHERE CON_CODIGO_ID = ?',
|
|
[clienteId, nomeContato || '', conversaId]);
|
|
}
|
|
|
|
// Adiciona mensagem na conversa existente
|
|
const msgId = await db.nextId(alias, 'GEN_CONVERSAS_MENSAGENS');
|
|
const textoProcessado = await ChatController.processarPlaceholders(alias, conversaId, mensagem);
|
|
await db.execute(alias, `
|
|
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
|
VALUES (?, ?, 'U', ?, ?, 'text', 'A', CURRENT_TIMESTAMP)
|
|
`, [msgId, conversaId, req.user?.id, textoProcessado]);
|
|
|
|
await db.execute(alias,
|
|
'UPDATE CHATC2_CONVERSAS SET CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?',
|
|
[conversaId]);
|
|
|
|
// Envia via Evolution
|
|
const resultado = await db.query(alias,
|
|
'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [instanciaId || null]);
|
|
if (resultado.length > 0) {
|
|
sendEvolutionMessage(resultado[0], numeroLimpo, textoProcessado, 'text', null).catch(function(){});
|
|
}
|
|
|
|
return res.json({ success: true, data: { id: conversaId, reutilizada: true } });
|
|
}
|
|
|
|
// Gera ID da nova conversa
|
|
const newId = await db.nextId(alias, 'GEN_CHATC2_CONVERSAS');
|
|
|
|
// Se não informou instância, pega a primeira ativa
|
|
let instId = instanciaId;
|
|
if (!instId) {
|
|
const inst = await db.query(alias,
|
|
"SELECT INS_CODIGO_ID FROM CHATC2_INSTANCIAS WHERE INS_SITUACAO = 'A' FETCH FIRST 1 ROWS ONLY");
|
|
if (inst.length > 0) instId = inst[0].INS_CODIGO_ID;
|
|
}
|
|
|
|
// Insere conversa
|
|
await db.execute(alias, `
|
|
INSERT INTO CHATC2_CONVERSAS (CON_CODIGO_ID, CON_EMPRESA_ID, CON_INSTANCIA_ID, CON_NUMERO,
|
|
CON_NOME_CONTATO, CON_CLIENTE_ID, CON_STATUS, CON_SITUACAO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG)
|
|
VALUES (?, ?, ?, ?, ?, ?, 'A', 'A', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
`, [newId, empresaId, instId, numeroLimpo, nomeContato || numeroLimpo, clienteId || null]);
|
|
|
|
// Insere primeira mensagem
|
|
const msgId = await db.nextId(alias, 'GEN_CONVERSAS_MENSAGENS');
|
|
const textoProcessadoNew = await ChatController.processarPlaceholders(alias, newId, mensagem);
|
|
|
|
await db.execute(alias, `
|
|
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
|
VALUES (?, ?, 'U', ?, ?, 'text', 'A', CURRENT_TIMESTAMP)
|
|
`, [msgId, newId, req.user?.id, textoProcessadoNew]);
|
|
|
|
// Envia via Evolution API
|
|
try {
|
|
const resultado = await db.query(alias,
|
|
'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [instId]);
|
|
if (resultado.length > 0) {
|
|
await sendEvolutionMessage(resultado[0], numeroLimpo, textoProcessadoNew, 'text', null);
|
|
}
|
|
} catch (evoErr) {
|
|
console.error('Erro ao enviar primeira mensagem via Evolution:', evoErr.message);
|
|
}
|
|
|
|
res.json({ success: true, data: { id: newId, msgId } });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Atribuir conversa a usuário
|
|
* POST /api/:alias/conversations/:id/assign
|
|
*/
|
|
static async assignConversation(req, res) {
|
|
try {
|
|
const { alias, id } = req.params;
|
|
const { usuarioId } = req.body;
|
|
|
|
const chk = await ChatController.checarConversaEmpresa(alias, id, req);
|
|
if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 404 ? 'Conversa não encontrada.' : 'Sem permissão.' });
|
|
|
|
await db.execute(alias,
|
|
"UPDATE CHATC2_CONVERSAS SET CON_USUARIO_ID = ?, CON_STATUS = 'A' WHERE CON_CODIGO_ID = ?",
|
|
[usuarioId, id]);
|
|
|
|
res.json({ success: true });
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
|
|
/**
|
|
* Atribuir conversa a uma equipe
|
|
* POST /api/:alias/conversations/:id/assign-team
|
|
*/
|
|
static async assignTeam(req, res) {
|
|
try {
|
|
const { alias, id } = req.params;
|
|
const { equipeId } = req.body;
|
|
|
|
const chk = await ChatController.checarConversaEmpresa(alias, id, req);
|
|
if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 404 ? 'Conversa não encontrada.' : 'Sem permissão.' });
|
|
|
|
await db.execute(alias,
|
|
'UPDATE CHATC2_CONVERSAS SET CON_EQUIPE_ID = ? WHERE CON_CODIGO_ID = ?',
|
|
[equipeId || null, id]);
|
|
|
|
res.json({ success: true });
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
|
|
/**
|
|
* Adicionar/remover etiqueta da conversa
|
|
* POST /api/:alias/conversations/:id/labels
|
|
*/
|
|
static async toggleLabel(req, res) {
|
|
try {
|
|
const { alias, id } = req.params;
|
|
const { etiquetaId, ativar } = req.body;
|
|
|
|
const chk = await ChatController.checarConversaEmpresa(alias, id, req);
|
|
if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 404 ? 'Conversa não encontrada.' : 'Sem permissão.' });
|
|
|
|
if (ativar) {
|
|
await db.execute(alias,
|
|
'INSERT INTO CHATC2_CONVERSAS_ETIQUETAS (CET_CONVERSA_ID, CET_ETIQUETA_ID) VALUES (?, ?)',
|
|
[id, etiquetaId]);
|
|
} else {
|
|
await db.execute(alias,
|
|
'DELETE FROM CHATC2_CONVERSAS_ETIQUETAS WHERE CET_CONVERSA_ID = ? AND CET_ETIQUETA_ID = ?',
|
|
[id, etiquetaId]);
|
|
}
|
|
|
|
res.json({ success: true });
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
|
|
/**
|
|
* Buscar mídia (imagem/audio) pelo ID
|
|
* GET /api/:alias/media/:mediaId
|
|
*/
|
|
static async getMedia(req, res) {
|
|
const { alias, mediaId } = req.params;
|
|
|
|
if (!alias || !mediaId) {
|
|
return res.status(400).json({ success: false, error: 'Parâmetros inválidos.' });
|
|
}
|
|
|
|
let responded = false;
|
|
function respond(status, data) {
|
|
if (responded) return;
|
|
responded = true;
|
|
res.status(status).json(data);
|
|
}
|
|
function respondBuffer(mimeType, nomeArquivo, buffer) {
|
|
if (responded) return;
|
|
responded = true;
|
|
const totalSize = buffer.length;
|
|
|
|
// Suporte a byte-range para áudio (permite seek no player HTML5)
|
|
const range = res.req.headers.range;
|
|
if (range && mimeType.startsWith('audio/')) {
|
|
const parts = range.replace(/bytes=/, '').split('-');
|
|
const start = parseInt(parts[0], 10);
|
|
const end = parts[1] ? parseInt(parts[1], 10) : totalSize - 1;
|
|
const chunkSize = (end - start) + 1;
|
|
res.status(206);
|
|
res.set('Content-Range', 'bytes ' + start + '-' + end + '/' + totalSize);
|
|
res.set('Accept-Ranges', 'bytes');
|
|
res.set('Content-Length', chunkSize);
|
|
res.set('Content-Type', mimeType);
|
|
res.set('Content-Disposition', 'inline; filename="' + encodeURIComponent(nomeArquivo) + '"');
|
|
res.send(buffer.slice(start, end + 1));
|
|
return;
|
|
}
|
|
|
|
res.set('Content-Type', mimeType);
|
|
res.set('Accept-Ranges', 'bytes');
|
|
res.set('Content-Length', totalSize);
|
|
res.set('Content-Disposition', 'inline; filename="' + encodeURIComponent(nomeArquivo) + '"');
|
|
res.send(buffer);
|
|
}
|
|
|
|
try {
|
|
// Autenticação: <img>/<audio>/<a> não enviam header, então aceitamos o
|
|
// token também via query string (?token=). Também aceita header.
|
|
const jwt = require('jsonwebtoken');
|
|
const authConfig = require('../config/auth');
|
|
const rawToken = req.query.token
|
|
|| (req.headers['authorization'] || '').replace(/^Bearer\s+/i, '')
|
|
|| req.headers['x-usu-token'];
|
|
let authUser = null;
|
|
if (rawToken) {
|
|
try {
|
|
authUser = jwt.verify(rawToken, authConfig.secret, { issuer: authConfig.issuer });
|
|
} catch (e) {
|
|
try {
|
|
const u = await db.query(alias,
|
|
"SELECT USU_CODIGO_ID FROM USUARIOS WHERE USU_TOKEN = ? AND USU_STATUS = 'A' AND COALESCE(USU_ACESSO_WEB,0) = 1",
|
|
[rawToken]);
|
|
if (u.length > 0) {
|
|
const emp = await db.query(alias,
|
|
'SELECT USE_EMPRESA_ID FROM USUARIOS_EMPRESA WHERE USE_USUARIO_ID = ?', [u[0].USU_CODIGO_ID]);
|
|
authUser = { id: u[0].USU_CODIGO_ID, empresas: emp.map(function (e) { return e.USE_EMPRESA_ID; }) };
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
}
|
|
if (!authUser) {
|
|
return respond(401, { success: false, error: 'Token de acesso obrigatório.' });
|
|
}
|
|
|
|
const result = await db.query(alias,
|
|
'SELECT MAT_CODIGO_ID, MAT_TIPO_ARQUIVO, MAT_NOME_ARQUIVO, MAT_ARQUIVO FROM CHATC2_MENSAGENS_ATENDIMENTOS WHERE MAT_CODIGO_ID = ?',
|
|
[mediaId]);
|
|
|
|
if (result.length === 0) {
|
|
return respond(404, { success: false, error: 'Mídia não encontrada.' });
|
|
}
|
|
|
|
// Autorização: a mídia precisa pertencer a uma empresa do usuário
|
|
const dono = await db.query(alias,
|
|
'SELECT c.CON_EMPRESA_ID AS EMP FROM CHATC2_MENSAGENS_ATENDIMENTOS m JOIN CHATC2_CONVERSAS c ON c.CON_CODIGO_ID = m.MAT_CONVERSA_ID WHERE m.MAT_CODIGO_ID = ?',
|
|
[mediaId]);
|
|
const empresaMidia = dono.length > 0 ? dono[0].EMP : null;
|
|
const minhasEmp = authUser.empresas || [];
|
|
if (empresaMidia && minhasEmp.indexOf(empresaMidia) === -1) {
|
|
return respond(403, { success: false, error: 'Sem permissão para esta mídia.' });
|
|
}
|
|
|
|
const m = result[0];
|
|
var mimeType = (m.MAT_TIPO_ARQUIVO || '').trim() || 'application/octet-stream';
|
|
// Corrige MIME types antigos que salvavam apenas "audio" ou "image"
|
|
if (mimeType === 'audio') mimeType = 'audio/webm';
|
|
else if (mimeType === 'image') mimeType = 'image/jpeg';
|
|
// Remove parâmetros do MIME type (ex: 'audio/ogg; codecs=opus' → 'audio/ogg')
|
|
mimeType = mimeType.split(';')[0].trim();
|
|
const nomeArquivo = (m.MAT_NOME_ARQUIVO || '').trim() || 'arquivo.bin';
|
|
|
|
// MAT_ARQUIVO chega como Buffer (firebird blob) ou string
|
|
let rawBuffer = m.MAT_ARQUIVO;
|
|
if (rawBuffer == null) {
|
|
return respond(404, { success: false, error: 'Mídia sem conteúdo.' });
|
|
}
|
|
if (!Buffer.isBuffer(rawBuffer)) {
|
|
rawBuffer = Buffer.from(typeof rawBuffer === 'string' ? rawBuffer : (rawBuffer.data || rawBuffer));
|
|
}
|
|
if (rawBuffer.length === 0) {
|
|
return respond(404, { success: false, error: 'Mídia vazia.' });
|
|
}
|
|
|
|
// Se o dado armazenado é base64 (string), decodifica
|
|
try {
|
|
var asString = rawBuffer.toString('utf8').trim();
|
|
if (/^[A-Za-z0-9+/=]+$/.test(asString) && asString.length > 20) {
|
|
const buffer = Buffer.from(asString, 'base64');
|
|
if (buffer.length > 0 && buffer.length < asString.length * 2) {
|
|
if (validarHeaderMidiaServida(buffer, mimeType)) {
|
|
console.log('[getMedia] Base64 decodificado e validado: ' + asString.length + ' chars -> ' + buffer.length + ' bytes, mime: ' + mimeType);
|
|
return respondBuffer(mimeType, nomeArquivo, buffer);
|
|
}
|
|
console.log('[getMedia] Base64 decodificado mas header invalido (header: ' + buffer.slice(0, 4).toString('hex') + '), recusando');
|
|
return respond(400, { success: false, error: 'Mídia corrompida (header de arquivo inválido).' });
|
|
}
|
|
}
|
|
} catch (e) { console.log('[getMedia] Erro ao decodificar base64:', e.message); }
|
|
|
|
// Fallback: usa o buffer bruto (com validação)
|
|
if (validarHeaderMidiaServida(rawBuffer, mimeType)) {
|
|
console.log('[getMedia] Servindo buffer bruto: ' + rawBuffer.length + ' bytes, mime: ' + mimeType);
|
|
respondBuffer(mimeType, nomeArquivo, rawBuffer);
|
|
} else {
|
|
console.log('[getMedia] Buffer bruto com header invalido (header: ' + rawBuffer.slice(0, 4).toString('hex') + '), recusando');
|
|
respond(400, { success: false, error: 'Mídia corrompida (header de arquivo inválido).' });
|
|
}
|
|
} catch (err) {
|
|
respond(500, { success: false, error: 'Erro ao carregar mídia: ' + err.message });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Associar contato a cliente
|
|
* POST /api/:alias/conversations/:id/link-client
|
|
*/
|
|
static async linkClient(req, res) {
|
|
try {
|
|
const { alias, id } = req.params;
|
|
const { clienteId } = req.body;
|
|
|
|
const chk = await ChatController.checarConversaEmpresa(alias, id, req);
|
|
if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 404 ? 'Conversa não encontrada.' : 'Sem permissão.' });
|
|
|
|
const cli = await db.query(alias, `
|
|
SELECT CLI_CODIGO_ID, CLI_NOME, CLI_CELULAR, CLI_FONE1, CLI_EMAIL, CLI_ENDERECO_FAT,
|
|
CLI_NUMERO_FAT, CLI_CIDADES_FAT_ID, CLI_BAIRROS_FAT_ID, CLI_COBRADOR_ID,
|
|
CLI_DIA_COBRANCA, CLI_PLANOS_ID, CLI_FOTO, CLI_NOME_FOTO
|
|
FROM CLIENTES WHERE CLI_CODIGO_ID = ?
|
|
`, [clienteId]);
|
|
if (cli.length === 0) return res.status(404).json({ success: false, error: 'Cliente não encontrado.' });
|
|
|
|
const c = cli[0];
|
|
|
|
// Atualiza conversa com dados do cliente
|
|
await db.execute(alias,
|
|
'UPDATE CHATC2_CONVERSAS SET CON_CLIENTE_ID = ?, CON_NOME_CONTATO = ? WHERE CON_CODIGO_ID = ?',
|
|
[clienteId, (c.CLI_NOME || '').trim(), id]);
|
|
|
|
// Busca dados adicionais para retornar
|
|
let cidadeNome = null;
|
|
if (c.CLI_CIDADES_FAT_ID) {
|
|
const cidade = await db.query(alias, 'SELECT CID_NOME FROM CIDADES WHERE CID_CODIGO_ID = ?', [c.CLI_CIDADES_FAT_ID]);
|
|
if (cidade.length > 0) cidadeNome = cidade[0].CID_NOME?.trim();
|
|
}
|
|
let bairroNome = null;
|
|
if (c.CLI_BAIRROS_FAT_ID) {
|
|
const bairro = await db.query(alias, 'SELECT BAR_DESCRICAO FROM BAIRROS WHERE BAR_CODIGO_ID = ?', [c.CLI_BAIRROS_FAT_ID]);
|
|
if (bairro.length > 0) bairroNome = bairro[0].BAR_DESCRICAO?.trim();
|
|
}
|
|
let cobradorNome = null;
|
|
if (c.CLI_COBRADOR_ID) {
|
|
const cob = await db.query(alias, 'SELECT COB_NOME FROM COBRADORES WHERE COB_CODIGO_ID = ?', [c.CLI_COBRADOR_ID]);
|
|
if (cob.length > 0) cobradorNome = cob[0].COB_NOME?.trim();
|
|
}
|
|
|
|
// Verifica inadimplência
|
|
const titulos = await db.query(alias,
|
|
'SELECT COUNT(*) AS T FROM CARNES WHERE CAR_CLIENTE_ID = ? AND CAR_SITUACAO = 0 AND CAR_DT_VENCIMENTO < CURRENT_DATE',
|
|
[clienteId]);
|
|
const inadimplente = titulos[0]?.T > 0;
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
id: c.CLI_CODIGO_ID,
|
|
nome: (c.CLI_NOME || '').trim(),
|
|
celular: (c.CLI_CELULAR || '').trim(),
|
|
telefone: (c.CLI_FONE1 || '').trim(),
|
|
email: (c.CLI_EMAIL || '').trim(),
|
|
endereco: (c.CLI_ENDERECO_FAT || '').trim(),
|
|
numero: (c.CLI_NUMERO_FAT || '').trim(),
|
|
cidade: cidadeNome,
|
|
bairro: bairroNome,
|
|
cobrador: cobradorNome,
|
|
inadimplente,
|
|
}
|
|
});
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
/**
|
|
* Valida que o cliente informado pertence à mesma empresa da conversa
|
|
* (e que o usuário tem acesso à conversa). Retorna { ok, clienteId } ou { status }.
|
|
*/
|
|
static async checarClienteDaConversa(alias, conversaId, clienteId, req) {
|
|
const chk = await ChatController.checarConversaEmpresa(alias, conversaId, req);
|
|
if (chk.status) return chk;
|
|
const cid = parseInt(clienteId, 10);
|
|
if (!cid) return { status: 400 };
|
|
const cli = await db.query(alias,
|
|
'SELECT CLI_EMPRESA_ID FROM CLIENTES WHERE CLI_CODIGO_ID = ?', [cid]);
|
|
if (cli.length === 0) return { status: 404 };
|
|
if (chk.empresaId && cli[0].CLI_EMPRESA_ID && String(cli[0].CLI_EMPRESA_ID) !== String(chk.empresaId))
|
|
return { status: 403 };
|
|
return { ok: true, clienteId: cid };
|
|
}
|
|
|
|
/**
|
|
* Lista títulos (boletos) em aberto do cliente vinculado à conversa,
|
|
* para o atendente escolher qual enviar.
|
|
* GET /api/:alias/conversations/:id/boletos?clienteId=
|
|
*/
|
|
static async getBoletos(req, res) {
|
|
try {
|
|
const { alias, id } = req.params;
|
|
const chk = await ChatController.checarClienteDaConversa(alias, id, req.query.clienteId, req);
|
|
if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 403 ? 'Sem permissão.' : (chk.status === 404 ? 'Cliente não encontrado.' : 'Parâmetros inválidos.') });
|
|
|
|
const carnes = await db.query(alias,
|
|
`SELECT CAR_CODIGO_ID, CAR_DT_VENCIMENTO, CAR_VALOR_PARCELA, CAR_LINHA_DIGITAVEL,
|
|
CAR_NOSSO_NUMERO, CAR_PIX_QRCODE, CAR_NUMERO_PARCELA, CAR_NUMERO_TOTAL_PARCELAS,
|
|
CAR_CODIGO_BARRAS
|
|
FROM CARNES WHERE CAR_CLIENTE_ID = ? AND CAR_SITUACAO = 0
|
|
ORDER BY CAR_DT_VENCIMENTO`, [chk.clienteId]);
|
|
|
|
const fmtDate = (dt) => {
|
|
if (!dt) return null;
|
|
if (typeof dt === 'string') return dt.split('T')[0];
|
|
if (dt instanceof Date) return dt.toISOString().split('T')[0];
|
|
return String(dt);
|
|
};
|
|
|
|
res.json({ success: true, data: carnes.map(function(c) {
|
|
return {
|
|
id: c.CAR_CODIGO_ID,
|
|
vencimento: fmtDate(c.CAR_DT_VENCIMENTO),
|
|
valor: c.CAR_VALOR_PARCELA,
|
|
parcela: c.CAR_NUMERO_PARCELA,
|
|
totalParcelas: c.CAR_NUMERO_TOTAL_PARCELAS,
|
|
temLinhaDigitavel: !!(c.CAR_LINHA_DIGITAVEL && String(c.CAR_LINHA_DIGITAVEL).trim()),
|
|
temPix: !!(c.CAR_PIX_QRCODE && String(c.CAR_PIX_QRCODE).trim()),
|
|
};
|
|
}) });
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
|
|
/**
|
|
* Envia um boleto escolhido (texto: linha digitável + PIX) ao cliente no chat.
|
|
* POST /api/:alias/conversations/:id/send-boleto Body: { clienteId, carneId }
|
|
*/
|
|
static async sendBoleto(req, res) {
|
|
try {
|
|
const { alias, id } = req.params;
|
|
const { clienteId, carneId } = req.body;
|
|
|
|
const chk = await ChatController.checarClienteDaConversa(alias, id, clienteId, req);
|
|
if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 403 ? 'Sem permissão.' : (chk.status === 404 ? 'Cliente não encontrado.' : 'Parâmetros inválidos.') });
|
|
|
|
const carneIdNum = parseInt(carneId, 10);
|
|
if (!carneIdNum) return res.status(400).json({ success: false, error: 'Título inválido.' });
|
|
|
|
// Título precisa pertencer ao cliente validado (evita IDOR)
|
|
const car = await db.query(alias,
|
|
`SELECT CAR_CODIGO_ID, CAR_DT_VENCIMENTO, CAR_VALOR_PARCELA, CAR_LINHA_DIGITAVEL,
|
|
CAR_PIX_QRCODE, CAR_NUMERO_PARCELA, CAR_NUMERO_TOTAL_PARCELAS, CAR_CODIGO_BARRAS
|
|
FROM CARNES WHERE CAR_CODIGO_ID = ? AND CAR_CLIENTE_ID = ?`, [carneIdNum, chk.clienteId]);
|
|
if (car.length === 0) return res.status(404).json({ success: false, error: 'Título não encontrado para este cliente.' });
|
|
const b = car[0];
|
|
|
|
const conv = await db.query(alias, 'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [id]);
|
|
if (conv.length === 0) return res.status(404).json({ success: false, error: 'Conversa não encontrada.' });
|
|
|
|
// Monta o texto do boleto
|
|
const fmtDate = (dt) => {
|
|
if (!dt) return '';
|
|
let s = typeof dt === 'string' ? dt.split('T')[0] : (dt instanceof Date ? dt.toISOString().split('T')[0] : String(dt));
|
|
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s);
|
|
return m ? (m[3] + '/' + m[2] + '/' + m[1]) : s;
|
|
};
|
|
const fmtValor = (v) => {
|
|
const n = Number(v);
|
|
if (isNaN(n)) return '';
|
|
return 'R$ ' + n.toFixed(2).replace('.', ',');
|
|
};
|
|
const linha = (b.CAR_LINHA_DIGITAVEL || '').trim() || (b.CAR_CODIGO_BARRAS || '').trim();
|
|
const pix = (b.CAR_PIX_QRCODE || '').trim();
|
|
|
|
let texto = '📄 *Boleto*';
|
|
if (b.CAR_NUMERO_PARCELA) {
|
|
texto += ' - Parcela ' + b.CAR_NUMERO_PARCELA + (b.CAR_NUMERO_TOTAL_PARCELAS ? '/' + b.CAR_NUMERO_TOTAL_PARCELAS : '');
|
|
}
|
|
texto += '\n';
|
|
if (b.CAR_VALOR_PARCELA != null) texto += '\n💰 Valor: ' + fmtValor(b.CAR_VALOR_PARCELA);
|
|
if (b.CAR_DT_VENCIMENTO) texto += '\n📅 Vencimento: ' + fmtDate(b.CAR_DT_VENCIMENTO);
|
|
if (linha) texto += '\n\n*Linha digitável:*\n' + linha;
|
|
if (pix) texto += '\n\n*PIX (copia e cola):*\n' + pix;
|
|
|
|
if (!linha && !pix) {
|
|
return res.status(400).json({ success: false, error: 'Este título não possui linha digitável nem PIX disponível.' });
|
|
}
|
|
|
|
// Salva mensagem do agente
|
|
const newId = await db.nextId(alias, 'GEN_CONVERSAS_MENSAGENS');
|
|
await db.execute(alias, `
|
|
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_PRIVADA, CME_SITUACAO, CME_DT_ENVIO)
|
|
VALUES (?, ?, 'U', ?, ?, 'text', 'N', 'A', CURRENT_TIMESTAMP)
|
|
`, [newId, id, req.user?.id, texto]);
|
|
await db.execute(alias,
|
|
'UPDATE CHATC2_CONVERSAS SET CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?', [id]);
|
|
|
|
// Envia via Evolution
|
|
if (conv[0].CON_NUMERO) {
|
|
try {
|
|
const instancia = await db.query(alias,
|
|
'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [conv[0].CON_INSTANCIA_ID]);
|
|
if (instancia.length > 0) {
|
|
await sendEvolutionMessage(instancia[0], conv[0].CON_NUMERO, texto, 'text', null);
|
|
}
|
|
} catch (evoErr) {
|
|
console.error('[Boleto] Erro ao enviar via Evolution:', evoErr.message);
|
|
}
|
|
}
|
|
|
|
res.json({ success: true, data: { id: newId } });
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
|
|
/**
|
|
* Busca mensagens/conversas por termo de texto.
|
|
* GET /api/:alias/search?q=termo&empresaId=
|
|
*/
|
|
static async search(req, res) {
|
|
try {
|
|
const { alias } = req.params;
|
|
const q = (req.query.q || '').trim();
|
|
const empresaId = parseInt(req.query.empresaId) || req.user?.empresas?.[0];
|
|
const limit = Math.min(parseInt(req.query.limit) || 30, 100);
|
|
|
|
if (!q || q.length < 2) {
|
|
return res.status(400).json({ success: false, error: 'Termo de busca muito curto (mínimo 2 caracteres).' });
|
|
}
|
|
if (!req.user?.empresas?.some(e => Number(e) === Number(empresaId))) {
|
|
return res.status(403).json({ success: false, error: 'Sem permissão.' });
|
|
}
|
|
|
|
const results = await db.query(alias, `
|
|
SELECT m.CME_CODIGO_ID, m.CME_CONVERSA_ID, m.CME_TEXTO, m.CME_TIPO,
|
|
m.CME_DT_ENVIO, m.CME_REMETENTE,
|
|
c.CON_NUMERO, c.CON_NOME_CONTATO, c.CON_STATUS
|
|
FROM CHATC2_CONVERSAS_MENSAGENS m
|
|
JOIN CHATC2_CONVERSAS c ON c.CON_CODIGO_ID = m.CME_CONVERSA_ID
|
|
WHERE c.CON_EMPRESA_ID = ?
|
|
AND c.CON_SITUACAO = 'A'
|
|
AND m.CME_SITUACAO = 'A'
|
|
AND m.CME_TEXTO CONTAINING ?
|
|
ORDER BY m.CME_DT_ENVIO DESC
|
|
FETCH FIRST ? ROWS ONLY
|
|
`, [empresaId, q, limit]);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: results.map(r => ({
|
|
mensagemId: r.CME_CODIGO_ID,
|
|
conversaId: r.CME_CONVERSA_ID,
|
|
texto: (r.CME_TEXTO || '').substring(0, 120),
|
|
tipo: (r.CME_TIPO || 'text').trim(),
|
|
dtEnvio: r.CME_DT_ENVIO,
|
|
remetente: (r.CME_REMETENTE || '').trim(),
|
|
numero: (r.CON_NUMERO || '').trim(),
|
|
nomeContato: (r.CON_NOME_CONTATO || '').trim(),
|
|
status: (r.CON_STATUS || '').trim(),
|
|
})),
|
|
count: results.length,
|
|
});
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
|
|
/**
|
|
* Recebe avaliação CSAT via formulário web
|
|
* POST /api/:alias/csat/avaliar
|
|
*/
|
|
static async csatCheck(req, res) {
|
|
try {
|
|
const { alias } = req.params;
|
|
const conversaId = parseInt(req.query.conversa);
|
|
if (!conversaId) return res.status(400).json({ success: false, error: 'conversa obrigatório.' });
|
|
|
|
const existe = await db.query(alias,
|
|
'SELECT CSA_NOTA, CSA_COMENTARIO FROM CHATC2_CSAT_AVALIACOES WHERE CSA_CONVERSA_ID = ?',
|
|
[conversaId]);
|
|
|
|
res.json({ success: true, avaliado: existe.length > 0 });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recebe avaliação CSAT via formulário web
|
|
* POST /api/:alias/csat/avaliar
|
|
*/
|
|
static async csatAvaliar(req, res) {
|
|
try {
|
|
const { alias } = req.params;
|
|
const { conversaId, empresaId, nota, comentario } = req.body;
|
|
|
|
if (!conversaId || !empresaId || !nota || nota < 1 || nota > 5) {
|
|
return res.status(400).json({ success: false, error: 'Dados inválidos.' });
|
|
}
|
|
|
|
// Busca dados da conversa
|
|
const conv = await db.query(alias,
|
|
'SELECT CON_CLIENTE_ID, CON_INSTANCIA_ID FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?',
|
|
[conversaId]
|
|
);
|
|
|
|
if (conv.length === 0) {
|
|
return res.status(404).json({ success: false, error: 'Conversa não encontrada.' });
|
|
}
|
|
|
|
// Salva avaliação
|
|
const newId = await db.nextId(alias, 'GEN_CHATC2_CSAT_AVALIACOES');
|
|
|
|
await db.execute(alias,
|
|
'INSERT INTO CHATC2_CSAT_AVALIACOES (CSA_CODIGO_ID, CSA_CONVERSA_ID, CSA_EMPRESA_ID, CSA_CLIENTE_ID, CSA_NOTA, CSA_COMENTARIO) VALUES (?, ?, ?, ?, ?, ?)',
|
|
[newId, conversaId, empresaId, conv[0].CON_CLIENTE_ID, nota, comentario || null]
|
|
);
|
|
|
|
await db.execute(alias,
|
|
"UPDATE CHATC2_CONVERSAS SET CON_CSAT_ENVIADO = 'S' WHERE CON_CODIGO_ID = ?",
|
|
[conversaId]
|
|
);
|
|
|
|
res.json({ success: true });
|
|
} catch (err) {
|
|
console.error('[CSAT] Erro:', err.message);
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Substitui placeholders no texto da mensagem.
|
|
* [CLIENTE] → nome do cliente cadastrado (ou [CONTATO] se não tiver)
|
|
* [CONTATO] → nome do contato WhatsApp
|
|
* [EMPRESAR] → razão social da empresa
|
|
* [EMPRESAF] → nome fantasia da empresa
|
|
*/
|
|
static async processarPlaceholders(alias, conversaId, texto) {
|
|
if (!texto || (!texto.includes('[') && !texto.includes(']'))) return texto;
|
|
|
|
try {
|
|
const conv = await db.query(alias,
|
|
'SELECT CON_CLIENTE_ID, CON_NOME_CONTATO, CON_EMPRESA_ID FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?',
|
|
[conversaId]);
|
|
if (conv.length === 0) return texto;
|
|
|
|
const contato = (conv[0].CON_NOME_CONTATO || '').trim();
|
|
const empresaId = conv[0].CON_EMPRESA_ID;
|
|
|
|
// Busca nome do cliente
|
|
let clienteNome = '';
|
|
if (conv[0].CON_CLIENTE_ID) {
|
|
const cli = await db.query(alias,
|
|
'SELECT CLI_NOME, CLI_NOME_FANTASIA FROM CLIENTES WHERE CLI_CODIGO_ID = ?',
|
|
[conv[0].CON_CLIENTE_ID]);
|
|
if (cli.length > 0) {
|
|
clienteNome = (cli[0].CLI_NOME_FANTASIA || cli[0].CLI_NOME || '').trim();
|
|
}
|
|
}
|
|
|
|
// Busca dados da empresa
|
|
let empresaR = '', empresaF = '';
|
|
if (empresaId) {
|
|
const emp = await db.query(alias,
|
|
'SELECT EMP_NOME, EMP_NOME_FANTASIA FROM EMPRESAS WHERE EMP_CODIGO_ID = ?',
|
|
[empresaId]);
|
|
if (emp.length > 0) {
|
|
empresaR = (emp[0].EMP_NOME || '').trim();
|
|
empresaF = (emp[0].EMP_NOME_FANTASIA || emp[0].EMP_NOME || '').trim();
|
|
} else {
|
|
console.log('[Placeholder] Empresa nao encontrada para ID:', empresaId);
|
|
}
|
|
} else {
|
|
console.log('[Placeholder] Conversa sem CON_EMPRESA_ID');
|
|
}
|
|
|
|
// Substitui: [CLIENTE] usa nome do cadastro, senão cai para [CONTATO]
|
|
let result = texto;
|
|
result = result.replace(/\[CLIENTE\]/gi, clienteNome || contato || '');
|
|
result = result.replace(/\[CONTATO\]/gi, contato || '');
|
|
result = result.replace(/\[EMPRESAR\]/gi, empresaR || empresaF || '');
|
|
result = result.replace(/\[EMPRESAF\]/gi, empresaF || empresaR || '');
|
|
|
|
console.log('[Placeholder] conversa:', conversaId, '| contato:', contato, '| cliente:', clienteNome, '| empresaR:', empresaR, '| empresaF:', empresaF, '| antes:', texto.substring(0, 80), '| depois:', result.substring(0, 80));
|
|
|
|
return result;
|
|
} catch (e) {
|
|
return texto; // fallback: retorna sem substituir
|
|
}
|
|
}
|
|
|
|
// ==================== AGENDAMENTOS ====================
|
|
|
|
/**
|
|
* Agenda retorno para uma conversa.
|
|
* POST /api/:alias/conversations/:id/schedule
|
|
*/
|
|
static async scheduleConversation(req, res) {
|
|
try {
|
|
const { alias, id } = req.params;
|
|
const { data, hora, nota } = req.body;
|
|
|
|
if (!data) return res.status(400).json({ success: false, error: 'Data é obrigatória.' });
|
|
|
|
const chk = await ChatController.checarConversaEmpresa(alias, id, req);
|
|
if (chk.status) return res.status(chk.status).json({ success: false, error: 'Conversa não encontrada ou sem permissão.' });
|
|
|
|
const newId = await db.nextId(alias, 'GEN_CHATC2_AGENDAMENTOS');
|
|
await db.execute(alias, `
|
|
INSERT INTO CHATC2_CONVERSAS_AGENDAMENTOS (CAG_CODIGO_ID, CAG_CONVERSA_ID, CAG_EMPRESA_ID, CAG_DATA, CAG_HORA, CAG_NOTA, CAG_USUARIO_ID)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
`, [newId, id, chk.empresaId, data, hora || null, nota || null, req.user?.id]);
|
|
|
|
res.json({ success: true, data: { id: newId } });
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
|
|
/**
|
|
* Lista agendamentos de uma data (ou hoje).
|
|
* GET /api/:alias/schedules?data=2026-12-25&empresaId=
|
|
*/
|
|
static async listSchedules(req, res) {
|
|
try {
|
|
const { alias } = req.params;
|
|
const empresaId = parseInt(req.query.empresaId) || req.user?.empresas?.[0];
|
|
const data = req.query.data || new Date().toISOString().split('T')[0];
|
|
|
|
if (!req.user?.empresas?.some(e => Number(e) === Number(empresaId))) {
|
|
return res.status(403).json({ success: false, error: 'Sem permissão.' });
|
|
}
|
|
|
|
const result = await db.query(alias, `
|
|
SELECT a.*, c.CON_NUMERO, c.CON_NOME_CONTATO, c.CON_CLIENTE_ID
|
|
FROM CHATC2_CONVERSAS_AGENDAMENTOS a
|
|
JOIN CHATC2_CONVERSAS c ON c.CON_CODIGO_ID = a.CAG_CONVERSA_ID
|
|
WHERE a.CAG_EMPRESA_ID = ? AND a.CAG_DATA = ? AND a.CAG_STATUS = 'P'
|
|
ORDER BY a.CAG_HORA NULLS LAST, a.CAG_DT_CRIACAO
|
|
`, [empresaId, data]);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.map(r => ({
|
|
id: r.CAG_CODIGO_ID,
|
|
conversaId: r.CAG_CONVERSA_ID,
|
|
data: r.CAG_DATA,
|
|
hora: r.CAG_HORA,
|
|
nota: (r.CAG_NOTA || '').trim(),
|
|
numero: (r.CON_NUMERO || '').trim(),
|
|
nomeContato: (r.CON_NOME_CONTATO || '').trim(),
|
|
clienteId: r.CON_CLIENTE_ID,
|
|
status: (r.CAG_STATUS || '').trim(),
|
|
})),
|
|
count: result.length,
|
|
});
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
|
|
/**
|
|
* Cria nova conversa a partir de um agendamento.
|
|
* POST /api/:alias/schedules/:id/reopen
|
|
* Retorna a nova conversa e o texto sugerido — NÃO envia automaticamente.
|
|
*/
|
|
static async reopenSchedule(req, res) {
|
|
try {
|
|
const { alias, id } = req.params;
|
|
|
|
const ag = await db.query(alias,
|
|
`SELECT a.*, c.CON_NUMERO, c.CON_NOME_CONTATO, c.CON_CLIENTE_ID, c.CON_EMPRESA_ID, c.CON_INSTANCIA_ID
|
|
FROM CHATC2_CONVERSAS_AGENDAMENTOS a
|
|
JOIN CHATC2_CONVERSAS c ON c.CON_CODIGO_ID = a.CAG_CONVERSA_ID
|
|
WHERE a.CAG_CODIGO_ID = ?`, [id]);
|
|
if (ag.length === 0) return res.status(404).json({ success: false, error: 'Agendamento não encontrado.' });
|
|
|
|
const a = ag[0];
|
|
const numeroLimpo = (a.CON_NUMERO || '').replace(/\D/g, '').substring(0, 15);
|
|
const nomeContato = (a.CON_NOME_CONTATO || a.CON_NUMERO || '').trim();
|
|
const nota = (a.CAG_NOTA || '').trim();
|
|
let clienteId = a.CON_CLIENTE_ID || null;
|
|
|
|
// Se não tem cliente vinculado, busca pelo número
|
|
if (!clienteId) {
|
|
const variantes = [numeroLimpo];
|
|
if (numeroLimpo.startsWith('55')) variantes.push(numeroLimpo.substring(2));
|
|
variantes.push(numeroLimpo.slice(-8));
|
|
|
|
for (const termo of variantes) {
|
|
const cli = await db.query(alias, `
|
|
SELECT CLI_CODIGO_ID, CLI_NOME FROM CLIENTES
|
|
WHERE CLI_EMPRESA_ID = ?
|
|
AND (REPLACE(REPLACE(REPLACE(COALESCE(CLI_CELULAR,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'
|
|
OR REPLACE(REPLACE(REPLACE(COALESCE(CLI_FONE1,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'
|
|
OR REPLACE(REPLACE(REPLACE(COALESCE(CLI_FONE2,''),'-',''),'(',''),')','') LIKE '%' || ? || '%')
|
|
FETCH FIRST 1 ROWS ONLY
|
|
`, [a.CAG_EMPRESA_ID, termo, termo, termo]);
|
|
if (cli.length > 0) { clienteId = cli[0].CLI_CODIGO_ID; break; }
|
|
}
|
|
|
|
if (!clienteId) {
|
|
for (const termo of variantes) {
|
|
const dep = await db.query(alias, `
|
|
SELECT DEPC_CLIENTE_ID FROM DEPENDENTES_CLI
|
|
WHERE DEPC_EMPRESA_ID = ? AND DEPC_SITUACAO = 'A'
|
|
AND REPLACE(REPLACE(REPLACE(COALESCE(DEPC_TELEFONE,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'
|
|
FETCH FIRST 1 ROWS ONLY
|
|
`, [a.CAG_EMPRESA_ID, termo]);
|
|
if (dep.length > 0) { clienteId = dep[0].DEPC_CLIENTE_ID; break; }
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cria NOVA conversa
|
|
const newId = await db.nextId(alias, 'GEN_CHATC2_CONVERSAS');
|
|
await db.execute(alias, `
|
|
INSERT INTO CHATC2_CONVERSAS (CON_CODIGO_ID, CON_EMPRESA_ID, CON_INSTANCIA_ID, CON_NUMERO,
|
|
CON_NOME_CONTATO, CON_CLIENTE_ID, CON_STATUS, CON_SITUACAO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG)
|
|
VALUES (?, ?, ?, ?, ?, ?, 'A', 'A', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
`, [newId, a.CAG_EMPRESA_ID, a.CON_INSTANCIA_ID, numeroLimpo, nomeContato, clienteId]);
|
|
|
|
// Monta texto sugerido (placeholders serão processados no frontend ao enviar)
|
|
const textoSugerido = nota
|
|
? '📅 *Retorno agendado*\n' + nota + '\n\nOlá [CLIENTE]! Conforme combinado, estou retornando o contato. Em que posso ajudar?'
|
|
: '👋 Olá [CLIENTE]! Conforme combinado, estou retornando o contato. Em que posso ajudar?';
|
|
|
|
// Marca agendamento como retomado
|
|
await db.execute(alias,
|
|
"UPDATE CHATC2_CONVERSAS_AGENDAMENTOS SET CAG_STATUS = 'R' WHERE CAG_CODIGO_ID = ?", [id]);
|
|
|
|
res.json({ success: true, conversaId: newId, textoSugerido: textoSugerido });
|
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
|
}
|
|
}
|
|
|
|
// ponytail: delegado para evolutionService
|
|
const evolution = require('../services/evolutionService');
|
|
async function sendEvolutionMessage(instancia, numero, texto, tipo, midiaBase64, nomeArquivo) {
|
|
if (midiaBase64 && (tipo === 'image' || tipo === 'audio' || tipo === 'video' || tipo === 'document')) {
|
|
return evolution.sendMedia(instancia, numero, tipo, midiaBase64, nomeArquivo);
|
|
}
|
|
return evolution.sendText(instancia, numero, texto);
|
|
}
|
|
|
|
module.exports = ChatController;
|