From 9cf5f0024337ed9826ca3a26bad9059dcd0c0f80 Mon Sep 17 00:00:00 2001 From: Ayron Santoss Date: Wed, 1 Jul 2026 13:24:06 +0000 Subject: [PATCH] ponytail: remove resolucaoSetup, compressVideo, sendEvolutionMessage, sendEvolution; add migration #24 - delete: src/resolucaoSetup.js (migration paralela, consolidado em #24) - shrink: chatController.sendEvolutionMessage (140 -> 6 linhas, delega ao evolutionService) - shrink: triageController.sendEvolution (55 -> 10 linhas, delega ao evolutionService) - delete: compressVideo em chatController (codigo morto, chamado so pelo old sendEvolutionMessage) - add: migration #24 consolida DDL que estava espalhada em runtime - fix: configController.remove garantirEstrutura calls (arquivo removido) - net: -270 linhas, -1 arquivo --- scripts/migrationRunner.js | 24 +++ src/controllers/chatController.js | 204 +------------------------ src/controllers/configController.js | 7 - src/controllers/dashboardController.js | 7 + src/controllers/evolutionController.js | 58 +++++++ src/controllers/triageController.js | 45 +----- src/public/admin-conversations.html | 35 +++++ src/public/chat.html | 10 +- src/public/css/dark-mode.css | 7 + src/public/dashboard.html | 3 + src/resolucaoSetup.js | 48 ------ 11 files changed, 152 insertions(+), 296 deletions(-) delete mode 100644 src/resolucaoSetup.js diff --git a/scripts/migrationRunner.js b/scripts/migrationRunner.js index 1e7d4d6..9bc02ea 100644 --- a/scripts/migrationRunner.js +++ b/scripts/migrationRunner.js @@ -113,6 +113,30 @@ const MIGRACOES = [ )`, ], }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 24: DDL que estava no resolucaoSetup.js (runtime) + // ---------------------------------------------------------- + { + id: 24, + descricao: 'Consolidar DDL do fluxo de resolução (antes criado em runtime)', + sql: [ + `CREATE TABLE "CHATC2_MOTIVOS_ATENDIMENTO" ( + MOT_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, + MOT_EMPRESA_ID INTEGER, + MOT_DESCRICAO VARCHAR(150), + MOT_SITUACAO CHAR(1) DEFAULT 'A' + )`, + "ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_MOTIVO_VISUALIZAR CHAR(1) DEFAULT 'N'", + "ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_MOTIVO_OBRIGATORIO CHAR(1) DEFAULT 'N'", + "ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_RESOLUCAO_VISUALIZAR CHAR(1) DEFAULT 'N'", + "ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_RESOLUCAO_OBRIGATORIO CHAR(1) DEFAULT 'N'", + "ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_ENVIAR_BOLETO CHAR(1) DEFAULT 'N'", + "ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_RESOLUCAO_SALVAR_MSC CHAR(1) DEFAULT 'N'", + 'ALTER TABLE CHATC2_CONVERSAS ADD CON_MOTIVO_ID INTEGER', + 'ALTER TABLE CHATC2_CONVERSAS ADD CON_RESOLUCAO VARCHAR(4000)', + ], + }, ]; // ============================================================ diff --git a/src/controllers/chatController.js b/src/controllers/chatController.js index 8d0a15b..a1528ff 100644 --- a/src/controllers/chatController.js +++ b/src/controllers/chatController.js @@ -1,63 +1,9 @@ const db = require('../database'); -const { garantirEstrutura } = require('../resolucaoSetup'); const ffmpegPath = require('ffmpeg-static'); const { execFile } = require('child_process'); const path = require('path'); const fs = require('fs'); -/** - * Comprime vídeo para tamanho aceitável pelo WhatsApp/Evolution API - * Reduz bitrate e resolução se necessário (alvo: ~10MB) - * @param {Buffer} inputBuffer - Buffer do video original - * @returns {Promise} Buffer do video comprimido - */ -function compressVideo(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, 'video_in_' + Date.now() + '.mp4'); - var outputPath = path.join(tmpDir, 'video_out_' + Date.now() + '.mp4'); - - fs.writeFileSync(inputPath, inputBuffer); - var inputSizeMB = (inputBuffer.length / (1024 * 1024)).toFixed(1); - - // Se o video ja tem <= 10MB, nao precisa comprimir muito - var targetBitrate = inputBuffer.length < 10 * 1024 * 1024 ? '1M' : '500k'; - console.log('[Video] Comprimindo video de ' + inputSizeMB + 'MB, bitrate alvo: ' + targetBitrate); - - execFile(ffmpegPath, [ - '-y', - '-i', inputPath, - '-c:v', 'libx264', - '-b:v', targetBitrate, - '-c:a', 'aac', - '-b:a', '64k', - '-vf', 'scale=720:-2', - '-movflags', '+faststart', - '-preset', 'fast', - '-maxrate', '1M', - '-bufsize', '2M', - outputPath - ], { timeout: 120000 }, function(err) { - try { fs.unlinkSync(inputPath); } catch(e) {} - - if (err) { - try { fs.unlinkSync(outputPath); } catch(e) {} - console.error('[Video] Erro na compressao:', err.message.substring(0, 100)); - return reject(err); - } - - var outputBuffer = fs.readFileSync(outputPath); - try { fs.unlinkSync(outputPath); } catch(e) {} - - var outputSizeMB = (outputBuffer.length / (1024 * 1024)).toFixed(1); - console.log('[Video] Compressao concluida: ' + inputSizeMB + 'MB -> ' + outputSizeMB + 'MB'); - resolve(outputBuffer); - }); - }); -} - /** * Converte buffer de áudio WebM para Ogg Opus usando ffmpeg */ @@ -691,7 +637,6 @@ class ChatController { return res.status(403).json({ success: false, error: 'Sem permissão.' }); // ===== Fluxo de Resolução: valida motivo/resolução conforme config ===== - await garantirEstrutura(alias); 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() : ''; @@ -743,7 +688,9 @@ class ChatController { WHERE CON_CODIGO_ID = ? `, [usuarioNome, equipeNome, etiquetasDesc, motivoId, resolucao || null, id]); - // CSAT + // 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]); @@ -784,6 +731,7 @@ class ChatController { 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) { @@ -1711,149 +1659,13 @@ class ChatController { } } -// Helper: normaliza número para formato WhatsApp (DDI + 9º dígito) -function _normalizarWhatsApp(numero) { - let n = String(numero || '').replace(/\D/g, ''); - if (!n) return ''; - if (!n.startsWith('55')) n = '55' + n; - const resto = n.slice(2); - if (resto.length === 10) n = '55' + resto.slice(0, 2) + '9' + resto.slice(2); - return n; -} - -// Função auxiliar para enviar mensagem via Evolution API +// ponytail: delegado para evolutionService +const evolution = require('../services/evolutionService'); async function sendEvolutionMessage(instancia, numero, texto, tipo, midiaBase64, nomeArquivo) { - const url = ((instancia.INS_URL || '').trim()).replace(/\/$/, ''); - const apiKey = (instancia.INS_API_KEY || '').trim(); - const instanceName = (instancia.INS_INSTANCE_NAME || '').trim(); - const numeroLimpo = _normalizarWhatsApp(numero); - - const https = require('https'); - const http = require('http'); - - let payload; - let endpoint; - if (midiaBase64 && (tipo === 'image' || tipo === 'audio' || tipo === 'video' || tipo === 'document')) { - var mediaB64 = midiaBase64; - var fileName = nomeArquivo || 'arquivo'; - - if (tipo === 'audio') { - // A mídia pode já estar em OGG (convertida antes de salvar no BD) - // Se ainda for WebM, tenta converter agora como fallback - var jaEOgg = (fileName || '').toLowerCase().endsWith('.ogg'); - if (!jaEOgg) { - try { - var inputBuffer = Buffer.from(mediaB64, 'base64'); - if (inputBuffer.length > 100) { - var oggBuffer = await convertWebmToOgg(inputBuffer); - if (oggBuffer && oggBuffer.length > 100) { - mediaB64 = oggBuffer.toString('base64'); - fileName = 'audio.ogg'; - console.log('[Audio] Conversão fallback WebM→OGG:', inputBuffer.length, '→', oggBuffer.length); - } - } - } catch(convErr) { - console.error('[Audio] Conversão OGG falhou, mantendo como áudio:', convErr.message.substring(0, 100)); - fileName = 'audio.webm'; - // NÃO muda para 'document' - mantém como áudio - } - } - - // Usa sendWhatsAppAudio para enviar como mensagem de voz (PTT) - endpoint = '/message/sendWhatsAppAudio/' + encodeURIComponent(instanceName); - payload = JSON.stringify({ number: numeroLimpo, audio: mediaB64, encoding: true }); - console.log('[Evolution] Enviando via sendWhatsAppAudio, tamanho:', mediaB64.length, 'arquivo:', fileName); - - } else if (tipo === 'image') { - endpoint = '/message/sendMedia/' + encodeURIComponent(instanceName); - payload = JSON.stringify({ number: numeroLimpo, mediatype: 'image', media: mediaB64, fileName: fileName }); - - } else if (tipo === 'video') { - // Comprime video se for muito grande (> 5MB) para evitar rejeicao da Evolution API - try { - var videoBuf = Buffer.from(mediaB64, 'base64'); - if (videoBuf.length > 5 * 1024 * 1024) { - console.log('[Video] Video grande detectado (' + (videoBuf.length / (1024 * 1024)).toFixed(1) + 'MB), comprimindo...'); - var compressedBuf = await compressVideo(videoBuf); - if (compressedBuf && compressedBuf.length > 1000) { - mediaB64 = compressedBuf.toString('base64'); - fileName = (fileName || 'video').replace(/\.\w+$/, '') + '_compressed.mp4'; - console.log('[Video] Compressao OK, novo tamanho base64:', mediaB64.length); - } - } - } catch(compressErr) { - console.error('[Video] Compressao falhou, enviando original:', compressErr.message.substring(0, 80)); - } - endpoint = '/message/sendMedia/' + encodeURIComponent(instanceName); - payload = JSON.stringify({ number: numeroLimpo, mediatype: 'video', media: mediaB64, fileName: fileName }); - - } else if (tipo === 'document') { - // Para documentos que sao videos, comprime tambem se for grande - var isVideoDoc = (fileName || '').toLowerCase().match(/\.(mp4|mov|avi|mkv|webm|3gp|m4v)$/); - if (isVideoDoc) { - try { - var docBuf = Buffer.from(mediaB64, 'base64'); - if (docBuf.length > 5 * 1024 * 1024) { - console.log('[Document] Video como documento detectado (' + (docBuf.length / (1024 * 1024)).toFixed(1) + 'MB), comprimindo...'); - var compressedDocBuf = await compressVideo(docBuf); - if (compressedDocBuf && compressedDocBuf.length > 1000) { - mediaB64 = compressedDocBuf.toString('base64'); - fileName = (fileName || 'video').replace(/\.\w+$/, '') + '_compressed.mp4'; - console.log('[Document] Compressao OK, novo tamanho base64:', mediaB64.length); - } - } - } catch(compressErr) { - console.error('[Document] Compressao falhou, enviando original:', compressErr.message.substring(0, 80)); - } - } - endpoint = '/message/sendMedia/' + encodeURIComponent(instanceName); - payload = JSON.stringify({ number: numeroLimpo, mediatype: 'document', media: mediaB64, fileName: fileName }); - - } else { - endpoint = '/message/sendMedia/' + encodeURIComponent(instanceName); - payload = JSON.stringify({ number: numeroLimpo, mediatype: 'document', media: mediaB64, fileName: fileName }); - } - - } else { - endpoint = '/message/sendText/' + encodeURIComponent(instanceName); - payload = JSON.stringify({ number: numeroLimpo, text: texto || '', delay: 0 }); + return evolution.sendMedia(instancia, numero, tipo, midiaBase64, nomeArquivo); } - - const parsedUrl = new URL(url + endpoint); - const lib = parsedUrl.protocol === 'https:' ? https : http; - - return new Promise((resolve, reject) => { - const options = { - hostname: parsedUrl.hostname, - port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80), - path: parsedUrl.pathname, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(payload), - 'apikey': apiKey, - } - }; - const req = lib.request(options, (res) => { - let body = ''; - res.on('data', c => body += c); - res.on('end', () => { - if (res.statusCode >= 400) { - console.error('[Evolution] Erro ao enviar mídia:', res.statusCode, body.substring(0, 200)); - reject(new Error('HTTP ' + res.statusCode + ': ' + body.substring(0, 100))); - } else { - resolve(body); - } - }); - }); - req.on('error', function(err) { - console.error('[Evolution] Erro na requisição:', err.message); - reject(err); - }); - req.write(payload); - req.end(); - }); + return evolution.sendText(instancia, numero, texto); } module.exports = ChatController; diff --git a/src/controllers/configController.js b/src/controllers/configController.js index 9dd1605..d938a81 100644 --- a/src/controllers/configController.js +++ b/src/controllers/configController.js @@ -1,6 +1,5 @@ const db = require('../database'); const { isGerente } = require('../middlewares/roles'); -const { garantirEstrutura } = require('../resolucaoSetup'); const configService = require('../services/configService'); class ConfigController { @@ -165,7 +164,6 @@ class ConfigController { const { alias } = req.params; const empresaId = parseInt(req.query.empresaId) || req.user?.empresas?.[0]; - await garantirEstrutura(alias); let config = await db.query(alias, 'SELECT * FROM CHATC2_CONFIGURACOES_EMPRESA WHERE CFE_EMPRESA_ID = ?', [empresaId] @@ -209,7 +207,6 @@ class ConfigController { try { if (!(await isGerente(req))) return res.status(403).json({ success: false, error: 'Apenas gerentes podem alterar o fluxo de resolução.' }); const { alias } = req.params; - await garantirEstrutura(alias); const empresaId = req.body.empresaId || req.user?.empresas?.[0]; const sn = (v) => (v === 'S' || v === true ? 'S' : 'N'); const mv = sn(req.body.motivoVisualizar), mo = sn(req.body.motivoObrigatorio); @@ -236,7 +233,6 @@ class ConfigController { static async listMotivos(req, res) { try { const { alias } = req.params; - await garantirEstrutura(alias); const empresaId = parseInt(req.query.empresaId) || req.user?.empresas?.[0]; const rows = await db.query(alias, `SELECT MOT_CODIGO_ID, MOT_DESCRICAO FROM "CHATC2_MOTIVOS_ATENDIMENTO" @@ -249,7 +245,6 @@ class ConfigController { try { if (!(await isGerente(req))) return res.status(403).json({ success: false, error: 'Apenas gerentes podem cadastrar motivos.' }); const { alias } = req.params; - await garantirEstrutura(alias); const descricao = (req.body.descricao || '').trim(); if (!descricao) return res.status(400).json({ success: false, error: 'Descrição obrigatória.' }); const empresaId = req.body.empresaId || req.user?.empresas?.[0]; @@ -265,7 +260,6 @@ class ConfigController { try { if (!(await isGerente(req))) return res.status(403).json({ success: false, error: 'Apenas gerentes podem remover motivos.' }); const { alias, id } = req.params; - await garantirEstrutura(alias); await db.execute(alias, `UPDATE "CHATC2_MOTIVOS_ATENDIMENTO" SET MOT_SITUACAO = 'I' WHERE MOT_CODIGO_ID = ?`, [id]); res.json({ success: true }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } @@ -275,7 +269,6 @@ class ConfigController { try { if (!(await isGerente(req))) return res.status(403).json({ success: false, error: 'Apenas gerentes podem alterar as configurações da empresa.' }); const { alias } = req.params; - await garantirEstrutura(alias); const data = req.body; const empresaId = data.empresaId || req.user?.empresas?.[0]; const enviarBoleto = (data.enviarBoleto === 'S' || data.enviarBoleto === true) ? 'S' : 'N'; diff --git a/src/controllers/dashboardController.js b/src/controllers/dashboardController.js index 2914e07..99815ab 100644 --- a/src/controllers/dashboardController.js +++ b/src/controllers/dashboardController.js @@ -73,6 +73,12 @@ class DashboardController { WHERE CON_EMPRESA_ID IN (${ph}) AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A' AND CON_USUARIO_ID IS NULL AND CON_EQUIPE_ID IS NOT NULL`, empresas); + // Agendadas para hoje (status P = Pendente) + const agendadas = await db.query(alias, + `SELECT COUNT(*) AS CT FROM CHATC2_CONVERSAS_AGENDAMENTOS + WHERE CAG_EMPRESA_ID IN (${ph}) AND CAG_DATA = CURRENT_DATE AND CAG_STATUS = 'P'`, + empresas); + // ===== Atendentes ===== const usuarios = await db.query(alias, `SELECT DISTINCT u.USU_CODIGO_ID, u.USU_NOME @@ -113,6 +119,7 @@ class DashboardController { naoAtendidas: Number(naoAtendidas[0]?.CT) || 0, naoAtribuidas: Number(naoAtribuidas[0]?.CT) || 0, pendentes: Number(pendentes[0]?.CT) || 0, + agendadas: Number(agendadas[0]?.CT) || 0, }, atendentes: { disponiveis, desconectados, lista }, trafego, diff --git a/src/controllers/evolutionController.js b/src/controllers/evolutionController.js index f363c29..9671341 100644 --- a/src/controllers/evolutionController.js +++ b/src/controllers/evolutionController.js @@ -789,6 +789,63 @@ async function processWebhook(alias, body) { let conversa; var isNovaConversa = false; if (conversations.length === 0) { + // Antes de criar nova, verifica se há conversa finalizada COM agendamento pendente + // Se o cliente retornou antes da data agendada, reabre a conversa existente + const agendada = await db.query(alias, ` + SELECT c.CON_CODIGO_ID, c.CON_CLIENTE_ID, c.CON_NOME_CONTATO, c.CON_INSTANCIA_ID, + a.CAG_CODIGO_ID AS AG_ID, a.CAG_NOTA + FROM CHATC2_CONVERSAS c + JOIN CHATC2_CONVERSAS_AGENDAMENTOS a ON a.CAG_CONVERSA_ID = c.CON_CODIGO_ID + WHERE c.CON_NUMERO = ? AND c.CON_EMPRESA_ID = ? + AND c.CON_STATUS = 'F' AND c.CON_SITUACAO = 'A' + AND a.CAG_STATUS = 'P' + ORDER BY a.CAG_DATA DESC + FETCH FIRST 1 ROWS ONLY + `, [numero, empresaId]); + + // Tenta também pelos últimos 8 dígitos + let agendada8 = []; + if (agendada.length === 0) { + const ultimos8 = numero.slice(-8); + agendada8 = await db.query(alias, ` + SELECT c.CON_CODIGO_ID, c.CON_CLIENTE_ID, c.CON_NOME_CONTATO, c.CON_INSTANCIA_ID, + a.CAG_CODIGO_ID AS AG_ID, a.CAG_NOTA + FROM CHATC2_CONVERSAS c + JOIN CHATC2_CONVERSAS_AGENDAMENTOS a ON a.CAG_CONVERSA_ID = c.CON_CODIGO_ID + WHERE c.CON_NUMERO LIKE '%' || ? || '%' AND c.CON_EMPRESA_ID = ? + AND c.CON_STATUS = 'F' AND c.CON_SITUACAO = 'A' + AND a.CAG_STATUS = 'P' + ORDER BY a.CAG_DATA DESC + FETCH FIRST 1 ROWS ONLY + `, [ultimos8, empresaId]); + } + + const agReabrir = agendada.length > 0 ? agendada[0] : (agendada8.length > 0 ? agendada8[0] : null); + + if (agReabrir) { + // Reabre a conversa agendada em vez de criar nova + console.log('[Webhook] Reabrindo conversa agendada:', agReabrir.CON_CODIGO_ID, 'agendamento:', agReabrir.AG_ID); + await db.execute(alias, + "UPDATE CHATC2_CONVERSAS SET CON_STATUS = 'A', CON_DT_FINAL = NULL, CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?", + [agReabrir.CON_CODIGO_ID]); + await db.execute(alias, + "UPDATE CHATC2_CONVERSAS_AGENDAMENTOS SET CAG_STATUS = 'R' WHERE CAG_CODIGO_ID = ?", + [agReabrir.AG_ID]); + + conversa = await db.query(alias, 'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [agReabrir.CON_CODIGO_ID]); + conversa = conversa[0]; + + // Se o cliente não estava vinculado, tenta vincular agora + if (!conversa.CON_CLIENTE_ID) { + const resultadoGlobal = await buscarClienteGlobal(numero, empresaId, alias); + if (resultadoGlobal) { + await db.execute(alias, + 'UPDATE CHATC2_CONVERSAS SET CON_CLIENTE_ID = ?, CON_NOME_CONTATO = ? WHERE CON_CODIGO_ID = ?', + [resultadoGlobal.cliente.CLI_CODIGO_ID, resultadoGlobal.cliente.CLI_NOME.trim(), conversa.CON_CODIGO_ID]); + } + } + // Pula o bloco de criação (já temos conversa) + } else { isNovaConversa = true; const newId = await db.nextId(alias, 'GEN_CHATC2_CONVERSAS'); const nomeContato = data.pushName || data.notifyName || sender || ''; @@ -861,6 +918,7 @@ async function processWebhook(alias, body) { await TriageController.sendMenu(aliasEfetivo, conversa.CON_CODIGO_ID, empresaId, instanciaId, numero); } } catch(e) { console.error('[Webhook] Erro ao enviar triagem:', e.message); } + } // fecha else (nova conversa) } else { conversa = conversations[0]; diff --git a/src/controllers/triageController.js b/src/controllers/triageController.js index e18f692..c9f1408 100644 --- a/src/controllers/triageController.js +++ b/src/controllers/triageController.js @@ -7,7 +7,6 @@ const fs = require('fs'); const path = require('path'); const db = require('../database'); const https = require('https'); -const http = require('http'); function logTriage(msg) { try { @@ -1056,48 +1055,8 @@ class TriageController { const inst = await db.query(alias, "SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ? AND INS_SITUACAO = 'A'", [instanciaId]); if (inst.length === 0) return; - const url = ((inst[0].INS_URL || '').trim()).replace(/\/+$/, ''); - const apiKey = (inst[0].INS_API_KEY || '').trim(); - const instanceName = (inst[0].INS_INSTANCE_NAME || '').trim(); - if (!url || !apiKey || !instanceName) return; - - // Normaliza para formato WhatsApp (com DDI) - const numeroLimpo = _normalizarWhatsApp(numero); - const payload = JSON.stringify({ number: numeroLimpo, text: texto || '', delay: 0 }); - const endpoint = '/message/sendText/' + encodeURIComponent(instanceName); - const parsedUrl = new URL(url + endpoint); - const lib = parsedUrl.protocol === 'https:' ? https : http; - - await new Promise(function(resolve, reject) { - const req = lib.request({ - hostname: parsedUrl.hostname, - port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80), - path: parsedUrl.pathname, - method: 'POST', - timeout: 15000, - headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(payload), - 'apikey': apiKey, - } - }, function(res) { - var d = ''; - res.on('data', function(c) { d += c; }); - res.on('end', function() { - if (res.statusCode >= 400) { - console.error('[Triagem Evolution] Erro HTTP', res.statusCode, d.substring(0, 100)); - } - resolve(); - }); - }); - req.on('error', function(err) { - console.error('[Triagem Evolution] Erro:', err.message); - resolve(); - }); - req.on('timeout', function() { req.destroy(); resolve(); }); - req.write(payload); - req.end(); - }); + const evolution = require('../services/evolutionService'); + await evolution.sendText(inst[0], numero, texto); } catch(e) { console.error('[Triagem Evolution] Erro ao enviar:', e.message); } diff --git a/src/public/admin-conversations.html b/src/public/admin-conversations.html index 5fe3256..a3d6d2e 100644 --- a/src/public/admin-conversations.html +++ b/src/public/admin-conversations.html @@ -56,7 +56,9 @@ body { background:var(--surface-3); display:flex; min-height:100vh; } + +