diff --git a/src/bot/handler.js b/src/bot/handler.js index dc41e66..c4a9d85 100644 --- a/src/bot/handler.js +++ b/src/bot/handler.js @@ -265,6 +265,11 @@ class BotHandler { console.log(`🎫 Iniciando processo de abertura de suporte`); await this.iniciarProcessoSuporte(chatId, user.id, target); break; + + case 'fechar_suporte': + console.log(`🔒 Iniciando processo de fechamento de suporte`); + await this.iniciarProcessoFecharSuporte(chatId, user.id, target); + break; case 'conectar': await this.handleConnect(chatId, target); @@ -531,7 +536,12 @@ Digite /help para ver todos os comandos disponíveis! • "novo chamado" • "suporte cliente 123.456.789-10" -**🔄 PROCESSO INTERATIVO:** +**� FECHAR SUPORTE:** +• "fechar suporte" +• "finalizar chamado" +• "encerrar suporte João Silva" + +**�🔄 PROCESSO INTERATIVO:** 1️⃣ Busca do cliente (nome ou CPF) 2️⃣ Seleção do serviço 3️⃣ Escolha da categoria do problema @@ -2037,6 +2047,12 @@ Digite /help para ver todos os comandos disponíveis! } else if (data.startsWith('suporte_servico_')) { console.log(`🔍 Condição suporte_servico_ atendida`); await this.handleSuporteServicoCallback(callbackQuery, data); + } else if (data.startsWith('fechar_suporte_cli_')) { + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.handleFecharSuporteClienteCallback(callbackQuery, data); + } else if (data.startsWith('fechar_suporte_id_')) { + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.handleFecharSuporteIdCallback(callbackQuery, data); } else if (data.startsWith('linha_')) { console.log(`🔍 Condição linha_ atendida`); await this.handleLinhaDigitavelCallback(callbackQuery, data); @@ -5862,6 +5878,23 @@ Digite /help para ver todos os comandos disponíveis! case 'confirmar': await this.finalizarCriacaoSuporte(sessaoAtiva.id, text); break; + + // Etapas para fechar suporte + case 'fechar_buscar_cliente': + await this.processarEtapaFecharBuscarCliente(sessaoAtiva.id, text); + break; + + case 'fechar_selecionar_cliente': + await this.processarEtapaFecharSelecionarCliente(sessaoAtiva.id, text); + break; + + case 'fechar_selecionar_suporte': + await this.processarEtapaFecharSelecionarSuporte(sessaoAtiva.id, text); + break; + + case 'fechar_aguardando_descricao': + await this.processarEtapaFecharDescricao(sessaoAtiva.id, text); + break; default: await this.bot.sendMessage(chatId, '❌ Etapa desconhecida. Reiniciando processo...'); @@ -6500,6 +6533,251 @@ Digite /help para ver todos os comandos disponíveis! this.suporteSessions.delete(sessionId); } + // ───────────────────────────────────────────────────────────────────────── + // Fechar Suporte + // ───────────────────────────────────────────────────────────────────────── + + /** + * Iniciar processo de fechamento de suporte + */ + async iniciarProcessoFecharSuporte(chatId, userId, target = '') { + if (!this.topsapp.isReady()) { + await this.bot.sendMessage(chatId, + `❌ **TopSapp não configurado**\n\n` + + `⚠️ O sistema TopSapp não está configurado. Entre em contato com o administrador.` + ); + return; + } + + const sessionId = `${userId}_${chatId}_${Date.now()}`; + const suporteSession = { + id: sessionId, + chatId, + userId, + etapa: 'fechar_buscar_cliente', + dados: {}, + iniciado: new Date() + }; + + this.suporteSessions.set(sessionId, suporteSession); + + await this.bot.sendMessage(chatId, + `🔒 **Fechamento de Suporte**\n\n` + + `🔍 Digite o **nome** ou **CPF** do cliente cujo suporte deseja fechar:`, + { parse_mode: 'Markdown' } + ); + + if (target && target.trim()) { + await this.processarEtapaFecharBuscarCliente(sessionId, target.trim()); + } + } + + /** + * Etapa 1 (fechar): Buscar cliente + */ + async processarEtapaFecharBuscarCliente(sessionId, query) { + const session = this.suporteSessions.get(sessionId); + if (!session) return; + + await this.bot.sendMessage(session.chatId, `🔍 Buscando cliente: "${query}"...`); + + try { + const resultado = await this.topsapp.buscarCliente(query); + + if (!resultado.success || !resultado.clientes || resultado.clientes.length === 0) { + await this.bot.sendMessage(session.chatId, + `❌ **Nenhum cliente encontrado**\n\n` + + `🔍 Não foi encontrado nenhum cliente com: "${query}"\n\n` + + `Por favor, tente novamente com outro nome ou CPF:` + ); + return; + } + + session.dados.clientesEncontrados = resultado.clientes; + + if (resultado.clientes.length === 1) { + session.dados.clienteSelecionado = resultado.clientes[0]; + session.dados.location = resultado.instancia || ''; + await this.processarEtapaFecharBuscarSuportes(sessionId); + } else { + session.etapa = 'fechar_selecionar_cliente'; + let message = `👥 **Clientes encontrados:**\n\n`; + const buttons = []; + resultado.clientes.forEach((cliente, index) => { + message += `**${index + 1}.** ${cliente.nome} — CPF: ${cliente.cpf || 'N/A'}\n`; + buttons.push([{ text: `${index + 1}. ${cliente.nome}`, callback_data: `fechar_suporte_cli_${sessionId}_${index}` }]); + }); + message += `\nDigite o **número** do cliente:`; + await this.bot.sendMessage(session.chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: buttons } + }); + } + } catch (error) { + await this.bot.sendMessage(session.chatId, `❌ **Erro na busca**\n\n⚠️ ${error.message}\n\nTente novamente:`); + } + } + + /** + * Etapa 2 (fechar): Selecionar cliente quando há múltiplos + */ + async processarEtapaFecharSelecionarCliente(sessionId, input) { + const session = this.suporteSessions.get(sessionId); + if (!session) return; + + const numero = parseInt(input.trim()); + if (isNaN(numero) || numero < 1 || numero > session.dados.clientesEncontrados.length) { + await this.bot.sendMessage(session.chatId, + `❌ Número inválido. Digite entre 1 e ${session.dados.clientesEncontrados.length}:` + ); + return; + } + + session.dados.clienteSelecionado = session.dados.clientesEncontrados[numero - 1]; + session.dados.location = ''; + await this.processarEtapaFecharBuscarSuportes(sessionId); + } + + /** + * Buscar suportes abertos do cliente selecionado + */ + async processarEtapaFecharBuscarSuportes(sessionId) { + const session = this.suporteSessions.get(sessionId); + if (!session) return; + + const cliente = session.dados.clienteSelecionado; + await this.bot.sendMessage(session.chatId, `🔍 Buscando suportes abertos de **${cliente.nome}**...`, { parse_mode: 'Markdown' }); + + try { + const resultado = await this.topsapp.obterSuportesAbertos(cliente.id, session.dados.location || ''); + + const suportes = resultado.success ? (resultado.data?.dados || []) : []; + + if (suportes.length === 0) { + await this.bot.sendMessage(session.chatId, + `ℹ️ **Nenhum suporte aberto**\n\n` + + `O cliente **${cliente.nome}** não possui suportes abertos no momento.`, + { parse_mode: 'Markdown' } + ); + this.suporteSessions.delete(sessionId); + return; + } + + session.dados.suportesAbertos = suportes; + + if (suportes.length === 1) { + session.dados.suporteSelecionado = suportes[0]; + session.etapa = 'fechar_aguardando_descricao'; + const s = suportes[0]; + const enderecoCompleto = [s.endereco, s.numero, s.bairro, s.cidade].filter(Boolean).join(', '); + await this.bot.sendMessage(session.chatId, + `🎫 **Suporte encontrado:**\n\n` + + `🆔 ID: ${s.id}\n` + + `📝 Problema: ${s.defeito || 'N/A'}\n` + + `📍 Endereço: ${enderecoCompleto || 'N/A'}\n` + + `📅 Data: ${s.data || 'N/A'}\n\n` + + `✏️ Digite a **descrição do fechamento** do suporte:`, + { parse_mode: 'Markdown' } + ); + } else { + session.etapa = 'fechar_selecionar_suporte'; + let message = `🎫 **Suportes abertos de ${cliente.nome}:**\n\n`; + const buttons = []; + suportes.forEach((s, index) => { + const enderecoCompleto = [s.endereco, s.numero, s.bairro].filter(Boolean).join(', '); + message += `**${index + 1}.** ID #${s.id}\n`; + message += ` 📝 ${s.defeito || 'Sem descrição'}\n`; + if (enderecoCompleto) message += ` 📍 ${enderecoCompleto}\n`; + message += ` 📅 ${s.data || ''}\n\n`; + buttons.push([{ text: `${index + 1}. #${s.id} — ${(s.defeito || 'Sem descrição').substring(0, 40)}`, callback_data: `fechar_suporte_id_${sessionId}_${index}` }]); + }); + message += `Selecione qual suporte deseja fechar:`; + await this.bot.sendMessage(session.chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: buttons } + }); + } + } catch (error) { + await this.bot.sendMessage(session.chatId, `❌ **Erro ao buscar suportes**\n\n⚠️ ${error.message}`); + this.suporteSessions.delete(sessionId); + } + } + + /** + * Etapa 3 (fechar): Selecionar suporte quando há múltiplos + */ + async processarEtapaFecharSelecionarSuporte(sessionId, input) { + const session = this.suporteSessions.get(sessionId); + if (!session) return; + + const numero = parseInt(input.trim()); + if (isNaN(numero) || numero < 1 || numero > session.dados.suportesAbertos.length) { + await this.bot.sendMessage(session.chatId, + `❌ Número inválido. Digite entre 1 e ${session.dados.suportesAbertos.length}:` + ); + return; + } + + session.dados.suporteSelecionado = session.dados.suportesAbertos[numero - 1]; + session.etapa = 'fechar_aguardando_descricao'; + + const s = session.dados.suporteSelecionado; + const enderecoCompleto = [s.endereco, s.numero, s.bairro, s.cidade].filter(Boolean).join(', '); + await this.bot.sendMessage(session.chatId, + `🎫 **Suporte selecionado:**\n\n` + + `🆔 ID: ${s.id}\n` + + `📝 Problema: ${s.defeito || 'N/A'}\n` + + `📍 Endereço: ${enderecoCompleto || 'N/A'}\n\n` + + `✏️ Digite a **descrição do fechamento** do suporte:`, + { parse_mode: 'Markdown' } + ); + } + + /** + * Etapa 4 (fechar): Receber descrição e executar fechamento + */ + async processarEtapaFecharDescricao(sessionId, descricao) { + const session = this.suporteSessions.get(sessionId); + if (!session) return; + + if (!descricao || descricao.trim().length < 3) { + await this.bot.sendMessage(session.chatId, + `❌ A descrição é obrigatória e deve ter ao menos 3 caracteres.\n\nDigite a descrição do fechamento:` + ); + return; + } + + const s = session.dados.suporteSelecionado; + const cliente = session.dados.clienteSelecionado; + + await this.bot.sendMessage(session.chatId, `⏳ Finalizando suporte #${s.id}...`); + + try { + const resultado = await this.topsapp.finalizarSuporte(s.id, descricao.trim(), session.dados.location || ''); + + if (resultado.success) { + await this.bot.sendMessage(session.chatId, + `✅ **Suporte finalizado com sucesso!**\n\n` + + `🎫 ID do Suporte: #${s.id}\n` + + `👤 Cliente: ${cliente.nome}\n` + + `📝 Problema: ${s.defeito || 'N/A'}\n` + + `✏️ Fechamento: ${descricao.trim()}`, + { parse_mode: 'Markdown' } + ); + logger.info('Suporte finalizado via bot', { idSuporte: s.id, cliente: cliente.nome, usuario: session.userId }); + } else { + await this.bot.sendMessage(session.chatId, + `❌ **Erro ao finalizar suporte**\n\n⚠️ ${resultado.error}\n\nTente novamente mais tarde.` + ); + } + } catch (error) { + logger.error('Erro ao finalizar suporte:', error); + await this.bot.sendMessage(session.chatId, `❌ **Erro ao finalizar suporte**\n\n⚠️ ${error.message}`); + } + + this.suporteSessions.delete(sessionId); + } + // ───────────────────────────────────────────────────────────────────────── // TopSapp — Vinculação de ONU após adição // ───────────────────────────────────────────────────────────────────────── @@ -6957,6 +7235,78 @@ Digite /help para ver todos os comandos disponíveis! await this.processarEtapaSelecionarCategoria(sessaoAtiva.id); } + /** + * Callback: seleção de cliente no fluxo de fechar suporte + * formato: fechar_suporte_cli__ + */ + async handleFecharSuporteClienteCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const userId = callbackQuery.from.id; + + // fechar_suporte_cli__ + const withoutPrefix = data.replace('fechar_suporte_cli_', ''); + const lastUnder = withoutPrefix.lastIndexOf('_'); + const sessionId = withoutPrefix.substring(0, lastUnder); + const index = parseInt(withoutPrefix.substring(lastUnder + 1)); + + const session = this.suporteSessions.get(sessionId); + if (!session || session.chatId !== chatId || session.userId !== userId) { + await this.bot.sendMessage(chatId, `❌ Sessão expirada. Use "fechar suporte" para iniciar novamente.`); + return; + } + + const clientes = session.dados.clientesEncontrados; + if (!clientes || index < 0 || index >= clientes.length) { + await this.bot.sendMessage(chatId, `❌ Seleção inválida. Tente novamente.`); + return; + } + + session.dados.clienteSelecionado = clientes[index]; + session.dados.location = ''; + await this.processarEtapaFecharBuscarSuportes(sessionId); + } + + /** + * Callback: seleção de suporte no fluxo de fechar suporte + * formato: fechar_suporte_id__ + */ + async handleFecharSuporteIdCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const userId = callbackQuery.from.id; + + // fechar_suporte_id__ + const withoutPrefix = data.replace('fechar_suporte_id_', ''); + const lastUnder = withoutPrefix.lastIndexOf('_'); + const sessionId = withoutPrefix.substring(0, lastUnder); + const index = parseInt(withoutPrefix.substring(lastUnder + 1)); + + const session = this.suporteSessions.get(sessionId); + if (!session || session.chatId !== chatId || session.userId !== userId) { + await this.bot.sendMessage(chatId, `❌ Sessão expirada. Use "fechar suporte" para iniciar novamente.`); + return; + } + + const suportes = session.dados.suportesAbertos; + if (!suportes || index < 0 || index >= suportes.length) { + await this.bot.sendMessage(chatId, `❌ Seleção inválida. Tente novamente.`); + return; + } + + session.dados.suporteSelecionado = suportes[index]; + session.etapa = 'fechar_aguardando_descricao'; + + const s = session.dados.suporteSelecionado; + const enderecoCompleto = [s.endereco, s.numero, s.bairro, s.cidade].filter(Boolean).join(', '); + await this.bot.sendMessage(session.chatId, + `🎫 **Suporte selecionado:**\n\n` + + `🆔 ID: ${s.id}\n` + + `📝 Problema: ${s.defeito || 'N/A'}\n` + + `📍 Endereço: ${enderecoCompleto || 'N/A'}\n\n` + + `✏️ Digite a **descrição do fechamento** do suporte:`, + { parse_mode: 'Markdown' } + ); + } + /** * Exibe informações do roteador/CPE via GenieACS. * Uso: "ver roteador " ou "ver roteador " diff --git a/src/nlp/processor.js b/src/nlp/processor.js index db1b55a..9106c6e 100644 --- a/src/nlp/processor.js +++ b/src/nlp/processor.js @@ -32,6 +32,7 @@ class NLPProcessor { 'dados_cliente': ['dados cliente', 'dados cadastrais', 'cadastro cliente', 'informações cliente', 'info cliente', 'perfil cliente', 'ficha cliente', 'registro cliente', 'ver dados'], 'listar_servicos': ['listar serviços', 'servicos cliente', 'planos cliente', 'contratos cliente', 'ver servicos', 'plano cliente', 'assinaturas', 'planos', 'serviços ativos', 'ver plano'], 'abrir_suporte': ['abrir suporte', 'criar suporte', 'adicionar suporte', 'novo suporte', 'suporte', 'chamado', 'ticket', 'atendimento', 'suporte tecnico', 'atendimento tecnico', 'abrir chamado', 'registrar chamado', 'criar chamado', 'visita tecnica', 'agendar visita', 'reclamação', 'reclamacao'], + 'fechar_suporte': ['fechar suporte', 'finalizar suporte', 'encerrar suporte', 'concluir suporte', 'fechar chamado', 'finalizar chamado', 'encerrar chamado', 'concluir chamado', 'fechar ticket', 'finalizar ticket', 'encerrar ticket', 'concluir atendimento', 'fechar atendimento', 'encerrar atendimento'], 'consultar_usuario': ['consultar usuario', 'consultar user', 'ver usuario', 'status usuario', 'status user', 'info usuario', 'info user', 'checar usuario', 'verificar usuario', 'pesquisar usuario', 'localizar usuario', 'buscar usuario', 'usuario conectado', 'user conectado', 'usuario online', 'user online', 'usuario ativo', 'user ativo'], 'diagnosticar': ['diagnosticar', 'diagnostico', 'diagnose', 'diagnosticar cliente', 'diagnostico cliente', 'checar cliente completo', 'verificar cliente completo', 'relatorio cliente', 'relatorio completo', 'status completo cliente', 'analisar cliente', 'inspecionar cliente', 'situacao do cliente', 'situação do cliente'], 'ver_roteador': ['ver roteador', 'info roteador', 'status roteador', 'roteador', 'cpe', 'ver cpe', 'info cpe', 'dados roteador', 'informações roteador', 'informacoes roteador', 'genieacs', 'ver device', 'info device', 'dispositivo cpe'] diff --git a/src/topsapp/api.js b/src/topsapp/api.js index 0fbe839..1c30067 100644 --- a/src/topsapp/api.js +++ b/src/topsapp/api.js @@ -840,6 +840,36 @@ class TopSappAPI { return result; } + /** + * Obter suportes abertos de um cliente + */ + async obterSuportesAbertos(idCliente, location = '', credentials) { + const instance = this.getInstanceByLocation(location); + + const session = await this.getValidSession(instance, credentials); + + return await this.makeRequest('ObterSuporte', { + idUsuario: session.idUsuario || 1, + idCliente: parseInt(idCliente, 10), + status: 0 // 0 = abertos + }, instance, credentials); + } + + /** + * Finalizar suporte + */ + async finalizarSuporte(idSuporte, descricao, location = '', credentials) { + const instance = this.getInstanceByLocation(location); + + const session = await this.getValidSession(instance, credentials); + + return await this.makeRequest('FinalizarSuporte', { + idUsuario: session.idUsuario || 1, + idSuporte: parseInt(idSuporte, 10), + descricao + }, instance, credentials); + } + /** * Obter lista de OLTs cadastradas no TopSapp */ diff --git a/src/topsapp/manager.js b/src/topsapp/manager.js index d0ca856..c05a753 100644 --- a/src/topsapp/manager.js +++ b/src/topsapp/manager.js @@ -1197,6 +1197,24 @@ class TopSappManager { }; } } + /** + * Obter suportes abertos de um cliente + */ + async obterSuportesAbertos(idCliente, location = '') { + if (!this.isReady()) return { success: false, error: 'TopSapp não configurado' }; + const credentials = this.config.getCredentialsByLocation(location); + return await this.api.obterSuportesAbertos(idCliente, location, credentials); + } + + /** + * Finalizar suporte + */ + async finalizarSuporte(idSuporte, descricao, location = '') { + if (!this.isReady()) return { success: false, error: 'TopSapp não configurado' }; + const credentials = this.config.getCredentialsByLocation(location); + return await this.api.finalizarSuporte(idSuporte, descricao, location, credentials); + } + /** * Obter lista de OLTs cadastradas no TopSapp */