diff --git a/scripts/migrationRunner.js b/scripts/migrationRunner.js index ea337b3..1e7d4d6 100644 --- a/scripts/migrationRunner.js +++ b/scripts/migrationRunner.js @@ -92,6 +92,27 @@ const MIGRACOES = [ 'ALTER TABLE CHATC2_EQUIPES ADD EQU_ETIQUETA_ID INTEGER', ], }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 23: Agendamento de conversas + // ---------------------------------------------------------- + { + id: 23, + descricao: 'Criar tabela CHATC2_CONVERSAS_AGENDAMENTOS (retorno agendado)', + sql: [ + `CREATE TABLE CHATC2_CONVERSAS_AGENDAMENTOS ( + CAG_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, + CAG_CONVERSA_ID INTEGER NOT NULL, + CAG_EMPRESA_ID INTEGER, + CAG_DATA DATE NOT NULL, + CAG_HORA TIME, + CAG_NOTA VARCHAR(300), + CAG_STATUS CHAR(1) DEFAULT 'P', + CAG_USUARIO_ID INTEGER, + CAG_DT_CRIACAO TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )`, + ], + }, ]; // ============================================================ diff --git a/src/controllers/chatController.js b/src/controllers/chatController.js index d08f4d7..8d0a15b 100644 --- a/src/controllers/chatController.js +++ b/src/controllers/chatController.js @@ -1569,6 +1569,146 @@ class ChatController { 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 }); } + } } // Helper: normaliza número para formato WhatsApp (DDI + 9º dígito) diff --git a/src/database.js b/src/database.js index 4dc468c..616bc07 100644 --- a/src/database.js +++ b/src/database.js @@ -97,6 +97,7 @@ const GENERATORS = [ 'GEN_CHATC2_MENUS_EMPRESA', 'GEN_CHATC2_CSAT_AVALIACOES', 'GEN_CHATC2_MOTIVOS_ATENDIMENTO', + 'GEN_CHATC2_AGENDAMENTOS', ]; async function nextId(alias, generatorName) { diff --git a/src/public/chat.html b/src/public/chat.html index 9083c23..2b61c79 100644 --- a/src/public/chat.html +++ b/src/public/chat.html @@ -825,9 +825,31 @@ body.dark-mode .msg.enviando { opacity: 0.4; }