From 1ddf9b7deff6ebf7cf8f22db0f813a2814de6a03 Mon Sep 17 00:00:00 2001 From: Ayron Santoss Date: Thu, 25 Jun 2026 12:30:47 +0000 Subject: [PATCH] atualizacoes --- scripts/migrationRunner.js | 12 ++ src/controllers/chatController.js | 94 ++++++++- src/controllers/clientController.js | 34 +++ src/controllers/configController.js | 17 +- src/controllers/dashboardController.js | 6 +- src/controllers/evolutionController.js | 6 +- src/controllers/triageController.js | 24 ++- src/public/admin-conversations.html | 2 +- src/public/chat.html | 19 +- src/public/client-detail.html | 28 ++- src/public/client-list.html | 2 +- src/public/csat.html | 275 +++++++++++++++++-------- src/public/css/dark-mode.css | 108 ++++++++-- src/public/css/main.css | 6 +- src/public/dashboard.html | 12 +- src/public/js/dark-mode.js | 25 +-- src/public/login.html | 2 +- src/public/routes.html | 2 +- src/public/settings.html | 181 ++++++++++++---- src/routes/genericRoutes.js | 3 + src/routes/index.js | 1 + 21 files changed, 679 insertions(+), 180 deletions(-) diff --git a/scripts/migrationRunner.js b/scripts/migrationRunner.js index 6939ba4..ea337b3 100644 --- a/scripts/migrationRunner.js +++ b/scripts/migrationRunner.js @@ -80,6 +80,18 @@ const MIGRACOES = [ 'CREATE INDEX IDX_CONV_STATUS ON CHATC2_CONVERSAS (CON_STATUS)', ], }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 22: Mensagem e etiqueta por equipe (triagem) + // ---------------------------------------------------------- + { + id: 22, + descricao: 'Adicionar EQU_MENSAGEM e EQU_ETIQUETA_ID em CHATC2_EQUIPES', + sql: [ + 'ALTER TABLE CHATC2_EQUIPES ADD EQU_MENSAGEM VARCHAR(500)', + 'ALTER TABLE CHATC2_EQUIPES ADD EQU_ETIQUETA_ID INTEGER', + ], + }, ]; // ============================================================ diff --git a/src/controllers/chatController.js b/src/controllers/chatController.js index eac8b3c..1fe0b26 100644 --- a/src/controllers/chatController.js +++ b/src/controllers/chatController.js @@ -639,6 +639,9 @@ class ChatController { } } 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) @@ -902,10 +905,11 @@ class ChatController { // 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, mensagem]); + `, [msgId, conversaId, req.user?.id, textoProcessado]); await db.execute(alias, 'UPDATE CHATC2_CONVERSAS SET CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?', @@ -915,7 +919,7 @@ class ChatController { const resultado = await db.query(alias, 'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [instanciaId || null]); if (resultado.length > 0) { - sendEvolutionMessage(resultado[0], numeroLimpo, mensagem, 'text', null).catch(function(){}); + sendEvolutionMessage(resultado[0], numeroLimpo, textoProcessado, 'text', null).catch(function(){}); } return res.json({ success: true, data: { id: conversaId, reutilizada: true } }); @@ -941,18 +945,19 @@ class ChatController { // 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, mensagem]); + `, [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, mensagem, 'text', null); + await sendEvolutionMessage(resultado[0], numeroLimpo, textoProcessadoNew, 'text', null); } } catch (evoErr) { console.error('Erro ao enviar primeira mensagem via Evolution:', evoErr.message); @@ -1428,6 +1433,26 @@ class ChatController { } 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 @@ -1470,6 +1495,67 @@ class ChatController { 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 + } + } } // Helper: normaliza número para formato WhatsApp (DDI + 9º dígito) diff --git a/src/controllers/clientController.js b/src/controllers/clientController.js index 053478b..73eb705 100644 --- a/src/controllers/clientController.js +++ b/src/controllers/clientController.js @@ -1009,6 +1009,40 @@ class ClientController { res.status(500).json({ success: false, error: err.message }); } } + /** + * Atualiza a data de agendamento de cobrança de um título. + * PUT /api/:alias/clients/:clienteId/carne/:carneId/agendamento + */ + static async atualizarAgendamento(req, res) { + try { + const { alias, clienteId, carneId } = req.params; + const { dataAgendamento } = req.body; + + // Verifica autenticação básica + const userId = req.user?.id; + if (!userId) return res.status(403).json({ success: false, error: 'Não autenticado.' }); + + if (!dataAgendamento) { + return res.status(400).json({ success: false, error: 'Data de agendamento é obrigatória.' }); + } + + // Verifica se o título pertence ao cliente + const carne = await db.query(alias, + 'SELECT CAR_CODIGO_ID FROM CARNES WHERE CAR_CODIGO_ID = ? AND CAR_CLIENTE_ID = ?', + [parseInt(carneId), parseInt(clienteId)]); + if (carne.length === 0) { + return res.status(404).json({ success: false, error: 'Título não encontrado para este cliente.' }); + } + + await db.execute(alias, + 'UPDATE CARNES SET CAR_DT_AGENDAMENTO_COBRANCA = ? WHERE CAR_CODIGO_ID = ?', + [dataAgendamento, parseInt(carneId)]); + + res.json({ success: true, message: 'Agendamento atualizado.' }); + } catch (err) { + res.status(500).json({ success: false, error: err.message }); + } + } } module.exports = ClientController; diff --git a/src/controllers/configController.js b/src/controllers/configController.js index 540e0d6..3843f03 100644 --- a/src/controllers/configController.js +++ b/src/controllers/configController.js @@ -25,7 +25,14 @@ class ConfigController { t.members = members.map(m => ({ id: m.USU_CODIGO_ID, nome: (m.USU_NOME || '').trim(), login: (m.USU_LOGIN || '').trim() })); } - res.json({ success: true, data: teams.map(t => ({ id: t.EQU_CODIGO_ID, nome: (t.EQU_NOME || '').trim(), ordem: t.EQU_ORDEM || 0, membros: t.members })) }); + res.json({ success: true, data: teams.map(t => ({ + id: t.EQU_CODIGO_ID, + nome: (t.EQU_NOME || '').trim(), + ordem: t.EQU_ORDEM || 0, + mensagem: (t.EQU_MENSAGEM || '').trim(), + etiquetaId: t.EQU_ETIQUETA_ID || null, + membros: t.members + })) }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } } @@ -39,8 +46,12 @@ class ConfigController { const newId = await db.nextId(alias, 'GEN_CHATC2_EQUIPES'); const ordem = req.body.ordem !== undefined ? req.body.ordem : 0; + const mensagem = (req.body.mensagem || '').trim(); + const etiquetaId = req.body.etiquetaId || null; - await db.execute(alias, 'INSERT INTO CHATC2_EQUIPES (EQU_CODIGO_ID, EQU_EMPRESA_ID, EQU_NOME, EQU_ORDEM) VALUES (?, ?, ?, ?)', [newId, empresaId, nome, ordem]); + await db.execute(alias, + 'INSERT INTO CHATC2_EQUIPES (EQU_CODIGO_ID, EQU_EMPRESA_ID, EQU_NOME, EQU_ORDEM, EQU_MENSAGEM, EQU_ETIQUETA_ID) VALUES (?, ?, ?, ?, ?, ?)', + [newId, empresaId, nome, ordem, mensagem || null, etiquetaId]); if (membros && membros.length > 0) { for (const userId of membros) { @@ -60,6 +71,8 @@ class ConfigController { if (nome) await db.execute(alias, 'UPDATE CHATC2_EQUIPES SET EQU_NOME = ? WHERE EQU_CODIGO_ID = ?', [nome, id]); if (req.body.ordem !== undefined) await db.execute(alias, 'UPDATE CHATC2_EQUIPES SET EQU_ORDEM = ? WHERE EQU_CODIGO_ID = ?', [req.body.ordem, id]); + if (req.body.mensagem !== undefined) await db.execute(alias, 'UPDATE CHATC2_EQUIPES SET EQU_MENSAGEM = ? WHERE EQU_CODIGO_ID = ?', [req.body.mensagem || null, id]); + if (req.body.etiquetaId !== undefined) await db.execute(alias, 'UPDATE CHATC2_EQUIPES SET EQU_ETIQUETA_ID = ? WHERE EQU_CODIGO_ID = ?', [req.body.etiquetaId || null, id]); if (membros) { await db.execute(alias, 'DELETE FROM CHATC2_USU_EQUIPES WHERE EQU_EQUIPE_ID = ?', [id]); diff --git a/src/controllers/dashboardController.js b/src/controllers/dashboardController.js index 2acf3f2..2914e07 100644 --- a/src/controllers/dashboardController.js +++ b/src/controllers/dashboardController.js @@ -89,15 +89,13 @@ class DashboardController { }); // ===== Tráfego das conversas (últimos 365 dias, por dia) ===== - const desde = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000); - const desdeStr = desde.toISOString().split('T')[0]; const trafegoRows = await db.query(alias, `SELECT CAST(COALESCE(CON_DT_INICIO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) AS DATE) AS DIA, COUNT(*) AS CT FROM CHATC2_CONVERSAS WHERE CON_EMPRESA_ID IN (${ph}) - AND COALESCE(CON_DT_INICIO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) >= ? + AND COALESCE(CON_DT_INICIO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) >= CURRENT_DATE - 365 GROUP BY CAST(COALESCE(CON_DT_INICIO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) AS DATE)`, - empresas.concat([desdeStr])); + empresas); const fmtDia = (d) => { if (!d) return null; diff --git a/src/controllers/evolutionController.js b/src/controllers/evolutionController.js index 430c0ec..f363c29 100644 --- a/src/controllers/evolutionController.js +++ b/src/controllers/evolutionController.js @@ -766,7 +766,8 @@ async function processWebhook(alias, body) { // Busca conversa existente pelo número exato ou pelos últimos 8 dígitos let conversations = await db.query(alias, - `SELECT CON_CODIGO_ID, CON_STATUS, CON_CLIENTE_ID, CON_SAUDACAO_ENVIADA, CON_CSAT_ENVIADO + `SELECT CON_CODIGO_ID, CON_STATUS, CON_CLIENTE_ID, CON_SAUDACAO_ENVIADA, CON_CSAT_ENVIADO, + CON_MENU_ESTADO, CON_USUARIO_ID, CON_EQUIPE_ID FROM CHATC2_CONVERSAS WHERE CON_NUMERO = ? AND CON_EMPRESA_ID = ? AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A' ORDER BY CON_DT_INICIO DESC`, @@ -776,7 +777,8 @@ async function processWebhook(alias, body) { if (conversations.length === 0) { const ultimos8 = numero.slice(-8); conversations = await db.query(alias, - `SELECT CON_CODIGO_ID, CON_STATUS, CON_CLIENTE_ID, CON_SAUDACAO_ENVIADA, CON_CSAT_ENVIADO + `SELECT CON_CODIGO_ID, CON_STATUS, CON_CLIENTE_ID, CON_SAUDACAO_ENVIADA, CON_CSAT_ENVIADO, + CON_MENU_ESTADO, CON_USUARIO_ID, CON_EQUIPE_ID FROM CHATC2_CONVERSAS WHERE CON_NUMERO LIKE '%' || ? || '%' AND CON_EMPRESA_ID = ? AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A' ORDER BY CON_DT_INICIO DESC`, diff --git a/src/controllers/triageController.js b/src/controllers/triageController.js index 569ec45..e18f692 100644 --- a/src/controllers/triageController.js +++ b/src/controllers/triageController.js @@ -47,7 +47,7 @@ class TriageController { const boletoNum = String(cfg.CFE_TRIAGEM_BOLETO_NUMERO || '0').trim(); const equipes = await db.query(alias, - "SELECT EQU_CODIGO_ID, EQU_NOME FROM CHATC2_EQUIPES WHERE EQU_EMPRESA_ID = ? AND EQU_SITUACAO = 'A' ORDER BY EQU_ORDEM, EQU_NOME", + "SELECT EQU_CODIGO_ID, EQU_NOME, EQU_MENSAGEM, EQU_ETIQUETA_ID FROM CHATC2_EQUIPES WHERE EQU_EMPRESA_ID = ? AND EQU_SITUACAO = 'A' ORDER BY EQU_ORDEM, EQU_NOME", [empresaId] ); @@ -234,7 +234,7 @@ class TriageController { if (estado === 'root' || estado === '') { logTriage('7-processResponse root - resposta: ' + JSON.stringify(resposta) + ' | boletoNum: ' + JSON.stringify(boletoNum)); const equipes = await db.query(alias, - "SELECT EQU_CODIGO_ID, EQU_NOME FROM CHATC2_EQUIPES WHERE EQU_EMPRESA_ID = ? AND EQU_SITUACAO = 'A' ORDER BY EQU_ORDEM, EQU_NOME", + "SELECT EQU_CODIGO_ID, EQU_NOME, EQU_MENSAGEM, EQU_ETIQUETA_ID FROM CHATC2_EQUIPES WHERE EQU_EMPRESA_ID = ? AND EQU_SITUACAO = 'A' ORDER BY EQU_ORDEM, EQU_NOME", [empresaId] ); @@ -248,6 +248,26 @@ class TriageController { [eq.EQU_CODIGO_ID, conversaId] ); + // Mensagem personalizada da equipe (se configurada) + const msgEquipe = (eq.EQU_MENSAGEM || '').trim(); + if (msgEquipe) { + const msgEqId = 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_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO) + VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP) + `, [msgEqId, conversaId, msgEquipe]); + await TriageController.sendEvolution(alias, instanciaId, numero, msgEquipe); + } + + // Etiqueta automática da equipe (se configurada) + if (eq.EQU_ETIQUETA_ID) { + try { + await db.execute(alias, + 'INSERT INTO CHATC2_CONVERSAS_ETIQUETAS (CET_CONVERSA_ID, CET_ETIQUETA_ID) VALUES (?, ?)', + [conversaId, eq.EQU_ETIQUETA_ID]); + } catch (_) { /* ignora duplicata */ } + } + // Verifica se existem submenus configurados para esta equipe var temSubmenus = await TriageController.enviarSubmenu(alias, conversaId, empresaId, instanciaId, numero, null, eq.EQU_CODIGO_ID); diff --git a/src/public/admin-conversations.html b/src/public/admin-conversations.html index 3d8eaee..e378e6e 100644 --- a/src/public/admin-conversations.html +++ b/src/public/admin-conversations.html @@ -24,7 +24,7 @@ body { background:var(--surface-3); display:flex; min-height:100vh; } .loading { text-align:center; padding:40px; color:var(--text-faint); } - +