alterações de melhorias

This commit is contained in:
2026-06-24 14:13:09 +00:00
parent d942694ec0
commit 29190cd71c
34 changed files with 2570 additions and 950 deletions
+133 -67
View File
@@ -181,70 +181,93 @@ class ChatController {
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);
const conversations = await Promise.all(result.map(async (row) => {
// Busca etiquetas
const labels = await db.query(alias, `
SELECT 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 = ? AND e.ETI_SITUACAO = 'A'
`, [row.CON_CODIGO_ID]);
// --- 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] = [];
// Busca foto do cliente se configurado
let foto = null;
if (row.CON_CLIENTE_ID) {
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') {
const cli = await db.query(alias,
'SELECT CLI_FOTO, CLI_NOME_FOTO FROM CLIENTES WHERE CLI_CODIGO_ID = ?', [row.CON_CLIENTE_ID]);
if (cli.length > 0) {
var rawFoto = cli[0].CLI_FOTO;
if (rawFoto) {
if (Buffer.isBuffer(rawFoto)) {
var str = rawFoto.toString('utf8');
if (/^[A-Za-z0-9+/]+=*$/.test(str.slice(0, 30))) {
foto = str;
} else {
var 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');
}
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 {
foto = rawFoto;
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;
}
}
}
}
return {
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 || '',
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,
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
@@ -490,8 +513,8 @@ class ChatController {
FROM CHATC2_CONVERSAS_MENSAGENS
WHERE CME_CONVERSA_ID = ? AND CME_SITUACAO = 'A'
ORDER BY CME_DT_ENVIO DESC
OFFSET ${offset} ROWS FETCH NEXT ${limit} ROWS ONLY
`, [id]);
OFFSET ? ROWS FETCH NEXT ? ROWS ONLY
`, [id, offset, limit]);
var mapped = msgs.reverse().map(function(m) { return {
id: m.CME_CODIGO_ID,
@@ -551,14 +574,12 @@ class ChatController {
"UPDATE CHATC2_CONVERSAS SET CON_STATUS = 'A', CON_DT_FINAL = NULL WHERE CON_CODIGO_ID = ?", [id]);
}
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
const newId = (maxId[0]?.ID || 0) + 1;
const newId = await db.nextId(alias, 'GEN_CONVERSAS_MENSAGENS');
let midiaId = null;
// Salva mídia se houver
if (midiaBase64) {
const maxMidia = await db.query(alias, 'SELECT MAX(MAT_CODIGO_ID) AS ID FROM CHATC2_MENSAGENS_ATENDIMENTOS');
midiaId = (maxMidia[0]?.ID || 0) + 1;
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
@@ -824,8 +845,7 @@ class ChatController {
}
// Adiciona mensagem na conversa existente
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
const msgId = (maxMsgId[0]?.ID || 0) + 1;
const msgId = 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_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'U', ?, ?, 'text', 'A', CURRENT_TIMESTAMP)
@@ -846,8 +866,7 @@ class ChatController {
}
// Gera ID da nova conversa
const maxId = await db.query(alias, 'SELECT MAX(CON_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS');
const newId = (maxId[0]?.ID || 0) + 1;
const newId = await db.nextId(alias, 'GEN_CHATC2_CONVERSAS');
// Se não informou instância, pega a primeira ativa
let instId = instanciaId;
@@ -865,8 +884,7 @@ class ChatController {
`, [newId, empresaId, instId, numeroLimpo, nomeContato || numeroLimpo, clienteId || null]);
// Insere primeira mensagem
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
const msgId = (maxMsgId[0]?.ID || 0) + 1;
const msgId = 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_SITUACAO, CME_DT_ENVIO)
@@ -1279,8 +1297,7 @@ class ChatController {
}
// Salva mensagem do agente
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
const newId = (maxId[0]?.ID || 0) + 1;
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)
@@ -1306,7 +1323,57 @@ class ChatController {
}
/**
* Recebe avaliação CSAT via formulário web
* 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 csatAvaliar(req, res) {
@@ -1329,8 +1396,7 @@ class ChatController {
}
// Salva avaliação
const maxId = await db.query(alias, 'SELECT MAX(CSA_CODIGO_ID) AS ID FROM CHATC2_CSAT_AVALIACOES');
const newId = (maxId[0]?.ID || 0) + 1;
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 (?, ?, ?, ?, ?, ?)',