From ef6f94067b2af458554fd9afaafa4df01dfdb80d Mon Sep 17 00:00:00 2001 From: Rodrigo Martins Date: Sun, 5 Apr 2026 09:30:06 -0300 Subject: [PATCH] ipoe: fluxo adicionado Signed-off-by: Rodrigo Martins --- src/bot/handler.js | 16935 +++++++++++++++++---------------- src/fiberhome/manager_tl1.js | 72 + src/ssh/manager.js | 190 + src/topsapp/api.js | 23 +- src/topsapp/manager.js | 28 +- 5 files changed, 8791 insertions(+), 8457 deletions(-) diff --git a/src/bot/handler.js b/src/bot/handler.js index 2d2a31a..a315507 100644 --- a/src/bot/handler.js +++ b/src/bot/handler.js @@ -1,8422 +1,8513 @@ -const NLPProcessor = require('../nlp/processor'); -const SSHManager = require('../ssh/manager'); -const { TopSappManager } = require('../topsapp'); -const FiberhomeManager = require('../fiberhome/manager'); -const FiberhomeTL1Manager = require('../fiberhome/manager_tl1'); -const { GenieACSManager } = require('../genieacs'); -const config = require('../config/config'); -const logger = require('../utils/logger'); -const DateProcessor = require('../utils/dateProcessor'); - -class BotHandler { - constructor(bot) { - this.bot = bot; - this.nlp = new NLPProcessor(); - this.ssh = new SSHManager(); - this.topsapp = new TopSappManager(); - this.fiberhome = new FiberhomeManager(); - this.fiberhomeTL1 = new FiberhomeTL1Manager(); - this.genieacs = new GenieACSManager(); - this.dateProcessor = new DateProcessor(); - this.userSessions = new Map(); // Armazena sessões de usuário - this.suporteSessions = new Map(); // Armazena sessões de suporte em andamento - - logger.info('BotHandler inicializado'); - console.log('🎯 BotHandler inicializado'); - } - - /** - * Verifica se o usuário tem permissão - */ - isAuthorized(userId) { - if (config.bot.allowedUsers.length === 0) { - return true; // Se não há lista de usuários, permite todos - } - return config.bot.allowedUsers.includes(userId); - } - - /** - * Processa mensagens recebidas - */ - async handleMessage(msg) { - const chatId = msg.chat.id; - const userId = msg.from.id; - const text = msg.text; - - // Log da mensagem - if (config.bot.debug) { - console.log(`📨 Mensagem de ${msg.from.first_name} (${userId}): ${text}`); - } - - // Verificar autorização - if (!this.isAuthorized(userId)) { - await this.bot.sendMessage(chatId, - `🚫 **Acesso Negado**\n\n` + - `❌ Você não tem permissão para usar este bot.\n` + - `🆔 Seu ID: \`${userId}\`\n\n` + - `📞 Entre em contato com o administrador para solicitar acesso.`, - { parse_mode: 'Markdown' } - ); - console.log(`🚫 Acesso negado para usuário ${msg.from.first_name} (${userId})`); - return; - } - - try { - // Verificar se há uma sessão de suporte ativa - const processouSuporte = await this.processarMensagemSuporte(msg); - if (processouSuporte) { - return; // Mensagem foi processada pelo fluxo de suporte - } - - // Verificar se o usuário está em uma sessão esperando input - const userSession = this.userSessions.get(userId); - if (userSession) { - await this.handleUserSessionInput(chatId, text, userId, userSession); - return; - } - - // Processar a mensagem com NLP - const intent = this.nlp.processMessage(text); - - if (config.bot.debug) { - console.log('🧠 Intent detectado:', intent); - } - - // Executar ação baseada na intenção - await this.executeAction(chatId, intent, msg.from); - - } catch (error) { - console.error('❌ Erro ao processar mensagem:', error); - await this.bot.sendMessage(chatId, '❌ Ocorreu um erro ao processar sua mensagem. Tente novamente.'); - } - } - - /** - * Executa ação baseada na intenção detectada - */ - async executeAction(chatId, intent, user) { - const { action, target, confidence, type } = intent; - const userId = user.id; // Extrair userId do objeto user - - // Se a confiança é muito baixa, pedir esclarecimento - if (confidence < 0.3 && action) { - const suggestions = this.nlp.suggestCorrection(action); - let message = '🤔 Não entendi muito bem. '; - - if (suggestions.length > 0) { - message += `Você quis dizer: ${suggestions.join(', ')}?`; - } else { - message += 'Digite /help para ver os comandos disponíveis.'; - } - - await this.bot.sendMessage(chatId, message); - return; - } - - // Executar ação específica - try { - switch (action) { - case 'start': - await this.handleStart(chatId, user); - break; - - case 'help': - await this.handleHelp(chatId); - break; - - case 'ssh': - await this.handleSSH(chatId, target); - break; - - case 'status': - await this.handleStatus(chatId, target); - break; - - case 'lista': - case 'equipamentos': - await this.handleListEquipments(chatId); - break; - - case 'listar_usuarios': - await this.handleListUsers(chatId, target); - break; - - case 'consultar_usuario': - console.log(`🔍 Executando handleConsultarUsuario com target: ${target}`); - await this.handleConsultarUsuario(chatId, target); - break; - - case 'admin': - case 'administracao': - await this.handleAdmin(chatId); - break; - - case 'logs': - await this.handleViewLogs(chatId); - break; - - case 'limpar_logs': - await this.handleClearLogs(chatId); - break; - - case 'usuarios_autorizados': - await this.handleListAuthorizedUsers(chatId); - break; - - case 'meu_id': - case 'meu_user_id': - await this.handleMyId(chatId, userId); - break; - - case 'ver_sinal': - console.log(`🔍 Executando handleVerSinal com target: ${target}`); - await this.handleVerSinal(chatId, target); - break; - - case 'remover_onu': - console.log(`🗑️ Executando handleRemoverOnu com target: ${target}`); - await this.handleRemoverOnu(chatId, target); - break; - - case 'adicionar_onu': - console.log(`➕ Executando handleAdicionarOnu com target: ${target}`); - await this.handleAdicionarOnu(chatId, target); - break; - - case 'adicionar_onu_vlan': - console.log(`➕ Executando handleAdicionarOnuVlan com target: ${target}, vlanId: ${intent.vlanId}`); - await this.handleAdicionarOnuVlan(chatId, target, intent.vlanId); - break; - - case 'adicionar_onu_fiberhome': - console.log(`➕ Executando handleAdicionarOnuFiberhome com target: ${target}`); - await this.handleAdicionarOnuFiberhome(chatId, target, userId); - break; - - case 'remover_onu_fiberhome': - console.log(`🗑️ Executando handleRemoverOnuFiberhome com target: ${target}`); - await this.handleRemoverOnuFiberhome(chatId, target, userId); - break; - - case 'buscar_cliente': - console.log(`🔍 Executando handleBuscarCliente com target: ${target}`); - await this.handleBuscarCliente(chatId, target); - break; - - case 'ver_faturas': - console.log(`📄 Executando handleVerFaturas com target: ${target}`); - await this.handleVerFaturas(chatId, target); - break; - - case 'gerar_boleto': - console.log(`💸 Executando handleGerarBoleto com target: ${target}`); - await this.handleGerarBoleto(chatId, target); - break; - - case 'bloquear_cliente': - console.log(`🔒 Executando handleBloquearCliente com target: ${target}`); - await this.handleBloquearCliente(chatId, target); - break; - - case 'liberar_cliente': - console.log(`🔓 Executando handleLiberarCliente com target: ${target}`); - await this.handleLiberarCliente(chatId, target); - break; - - case 'listar_servicos': - console.log(`📋 Executando handleListarServicos com target: ${target}`); - await this.handleListarServicos(chatId, target); - break; - - case 'diagnosticar': - console.log(`🩺 Executando handleDiagnosticarCliente com target: ${target}`); - await this.handleDiagnosticarCliente(chatId, target); - break; - - case 'dados_cliente': - case 'dados_cadastrais': - console.log(`📋 Executando handleDadosClientePorBusca com target: ${target}`); - await this.handleDadosClientePorBusca(chatId, target); - break; - - case 'status_topsapp': - console.log(`🏢 Executando handleStatusTopSapp`); - await this.handleStatusTopSapp(chatId); - break; - - case 'status_onu': - console.log(`📡 Executando handleStatusONU com target: ${target}`); - await this.handleStatusONU(chatId, target); - break; - - case 'fiberhome_status': - console.log(`🔌 Executando handleFiberhomeStatus`); - await this.handleFiberhomeStatus(chatId); - break; - - case 'ver_roteador': - console.log(`📡 Executando handleVerRoteador com target: ${target}`); - await this.handleVerRoteador(chatId, target); - break; - - case 'configurar_roteador': - console.log(`⚙️ Executando handleConfigurarRoteador com target: ${target}`); - await this.handleConfigurarRoteador(chatId, userId, target); - break; - - case 'abrir_suporte': - case 'adicionar_suporte': - case 'criar_suporte': - 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 'mudar_ipoe': - console.log(`🔗 Executando handleMudarIPoE com target: ${target}`); - await this.handleMudarIPoE(chatId, target); - break; - - case 'conectar': - await this.handleConnect(chatId, target); - break; - - case 'desconectar': - await this.handleDisconnect(chatId, target); - break; - - case 'desconectar_usuario': - await this.handleDisconnectUser(chatId, target); - break; - - case 'reiniciar': - await this.handleRestart(chatId, target); - break; - - case 'ping': - await this.handlePing(chatId, target); - break; - - default: - if (!action) { - await this.handleUnknown(chatId, intent.originalText || ''); - } else { - await this.bot.sendMessage(chatId, `❓ Ação '${action}' não implementada ainda.`); - } - } - } catch (actionError) { - console.error(`❌ Erro ao executar ação '${action}':`, actionError); - await this.bot.sendMessage(chatId, `❌ Erro ao executar ação: ${actionError.message}`); - } - } - - /** - * Comando /start - */ - async handleStart(chatId, user) { - const welcomeMessage = ` -🤖 *Bem-vindo ao Bot de Ajuda!* - -Olá ${user.first_name}! Agora você não precisa mandar mensagem pro Rodrigo ou no grupo da ID pra ficar vendo coisas triviais: - -🔐 *Conexões SSH* - Acesso remoto a equipamentos -🌐 *Status de rede* - Verificação de conectividade -⚙️ *Comandos remotos* - Execução de tarefas - -*Exemplos do que você pode dizer:* -• "Conectar no router1" -• "Verificar status do switch1" -• "Reiniciar equipamento" -• "Lista de equipamentos" - -Digite /help para ver todos os comandos disponíveis! - `; - - await this.bot.sendMessage(chatId, welcomeMessage, { parse_mode: 'Markdown' }); - } - - /** - * Comando /help - */ - async handleHelp(chatId) { - const helpMessage = `🤖 **BOT DE AJUDA TÉCNICA** - -🔧 **COMANDOS PRINCIPAIS:** -• /start - Inicia o bot -• /help - Esta ajuda -• /equipamentos - Lista equipamentos - -🏢 **GESTÃO DE CLIENTES:** -• "buscar cliente João Silva" -• "dados cliente 123.456.789-10" -• "ver faturas cliente" -• "abrir suporte" - -📡 **OLT/ONU:** -• "ver sinal DACM91569513" -• "adicionar onu SERIAL" -• "adicionar onu fiberhome SERIAL" -• "remover onu SERIAL" - -🌐 **GENIEACS (CPE/ROTEADOR):** -• "ver roteador SERIAL" -• "configurar roteador SERIAL" -• "configurar roteador João Silva" - -👥 **JUNIPER:** -• "consultar usuario USER" -• "desconectar cliente USER" -• "listar usuários conectados" - -🔐 **SSH:** -• "ssh juniper" -• "acessar concentrador" - -📋 **Para ver comandos detalhados, use:**`; - - const keyboard = [ - [ - { text: '🏢 TopSapp', callback_data: 'help_topsapp' }, - { text: '📡 OLT/ONU', callback_data: 'help_olt' } - ], - [ - { text: '🌐 GenieACS', callback_data: 'help_genieacs' }, - { text: '👥 Juniper', callback_data: 'help_juniper' } - ], - [ - { text: '🔐 SSH', callback_data: 'help_ssh' }, - { text: '🎫 Suporte', callback_data: 'help_suporte' } - ], - [ - { text: '🛡️ Admin', callback_data: 'help_admin' } - ] - ]; - - await this.bot.sendMessage(chatId, helpMessage, { - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: keyboard - } - }); - } - - /** - * Handler para callbacks de help detalhado - */ - async handleHelpDetailCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const helpType = data.replace('help_', ''); - - await this.bot.answerCallbackQuery(callbackQuery.id); - - let helpMessage = ''; - let backButton = { text: '⬅️ Voltar', callback_data: 'help_main' }; - - switch (helpType) { - case 'main': - await this.handleHelp(chatId); - return; - - case 'topsapp': - helpMessage = `🏢 **TOPSAPP - GESTÃO DE CLIENTES** - -**🔍 BUSCAR CLIENTES:** -• "buscar cliente João Silva" -• "buscar cliente 123.456.789-10" -• "buscar cliente Guarai João" - -**📋 DADOS CADASTRAIS:** -• "dados cliente João Silva" -• "dados cadastrais 123.456.789-10" - -**📄 FATURAS:** -• "ver faturas 123.456.789-10" -• "gerar boleto fatura123" - -**⚙️ SERVIÇOS:** -• "listar serviços 123.456.789-10" - -**🔒 BLOQUEIO/LIBERAÇÃO:** -• "bloquear 123.456.789-10" -• "liberar 123.456.789-10" - -**📊 STATUS:** -• "status topsapp"`; - break; - - case 'olt': - helpMessage = `📡 **OLT/ONU - COMANDOS** - -**📶 VERIFICAR SINAL:** -• "ver sinal DACM91569513" -• "verificar sinal ABCD12345678" -• "sinal SERIAL_NUMBER" - -**➕ ADICIONAR ONU:** -• "adicionar onu DACM91569513" -• "cadastrar ABCD12345678" -• "add onu SERIAL_NUMBER" - -**➕ ADICIONAR ONU COM VLAN CUSTOMIZADA:** -• "adicionar onu vlan 603 DACM91569513" -• "add onu vlan 700 ABCD12345678" -• "adicionar vlan 603 SERIAL_NUMBER" - -**➕ ADICIONAR ONU FIBERHOME VIA TL1:** -• "adicionar onu fiberhome FHTT12345678" -• "fiberhome onu FHTT12345678" -• Processo interativo com: - - Nome/alias do usuário - - Nome da OLT - - Porta PON (ex: 7-1) - - Tipo da ONU (AN5506-02-B ou AN5506-01-A) - - VLAN (padrão: 1002) -• Configura automaticamente: - - WAN Services - - Login Web (admin/clicknet10) - - Reinicia a ONU - -**🗑️ REMOVER ONU:** -• "remover onu DACM91569513" -• "deletar ABCD12345678" -• "remover SERIAL_NUMBER" - -**🔄 REINICIAR ONU:** -• Após verificar sinal, use botão "🔌 Reiniciar ONU" - -**⬆️ ATUALIZAR FIRMWARE:** -• Após verificar sinal, use botão "⬆️ Atualizar ONU" - -**🏢 OLTs DISPONÍVEIS:** -• OLT FIBERHOME (131.0.192.9) -• OLT DITO (192.168.190.7) -• OLT COLMEIA (192.168.190.8) -• OLT CANAA (192.168.190.9) -• OLT CANAA 2 (192.168.190.10) -• OLT PEDRO AFONSO 1 (192.168.100.2) -• OLT CANAVIEIRAS (192.168.100.4)`; - break; - - case 'juniper': - helpMessage = `👥 **JUNIPER - GERENCIAR USUÁRIOS** - -**� CONSULTAR USUÁRIO:** -• "consultar usuario USUARIO" -• "ver usuario USUARIO" -• "status usuario USUARIO" - -**�🚫 DESCONECTAR:** -• "desconectar cliente USUARIO" -• "kick usuário USUARIO" -• "remover cliente USUARIO" - -**📊 LISTAR:** -• "listar usuários conectados" - -**🔧 STATUS:** -• "verificar concentrador" -• "status do juniper" - -**🌐 PING:** -• "ping 131.0.192.254" - -**🔄 REINICIAR:** -• "reiniciar juniper"`; - break; - - case 'ssh': - helpMessage = `🔐 **SSH E CONEXÕES** - -**📡 CONECTAR:** -• /ssh -• "ssh juniper" -• "acessar concentrador" - -**📋 LISTAR:** -• /equipamentos - -**🔧 EXEMPLOS:** -• "conectar no concentrador" -• "acessar juniper" -• "ssh olt"`; - break; - - case 'suporte': - helpMessage = `🎫 **SISTEMA DE SUPORTE** - -**🆕 ABRIR SUPORTE:** -• "abrir suporte" -• "criar suporte para João Silva" -• "novo chamado" -• "suporte cliente 123.456.789-10" - -**� 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 -4️⃣ Seleção do técnico responsável -5️⃣ Agendamento de data/hora -6️⃣ Descrição detalhada -7️⃣ Confirmação e protocolo - -**📅 EXEMPLOS DE DATA/HORA:** -• "amanhã às 14h" -• "sexta-feira 09:00" -• "15/08 às 16h30" -• "hoje 18h" -• "em 3 dias às 10h" -• "segunda-feira 08:30"`; - break; - - case 'genieacs': - helpMessage = `🌐 **GENIEACS - CPE / ROTEADOR** - -**🔍 VER ROTEADOR:** -• "ver roteador SERIAL" -• "ver roteador TAG" -• Exibe: SSID, senha, canais, clientes Wi-Fi, PPPoE, velocidade, IP LAN, DHCP - -**⚙️ CONFIGURAR ROTEADOR:** -• "configurar roteador SERIAL" -• "configurar roteador TAG" -• "configurar roteador João Silva" (busca no TopSapp) -• "configurar roteador 123.456.789-10" (CPF) -• Abre menu interativo para alterar: - - SSID e senha (2.4 GHz e 5 GHz) - - Canal (fixo ou automático) - - DNS WAN e DNS LAN - - DHCP início/fim - - IP LAN e máscara - -**📊 DIAGNÓSTICO:** -• Disponível durante o diagnóstico de serviço -• Exibe: taxa de download/upload, tráfego total, uptime PPPoE, estado do link físico - -**📌 OBSERVAÇÕES:** -• Alterações com CPE online: ✅ aplicadas imediatamente -• Alterações com CPE offline: ⏳ aguardam próximo contato do equipamento`; - break; - - case 'admin': - helpMessage = `🛡️ **COMANDOS ADMINISTRATIVOS** - -**👥 USUÁRIOS:** -• "admin" -• "usuarios autorizados" -• "meu id" - -**📊 STATUS:** -• "fiberhome status" -• "status topsapp" - -**🔧 FIBERHOME ESPECÍFICOS:** -• "status onu FHTT12345678" -• "verificar onu FHTT12345678" -• "onu status DeviceName" - -**🔒 SEGURANÇA:** -• Bot restrito a usuários autorizados -• Para solicitar acesso, forneça seu ID`; - break; - - default: - helpMessage = '❌ Seção de ajuda não encontrada.'; - } - - await this.bot.sendMessage(chatId, helpMessage, { - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: [[backButton]] - } - }); - } - - /** - * Listar equipamentos - */ - async handleListEquipments(chatId) { - const equipments = this.ssh.listEquipments(); - - if (equipments.length === 0) { - await this.bot.sendMessage(chatId, '📋 Nenhum equipamento configurado.'); - return; - } - - let message = '📋 *Equipamentos Disponíveis:*\n\n'; - - equipments.forEach((eq, index) => { - message += `${index + 1}. *${eq.name}*\n`; - message += ` 📍 ${eq.host}:${eq.port}\n`; - message += ` 🔑 ID: \`${eq.id}\`\n\n`; - }); - - message += '💡 *Como usar:* Digite "conectar " ou "ssh "'; - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - } - - /** - * Conexão SSH - */ - async handleSSH(chatId, target) { - if (!target) { - await this.bot.sendMessage(chatId, '❓ Especifique o equipamento. Exemplo: "ssh router1"'); - return; - } - - await this.bot.sendMessage(chatId, `🔄 Conectando em ${target}...`); - - try { - const result = await this.ssh.connect(target); - - if (result.success) { - await this.bot.sendMessage(chatId, `✅ ${result.message}`); - - // Oferecer opções de comandos - const keyboard = { - inline_keyboard: [ - [ - { text: '📊 Status', callback_data: `cmd_status_${result.connectionId}` }, - { text: '🔄 Reiniciar', callback_data: `cmd_reboot_${result.connectionId}` } - ], - [ - { text: '📋 Comando personalizado', callback_data: `cmd_custom_${result.connectionId}` }, - { text: '❌ Desconectar', callback_data: `cmd_disconnect_${result.connectionId}` } - ] - ] - }; - - await this.bot.sendMessage(chatId, '🎛️ O que você gostaria de fazer?', { - reply_markup: keyboard - }); - } else { - await this.bot.sendMessage(chatId, `❌ Falha na conexão: ${result.message}`); - } - } catch (error) { - await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); - } - } - - /** - * Verificar status - */ - async handleStatus(chatId, target) { - if (!target) { - // Mostrar status geral - const connections = this.ssh.getActiveConnections(); - - if (connections.length === 0) { - await this.bot.sendMessage(chatId, '📊 Nenhuma conexão ativa no momento.'); - return; - } - - let message = '📊 *Conexões Ativas:*\n\n'; - connections.forEach((conn, index) => { - message += `${index + 1}. ${conn.equipment} (${conn.host})\n`; - message += ` ⏰ Conectado: ${conn.connectedAt.toLocaleString()}\n\n`; - }); - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - return; - } - - await this.bot.sendMessage(chatId, `🔍 Verificando status de ${target}...`); - - try { - const result = await this.ssh.testConnection(target); - - if (result.success) { - await this.bot.sendMessage(chatId, `✅ ${result.message}`); - } else { - await this.bot.sendMessage(chatId, `❌ ${result.message}`); - } - } catch (error) { - await this.bot.sendMessage(chatId, `❌ Erro ao verificar status: ${error.message}`); - } - } - - /** - * Conectar a um equipamento - */ - async handleConnect(chatId, target) { - await this.handleSSH(chatId, target); - } - - /** - * Desconectar - */ - async handleDisconnect(chatId, target) { - if (!target || target === 'todas' || target === 'all') { - // Desconectar todas - this.ssh.closeAllConnections(); - await this.bot.sendMessage(chatId, '🔌 Todas as conexões foram encerradas.'); - return; - } - - // Implementar lógica para desconectar equipamento específico - await this.bot.sendMessage(chatId, `🔌 Desconectando ${target}...`); - } - - /** - * Reiniciar equipamento - */ - async handleRestart(chatId, target) { - if (!target) { - await this.bot.sendMessage(chatId, '❓ Especifique qual equipamento reiniciar.'); - return; - } - - await this.bot.sendMessage(chatId, `⚠️ Reiniciar ${target}? Esta ação pode causar indisponibilidade.`, { - reply_markup: { - inline_keyboard: [ - [ - { text: '✅ Confirmar', callback_data: `confirm_restart_${target}` }, - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ] - } - }); - } - - /** - * Ping - */ - async handlePing(chatId, target) { - if (!target) { - await this.bot.sendMessage(chatId, '❓ Especifique o IP ou hostname para fazer ping.'); - return; - } - - await this.bot.sendMessage(chatId, `🏓 Fazendo ping para ${target}...`); - - // Implementar ping usando comandos do sistema - // Por enquanto, simular resposta - setTimeout(async () => { - await this.bot.sendMessage(chatId, `🏓 Ping para ${target}: 12ms - Conexão OK`); - }, 2000); - } - - /** - * Desconectar usuário específico no concentrador Juniper - */ - async handleDisconnectUser(chatId, username) { - if (!username) { - await this.bot.sendMessage(chatId, '❓ Especifique o usuário a ser desconectado. Exemplo: "desconectar cliente joao123"'); - return; - } - - await this.bot.sendMessage(chatId, `🔄 Desconectando usuário ${username} do concentrador...`); - - try { - // Usar concentrador Juniper como padrão - const result = await this.ssh.disconnectJuniperUser('juniper', username); - - if (result.success) { - await this.bot.sendMessage(chatId, `✅ ${result.message}`); - - if (result.output && result.output.trim()) { - const outputMessage = `📋 *Saída do comando:*\n\`\`\`\n${result.output}\n\`\`\``; - await this.bot.sendMessage(chatId, outputMessage, { parse_mode: 'Markdown' }); - } - } else { - await this.bot.sendMessage(chatId, `❌ ${result.message}`); - - if (result.error && result.error.trim()) { - const errorMessage = `⚠️ *Detalhes do erro:*\n\`\`\`\n${result.error}\n\`\`\``; - await this.bot.sendMessage(chatId, errorMessage, { parse_mode: 'Markdown' }); - } - } - } catch (error) { - console.error('Erro ao desconectar usuário:', error); - await this.bot.sendMessage(chatId, `❌ Erro interno: ${error.message}`); - } - } - - /** - * Listar usuários conectados no concentrador - */ - async handleListUsers(chatId, target = 'juniper') { - await this.bot.sendMessage(chatId, '🔍 Listando usuários conectados no concentrador...'); - - try { - const result = await this.ssh.listJuniperUsers(target); - - if (result.success) { - let message = '📋 *Usuários Conectados:*\n\n'; - - if (result.output && result.output.trim()) { - // Processar saída para extrair informações úteis - const lines = result.output.split('\n'); - let userCount = 0; - - for (const line of lines) { - if (line.trim() && !line.includes('Total') && !line.includes('---')) { - userCount++; - message += `${userCount}. \`${line.trim()}\`\n`; - } - } - - if (userCount === 0) { - message += '🟢 Nenhum usuário conectado no momento.\n'; - } else { - message += `\n👥 Total: ${userCount} usuário(s) conectado(s)`; - } - } else { - message += '🟢 Nenhum usuário conectado no momento.'; - } - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - - // Oferecer opções para desconectar usuários se houver algum conectado - if (result.output && result.output.trim() && !result.output.includes('Total: 0')) { - const keyboard = { - inline_keyboard: [ - [ - { text: '🔄 Atualizar lista', callback_data: 'refresh_users' }, - { text: '❌ Desconectar usuário', callback_data: 'disconnect_user_prompt' } - ] - ] - }; - - await this.bot.sendMessage(chatId, '🎛️ O que você gostaria de fazer?', { - reply_markup: keyboard - }); - } - - } else { - await this.bot.sendMessage(chatId, `❌ ${result.message}`); - } - } catch (error) { - console.error('Erro ao listar usuários:', error); - await this.bot.sendMessage(chatId, `❌ Erro interno: ${error.message}`); - } - } - - /** - * Consultar status de um assinante específico no concentrador Juniper - */ - async handleConsultarUsuario(chatId, username) { - if (!username) { - await this.bot.sendMessage(chatId, '❓ Especifique o login do usuário. Exemplo: "consultar usuario joao@provedor"'); - return; - } - - await this.bot.sendMessage(chatId, `🔍 Consultando usuário *${username}* no concentrador...`, { parse_mode: 'Markdown' }); - - try { - const result = await this.ssh.getJuniperSubscriberInfo('juniper', username); - - if (!result.success) { - await this.bot.sendMessage(chatId, `❌ ${result.message}`); - return; - } - - if (!result.connected) { - await this.bot.sendMessage(chatId, `🔴 *${username}* não está conectado no concentrador.`, { parse_mode: 'Markdown' }); - return; - } - - const message = - `✅ *Usuário conectado!*\n\n` + - `👤 *Login:* \`${result.username}\`\n` + - `🌐 *Endereço IP:* \`${result.ip || 'N/A'}\`\n` + - `🔌 *Interface PPPoE:* \`${result.interface || 'N/A'}\`\n` + - `\n📋 *Linha bruta:*\n\`\`\`\n${result.rawLine}\n\`\`\``; - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - - } catch (error) { - console.error('Erro ao consultar usuário:', error); - await this.bot.sendMessage(chatId, `❌ Erro interno: ${error.message}`); - } - } - - /** - * Diagnóstico completo de um cliente: TopSapp + Juniper PPPoE + ONU - */ - async handleDiagnosticarCliente(chatId, query) { - if (!query) { - await this.bot.sendMessage(chatId, - `🩺 *Diagnóstico de Cliente*\n\n` + - `💡 *Como usar:*\n` + - `• "diagnosticar João da Silva"\n` + - `• "diagnosticar 123.456.789-10"\n\n` + - `O diagnóstico verifica: status no TopSapp, sessão PPPoE no concentrador e status da ONU.`, - { parse_mode: 'Markdown' } - ); - return; - } - - try { - await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); - - let location = ''; - let searchQuery = query; - - if (/guarai|colmeia/i.test(query)) { - location = 'Guarai/Colmeia'; - searchQuery = query.replace(/(guarai|colmeia)/gi, '').trim(); - } else if (/pedro afonso/i.test(query)) { - location = 'Pedro Afonso'; - searchQuery = query.replace(/pedro afonso/gi, '').trim(); - } - - const resultado = await this.topsapp.buscarCliente(searchQuery, location); - - if (!resultado.success) { - await this.bot.sendMessage(chatId, `❌ Erro na busca: ${resultado.error}`); - return; - } - - if (!resultado.clientes || resultado.clientes.length === 0) { - await this.bot.sendMessage(chatId, `🔍 Nenhum cliente encontrado para: "${searchQuery}"`); - return; - } - - const instancia = resultado.instancia || location; - - if (resultado.clientes.length === 1) { - await this._mostrarServicosParaDiagnostico(chatId, resultado.clientes[0].id, instancia); - return; - } - - // Múltiplos clientes → exibir teclado de seleção - let message = `🔍 *${resultado.clientes.length} clientes encontrados*\n\n`; - const keyboard = []; - - resultado.clientes.slice(0, 8).forEach((cliente, index) => { - message += `*${index + 1}.* ${cliente.nome}`; - if (cliente.cpf) message += ` — CPF: \`${cliente.cpf}\``; - message += `\n`; - - keyboard.push([{ - text: `🩺 Diagnosticar: ${cliente.nome.substring(0, 30)}`, - callback_data: `diag_cliente_${cliente.id}_${instancia}` - }]); - }); - - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: { inline_keyboard: keyboard } - }); - - } catch (error) { - console.error('❌ Erro em handleDiagnosticarCliente:', error); - await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); - } - } - - async _mostrarServicosParaDiagnostico(chatId, idCliente, location) { - const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); - - if (!servicosResult.success) { - await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviços: ${servicosResult.error}`); - return; - } - - const servicos = (servicosResult.servicos || []).filter(s => { - const st = (s.id_status || s.statusNumerico); - return st === 1 || st === '1' || String(s.status).toLowerCase() === 'liberado' || String(s.status).toLowerCase() === 'ativo'; - }); - - if (servicos.length === 0) { - await this.bot.sendMessage(chatId, `⚠️ Cliente sem serviços ativos para diagnosticar.`); - return; - } - - if (servicos.length === 1) { - await this.runDiagnosticoServico(chatId, idCliente, servicos[0].id_servico || servicos[0].id, location, servicos[0]); - return; - } - - let message = `🩺 *Selecione o serviço para diagnosticar:*\n\n`; - const keyboard = []; - - servicos.forEach((s, i) => { - const nome = s.descricao || s.nome || `Serviço ${i + 1}`; - const login = s.usuario || s.pppoe_login || s.login || ''; - message += `*${i + 1}.* ${nome}${login ? ` — \`${login}\`` : ''}\n`; - keyboard.push([{ - text: `🔍 ${nome.substring(0, 35)}`, - callback_data: `diag_servico_${idCliente}_${s.id_servico || s.id}_${location}` - }]); - }); - - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: { inline_keyboard: keyboard } - }); - } - - async handleDiagClienteCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.replace('diag_cliente_', '').split('_'); - const idCliente = parts[0]; - const location = parts.slice(1).join('_'); - - await this.bot.answerCallbackQuery(callbackQuery.id); - await this._mostrarServicosParaDiagnostico(chatId, idCliente, location); - } - - async handleDiagServicoCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.replace('diag_servico_', '').split('_'); - const idCliente = parts[0]; - const idServico = parts[1]; - const location = parts.slice(2).join('_'); - - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.runDiagnosticoServico(chatId, idCliente, idServico, location, null); - } - - async runDiagnosticoServico(chatId, idCliente, idServico, location, servicoPreCarregado) { - try { - await this.bot.sendMessage(chatId, '⏳ Executando diagnóstico completo...'); - - // Buscar serviços caso não tenhamos o objeto do serviço - let servico = servicoPreCarregado; - if (!servico) { - const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); - if (!servicosResult.success) { - await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviços: ${servicosResult.error}`); - return; - } - const idNum = parseInt(idServico, 10); - servico = (servicosResult.servicos || []).find(s => - (s.id_servico || s.id) === idNum || String(s.id_servico || s.id) === String(idServico) - ); - } - - if (!servico) { - await this.bot.sendMessage(chatId, `❌ Serviço #${idServico} não encontrado.`); - return; - } - - const pppoeLogin = servico.usuario || servico.pppoe_login || servico.login || null; - const serial = servico.serial || servico.comodato_serial || servico.mac || null; - const statusServico = this.topsapp.getStatusServicoDetalhado - ? this.topsapp.getStatusServicoDetalhado(servico.id_status) - : (servico.status || 'N/A'); - const nomePlano = servico.descricao || servico.nome || 'N/A'; - const endereco = servico.instalacao_endereco || servico.end_rua || null; - const cidade = servico.instalacao_cidade || null; - // Campo base/AP usado para rotear para a OLT correta - const baseInfo = servico.base || (servico.id_ap ? `AP ${servico.id_ap}` : null); - - // Executar consultas em paralelo: Juniper + ONU + GenieACS (por TAG/login) - // No concentrador Juniper usa-se só o usuário (antes do @) - const pppoeUser = pppoeLogin ? pppoeLogin.split('@')[0] : null; - const [juniperResult, onuResult, genieResult] = await Promise.all([ - pppoeUser - ? this.ssh.getJuniperSubscriberInfo('juniper', pppoeUser).catch(e => ({ success: false, error: e.message })) - : Promise.resolve({ success: false, skipped: true }), - serial - ? this._diagnosticarONU(serial, baseInfo).catch(e => ({ success: false, error: e.message })) - : Promise.resolve({ success: false, skipped: true }), - pppoeLogin && this.genieacs.isReady() - ? this.genieacs.buscarRoteador(pppoeLogin).catch(() => ({ success: false })) - : Promise.resolve({ success: false, skipped: true }) - ]); - - // Sincronização TAG GenieACS ↔ PPPoE TopSapp (fire-and-forget, silencioso) - // Garante que o login PPPoE do cliente fica registrado como TAG no GenieACS, - // permitindo buscas futuras por "ver roteador ". - if (serial && pppoeLogin && this.genieacs.isReady()) { - this.genieacs.sincronizarTagPPPoE(serial, pppoeLogin) - .then(r => { - if (r.success && !r.already) { - logger.info(`[GenieACS] TAG "${r.tag}" sincronizada em ${r.deviceId}`); - console.log(`🏷️ [GenieACS] TAG "${r.tag}" adicionada ao device ${r.deviceId}`); - } - }) - .catch(e => logger.warn(`[GenieACS] Falha ao sincronizar TAG: ${e.message}`)); - } - - // Montar relatório - let msg = `🩺 *Diagnóstico do Cliente*\n`; - msg += `━━━━━━━━━━━━━━━━━━━━━\n\n`; - - // Bloco 1: TopSapp - msg += `📋 *Serviço TopSapp*\n`; - msg += ` 📦 Plano: ${nomePlano}\n`; - msg += ` 🔌 Status: ${statusServico}\n`; - if (pppoeLogin) msg += ` 👤 Login PPPoE: \`${pppoeLogin}\`\n`; - if (servico.dhcp_username) msg += ` 👤 DHCP Username: \`${servico.dhcp_username}\`\n`; - if (servico.dhcp_agent_circuit_id) msg += ` 🔗 Agent Circuit ID: \`${servico.dhcp_agent_circuit_id}\`\n`; - if (endereco) msg += ` 📍 Endereço: ${endereco}${cidade ? `, ${cidade}` : ''}\n`; - msg += '\n'; - - // Bloco 2: Concentrador Juniper - msg += `🌐 *Concentrador Juniper*\n`; - if (juniperResult.skipped) { - msg += ` ⚫ Login PPPoE não informado no cadastro\n`; - } else if (!juniperResult.success) { - msg += ` ⚠️ Erro ao consultar: ${juniperResult.error || 'falha na consulta'}\n`; - } else if (juniperResult.connected) { - msg += ` 🟢 *Conectado*\n`; - msg += ` 🌐 IP: \`${juniperResult.ip || 'N/A'}\`\n`; - msg += ` 🔌 Interface: \`${juniperResult.interface || 'N/A'}\`\n`; - } else { - msg += ` 🔴 *Desconectado* (sem sessão PPPoE ativa)\n`; - } - msg += '\n'; - - // Bloco 3: ONU - msg += `📡 *ONU / ONT*\n`; - if (onuResult.skipped) { - msg += ` ⚫ Serial não informado no cadastro\n`; - } else if (!onuResult.success) { - msg += ` ❌ ${onuResult.error || 'ONU não encontrada'}\n`; - if (serial) msg += ` 📋 Serial buscado: \`${serial}\`\n`; - } else { - const rxLabel = onuResult.rxPower || 'N/A'; - const rxNum = parseFloat(rxLabel); - const rxEmoji = isNaN(rxNum) ? '⚫' : rxNum >= -27 ? '🟢' : rxNum >= -30 ? '🟡' : '🔴'; - - msg += ` 📋 Serial: \`${serial}\`\n`; - msg += ` 🏢 OLT: ${onuResult.oltName || onuResult.oltType || 'N/A'}\n`; - if (onuResult.slot || onuResult.pon) { - msg += ` 📍 Slot/PON: ${onuResult.slot || '?'}/${onuResult.pon || '?'}\n`; - } - msg += ` 🔌 Estado: ${onuResult.online ? '🟢 Online' : '🔴 Offline'}\n`; - msg += ` ${rxEmoji} Rx Power: *${rxLabel}*\n`; - - if (onuResult.portaEthernet) { - const ethOk = /forward|up|active|1gbit|100mbit/i.test(onuResult.portaEthernet); - msg += ` ${ethOk ? '🟢' : '🔴'} Porta Ethernet: ${onuResult.portaEthernet}\n`; - } - } - - await this.bot.sendMessage(chatId, msg, { parse_mode: 'Markdown' }); - - // Bloco 4: Roteador/CPE via GenieACS (exibido apenas se encontrado pela TAG) - if (genieResult && genieResult.success) { - const esc = (v) => { - if (v === null || v === undefined || v === '') return 'N/A'; - return String(v).replace(/[_*`\[]/g, '\\$&'); - }; - const d = genieResult.device; - const tagLabel = d.tags && d.tags.length > 0 ? d.tags.map(esc).join(', ') : null; - - let msgCPE = `📡 *Roteador / CPE (GenieACS)*\n`; - msgCPE += ` 🖥️ Nome: ${esc(d.productClass)}\n`; - msgCPE += ` 🏭 Fabricante: ${esc(d.manufacturer)}\n`; - msgCPE += ` 🔧 SW: ${esc(d.softwareVersion)}\n`; - if (tagLabel) msgCPE += ` 🏷️ TAG: \`${tagLabel}\`\n`; - if (d.ssid24) msgCPE += ` 📶 SSID 2.4GHz: ${esc(d.ssid24)} (Canal ${esc(d.channel24)}, Auto: ${esc(d.autoChannel24)})\n`; - if (d.senha24) msgCPE += ` 🔑 Senha 2.4GHz: \`${esc(d.senha24)}\`\n`; - if (d.clients24 !== null && d.clients24 !== undefined) msgCPE += ` 👥 Clientes 2.4GHz: ${esc(d.clients24)}\n`; - if (d.ssid5) msgCPE += ` 📶 SSID 5GHz: ${esc(d.ssid5)} (Canal ${esc(d.channel5)}, Auto: ${esc(d.autoChannel5)})\n`; - if (d.senha5) msgCPE += ` 🔑 Senha 5GHz: \`${esc(d.senha5)}\`\n`; - if (d.clients5 !== null && d.clients5 !== undefined) msgCPE += ` 👥 Clientes 5GHz: ${esc(d.clients5)}\n`; - - // PPPoE WAN - if (d.pppoeStatus || d.pppoeIP) { - msgCPE += `\n 🌐 *PPPoE WAN*\n`; - if (d.pppoeStatus) msgCPE += ` ├ Status: ${esc(d.pppoeStatus)}\n`; - if (d.pppoeIP) msgCPE += ` ├ IP: \`${esc(d.pppoeIP)}\`\n`; - if (d.pppoeUptime) msgCPE += ` ├ Uptime: ${esc(d.pppoeUptime)}\n`; - if (d.gatewayWAN) msgCPE += ` ├ Gateway: ${esc(d.gatewayWAN)}\n`; - if (d.dnsWAN) msgCPE += ` ├ DNS WAN: ${esc(d.dnsWAN)}\n`; - if (d.linkFisicoWAN) msgCPE += ` ├ Link Físico: ${esc(d.linkFisicoWAN)}\n`; - if (d.trafegoRecebido) msgCPE += ` ├ Recebido: ${esc(d.trafegoRecebido)}\n`; - if (d.trafegoEnviado) msgCPE += ` ├ Enviado: ${esc(d.trafegoEnviado)}\n`; - if (d.taxaDownload) msgCPE += ` ├ ↓ Download: ${esc(d.taxaDownload)}\n`; - if (d.taxaUpload) msgCPE += ` ├ ↑ Upload: ${esc(d.taxaUpload)}\n`; - if (d.pppoeLastError && d.pppoeLastError !== 'ERROR_NONE') msgCPE += ` └ Último Erro: ${esc(d.pppoeLastError)}\n`; - } - - // DHCP LAN - if (d.dhcpInicio || d.dhcpFim || d.dnsLAN || d.ipLAN || d.mascaraLAN) { - msgCPE += `\n 🏠 *LAN/DHCP*\n`; - if (d.ipLAN) msgCPE += ` ├ IP LAN: \`${esc(d.ipLAN)}\`\n`; - if (d.mascaraLAN) msgCPE += ` ├ Máscara: ${esc(d.mascaraLAN)}\n`; - if (d.dhcpInicio) msgCPE += ` ├ DHCP Início: ${esc(d.dhcpInicio)}\n`; - if (d.dhcpFim) msgCPE += ` ├ DHCP Fim: ${esc(d.dhcpFim)}\n`; - if (d.dnsLAN) msgCPE += ` └ DNS LAN: ${esc(d.dnsLAN)}\n`; - } - - await this.bot.sendMessage(chatId, msgCPE, { parse_mode: 'Markdown' }); - } - - } catch (error) { - console.error('❌ Erro em runDiagnosticoServico:', error); - await this.bot.sendMessage(chatId, `❌ Erro no diagnóstico: ${error.message}`); - } - } - - /** - * Mapeia o campo base/AP do TopSapp para o ID da OLT Datacom (ou 'fiberhome') - */ - _resolverOLTporBase(baseInfo) { - if (!baseInfo) return null; - const b = String(baseInfo).toUpperCase(); - if (/INTERNET\s+FIBRA\s+OPTICA/i.test(b)) return 'fiberhome'; - if (/COLMEIA/i.test(b)) return 'olt_colmeia'; - if (/DITO/i.test(b)) return 'olt_dito'; - if (/CANAA\s*2/i.test(b)) return 'olt_canaa2'; - if (/CANAA/i.test(b)) return 'olt_canaa'; - return null; - } - - /** - * Diagnóstico de ONU: detecta tipo (Datacom/FiberHome) e retorna dados estruturados - * @param {string} serial - Serial da ONU - * @param {string|null} baseInfo - Campo base/AP do TopSapp para rotear para OLT correta - */ - async _diagnosticarONU(serial, baseInfo) { - const serialUpper = serial.toUpperCase(); - const oltHint = this._resolverOLTporBase(baseInfo); - - // Usar base description para decidir se é FiberHome - if (oltHint === 'fiberhome' || - (!serialUpper.startsWith('DACM') && (serialUpper.startsWith('FHTT') || serialUpper.startsWith('FHGT')))) { - return await this._diagnosticarONUFiberhome(serial); - } - - return await this._diagnosticarONUDatacom(serial, oltHint); - } - - async _diagnosticarONUDatacom(serial, oltIdHint) { - let found = null; - - // 1. Tentar na OLT indicada pelo campo base/AP do cadastro - if (oltIdHint && this.ssh.equipamentos.has(oltIdHint)) { - const r = await this.ssh.findOnuBySerial(oltIdHint, serial).catch(() => null); - if (r && r.success) { - const olt = this.ssh.equipamentos.get(oltIdHint); - found = { ...r, oltId: oltIdHint, oltName: olt.name }; - } - } - - // 2. Fallback: buscar em todas as OLTs Datacom - if (!found) { - const olts = this.ssh.listOLTs(); - if (olts.length === 0) { - return { success: false, error: 'Nenhuma OLT Datacom configurada' }; - } - const searches = olts.map(olt => - this.ssh.findOnuBySerial(olt.id, serial) - .then(r => ({ ...r, oltId: olt.id, oltName: olt.name })) - .catch(() => ({ success: false })) - ); - const results = await Promise.allSettled(searches); - for (const r of results) { - if (r.status === 'fulfilled' && r.value.success) { - found = r.value; - break; - } - } - } - - if (!found) { - return { success: false, error: `ONU ${serial} não encontrada em nenhuma OLT Datacom` }; - } - - const { oltId, oltName, onuInfo } = found; - const { slot, onuId } = onuInfo; - - // Buscar sinal e porta ethernet em paralelo - const [signalResult, ethResult] = await Promise.all([ - this.ssh.checkOnuSignal(oltId, slot, onuId).catch(e => ({ success: false, error: e.message })), - this.ssh.checkOnuEthernetDatacom(oltId, slot, onuId).catch(e => ({ success: false })) - ]); - - // Parsear sinal - const rawSignal = signalResult.output || ''; - const stateMatch = rawSignal.match(/Operational\s+state\s*:\s*(\S+)/i); - const rxMatch = rawSignal.match(/Rx\s+Optical\s+Power\s*\[dBm\]\s*:\s*([-\d.]+)/i); - - const operationalState = stateMatch ? stateMatch[1] : null; - const rxPower = rxMatch ? `${rxMatch[1]} dBm` : null; - const online = operationalState ? /up|active|online/i.test(operationalState) : false; - - // Parsear ethernet - let portaEthernet = null; - if (ethResult.success) { - const ethRaw = ethResult.rawOutput || ''; - const ethMatch = ethRaw.match(/\b(Forwarding|Down|Up|Blocking)\b[^\n]*([\d]+(?:Gbit|Mbit)[^\n]*|N\/A)?/i); - if (ethMatch) { - portaEthernet = ethMatch[0].trim().replace(/\s+/g, ' '); - } - } - - return { - success: true, - type: 'datacom', - oltName, - slot, - pon: slot ? slot.split('/')[2] || slot : null, - onuId, - online, - rxPower, - operationalState, - portaEthernet - }; - } - - async _diagnosticarONUFiberhome(serial) { - const resultado = await this.fiberhome.obterStatusONU(serial); - - if (!resultado.success) { - return { success: false, error: resultado.error || 'ONU não encontrada na FiberHome' }; - } - - // Preferir porta com dados reais de velocidade; cair para primeira disponível - const portas = resultado.portasEthernet || []; - const portaComDados = portas.find(p => p.status !== 'unknown' && p.speed); - const portaAtiva = portas.find(p => p.status === 'active'); - const portaBase = portaComDados || portaAtiva || (portas.length > 0 ? portas[0] : null); - - let portaEthernetStr = null; - if (portaBase && portaBase.status !== 'unknown') { - const statusLabel = portaBase.status === 'active' ? 'up' : 'down'; - portaEthernetStr = portaBase.speed - ? `${statusLabel} — ${portaBase.speed}` - : statusLabel; - } - - // rxPower: retornar null se for 'N/A' para que a UI mostre 'N/A' via fallback - const rxPower = resultado.rxPower && resultado.rxPower !== 'N/A' ? resultado.rxPower : null; - - return { - success: true, - type: 'fiberhome', - oltName: 'FiberHome AN5516', - slot: resultado.slot, - pon: resultado.pon, - onuId: resultado.onuId, - online: resultado.online, - rxPower, - txPower: resultado.txPower !== 'N/A' ? resultado.txPower : null, - portaEthernet: portaEthernetStr - }; - } - - /** - * Verificar sinal de ONU - */ - async handleVerSinal(chatId, serialNumber) { - console.log(`🎯 handleVerSinal chamado - chatId: ${chatId}, serialNumber: ${serialNumber}`); - - if (!serialNumber) { - console.log('❌ Serial number não fornecido'); - await this.bot.sendMessage(chatId, '❓ Especifique o serial number da ONU. Exemplo: "ver sinal DACM91569513"'); - return; - } - - // Obter lista de OLTs disponíveis - const olts = this.ssh.listOLTs(); - console.log(`📋 OLTs Datacom encontradas: ${olts.length}`); - - // Criar menu de seleção de OLT - const keyboard = { - inline_keyboard: [] - }; - - // Adicionar OLT Fiberhome como primeira opção - keyboard.inline_keyboard.push([ - { text: '🔶 OLT FIBERHOME', callback_data: `check_signal_fiberhome_${serialNumber}` } - ]); - - // Adicionar separador visual - if (olts.length > 0) { - keyboard.inline_keyboard.push([ - { text: '➖ OLTs DATACOM ➖', callback_data: 'separator' } - ]); - - // Adicionar botões para cada OLT Datacom (2 por linha) - for (let i = 0; i < olts.length; i += 2) { - const row = []; - - row.push({ - text: olts[i].name, - callback_data: `check_signal_${olts[i].id}_${serialNumber}` - }); - - if (i + 1 < olts.length) { - row.push({ - text: olts[i + 1].name, - callback_data: `check_signal_${olts[i + 1].id}_${serialNumber}` - }); - } - - keyboard.inline_keyboard.push(row); - } - } - - // Adicionar botão de cancelar - keyboard.inline_keyboard.push([ - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ]); - - console.log(`📤 Enviando mensagem com Fiberhome + ${olts.length} OLTs Datacom para seleção`); - await this.bot.sendMessage(chatId, `🔍 *Verificar sinal da ONU*\nSerial: \`${serialNumber}\`\n\nSelecione onde deseja pesquisar:`, { - parse_mode: 'Markdown', - reply_markup: keyboard - }); - console.log(`✅ Mensagem de seleção de OLT enviada com sucesso`); - } - - /** - * Executa verificação de sinal em uma OLT específica - */ - async handleCheckSignalOLT(chatId, oltId, serialNumber) { - const olt = this.ssh.equipamentos.get(oltId); - if (!olt) { - await this.bot.sendMessage(chatId, `❌ OLT ${oltId} não encontrada.`); - return; - } - - await this.bot.sendMessage(chatId, `🔍 Procurando ONU ${serialNumber} na ${olt.name}...`); - - try { - const result = await this.ssh.checkOnuSignalBySerial(oltId, serialNumber); - - if (result.success) { - // Escapar caracteres especiais do Markdown - const oltNameEscaped = olt.name.replace(/_/g, '\\_'); - const serialEscaped = serialNumber.replace(/_/g, '\\_'); - - let message = `📡 *Sinal da ONU ${serialEscaped}*\n\n`; - message += `🏢 *OLT:* ${oltNameEscaped} (${olt.host})\n`; - message += `📍 *Localização:* Slot ${result.slot}, ONU ${result.onuId}\n\n`; - message += `📊 *Informações do Sinal:*\n`; - message += `\`\`\`\n${result.signalOutput}\n\`\`\``; - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - - // Oferecer opções adicionais - const keyboard = { - inline_keyboard: [ - [ - { text: '🔄 Atualizar sinal', callback_data: `check_signal_${oltId}_${serialNumber}` }, - { text: '📋 Ver configuração', callback_data: `show_config_${oltId}_${result.slot}_${result.onuId}` } - ], - [ - { text: '🔌 Reiniciar ONU', callback_data: `reset_onu_${oltId}_${result.slot}_${result.onuId}` }, - { text: '⬆️ Atualizar ONU', callback_data: `update_onu_${oltId}_${result.slot}_${result.onuId}` } - ], - [ - { text: '🔍 Pesquisar em outra OLT', callback_data: `ver_sinal_${serialNumber}` } - ] - ] - }; - - await this.bot.sendMessage(chatId, '🎛️ O que você gostaria de fazer?', { - reply_markup: keyboard - }); - - } else { - await this.bot.sendMessage(chatId, `❌ ${result.message}`); - - // Oferecer pesquisar em outras OLTs - const keyboard = { - inline_keyboard: [ - [ - { text: '🔍 Pesquisar em outra OLT', callback_data: `ver_sinal_${serialNumber}` }, - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ] - }; - - await this.bot.sendMessage(chatId, 'ONU não encontrada nesta OLT. Deseja pesquisar em outra?', { - reply_markup: keyboard - }); - } - } catch (error) { - console.error('Erro ao verificar sinal:', error); - await this.bot.sendMessage(chatId, `❌ Erro interno: ${error.message}`); - } - } - - /** - * Comando para remover ONU - */ - async handleRemoverOnu(chatId, serialNumber) { - if (!serialNumber) { - await this.bot.sendMessage(chatId, '❌ Por favor, informe o serial da ONU que deseja remover.\n\nExemplo: `remover FHTT12345678`', { parse_mode: 'Markdown' }); - return; - } - - // Perguntar em qual OLT buscar - const oltOptions = this.ssh.listOLTs(); - - if (oltOptions.length === 0) { - await this.bot.sendMessage(chatId, '❌ Nenhuma OLT disponível configurada.'); - return; - } - - let keyboard = { - inline_keyboard: oltOptions.map(olt => [ - { text: `🏢 ${olt.name}`, callback_data: `remove_onu_${olt.id}_${serialNumber}` } - ]) - }; - - // Adicionar opção de cancelar - keyboard.inline_keyboard.push([ - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ]); - - // Escapar caracteres especiais do serial - const serialEscaped = serialNumber.replace(/_/g, '\\_'); - - await this.bot.sendMessage(chatId, - `🗑️ *Remover ONU ${serialEscaped}*\n\n` + - '🏢 Selecione a OLT onde a ONU está configurada:', - { - reply_markup: keyboard, - parse_mode: 'Markdown' - } - ); - } - - /** - * Comando para adicionar ONU - */ - async handleAdicionarOnu(chatId, serialNumber) { - if (!serialNumber) { - await this.bot.sendMessage(chatId, '❌ Por favor, informe o serial da ONU que deseja adicionar.\n\nExemplo: `adicionar onu FHTT12345678`', { parse_mode: 'Markdown' }); - return; - } - - // Perguntar em qual OLT adicionar - const oltOptions = this.ssh.listOLTs(); - - if (oltOptions.length === 0) { - await this.bot.sendMessage(chatId, '❌ Nenhuma OLT disponível configurada.'); - return; - } - - let keyboard = { - inline_keyboard: oltOptions.map(olt => [ - { text: `🏢 ${olt.name}`, callback_data: `add_onu_${olt.id}_${serialNumber}` } - ]) - }; - - // Adicionar opção de cancelar - keyboard.inline_keyboard.push([ - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ]); - - // Escapar caracteres especiais - const serialEscaped = serialNumber.replace(/_/g, '\\_'); - - await this.bot.sendMessage(chatId, - `➕ *Adicionar ONU ${serialEscaped}*\n\n` + - '🏢 Selecione a OLT onde deseja adicionar a ONU:', - { - reply_markup: keyboard, - parse_mode: 'Markdown' - } - ); - } - - /** - * Comando para adicionar ONU com VLAN customizada - */ - async handleAdicionarOnuVlan(chatId, serialNumber, vlanId) { - if (!serialNumber) { - await this.bot.sendMessage(chatId, '❌ Por favor, informe o serial da ONU.\n\nExemplo: `adicionar onu vlan 603 FHTT12345678`', { parse_mode: 'Markdown' }); - return; - } - - if (!vlanId || isNaN(vlanId) || vlanId < 1 || vlanId > 4094) { - await this.bot.sendMessage(chatId, '❌ VLAN ID inválida. Deve ser um número entre 1 e 4094.\n\nExemplo: `adicionar onu vlan 603 FHTT12345678`', { parse_mode: 'Markdown' }); - return; - } - - // Perguntar em qual OLT adicionar - const oltOptions = this.ssh.listOLTs(); - - if (oltOptions.length === 0) { - await this.bot.sendMessage(chatId, '❌ Nenhuma OLT disponível configurada.'); - return; - } - - let keyboard = { - inline_keyboard: oltOptions.map(olt => [ - { text: `🏢 ${olt.name}`, callback_data: `add_onu_vlan_${olt.id}_${vlanId}_${serialNumber}` } - ]) - }; - - // Adicionar opção de cancelar - keyboard.inline_keyboard.push([ - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ]); - - // Escapar caracteres especiais - const serialEscaped = serialNumber.replace(/_/g, '\\_'); - - await this.bot.sendMessage(chatId, - `➕ *Adicionar ONU com VLAN Customizada*\n\n` + - `📱 *Serial:* ${serialEscaped}\n` + - `🏷️ *VLAN:* ${vlanId}\n\n` + - '🏢 Selecione a OLT onde deseja adicionar a ONU:', - { - reply_markup: keyboard, - parse_mode: 'Markdown' - } - ); - } - - /** - * Comando para adicionar ONU Fiberhome via TL1 - */ - async handleAdicionarOnuFiberhome(chatId, serialNumber, userId) { - if (!serialNumber) { - await this.bot.sendMessage(chatId, - '❌ Por favor, informe o serial da ONU Fiberhome.\n\n' + - 'Exemplo: `adicionar onu fiberhome FHTT12345678`', - { parse_mode: 'Markdown' } - ); - return; - } - - console.log(`📝 Criando sessão para usuário ${userId}, serial: ${serialNumber}`); - - // Criar sessão para coletar informações - this.userSessions.set(userId, { - action: 'adicionar_onu_fiberhome_step1', - serial: serialNumber, - timestamp: Date.now() - }); - - console.log(`✅ Sessão criada. Sessões ativas:`, this.userSessions.size); - - // Escapar caracteres especiais - const serialEscaped = serialNumber.replace(/_/g, '\\_'); - - // Perguntar o alias/nome do usuário - await this.bot.sendMessage(chatId, - `➕ *Adicionar ONU Fiberhome*\n\n` + - `📱 *Serial:* ${serialEscaped}\n` + - `🔍 A porta PON será descoberta automaticamente\n\n` + - '👤 Digite o *nome/alias* do usuário:', - { parse_mode: 'Markdown' } - ); - } - - /** - * Processar input do usuário durante sessões - */ - async handleUserSessionInput(chatId, text, userId, session) { - try { - console.log(`🔄 Processando input de sessão. Action: ${session.action}, Type: ${session.type}, Text: ${text}`); - - // Verificar se é sessão baseada em action (Fiberhome ONU) - if (session.action) { - // Sessões de configuração de roteador (ação dinâmica com prefixo) - if (session.action.startsWith('router_cfg_wait_')) { - await this._processarInputConfigurarRoteador(chatId, userId, session, text); - return; - } - - switch (session.action) { - case 'adicionar_onu_fiberhome_step1': - // Recebeu o nome do usuário, adicionar sufixo e perguntar pelo tipo da ONU - const usuarioBase = text.trim(); - session.usuario = usuarioBase.includes('@') ? usuarioBase : `${usuarioBase}@clicknetguarai.com.br`; - session.action = 'adicionar_onu_fiberhome_step2'; - this.userSessions.set(userId, session); - - const keyboard = { - inline_keyboard: [ - [{ text: '🔌 AN5506-02-B (2 portas)', callback_data: 'fh_onu_type_AN5506-02-B' }], - [{ text: '🔌 AN5506-01-A (1 porta)', callback_data: 'fh_onu_type_AN5506-01-A' }], - [{ text: '❌ Cancelar', callback_data: 'cancel_action' }] - ] - }; - - await this.bot.sendMessage(chatId, - `👤 **Usuário:** ${session.usuario}\n\n` + - '🔌 Selecione o **tipo da ONU**:', - { reply_markup: keyboard, parse_mode: 'Markdown' } - ); - return; - - case 'adicionar_onu_fiberhome_step3': - // Recebeu confirmação de VLAN customizada - const vlanInput = text.trim(); - if (vlanInput && !isNaN(vlanInput)) { - const vlan = parseInt(vlanInput); - if (vlan >= 1 && vlan <= 4094) { - session.vlan = vlan; - } else { - await this.bot.sendMessage(chatId, '❌ VLAN inválida. Usando VLAN padrão 1002.'); - session.vlan = 1002; - } - } else { - session.vlan = 1002; - } - - // Limpar sessão - this.userSessions.delete(userId); - - // Executar adição da ONU - await this.executarAdicaoOnuFiberhome(chatId, session, userId); - return; - - case 'topsapp_onu_aguardando_nome': - await this.topsappOnuBuscarCliente(chatId, userId, text.trim(), session.onuData); - return; - } - } - - // Verificar se é sessão baseada em type (outros fluxos) - if (session.type) { - switch (session.type) { - case 'waiting_custom_command': - await this.executeCustomCommand(chatId, session.connectionId, text); - this.userSessions.delete(userId); - return; - - case 'waiting_username_disconnect': - await this.handleDisconnectUser(chatId, text.trim()); - this.userSessions.delete(userId); - return; - } - } - - // Estado desconhecido - console.log(`❓ Estado de sessão desconhecido: action=${session.action}, type=${session.type}`); - this.userSessions.delete(userId); - await this.bot.sendMessage(chatId, '❓ Estado de sessão desconhecido. Tente novamente.'); - } catch (error) { - console.error('❌ Erro ao processar input da sessão:', error); - this.userSessions.delete(userId); - await this.bot.sendMessage(chatId, '❌ Erro ao processar sua resposta. Tente novamente.'); - } - } - - /** - * Executar adição da ONU Fiberhome - */ - async executarAdicaoOnuFiberhome(chatId, session, userId = null) { - try { - const { serial, usuario, vlan, onuType } = session; - - // Escapar caracteres especiais - const serialEscaped = serial.replace(/_/g, '\\_'); - - await this.bot.sendMessage(chatId, - `⏳ Adicionando ONU Fiberhome...\n\n` + - `🔍 Descobrindo porta PON automaticamente...\n` + - `👤 *Usuário:* ${usuario}\n` + - `📱 *Serial:* ${serialEscaped}\n` + - `🔌 *Tipo:* ${onuType}\n` + - `🏷️ *VLAN:* ${vlan}`, - { parse_mode: 'Markdown' } - ); - - // Executar comando TL1 via Fiberhome Manager - const resultado = await this.fiberhomeTL1.adicionarOnuFiberhome( - serial, - usuario, - vlan, - onuType - ); - - if (resultado.success) { - // Escapar caracteres especiais do Markdown - const oltEscaped = resultado.details.olt.replace(/_/g, '\\_'); - const serialEscaped = serial.replace(/_/g, '\\_'); - - await this.bot.sendMessage(chatId, - `✅ *ONU Fiberhome Adicionada com Sucesso!*\n\n` + - `👤 *Usuário:* ${usuario}\n` + - `🏢 *OLT:* ${oltEscaped}\n` + - `📡 *Porta descoberta:* Slot ${resultado.details.slot}, PON ${resultado.details.ponPort}\n` + - `📱 *Serial:* ${serialEscaped}\n` + - `🔌 *Tipo:* ${onuType}\n` + - `🏷️ *VLAN:* ${vlan}\n` + - `⚙️ *Portas configuradas:* ${resultado.details.portas}\n\n` + - `🔐 Login Web: \`admin / clicknet10\`\n\n` + - `⚡ A ONU foi reiniciada e estará disponível em alguns instantes.`, - { parse_mode: 'Markdown' } - ); - - // Convidar para registrar no TopSapp - if (userId) { - this.userSessions.set(userId, { - action: 'topsapp_onu_pre_busca', - onuData: { - tipo: 'fiberhome', - location: 'guarai_colmeia', - oltHost: resultado.details.olt, - serial, - portaAlias: `${resultado.details.slot}-${resultado.details.ponPort}` - } - }); - await this.bot.sendMessage(chatId, - `📋 *Registrar no TopSapp?*\n\nDeseja vincular esta ONU a um cliente no sistema TopSapp?`, - { - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: [[ - { text: '✅ Sim, vincular', callback_data: 'topsapp_onu_sim' }, - { text: '❌ Não', callback_data: 'cancel_action' } - ]] - } - } - ); - } - - } else { - await this.bot.sendMessage(chatId, - `❌ *Erro ao adicionar ONU Fiberhome*\n\n` + - `⚠️ ${resultado.error}`, - { parse_mode: 'Markdown' } - ); - } - - } catch (error) { - console.error('❌ Erro ao executar adição de ONU Fiberhome:', error); - await this.bot.sendMessage(chatId, - `❌ *Erro ao adicionar ONU Fiberhome*\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Handler para remover ONU Fiberhome via TL1 - */ - async handleRemoverOnuFiberhome(chatId, serialNumber, userId) { - try { - // Validar serial - if (!serialNumber || serialNumber.length !== 12) { - await this.bot.sendMessage(chatId, - '❌ Serial inválido. O serial deve conter 12 caracteres.\n\n' + - 'Exemplo: `FHTT199cf3c8`' - ); - return; - } - - // Escapar caracteres especiais - const serialEscaped = serialNumber.replace(/_/g, '\\_'); - - // Solicitar confirmação - const keyboard = { - inline_keyboard: [ - [ - { text: '✅ Sim, remover', callback_data: `confirm_remove_fh_${serialNumber}` }, - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ] - }; - - await this.bot.sendMessage(chatId, - `🗑️ *Remover ONU Fiberhome*\n\n` + - `📱 *Serial:* ${serialEscaped}\n\n` + - `⚠️ Esta ação irá remover a ONU e todas as suas configurações.\n\n` + - `Deseja confirmar a remoção?`, - { - reply_markup: keyboard, - parse_mode: 'Markdown' - } - ); - - } catch (error) { - console.error('❌ Erro ao preparar remoção de ONU Fiberhome:', error); - await this.bot.sendMessage(chatId, - `❌ *Erro ao preparar remoção*\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Comando não reconhecido - */ - async handleUnknown(chatId, text) { - const suggestions = this.nlp.suggestCorrection(text); - - let message = '❓ Não entendi o comando. '; - - if (suggestions.length > 0) { - message += `Talvez você quis dizer: ${suggestions.join(', ')}?\n\n`; - } - - message += 'Digite /help para ver todos os comandos disponíveis.'; - - await this.bot.sendMessage(chatId, message); - } - - /** - * Processa callbacks de botões inline - */ - async handleCallbackQuery(callbackQuery) { - const chatId = callbackQuery.message.chat.id; - const data = callbackQuery.data; - const userId = callbackQuery.from.id; - - // Verificar autorização - if (!this.isAuthorized(userId)) { - await this.bot.answerCallbackQuery(callbackQuery.id, '🚫 Acesso negado'); - console.log(`🚫 Callback negado para usuário (${userId})`); - return; - } - - try { - console.log(`🔍 Callback recebido: ${data}`); - console.log(`🔍 Verifica confirm_remove_: ${data.startsWith('confirm_remove_')}`); - - if (data.startsWith('cmd_')) { - console.log(`🔍 Condição cmd_ atendida`); - await this.handleCommandCallback(callbackQuery, data); - } else if (data.startsWith('confirm_remove_fh_')) { - console.log(`🔍 Chamando handleConfirmRemoveFiberhomeCallback`); - try { - await this.handleConfirmRemoveFiberhomeCallback(callbackQuery, data); - } catch (confirmError) { - console.error(`❌ Erro em handleConfirmRemoveFiberhomeCallback:`, confirmError); - await this.bot.answerCallbackQuery(callbackQuery.id, 'Erro na confirmação'); - } - } else if (data.startsWith('confirm_remove_')) { - console.log(`🔍 Chamando handleConfirmRemoveCallback`); - try { - await this.handleConfirmRemoveCallback(callbackQuery, data); - } catch (confirmError) { - console.error(`❌ Erro em handleConfirmRemoveCallback:`, confirmError); - await this.bot.answerCallbackQuery(callbackQuery.id, 'Erro na confirmação'); - } - } else if (data.startsWith('confirm_add_')) { - console.log(`🔍 Chamando handleConfirmAddCallback`); - try { - await this.handleConfirmAddCallback(callbackQuery, data); - } catch (confirmError) { - console.error(`❌ Erro em handleConfirmAddCallback:`, confirmError); - await this.bot.answerCallbackQuery(callbackQuery.id, 'Erro na confirmação'); - } - } else if (data.startsWith('confirm_block_service_')) { - console.log(`🔍 Chamando handleConfirmBlockServiceCallback`); - await this.handleConfirmBlockServiceCallback(callbackQuery, data); - } else if (data.startsWith('confirm_unblock_service_')) { - console.log(`🔍 Chamando handleConfirmUnblockServiceCallback`); - await this.handleConfirmUnblockServiceCallback(callbackQuery, data); - } else if (data.startsWith('confirm_block_all_')) { - console.log(`🔍 Chamando handleConfirmBlockAllCallback`); - await this.handleConfirmBlockAllCallback(callbackQuery, data); - } else if (data.startsWith('confirm_unblock_all_')) { - console.log(`🔍 Chamando handleConfirmUnblockAllCallback`); - await this.handleConfirmUnblockAllCallback(callbackQuery, data); - } else if (data.startsWith('confirm_block_')) { - console.log(`🔍 Chamando handleConfirmBlockCallback`); - await this.handleConfirmBlockCallback(callbackQuery, data); - } else if (data.startsWith('confirm_unblock_')) { - console.log(`🔍 Chamando handleConfirmUnblockCallback`); - await this.handleConfirmUnblockCallback(callbackQuery, data); - } else if (data.startsWith('ipoe_cliente_')) { - console.log(`🔍 Condição ipoe_cliente_ atendida`); - await this.handleIPoEClienteCallback(callbackQuery, data); - } else if (data.startsWith('ipoe_servico_')) { - console.log(`🔍 Condição ipoe_servico_ atendida`); - await this.handleIPoEServicoCallback(callbackQuery, data); - } else if (data.startsWith('confirm_ipoe_')) { - console.log(`🔍 Condição confirm_ipoe_ atendida`); - await this.handleConfirmIPoECallback(callbackQuery, data); - } else if (data.startsWith('block_service_')) { - console.log(`🔍 Chamando handleBlockServiceSelection`); - await this.handleBlockServiceSelection(callbackQuery, data); - } else if (data.startsWith('unblock_service_')) { - console.log(`🔍 Chamando handleUnblockServiceCallback`); - await this.handleUnblockServiceCallback(callbackQuery, data); - } else if (data.startsWith('confirm_')) { - console.log(`🔍 Condição confirm_ atendida`); - await this.handleConfirmCallback(callbackQuery, data); - } else if (data.startsWith('check_signal_')) { - console.log(`🔍 Condição check_signal_ atendida`); - await this.handleCheckSignalCallback(callbackQuery, data); - } else if (data.startsWith('ver_sinal_')) { - console.log(`🔍 Condição ver_sinal_ atendida`); - const serialNumber = data.replace('ver_sinal_', ''); - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.handleVerSinal(chatId, serialNumber); - } else if (data.startsWith('show_config_')) { - console.log(`🔍 Condição show_config_ atendida`); - await this.handleShowConfigCallback(callbackQuery, data); - } else if (data.startsWith('reset_onu_')) { - console.log(`🔍 Condição reset_onu_ atendida`); - await this.handleResetOnuCallback(callbackQuery, data); - } else if (data.startsWith('update_onu_')) { - console.log(`🔍 Condição update_onu_ atendida`); - await this.handleUpdateOnuCallback(callbackQuery, data); - } else if (data.startsWith('firmware_')) { - console.log(`🔍 Condição firmware_ atendida`); - await this.handleFirmwareCallback(callbackQuery, data); - } else if (data.startsWith('remove_onu_')) { - console.log(`🔍 Chamando handleRemoveOnuCallback`); - await this.handleRemoveOnuCallback(callbackQuery, data); - } else if (data.startsWith('remove_readd_vlan_')) { - console.log(`🔍 Chamando handleRemoveAndReAddCallback (vlan)`); - await this.handleRemoveAndReAddCallback(callbackQuery, data); - } else if (data.startsWith('remove_readd_')) { - console.log(`🔍 Chamando handleRemoveAndReAddCallback`); - await this.handleRemoveAndReAddCallback(callbackQuery, data); - } else if (data.startsWith('topsapp_onu_')) { - console.log(`🔍 Condição topsapp_onu_ atendida`); - await this.handleTopSappOnuCallback(callbackQuery, data); - } else if (data.startsWith('add_onu_vlan_')) { - console.log(`🔍 Chamando handleAddOnuVlanCallback`); - await this.handleAddOnuVlanCallback(callbackQuery, data); - } else if (data.startsWith('add_onu_')) { - console.log(`🔍 Chamando handleAddOnuCallback`); - await this.handleAddOnuCallback(callbackQuery, data); - } else if (data.startsWith('fh_onu_type_')) { - console.log(`🔍 Chamando handleFiberhomeOnuTypeCallback`); - await this.handleFiberhomeOnuTypeCallback(callbackQuery, data); - } else if (data.startsWith('fh_vlan_')) { - console.log(`🔍 Chamando handleFiberhomeVlanCallback`); - await this.handleFiberhomeVlanCallback(callbackQuery, data); - } else if (data.startsWith('fiberhome_details_')) { - console.log(`🔍 Condição fiberhome_details_ atendida`); - const serialNumber = data.replace('fiberhome_details_', ''); - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.handleStatusONU(chatId, serialNumber); - } else if (data.startsWith('router_cfg_')) { - console.log(`🔍 Condição router_cfg_ atendida`); - await this.handleRouterCfgCallback(callbackQuery, data); - } else if (data.startsWith('cfg_roteador_cli_')) { - console.log(`🔍 Condição cfg_roteador_cli_ atendida`); - await this.bot.answerCallbackQuery(callbackQuery.id); - const parts = data.replace('cfg_roteador_cli_', '').split('_'); - const idCliente = parts[0]; - const location = parts.slice(1).join('_'); - await this._configurarRoteadorSelecionarServico(chatId, userId, idCliente, location); - } else if (data.startsWith('cfg_roteador_srv_')) { - console.log(`🔍 Condição cfg_roteador_srv_ atendida`); - await this.bot.answerCallbackQuery(callbackQuery.id); - // formato: cfg_roteador_srv___ - const rest = data.replace('cfg_roteador_srv_', ''); - const parts = rest.split('_'); - const idCliente = parts[0]; - const idServico = parts[1]; - const location = parts.slice(2).join('_'); - // Recarregar o serviço para ter o objeto completo com pppoeLogin - const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); - if (!servicosResult.success) { - await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviço: ${servicosResult.error}`); - } else { - const servico = (servicosResult.servicos || []).find(s => - String(s.id_servico || s.id) === String(idServico) - ); - if (!servico) { - await this.bot.sendMessage(chatId, `❌ Serviço #${idServico} não encontrado.`); - } else { - await this._configurarRoteadorPorServico(chatId, userId, servico, location); - } - } - } else if (data.startsWith('diag_cliente_')) { - console.log(`🔍 Condição diag_cliente_ atendida`); - await this.handleDiagClienteCallback(callbackQuery, data); - } else if (data.startsWith('diag_servico_')) { - console.log(`🔍 Condição diag_servico_ atendida`); - await this.handleDiagServicoCallback(callbackQuery, data); - } else if (data.startsWith('dados_cliente_')) { - console.log(`🔍 Condição dados_cliente_ atendida`); - await this.handleDadosClienteCallback(callbackQuery, data); - } else if (data.startsWith('faturas_cliente_')) { - console.log(`🔍 Condição faturas_cliente_ atendida`); - await this.handleFaturasClienteCallback(callbackQuery, data); - } else if (data.startsWith('faturas_servico_')) { - console.log(`🔍 Condição faturas_servico_ atendida`); - await this.handleFaturasServicoCallback(callbackQuery, data); - } else if (data.startsWith('servicos_cliente_')) { - console.log(`🔍 Condição servicos_cliente_ atendida`); - await this.handleServicosClienteCallback(callbackQuery, data); - } else if (data.startsWith('help_')) { - console.log(`🔍 Condição help_ atendida`); - await this.handleHelpDetailCallback(callbackQuery, data); - } else if (data === 'separator') { - // Ignorar clique no separador visual - await this.bot.answerCallbackQuery(callbackQuery.id); - } else if (data === 'cancel_action') { - await this.bot.answerCallbackQuery(callbackQuery.id, 'Ação cancelada'); - - // Limpar sessão do usuário se existir - if (this.userSessions.has(userId)) { - this.userSessions.delete(userId); - } - - await this.bot.editMessageText('❌ Ação cancelada.', { - chat_id: chatId, - message_id: callbackQuery.message.message_id - }); - } else if (data === 'refresh_users') { - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.handleListUsers(chatId); - } else if (data === 'disconnect_user_prompt') { - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.bot.editMessageText('👤 Digite o nome do usuário que deseja desconectar:', { - chat_id: chatId, - message_id: callbackQuery.message.message_id - }); - - // Armazenar estado para próxima mensagem - this.userSessions.set(callbackQuery.from.id, { - type: 'waiting_username_disconnect' - }); - } else if (data.startsWith('suporte_cliente_')) { - console.log(`🔍 Condição suporte_cliente_ atendida`); - await this.handleSuporteClienteCallback(callbackQuery, data); - } else if (data.startsWith('suporte_servico_')) { - console.log(`🔍 Condição suporte_servico_ atendida`); - await this.handleSuporteServicoCallback(callbackQuery, data); - } else if (data.startsWith('suporte_tecnico_')) { - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.handleSuporteTecnicoCallback(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); - } else if (data.startsWith('pix_')) { - console.log(`🔍 Condição pix_ atendida`); - await this.handlePixCallback(callbackQuery, data); - } else { - console.log(`❓ Callback não reconhecido: ${data}`); - await this.bot.answerCallbackQuery(callbackQuery.id, 'Comando não reconhecido'); - } - } catch (error) { - console.error('❌ Erro no callback:', error); - await this.bot.answerCallbackQuery(callbackQuery.id, 'Erro interno'); - } - } - - /** - * Processa callbacks de comandos - */ - async handleCommandCallback(callbackQuery, data) { - const parts = data.split('_'); - const action = parts[1]; - const connectionId = parts.slice(2).join('_'); - - await this.bot.answerCallbackQuery(callbackQuery.id); - - switch (action) { - case 'status': - await this.executeSSHCommand(callbackQuery, connectionId, 'uptime'); - break; - case 'reboot': - await this.confirmRebootCallback(callbackQuery, connectionId); - break; - case 'disconnect': - await this.disconnectCallback(callbackQuery, connectionId); - break; - case 'custom': - await this.requestCustomCommand(callbackQuery, connectionId); - break; - } - } - - /** - * Executa comando SSH e mostra resultado - */ - async executeSSHCommand(callbackQuery, connectionId, command) { - const chatId = callbackQuery.message.chat.id; - - try { - const result = await this.ssh.executeCommand(connectionId, command); - - let message = `💻 *Comando:* \`${command}\`\n\n`; - - if (result.success) { - message += `✅ *Resultado:*\n\`\`\`\n${result.output}\`\`\``; - } else { - message += `❌ *Erro:*\n\`\`\`\n${result.error}\`\`\``; - } - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - } catch (error) { - await this.bot.sendMessage(chatId, `❌ Erro ao executar comando: ${error.message}`); - } - } - - /** - * Confirma reinicialização - */ - async confirmRebootCallback(callbackQuery, connectionId) { - const chatId = callbackQuery.message.chat.id; - - await this.bot.editMessageText('⚠️ Tem certeza que deseja reiniciar o equipamento?', { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - reply_markup: { - inline_keyboard: [ - [ - { text: '✅ SIM, reiniciar', callback_data: `confirm_reboot_${connectionId}` }, - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ] - } - }); - } - - /** - * Desconecta conexão SSH - */ - async disconnectCallback(callbackQuery, connectionId) { - const chatId = callbackQuery.message.chat.id; - - const success = this.ssh.disconnect(connectionId); - - if (success) { - await this.bot.editMessageText('🔌 Conexão encerrada com sucesso.', { - chat_id: chatId, - message_id: callbackQuery.message.message_id - }); - } else { - await this.bot.editMessageText('❌ Conexão não encontrada ou já encerrada.', { - chat_id: chatId, - message_id: callbackQuery.message.message_id - }); - } - } - - /** - * Solicita comando personalizado - */ - async requestCustomCommand(callbackQuery, connectionId) { - const chatId = callbackQuery.message.chat.id; - - // Armazenar estado para próxima mensagem - this.userSessions.set(callbackQuery.from.id, { - type: 'waiting_custom_command', - connectionId: connectionId - }); - - await this.bot.editMessageText('💻 Digite o comando que deseja executar:', { - chat_id: chatId, - message_id: callbackQuery.message.message_id - }); - } - - /** - * Processa callbacks de confirmação - */ - async handleConfirmCallback(callbackQuery, data) { - const parts = data.split('_'); - const action = parts[1]; - - if (action === 'restart' || action === 'reboot') { - const target = parts.slice(2).join('_'); - await this.executeRestart(callbackQuery, target); - } else if (action === 'reset' && parts[2] === 'onu') { - // Formato: confirm_reset_onu_oltid_slot_onuid - await this.handleConfirmResetOnu(callbackQuery, data); - } else if (action === 'firmware') { - // Formato: confirm_firmware_filename_oltid_slot_onuid - await this.handleConfirmFirmware(callbackQuery, data); - } - } - - /** - * Executa reinicialização confirmada - */ - async executeRestart(callbackQuery, target) { - const chatId = callbackQuery.message.chat.id; - - await this.bot.editMessageText(`🔄 Reiniciando ${target}...`, { - chat_id: chatId, - message_id: callbackQuery.message.message_id - }); - - // Simular reinicialização (implementar comando real aqui) - setTimeout(async () => { - await this.bot.sendMessage(chatId, `✅ ${target} foi reiniciado com sucesso.`); - }, 3000); - } - - /** - * MÉTODO REMOVIDO - handleUserSessionInput duplicado foi mesclado acima - */ - - /** - * Executa comando personalizado em uma conexão SSH - */ - async executeCustomCommand(chatId, connectionId, command) { - await this.bot.sendMessage(chatId, `💻 Executando: \`${command}\``, { parse_mode: 'Markdown' }); - - try { - const result = await this.ssh.executeCommand(connectionId, command); - - let message = `💻 *Comando:* \`${command}\`\n\n`; - - if (result.success) { - message += `✅ *Resultado:*\n\`\`\`\n${result.output || 'Comando executado com sucesso'}\`\`\``; - } else { - message += `❌ *Erro:*\n\`\`\`\n${result.error || 'Erro desconhecido'}\`\`\``; - } - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - } catch (error) { - await this.bot.sendMessage(chatId, `❌ Erro ao executar comando: ${error.message}`); - } - } - - /** - * Processa callback de verificação de sinal - */ - async handleCheckSignalCallback(callbackQuery, data) { - const parts = data.split('_'); - const chatId = callbackQuery.message.chat.id; - - // Verificar se é Fiberhome (formato: check_signal_fiberhome_SERIAL) - if (parts.length === 4 && parts[2] === 'fiberhome') { - const serialNumber = parts[3]; - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.handleCheckSignalFiberhome(chatId, serialNumber); - return; - } - - // Formato padrão Datacom: check_signal_olt_dito_SERIAL ou check_signal_olt_pedro_afonso_SERIAL - let oltId, serialNumber; - - if (parts.length === 6 && parts[2] === 'olt' && parts[3] === 'pedro' && parts[4] === 'afonso') { - // Caso: check_signal_olt_pedro_afonso_SERIAL - oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" - serialNumber = parts[5]; - } else if (parts.length === 5) { - // Caso: check_signal_olt_dito_SERIAL - oltId = parts[2] + '_' + parts[3]; // "olt_dito" - serialNumber = parts[4]; - } else if (parts.length === 4) { - // Caso: check_signal_oltid_SERIAL (se existir formato mais simples) - oltId = parts[2]; - serialNumber = parts[3]; - } else { - // Tentar reconstruir dinamicamente - oltId = parts.slice(2, -1).join('_'); // Tudo exceto o último (que é o serial) - serialNumber = parts[parts.length - 1]; // Último elemento - } - - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.handleCheckSignalOLT(chatId, oltId, serialNumber); - } - - /** - * Executa verificação de sinal na OLT Fiberhome - */ - async handleCheckSignalFiberhome(chatId, serialNumber) { - // Escapar caracteres especiais - const serialEscaped = serialNumber.replace(/_/g, '\\_'); - - await this.bot.sendMessage(chatId, `🔍 Procurando ONU ${serialEscaped} na OLT Fiberhome...`); - - try { - // Usar a mesma lógica do handleStatusONU - let resultado; - if (serialNumber.length === 12 && /^[A-Z0-9]+$/.test(serialNumber)) { - // É um serial - resultado = await this.fiberhome.obterStatusONU(serialNumber); - } else { - // Pode ser device name - resultado = await this.fiberhome.obterStatusONUPorNome(serialNumber); - } - - if (resultado.success) { - // Formatar resposta similar às OLTs Datacom - let message = `📡 *Sinal da ONU ${serialEscaped}*\n\n`; - message += `🏢 *OLT:* Fiberhome UNM2000 (131.0.192.9)\n`; - message += `📍 *Localização:* Slot ${resultado.slot}, PON ${resultado.pon}, ONU ${resultado.onuId}\n`; - message += `🔌 *Status:* ${resultado.online ? '🟢 Online' : '🔴 Offline'}\n\n`; - - message += `📊 *Informações do Sinal:*\n`; - message += `📡 *RX Power:* ${resultado.rxPower}\n`; - message += `📤 *TX Power:* ${resultado.txPower}\n`; - message += `📏 *Distância:* ${resultado.distance}\n\n`; - - // Nota sobre leitura de potência - message += `_💡 Valores medidos pela OLT via protocolo TL1_\n\n`; - - // Status das portas Ethernet (resumido) - if (resultado.portasEthernet.length > 0) { - const portasAtivas = resultado.portasEthernet.filter(p => p.status === 'active' || p.status === 'up').length; - message += `🔗 *Portas Ethernet:* ${portasAtivas}/${resultado.portasEthernet.length} ativas\n`; - } - - // Serviços WAN - if (resultado.servicosWAN.length > 0) { - const servicosAtivos = resultado.servicosWAN.filter(s => s.status === 'active').length; - message += `🌐 *Serviços WAN:* ${servicosAtivos} ativo(s)\n`; - } - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - - // Oferecer opções adicionais (similar às OLTs Datacom) - const keyboard = { - inline_keyboard: [ - [ - { text: '🔄 Atualizar sinal', callback_data: `check_signal_fiberhome_${serialNumber}` }, - { text: '📋 Detalhes completos', callback_data: `fiberhome_details_${serialNumber}` } - ], - [ - { text: '🔍 Pesquisar em OLT Datacom', callback_data: `ver_sinal_${serialNumber}` } - ] - ] - }; - - await this.bot.sendMessage(chatId, '🎛️ O que você gostaria de fazer?', { - reply_markup: keyboard - }); - - } else { - await this.bot.sendMessage(chatId, `❌ ONU ${serialNumber} não encontrada na OLT Fiberhome.`); - - // Oferecer pesquisar em OLTs Datacom - const keyboard = { - inline_keyboard: [ - [ - { text: '🔍 Pesquisar em OLT Datacom', callback_data: `ver_sinal_${serialNumber}` }, - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ] - }; - - await this.bot.sendMessage(chatId, 'ONU não encontrada na Fiberhome. Deseja pesquisar nas OLTs Datacom?', { - reply_markup: keyboard - }); - } - } catch (error) { - console.error('Erro ao verificar sinal Fiberhome:', error); - await this.bot.sendMessage(chatId, `❌ Erro interno: ${error.message}`); - } - } - - /** - * Processa callback de mostrar configuração - */ - async handleShowConfigCallback(callbackQuery, data) { - const parts = data.split('_'); - // Formato: show_config_olt_dito_slot_onuid ou show_config_olt_pedro_afonso_slot_onuid - let oltId, slot, onuId; - - if (parts.length === 7 && parts[2] === 'olt' && parts[3] === 'pedro' && parts[4] === 'afonso') { - // Caso: show_config_olt_pedro_afonso_slot_onuid - oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" - slot = parts[5]; - onuId = parts[6]; - } else if (parts.length === 6) { - // Caso: show_config_olt_dito_slot_onuid - oltId = parts[2] + '_' + parts[3]; // "olt_dito" - slot = parts[4]; - onuId = parts[5]; - } else if (parts.length === 5) { - // Caso: show_config_oltid_slot_onuid (se existir formato mais simples) - oltId = parts[2]; - slot = parts[3]; - onuId = parts[4]; - } else { - // Tentar reconstruir dinamicamente - // Os dois últimos são sempre slot e onuId - onuId = parts[parts.length - 1]; - slot = parts[parts.length - 2]; - oltId = parts.slice(2, -2).join('_'); // Tudo entre o terceiro e os dois últimos - } - - const chatId = callbackQuery.message.chat.id; - - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.bot.sendMessage(chatId, `📋 Buscando configuração da ONU ${onuId} no slot ${slot}...`); - - try { - const connection = await this.ssh.connect(oltId); - - if (!connection.success) { - await this.bot.sendMessage(chatId, `❌ Erro na conexão: ${connection.message}`); - return; - } - - const configCommand = `show running-config interface gpon ${slot} onu ${onuId}`; - const spCommand = `show running-config service-port | context-match "gpon ${slot} onu ${onuId}"`; - - const [configResult, spResult] = await Promise.all([ - this.ssh.executeCommand(connection.connectionId, configCommand), - this.ssh.executeCommand(connection.connectionId, spCommand) - ]); - - this.ssh.disconnect(connection.connectionId); - - if (configResult.success) { - // Extrair service-ports - const servicePorts = spResult.success - ? this.ssh.parseServicePorts(spResult.output) - : []; - - let message = `📋 *Configuração da ONU*\n`; - message += `📍 Slot: ${slot}, ONU: ${onuId}\n`; - - if (servicePorts.length > 0) { - message += `🔌 *Service-port${servicePorts.length > 1 ? 's' : ''}:* ${servicePorts.join(', ')}\n`; - } else { - message += `🔌 *Service-port:* não encontrado\n`; - } - - message += `\n\`\`\`\n${configResult.output}\n\`\`\``; - - if (spResult.success && spResult.output && spResult.output.trim()) { - message += `\n\n*Service-port(s):*\n\`\`\`\n${spResult.output.trim()}\n\`\`\``; - } - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - } else { - await this.bot.sendMessage(chatId, `❌ Erro ao obter configuração: ${configResult.error}`); - } - } catch (error) { - console.error('Erro ao mostrar configuração:', error); - await this.bot.sendMessage(chatId, `❌ Erro interno: ${error.message}`); - } - } - - /** - * Processa callback de reiniciar ONU - */ - async handleResetOnuCallback(callbackQuery, data) { - console.log(`🔌 handleResetOnuCallback chamado com data: ${data}`); - - const parts = data.split('_'); - console.log(`📝 Parts: ${JSON.stringify(parts)}`); - - // Formato: reset_onu_olt_dito_slot_onuid ou reset_onu_olt_pedro_afonso_slot_onuid - let oltId, slot, onuId; - - if (parts.length === 7 && parts[2] === 'olt' && parts[3] === 'pedro' && parts[4] === 'afonso') { - // Caso: reset_onu_olt_pedro_afonso_slot_onuid - oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" - slot = parts[5]; - onuId = parts[6]; - } else if (parts.length === 6) { - // Caso: reset_onu_olt_dito_slot_onuid - oltId = parts[2] + '_' + parts[3]; // "olt_dito" - slot = parts[4]; - onuId = parts[5]; - } else if (parts.length === 5) { - // Caso: reset_onu_oltid_slot_onuid (se existir formato mais simples) - oltId = parts[2]; - slot = parts[3]; - onuId = parts[4]; - } else { - // Tentar reconstruir dinamicamente - // Os dois últimos são sempre slot e onuId - onuId = parts[parts.length - 1]; - slot = parts[parts.length - 2]; - oltId = parts.slice(2, -2).join('_'); // Tudo entre o terceiro e os dois últimos - } - - console.log(`🎯 Parsed - OLT: ${oltId}, Slot: ${slot}, ONU: ${onuId}`); - - const chatId = callbackQuery.message.chat.id; - - // Primeiro mostrar confirmação - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.bot.editMessageText( - `⚠️ **Confirmação de Reinicialização**\n\n` + - `Tem certeza que deseja reiniciar a ONU?\n\n` + - `📍 **Slot:** ${slot}\n` + - `🔌 **ONU ID:** ${onuId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `⚠️ Esta ação irá desconectar temporariamente o cliente.`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: [ - [ - { text: '✅ SIM, Reiniciar', callback_data: `confirm_reset_onu_${oltId}_${slot}_${onuId}` }, - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ] - } - } - ); - } - - /** - * Processa confirmação de reinicialização de ONU - */ - async handleConfirmResetOnu(callbackQuery, data) { - console.log(`🔄 handleConfirmResetOnu chamado com data: ${data}`); - - const parts = data.replace('confirm_reset_onu_', '').split('_'); - console.log(`📝 Parts após parsing: ${JSON.stringify(parts)}`); - - // Formato após remoção do prefixo: olt_dito_slot_onuid - let oltId, slot, onuId; - - if (parts.length === 4) { - // Caso: olt_dito_slot_onuid - oltId = parts[0] + '_' + parts[1]; // "olt_dito" - slot = parts[2]; - onuId = parts[3]; - } else if (parts.length === 3) { - // Caso: oltid_slot_onuid - oltId = parts[0]; - slot = parts[1]; - onuId = parts[2]; - } else { - // Tentar reconstruir dinamicamente - onuId = parts[parts.length - 1]; - slot = parts[parts.length - 2]; - oltId = parts.slice(0, -2).join('_'); - } - - console.log(`🎯 Parsed - OLT: ${oltId}, Slot: ${slot}, ONU: ${onuId}`); - - const chatId = callbackQuery.message.chat.id; - - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.bot.editMessageText( - `🔄 **Reiniciando ONU...**\n\n` + - `📍 Slot: ${slot}\n` + - `🔌 ONU ID: ${onuId}\n` + - `🏢 OLT: ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `⏳ Aguarde, isso pode levar alguns segundos...`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - - try { - const result = await this.ssh.resetOnuAlternative(oltId, slot, onuId); - - if (result.success) { - await this.bot.editMessageText( - `✅ **ONU Reiniciada com Sucesso!**\n\n` + - `📍 Slot: ${slot}\n` + - `🔌 ONU ID: ${onuId}\n` + - `🏢 OLT: ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `🔄 A ONU deve voltar a funcionar em alguns instantes.\n` + - `📊 Recomenda-se verificar o sinal novamente em 1-2 minutos.`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } else { - await this.bot.editMessageText( - `❌ **Erro ao Reiniciar ONU**\n\n` + - `📍 Slot: ${slot}\n` + - `🔌 ONU ID: ${onuId}\n` + - `🏢 OLT: ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `❌ Erro: ${result.message}`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } catch (error) { - console.error('Erro ao reiniciar ONU:', error); - await this.bot.editMessageText( - `❌ **Erro Interno**\n\n` + - `Não foi possível reiniciar a ONU.\n` + - `Erro: ${error.message}`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } - - /** - * Processa callback de atualização de firmware da ONU - */ - async handleUpdateOnuCallback(callbackQuery, data) { - const parts = data.split('_'); - // Formato: update_onu_olt_dito_slot_onuid ou update_onu_olt_pedro_afonso_slot_onuid - let oltId, slot, onuId; - - if (parts.length === 7 && parts[2] === 'olt' && parts[3] === 'pedro' && parts[4] === 'afonso') { - // Caso: update_onu_olt_pedro_afonso_slot_onuid - oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" - slot = parts[5]; - onuId = parts[6]; - } else if (parts.length === 6) { - // Caso: update_onu_olt_dito_slot_onuid - oltId = parts[2] + '_' + parts[3]; // "olt_dito" - slot = parts[4]; - onuId = parts[5]; - } else if (parts.length === 5) { - // Caso: update_onu_oltid_slot_onuid (se existir formato mais simples) - oltId = parts[2]; - slot = parts[3]; - onuId = parts[4]; - } else { - // Tentar reconstruir dinamicamente - // Os dois últimos são sempre slot e onuId - onuId = parts[parts.length - 1]; - slot = parts[parts.length - 2]; - oltId = parts.slice(2, -2).join('_'); // Tudo entre o terceiro e os dois últimos - } - - const chatId = callbackQuery.message.chat.id; - - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.bot.sendMessage(chatId, `🔍 Identificando modelo da ONU ${onuId} no slot ${slot}...`); - - try { - // Obter detalhes da ONU para identificar o modelo - const details = await this.ssh.getOnuDetails(oltId, slot, onuId); - - if (!details.success) { - await this.bot.sendMessage(chatId, `❌ Erro ao obter detalhes da ONU: ${details.message}`); - return; - } - - const equipmentId = details.equipmentId; - console.log(`🔍 Equipment ID detectado: ${equipmentId}`); - - // Determinar opções de firmware baseado no modelo - let firmwareOptions = []; - - if (equipmentId.includes('DM986-100')) { - firmwareOptions = [ - { text: '📦 FW INTERMEDIÁRIA', callback_data: `firmware_DM986-100-inter.tar_${oltId}_${slot}_${onuId}` }, - { text: '📦 FW ATUAL (v56)', callback_data: `firmware_DM986-100-v56.tar_${oltId}_${slot}_${onuId}` }, - { text: '📦 FW ROUTER (R300)', callback_data: `firmware_DM986-100-R300.tar_${oltId}_${slot}_${onuId}` } - ]; - } else if (equipmentId.includes('DM986-414')) { - firmwareOptions = [ - { text: '📦 FW INTERMEDIÁRIA', callback_data: `firmware_DM986-414-inter.tar_${oltId}_${slot}_${onuId}` }, - { text: '📦 FW ATUAL (v70)', callback_data: `firmware_DM986-414-v70.tar_${oltId}_${slot}_${onuId}` } - ]; - } else { - await this.bot.sendMessage(chatId, - `❌ **Modelo não suportado**\n\n` + - `📍 Slot: ${slot}, ONU: ${onuId}\n` + - `🔍 Equipment ID: ${equipmentId}\n\n` + - `⚠️ Apenas DM986-100 e DM986-414 são suportados para atualização.` - ); - return; - } - - // Criar teclado com opções de firmware - const keyboard = { - inline_keyboard: [ - ...firmwareOptions.map(option => [option]), - [{ text: '❌ Cancelar', callback_data: 'cancel_action' }] - ] - }; - - await this.bot.sendMessage(chatId, - `⬆️ **Atualização de Firmware**\n\n` + - `📍 **Slot:** ${slot}\n` + - `🔌 **ONU ID:** ${onuId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n` + - `🔍 **Modelo:** ${equipmentId}\n\n` + - `📦 Selecione a versão do firmware:`, - { - parse_mode: 'Markdown', - reply_markup: keyboard - } - ); - - } catch (error) { - console.error('Erro ao processar atualização de ONU:', error); - await this.bot.sendMessage(chatId, `❌ Erro interno: ${error.message}`); - } - } - - /** - * Processa callback de seleção de firmware - */ - async handleFirmwareCallback(callbackQuery, data) { - // Formato: firmware_DM986-100-v56.tar_olt_dito_slot_onuid - const parts = data.replace('firmware_', '').split('_'); - - let firmwareFile, oltId, slot, onuId; - - // O arquivo de firmware é sempre o primeiro elemento - firmwareFile = parts[0]; - - // Processar o resto baseado no número de partes - if (parts.length === 5) { - // Caso: DM986-100-v56.tar_olt_dito_slot_onuid - oltId = parts[1] + '_' + parts[2]; // "olt_dito" - slot = parts[3]; - onuId = parts[4]; - } else if (parts.length === 4) { - // Caso: DM986-100-v56.tar_oltid_slot_onuid - oltId = parts[1]; - slot = parts[2]; - onuId = parts[3]; - } else { - // Tentar reconstruir dinamicamente - onuId = parts[parts.length - 1]; - slot = parts[parts.length - 2]; - oltId = parts.slice(1, -2).join('_'); - } - - const chatId = callbackQuery.message.chat.id; - - // Mostrar confirmação - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.bot.editMessageText( - `⚠️ **Confirmação de Atualização de Firmware**\n\n` + - `📦 **Firmware:** ${firmwareFile}\n` + - `📍 **Slot:** ${slot}\n` + - `🔌 **ONU ID:** ${onuId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `⚠️ **ATENÇÃO:**\n` + - `• A atualização pode demorar alguns minutos\n` + - `• A ONU ficará offline durante o processo\n` + - `• Não interrompa a operação\n\n` + - `Tem certeza que deseja continuar?`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: [ - [ - { text: '✅ SIM, Atualizar', callback_data: `confirm_firmware_${firmwareFile}_${oltId}_${slot}_${onuId}` }, - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ] - } - } - ); - } - - /** - * Processa confirmação de atualização de firmware - */ - async handleConfirmFirmware(callbackQuery, data) { - // Formato: confirm_firmware_DM986-100-v56.tar_olt_dito_slot_onuid - const parts = data.replace('confirm_firmware_', '').split('_'); - - let firmwareFile, oltId, slot, onuId; - - firmwareFile = parts[0]; - - if (parts.length === 5) { - oltId = parts[1] + '_' + parts[2]; - slot = parts[3]; - onuId = parts[4]; - } else if (parts.length === 4) { - oltId = parts[1]; - slot = parts[2]; - onuId = parts[3]; - } else { - onuId = parts[parts.length - 1]; - slot = parts[parts.length - 2]; - oltId = parts.slice(1, -2).join('_'); - } - - const chatId = callbackQuery.message.chat.id; - - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.bot.editMessageText( - `⬆️ **Atualizando Firmware...**\n\n` + - `📦 Firmware: ${firmwareFile}\n` + - `📍 Slot: ${slot}\n` + - `🔌 ONU ID: ${onuId}\n` + - `🏢 OLT: ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `⏳ Enviando comando de atualização...`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - - try { - const result = await this.ssh.updateOnuFirmware(oltId, slot, onuId, firmwareFile); - - if (result.success) { - await this.bot.editMessageText( - `✅ **Comando de Atualização Enviado!**\n\n` + - `📦 **Firmware:** ${firmwareFile}\n` + - `📍 **Slot:** ${slot}\n` + - `🔌 **ONU ID:** ${onuId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `✅ **Status:** Comando enviado com sucesso\n` + - `⏳ **Processamento:** A atualização está sendo processada pela OLT\n` + - `🕒 **Tempo estimado:** 5-15 minutos\n` + - `📡 **Importante:** Monitore o status da ONU\n\n` + - `💡 *A ONU ficará offline durante a atualização*`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } else { - await this.bot.editMessageText( - `❌ **Erro ao Enviar Comando**\n\n` + - `📦 **Firmware:** ${firmwareFile}\n` + - `📍 **Slot:** ${slot}\n` + - `🔌 **ONU ID:** ${onuId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `❌ **Erro:** ${result.message}\n\n` + - `🔄 Tente novamente ou verifique a conexão`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } catch (error) { - console.error('Erro ao atualizar firmware:', error); - await this.bot.editMessageText( - `❌ **Erro Interno**\n\n` + - `Não foi possível atualizar o firmware.\n` + - `Erro: ${error.message}`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } - - /** - * Handle callback para iniciar processo de remoção de ONU - */ - async handleRemoveOnuCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.split('_'); // remove_onu_olt_dito_[serialNumber] ou remove_onu_olt_pedro_afonso_[serialNumber] - - let oltId, serialNumber; - - if (parts.length >= 6 && parts[3] === 'pedro' && parts[4] === 'afonso') { - // Caso: remove_onu_olt_pedro_afonso_SERIAL - oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" - serialNumber = parts.slice(5).join('_'); // Serial pode ter underscores - } else if (parts.length >= 5) { - // Caso: remove_onu_olt_dito_SERIAL ou outras OLTs de duas palavras - oltId = parts[2] + '_' + parts[3]; // "olt_dito" - serialNumber = parts.slice(4).join('_'); // Serial pode ter underscores - } else { - // Fallback - caso não siga o padrão esperado - oltId = parts[2]; - serialNumber = parts.slice(3).join('_'); - } - - await this.bot.answerCallbackQuery(callbackQuery.id); - - await this.bot.editMessageText( - `🔍 **Buscando ONU ${serialNumber}**\n\n` + - `🏢 OLT: ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n` + - `📱 Serial: ${serialNumber}\n\n` + - `⏳ Localizando ONU e service-port...`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - - try { - const result = await this.ssh.removeOnuBySerial(oltId, serialNumber); - - if (result.success && result.readyForRemoval) { - // ONU encontrada - mostrar informações e pedir confirmação - // O service-port NÃO é incluído no callback_data pois removeOnu busca todos automaticamente - const keyboard = { - inline_keyboard: [ - [ - { text: '✅ CONFIRMAR REMOÇÃO', callback_data: `confirm_remove_${oltId}_${result.slot.replace(/\//g, '-')}_${result.onuId}_${serialNumber}` } - ], - [ - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ] - }; - - const servicePortsInfo = result.servicePorts && result.servicePorts.length > 1 - ? `🔌 **Service-ports:** ${result.servicePorts.join(', ')} *(${result.servicePorts.length} encontrados — todos serão removidos)*` - : `🔌 **Service-port:** ${result.servicePort}`; - - await this.bot.editMessageText( - `⚠️ **CONFIRMAÇÃO DE REMOÇÃO**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `📍 **Localização:** Slot ${result.slot}, ONU ${result.onuId}\n` + - `${servicePortsInfo}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + - `⚠️ **ATENÇÃO:** Esta ação não pode ser desfeita!\n` + - `A ONU será completamente removida da OLT.\n\n` + - `🔴 **Confirme que deseja remover esta ONU:**`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown', - reply_markup: keyboard - } - ); - } else { - await this.bot.editMessageText( - `❌ **ONU não encontrada**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + - `❌ **Erro:** ${result.message}\n\n` + - `💡 Verifique se o serial está correto ou se a ONU existe nesta OLT`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } catch (error) { - console.error('Erro ao buscar ONU para remoção:', error); - await this.bot.editMessageText( - `❌ **Erro interno**\n\n` + - `Não foi possível buscar a ONU.\n` + - `Erro: ${error.message}`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } - - /** - * Handle callback para confirmar remoção de ONU - */ - async handleConfirmRemoveCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - console.log(`🔍 handleConfirmRemoveCallback chamado com data: ${data}`); - - // Novo formato (sem servicePort): confirm_remove_olt_dito_[slot]_[onuId]_[serialNumber] - const parts = data.split('_'); - console.log(`🔍 Parts: ${JSON.stringify(parts)}`); - - let oltId, slot, onuId, serialNumber; - - if (parts.length >= 8 && parts[3] === 'pedro' && parts[4] === 'afonso') { - // Caso: confirm_remove_olt_pedro_afonso_slot_onuId_serialNumber - oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" - slot = parts[5].replace(/-/g, '/'); - onuId = parts[6]; - serialNumber = parts.slice(7).join('_'); - } else if (parts.length >= 7) { - // Caso: confirm_remove_olt_dito_slot_onuId_serialNumber - oltId = parts[2] + '_' + parts[3]; // "olt_dito" - slot = parts[4].replace(/-/g, '/'); - onuId = parts[5]; - serialNumber = parts.slice(6).join('_'); - } else { - // Fallback - oltId = parts[2]; - slot = parts[3].replace(/-/g, '/'); - onuId = parts[4]; - serialNumber = parts.slice(5).join('_'); - } - - console.log(`🔍 Parsed - oltId: ${oltId}, slot: ${slot}, onuId: ${onuId}, serialNumber: ${serialNumber}`); - - await this.bot.answerCallbackQuery(callbackQuery.id); - - await this.bot.editMessageText( - `🗑️ **Removendo ONU...**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `📍 **Localização:** Slot ${slot}, ONU ${onuId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + - `⏳ Buscando e removendo todos os service-ports...`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - - try { - const result = await this.ssh.removeOnu(oltId, slot, onuId); - - if (result.success) { - const portsRemovidos = result.servicePorts && result.servicePorts.length > 0 - ? result.servicePorts.join(', ') - : 'N/A'; - const portsLabel = result.servicePorts && result.servicePorts.length > 1 - ? `• Removidos ${result.servicePorts.length} service-ports (${portsRemovidos})` - : `• Removido service-port ${portsRemovidos}`; - - await this.bot.editMessageText( - `✅ **ONU Removida com Sucesso!**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `📍 **Localização:** Slot ${slot}, ONU ${onuId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + - `✅ **Status:** ONU removida completamente\n` + - `🗑️ **Ações realizadas:**\n` + - `• Removida interface ONU\n` + - `${portsLabel}\n` + - `• Configuração commitada\n\n` + - `💡 A ONU pode ser reconfigurada quando necessário`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } else { - await this.bot.editMessageText( - `❌ **Erro na Remoção**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `📍 **Localização:** Slot ${slot}, ONU ${onuId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + - `❌ **Erro:** ${result.message}\n\n` + - `🔄 Verifique o status da ONU ou tente novamente`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } catch (error) { - console.error('Erro ao remover ONU:', error); - await this.bot.editMessageText( - `❌ **Erro Interno**\n\n` + - `Não foi possível remover a ONU.\n` + - `Erro: ${error.message}`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } - - /** - * Handle callback para confirmar remoção de ONU Fiberhome via TL1 - */ - async handleConfirmRemoveFiberhomeCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - console.log(`🔍 handleConfirmRemoveFiberhomeCallback chamado com data: ${data}`); - - // Formato: confirm_remove_fh_SERIAL - const serialNumber = data.replace('confirm_remove_fh_', '').toUpperCase(); - console.log(`🔍 Serial a remover: ${serialNumber}`); - - await this.bot.answerCallbackQuery(callbackQuery.id); - - // Escapar caracteres especiais - const serialEscaped = serialNumber.replace(/_/g, '\\_'); - - await this.bot.editMessageText( - `🗑️ *Removendo ONU Fiberhome...*\n\n` + - `📱 *Serial:* ${serialEscaped}\n\n` + - `⏳ Aguarde...`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - - try { - // Executar comando TL1 de remoção - const resultado = await this.fiberhomeTL1.removerOnuFiberhome(serialNumber); - - if (resultado.success) { - await this.bot.editMessageText( - `✅ *ONU Fiberhome Removida com Sucesso!*\n\n` + - `📱 *Serial:* ${serialEscaped}\n` + - `🏢 *OLT:* ${resultado.details.olt.replace(/_/g, '\\_')}\n` + - `📡 *Porta:* ${resultado.details.porta}\n\n` + - `🔄 A ONU foi removida do sistema.`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } else { - await this.bot.editMessageText( - `❌ *Erro na Remoção*\n\n` + - `📱 *Serial:* ${serialEscaped}\n\n` + - `⚠️ *Erro:* ${resultado.error}\n\n` + - `🔄 Verifique o status da ONU ou tente novamente`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } catch (error) { - console.error('❌ Erro ao remover ONU Fiberhome:', error); - await this.bot.editMessageText( - `❌ *Erro Interno*\n\n` + - `📱 *Serial:* ${serialEscaped}\n\n` + - `⚠️ ${error.message}`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } - - /** - * Handle callback para iniciar processo de adição de ONU - */ - /** - * Handler: remove ONU já cadastrada e tenta readicioná-la automaticamente. - * Formatos de callback_data: - * remove_readd____ - * remove_readd_vlan_____ - */ - async handleRemoveAndReAddCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const hasVlan = data.startsWith('remove_readd_vlan_'); - const parts = data.split('_'); - - let oltId, slot, onuId, vlanId, serialNumber; - - if (hasVlan) { - // remove_readd_vlan_olt_pedro_afonso_slot_onuId_vlanId_serial - // remove_readd_vlan_olt_dito_slot_onuId_vlanId_serial - // parts[0]=remove [1]=readd [2]=vlan [3]=olt [4]=dito|pedro [5]=afonso|slot ... - if (parts[4] === 'pedro' && parts[5] === 'afonso') { - oltId = parts[3] + '_' + parts[4] + '_' + parts[5]; - slot = parts[6].replace(/-/g, '/'); - onuId = parts[7]; - vlanId = parts[8]; - serialNumber = parts.slice(9).join('_'); - } else { - oltId = parts[3] + '_' + parts[4]; - slot = parts[5].replace(/-/g, '/'); - onuId = parts[6]; - vlanId = parts[7]; - serialNumber = parts.slice(8).join('_'); - } - } else { - // remove_readd_olt_pedro_afonso_slot_onuId_serial - // remove_readd_olt_dito_slot_onuId_serial - // parts[0]=remove [1]=readd [2]=olt [3]=dito|pedro [4]=afonso|slot ... - if (parts[3] === 'pedro' && parts[4] === 'afonso') { - oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; - slot = parts[5].replace(/-/g, '/'); - onuId = parts[6]; - serialNumber = parts.slice(7).join('_'); - } else { - oltId = parts[2] + '_' + parts[3]; - slot = parts[4].replace(/-/g, '/'); - onuId = parts[5]; - serialNumber = parts.slice(6).join('_'); - } - } - - console.log(`🗑️➕ remove_readd - oltId: ${oltId}, slot: ${slot}, onuId: ${onuId}, vlanId: ${vlanId || 'auto'}, serial: ${serialNumber}`); - - await this.bot.answerCallbackQuery(callbackQuery.id); - - await this.bot.editMessageText( - `🗑️ **Removendo ONU cadastrada...**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `📍 **Slot:** ${slot}, ONU ${onuId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + - `⏳ Aguarde...`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - - try { - const removeResult = await this.ssh.removeOnu(oltId, slot, onuId); - - if (!removeResult.success) { - await this.bot.editMessageText( - `❌ **Erro ao remover ONU**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + - `❌ **Erro:** ${removeResult.message}\n\n` + - `🔄 Verifique o status da ONU e tente novamente.`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - return; - } - - // ONU removida — aguardar brevemente e tentar redescobrir - await this.bot.editMessageText( - `✅ **ONU removida!** Aguardando redescoberta...\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + - `⏳ Verificando discovered-onus...`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - - // Aguardar 4 segundos para a ONU voltar à lista de discovered - await new Promise(resolve => setTimeout(resolve, 4000)); - - const addResult = await this.ssh.addOnuBySerial(oltId, serialNumber, vlanId || undefined); - - if (addResult.success && addResult.readyForAddition) { - const effectiveVlan = vlanId || addResult.vlanId; - const keyboard = { - inline_keyboard: [ - [ - { text: '✅ CONFIRMAR ADIÇÃO', callback_data: `confirm_add_${oltId}_${addResult.slot.replace(/\//g, '-')}_${addResult.onuId}_${effectiveVlan}_${serialNumber}` } - ], - [ - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ] - }; - - await this.bot.editMessageText( - `✅ **ONU pronta para adição!**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `📍 **Slot descoberto:** ${addResult.slot}\n` + - `🔢 **ONU ID a usar:** ${addResult.onuId}\n` + - `🏷️ **VLAN ID:** ${effectiveVlan}${vlanId ? ' (customizada)' : ''}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + - `📋 **ONU IDs em uso no slot:** ${addResult.usedIds.join(', ')}\n\n` + - `✅ **Confirme que deseja adicionar esta ONU:**`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown', - reply_markup: keyboard - } - ); - } else { - // ONU não apareceu em discovered-onus ainda - await this.bot.editMessageText( - `✅ **ONU removida com sucesso!**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + - `⚠️ A ONU ainda não apareceu como disponível para adição.\n\n` + - `💡 Aguarde alguns instantes e tente:\n` + - `\`adicionar onu ${serialNumber}\``, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } catch (error) { - console.error('Erro em handleRemoveAndReAddCallback:', error); - await this.bot.editMessageText( - `❌ **Erro interno**\n\n` + - `Não foi possível concluir a operação.\n` + - `Erro: ${error.message}`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } - - /** - * Handle callback para iniciar processo de adição de ONU - */ - async handleAddOnuCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.split('_'); // add_onu_olt_dito_[serialNumber] ou add_onu_olt_pedro_afonso_[serialNumber] - - let oltId, serialNumber; - - if (parts.length >= 6 && parts[3] === 'pedro' && parts[4] === 'afonso') { - // Caso: add_onu_olt_pedro_afonso_SERIAL - oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" - serialNumber = parts.slice(5).join('_'); // Serial pode ter underscores - } else if (parts.length >= 5) { - // Caso: add_onu_olt_dito_SERIAL ou outras OLTs de duas palavras - oltId = parts[2] + '_' + parts[3]; // "olt_dito" - serialNumber = parts.slice(4).join('_'); // Serial pode ter underscores - } else { - // Fallback - caso não siga o padrão esperado - oltId = parts[2]; - serialNumber = parts.slice(3).join('_'); - } - - await this.bot.answerCallbackQuery(callbackQuery.id); - - await this.bot.editMessageText( - `🔍 **Analisando ONU ${serialNumber}**\n\n` + - `🏢 OLT: ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n` + - `📱 Serial: ${serialNumber}\n\n` + - `⏳ Descobrindo slot e próximo ONU ID disponível...`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - - try { - const result = await this.ssh.addOnuBySerial(oltId, serialNumber); - - if (result.success && result.readyForAddition) { - // ONU analisada - mostrar informações e pedir confirmação - const keyboard = { - inline_keyboard: [ - [ - { text: '✅ CONFIRMAR ADIÇÃO', callback_data: `confirm_add_${oltId}_${result.slot.replace(/\//g, '-')}_${result.onuId}_${result.vlanId}_${serialNumber}` } - ], - [ - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ] - }; - - await this.bot.editMessageText( - `✅ **CONFIRMAÇÃO DE ADIÇÃO**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `📍 **Slot descoberto:** ${result.slot}\n` + - `🔢 **ONU ID a usar:** ${result.onuId}\n` + - `🏷️ **VLAN ID:** ${result.vlanId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `📋 **ONU IDs em uso no slot:** ${result.usedIds.join(', ')}\n\n` + - `⚡ **Configurações que serão aplicadas:**\n` + - `• Line-profile: DM986-100\n` + - `• Ethernet: porta 1\n` + - `• Native VLAN: ${result.vlanId}\n` + - `• Service-port: novo (auto)\n\n` + - `✅ **Confirme que deseja adicionar esta ONU:**`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown', - reply_markup: keyboard - } - ); - } else { - // ONU não encontrada em discovered-onus — verificar se já está cadastrada no running-config - if (result.message && result.message.includes('discovered-onus')) { - await this.bot.editMessageText( - `🔍 **ONU não disponível para adição**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `⏳ Verificando se a ONU já está cadastrada...`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - - const findResult = await this.ssh.findOnuBySerial(oltId, serialNumber); - - if (findResult.success && findResult.onuInfo && findResult.onuInfo.found) { - const { slot, onuId } = findResult.onuInfo; - const keyboard = { - inline_keyboard: [ - [ - { text: '🗑️ Remover e adicionar novamente', callback_data: `remove_readd_${oltId}_${slot.replace(/\//g, '-')}_${onuId}_${serialNumber}` } - ], - [ - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ] - }; - await this.bot.editMessageText( - `⚠️ **ONU já cadastrada na OLT**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `📍 **Localização atual:** Slot ${slot}, ONU ${onuId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `A ONU já está cadastrada e não aparece como disponível para adição.\n\n` + - `Deseja **remover** o cadastro atual e adicioná-la novamente?`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown', - reply_markup: keyboard - } - ); - } else { - await this.bot.editMessageText( - `❌ **ONU não disponível**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `A ONU não foi encontrada em discovered-onus nem no running-config.\n\n` + - `💡 **Dica:** Verifique se a ONU está ligada e conectada à OLT.\n` + - `Use o comando: \`show interface gpon discovered-onus\``, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } else { - let errorMessage = `❌ **Não foi possível adicionar a ONU**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n`; - - if (result.message) errorMessage += `❌ **Erro:** ${result.message}\n\n`; - - if (result.message && result.message.includes('todos os ONU IDs')) { - errorMessage += `💡 **Dica:** O slot está lotado. Considere usar outro slot ou remover ONUs inutilizadas.`; - } else { - errorMessage += `💡 Verifique a conectividade com a OLT ou se a ONU está descoberta.`; - } - - await this.bot.editMessageText(errorMessage, { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - }); - } - } - } catch (error) { - console.error('Erro ao analisar ONU para adição:', error); - await this.bot.editMessageText( - `❌ **Erro interno**\n\n` + - `Não foi possível analisar a ONU.\n` + - `Erro: ${error.message}`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } - - /** - * Handle callback para iniciar processo de adição de ONU com VLAN customizada - */ - async handleAddOnuVlanCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - // Formato: add_onu_vlan_olt_dito_603_SERIAL ou add_onu_vlan_olt_pedro_afonso_603_SERIAL - // Índices: [0]_[1]_[2]_[3]_[4]_[5]_[6] - const parts = data.split('_'); - - let oltId, vlanId, serialNumber; - - if (parts.length >= 9 && parts[4] === 'pedro' && parts[5] === 'afonso') { - // Caso: add_onu_vlan_olt_pedro_afonso_VLANID_SERIAL - oltId = parts[3] + '_' + parts[4] + '_' + parts[5]; // "olt_pedro_afonso" - vlanId = parts[6]; - serialNumber = parts.slice(7).join('_'); // Serial pode ter underscores - } else if (parts.length >= 7) { - // Caso: add_onu_vlan_olt_dito_VLANID_SERIAL ou add_onu_vlan_olt_colmeia_VLANID_SERIAL - oltId = parts[3] + '_' + parts[4]; // "olt_dito" ou "olt_colmeia" - vlanId = parts[5]; - serialNumber = parts.slice(6).join('_'); // Serial pode ter underscores - } else { - // Fallback - oltId = parts[3]; - vlanId = parts[4]; - serialNumber = parts.slice(5).join('_'); - } - - await this.bot.answerCallbackQuery(callbackQuery.id); - - await this.bot.editMessageText( - `🔍 **Analisando ONU ${serialNumber}**\n\n` + - `🏢 OLT: ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n` + - `📱 Serial: ${serialNumber}\n` + - `🏷️ VLAN customizada: ${vlanId}\n\n` + - `⏳ Descobrindo slot e próximo ONU ID disponível...`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - - try { - // Usar a mesma função addOnuBySerial mas sem a vlanId (vamos usar a customizada) - const result = await this.ssh.addOnuBySerial(oltId, serialNumber); - - if (result.success && result.readyForAddition) { - // ONU analisada - mostrar informações e pedir confirmação - const keyboard = { - inline_keyboard: [ - [ - { text: '✅ CONFIRMAR ADIÇÃO', callback_data: `confirm_add_${oltId}_${result.slot.replace(/\//g, '-')}_${result.onuId}_${vlanId}_${serialNumber}` } - ], - [ - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ] - }; - - await this.bot.editMessageText( - `✅ **CONFIRMAÇÃO DE ADIÇÃO COM VLAN CUSTOMIZADA**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `📍 **Slot descoberto:** ${result.slot}\n` + - `🔢 **ONU ID a usar:** ${result.onuId}\n` + - `🏷️ **VLAN ID customizada:** ${vlanId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + - `📋 **ONU IDs em uso no slot:** ${result.usedIds.join(', ')}\n\n` + - `⚡ **Configurações que serão aplicadas:**\n` + - `• Line-profile: DM986-100\n` + - `• Ethernet: porta 1\n` + - `• Native VLAN: ${vlanId} (customizada)\n` + - `• Service-port: novo (auto) com VLAN ${vlanId}\n\n` + - `✅ **Confirme que deseja adicionar esta ONU:**`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown', - reply_markup: keyboard - } - ); - } else { - // ONU não encontrada em discovered-onus — verificar se já está cadastrada no running-config - if (result.message && result.message.includes('discovered-onus')) { - await this.bot.editMessageText( - `🔍 **ONU não disponível para adição**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `🏷️ **VLAN:** ${vlanId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + - `⏳ Verificando se a ONU já está cadastrada...`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - - const findResult = await this.ssh.findOnuBySerial(oltId, serialNumber); - - if (findResult.success && findResult.onuInfo && findResult.onuInfo.found) { - const { slot, onuId } = findResult.onuInfo; - const keyboard = { - inline_keyboard: [ - [ - { text: '🗑️ Remover e adicionar novamente', callback_data: `remove_readd_vlan_${oltId}_${slot.replace(/\//g, '-')}_${onuId}_${vlanId}_${serialNumber}` } - ], - [ - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ] - }; - await this.bot.editMessageText( - `⚠️ **ONU já cadastrada na OLT**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `📍 **Localização atual:** Slot ${slot}, ONU ${onuId}\n` + - `🏷️ **VLAN a usar:** ${vlanId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + - `A ONU já está cadastrada e não aparece como disponível para adição.\n\n` + - `Deseja **remover** o cadastro atual e adicioná-la novamente com VLAN ${vlanId}?`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown', - reply_markup: keyboard - } - ); - } else { - await this.bot.editMessageText( - `❌ **ONU não disponível**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `🏷️ **VLAN:** ${vlanId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + - `A ONU não foi encontrada em discovered-onus nem no running-config.\n\n` + - `💡 **Dica:** Verifique se a ONU está ligada e conectada à OLT.\n` + - `Use o comando: \`show interface gpon discovered-onus\``, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } else { - let errorMessage = `❌ **Não foi possível adicionar a ONU**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `🏷️ **VLAN:** ${vlanId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n`; - - if (result.message) errorMessage += `❌ **Erro:** ${result.message}\n\n`; - - if (result.message && result.message.includes('todos os ONU IDs')) { - errorMessage += `💡 **Dica:** O slot está lotado. Considere usar outro slot ou remover ONUs inutilizadas.`; - } else { - errorMessage += `💡 Verifique a conectividade com a OLT ou se a ONU está descoberta.`; - } - - await this.bot.editMessageText(errorMessage, { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - }); - } - } - } catch (error) { - console.error('Erro ao analisar ONU para adição com VLAN customizada:', error); - await this.bot.editMessageText( - `❌ **Erro interno**\n\n` + - `Não foi possível analisar a ONU.\n` + - `Erro: ${error.message}`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } - - /** - * Handle callback para tipo de ONU Fiberhome - */ - async handleFiberhomeOnuTypeCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const userId = callbackQuery.from.id; - - // Formato: fh_onu_type_AN5506-02-B ou fh_onu_type_AN5506-01-A - const onuType = data.replace('fh_onu_type_', ''); - - // Recuperar sessão - const session = this.userSessions.get(userId); - if (!session || session.action !== 'adicionar_onu_fiberhome_step2') { - await this.bot.answerCallbackQuery(callbackQuery.id, '❌ Sessão expirada'); - await this.bot.sendMessage(chatId, '❌ Sessão expirada. Por favor, comece novamente.'); - this.userSessions.delete(userId); - return; - } - - session.onuType = onuType; - session.action = 'adicionar_onu_fiberhome_step3'; - this.userSessions.set(userId, session); - - await this.bot.answerCallbackQuery(callbackQuery.id); - - // Perguntar sobre VLAN - const keyboard = { - inline_keyboard: [ - [{ text: '✅ Usar VLAN 1002 (padrão)', callback_data: 'fh_vlan_1002' }], - [{ text: '🔧 Usar outra VLAN', callback_data: 'fh_vlan_custom' }], - [{ text: '❌ Cancelar', callback_data: 'cancel_action' }] - ] - }; - - await this.bot.editMessageText( - `🔌 **Tipo da ONU:** ${onuType}\n\n` + - `🏷️ **VLAN:**\n` + - `A VLAN padrão é 1002. Deseja usar esta VLAN ou configurar outra?`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - reply_markup: keyboard, - parse_mode: 'Markdown' - } - ); - } - - /** - * Handle callback para VLAN Fiberhome - */ - async handleFiberhomeVlanCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const userId = callbackQuery.from.id; - - // Recuperar sessão - const session = this.userSessions.get(userId); - if (!session || session.action !== 'adicionar_onu_fiberhome_step3') { - await this.bot.answerCallbackQuery(callbackQuery.id, '❌ Sessão expirada'); - await this.bot.sendMessage(chatId, '❌ Sessão expirada. Por favor, comece novamente.'); - this.userSessions.delete(userId); - return; - } - - await this.bot.answerCallbackQuery(callbackQuery.id); - - if (data === 'fh_vlan_1002') { - // Usar VLAN padrão 1002 - session.vlan = 1002; - - // Limpar sessão - this.userSessions.delete(userId); - - // Executar adição da ONU - await this.executarAdicaoOnuFiberhome(chatId, session, userId); - - } else if (data === 'fh_vlan_custom') { - // Pedir VLAN customizada - manter sessão no step3 para receber input do usuário - await this.bot.editMessageText( - `🏷️ **VLAN Customizada**\n\n` + - `Digite o número da VLAN que deseja usar (1-4094):`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } - - /** - * Handle callback para confirmar adição de ONU - */ - async handleConfirmAddCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const userId = callbackQuery.from.id; - console.log(`🔍 handleConfirmAddCallback chamado com data: ${data}`); - - // Parse do formato: confirm_add_olt_dito_1-1-3_0_603_DACM915E0F76 - const parts = data.split('_'); - console.log(`🔍 Parts: ${JSON.stringify(parts)}`); - - let oltId, slot, onuId, vlanId, serialNumber; - - if (parts.length >= 8 && parts[2] === 'olt' && parts[3] === 'pedro' && parts[4] === 'afonso') { - // Caso: confirm_add_olt_pedro_afonso_1-1-3_0_603_DACM915E0F76 - oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" - slot = parts[5].replace(/-/g, '/'); // Converter de volta: "1-1-3" -> "1/1/3" - onuId = parts[6]; - vlanId = parts[7]; - serialNumber = parts.slice(8).join('_'); // Serial pode ter underscores - } else if (parts.length >= 7) { - // Caso padrão: confirm_add_olt_dito_1-1-3_0_603_DACM915E0F76 - oltId = parts[2] + '_' + parts[3]; // "olt_dito" - slot = parts[4].replace(/-/g, '/'); // Converter de volta: "1-1-3" -> "1/1/3" - onuId = parts[5]; - vlanId = parts[6]; - serialNumber = parts.slice(7).join('_'); // Serial pode ter underscores - } else if (parts.length >= 6) { - // Fallback para OLTs com nome simples - oltId = parts[2]; // "olt" - slot = parts[3].replace(/-/g, '/'); // Converter de volta - onuId = parts[4]; - vlanId = parts[5]; - serialNumber = parts.slice(6).join('_'); - } else { - console.error(`❌ Formato de callback inválido: ${data}`); - await this.bot.answerCallbackQuery(callbackQuery.id, { text: 'Erro no formato do callback' }); - return; - } - - console.log(`🔍 Parsed - oltId: ${oltId}, slot: ${slot}, onuId: ${onuId}, vlanId: ${vlanId}, serialNumber: ${serialNumber}`); - - await this.bot.answerCallbackQuery(callbackQuery.id); - - await this.bot.editMessageText( - `➕ **Adicionando ONU...**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `📍 **Localização:** Slot ${slot}, ONU ${onuId}\n` + - `🏷️ **VLAN:** ${vlanId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `⏳ Executando comandos de configuração...\n` + - `⚡ Este processo pode levar até 90 segundos`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - - try { - const result = await this.ssh.addOnu(oltId, slot, onuId, serialNumber, parseInt(vlanId)); - - if (result.success) { - const servicePortInfo = result.servicePortId ? - `🔌 **Service-port:** ${result.servicePortId} (Auto-gerado)` : - `🔌 **Service-port:** Criado automaticamente`; - - await this.bot.editMessageText( - `✅ **ONU Adicionada com Sucesso!**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `📍 **Localização:** Slot ${slot}, ONU ${onuId}\n` + - `🏷️ **VLAN:** ${vlanId}\n` + - `${servicePortInfo}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `✅ **Configurações aplicadas:**\n` + - `• Line-profile: DM986-100\n` + - `• Serial number: ${serialNumber}\n` + - `• Ethernet: porta 1\n` + - `• Native VLAN: ${vlanId}\n` + - `• Service-port: ${result.servicePortId || 'auto-gerado'} criado e configurado\n` + - `• Configuração commitada\n\n` + - `🎉 A ONU está pronta para uso!`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - - // Convidar para registrar no TopSapp - const tsLocation = oltId.includes('pedro_afonso') ? 'pedro_afonso' : 'guarai_colmeia'; - const oltEquipment = this.ssh.equipamentos.get(oltId); - this.userSessions.set(userId, { - action: 'topsapp_onu_pre_busca', - onuData: { - tipo: 'datacom', - location: tsLocation, - oltId, - oltHost: oltEquipment ? oltEquipment.host : '', - serial: serialNumber, - onuId, - servicePort: result.servicePortId ? parseInt(result.servicePortId) : null, - portaAlias: slot - } - }); - await this.bot.sendMessage(chatId, - `📋 *Registrar no TopSapp?*\n\nDeseja vincular esta ONU a um cliente no sistema TopSapp?`, - { - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: [[ - { text: '✅ Sim, vincular', callback_data: 'topsapp_onu_sim' }, - { text: '❌ Não', callback_data: 'cancel_action' } - ]] - } - } - ); - - } else { - await this.bot.editMessageText( - `❌ **Erro na Adição**\n\n` + - `📱 **Serial:** ${serialNumber}\n` + - `📍 **Localização:** Slot ${slot}, ONU ${onuId}\n` + - `🏷️ **VLAN:** ${vlanId}\n` + - `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + - `❌ **Erro:** ${result.message}\n\n` + - `🔄 Verifique a conectividade com a OLT ou tente novamente`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } catch (error) { - console.error('Erro ao adicionar ONU:', error); - await this.bot.editMessageText( - `❌ **Erro Interno**\n\n` + - `Não foi possível adicionar a ONU.\n` + - `Erro: ${error.message}`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - parse_mode: 'Markdown' - } - ); - } - } - - /** - * Painel administrativo - */ - async handleAdmin(chatId) { - const userId = chatId; // Para chats individuais, chatId = userId - - // Verificar se é usuário autorizado - if (!this.isAuthorized(userId)) { - await this.bot.sendMessage(chatId, '🚫 Acesso negado ao painel administrativo.'); - return; - } - - await this.bot.sendMessage(chatId, - `🛡️ **Painel Administrativo**\n\n` + - `👥 **Gestão de Usuários:**\n` + - `• \`usuarios autorizados\` - Ver lista de usuários\n` + - `• \`meu id\` - Ver seu ID do Telegram\n\n` + - `📊 **Sistema:**\n` + - `• \`status\` - Status do bot\n` + - `• \`equipamentos\` - Lista de equipamentos\n` + - `• \`logs\` - Ver logs recentes\n` + - `• \`limpar logs\` - Limpar logs do sistema\n\n` + - `🔧 **Comandos disponíveis:**\n` + - `• Ver sinal, adicionar ONU, remover ONU\n` + - `• Resetar ONU, atualizar firmware\n` + - `• Gerenciar usuários Juniper`, - { parse_mode: 'Markdown' } - ); - } - - /** - * Listar usuários autorizados - */ - async handleListAuthorizedUsers(chatId) { - const userId = chatId; - - if (!this.isAuthorized(userId)) { - await this.bot.sendMessage(chatId, '🚫 Acesso negado.'); - return; - } - - const authorizedUsers = config.bot.allowedUsers; - - if (authorizedUsers.length === 0) { - await this.bot.sendMessage(chatId, - `👥 **Usuários Autorizados**\n\n` + - `⚠️ Nenhuma restrição configurada\n` + - `🌐 Bot está aberto para todos os usuários\n\n` + - `💡 Configure ALLOWED_USERS no .env para restringir` - ); - } else { - const userList = authorizedUsers.map((id, index) => - `${index + 1}. ID: \`${id}\`` - ).join('\n'); - - await this.bot.sendMessage(chatId, - `👥 **Usuários Autorizados**\n\n` + - `📊 Total: ${authorizedUsers.length} usuário(s)\n\n` + - `${userList}\n\n` + - `🆔 Seu ID: \`${userId}\``, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Exibir ID do usuário - */ - async handleMyId(chatId, userId) { - await this.bot.sendMessage(chatId, - `🆔 **Sua Identificação**\n\n` + - `👤 User ID: \`${userId}\`\n` + - `💬 Chat ID: \`${chatId}\`\n\n` + - `📋 Use estes IDs para configurar permissões`, - { parse_mode: 'Markdown' } - ); - } - - /** - * Status do TopSapp - */ - async handleStatusTopSapp(chatId) { - try { - await this.bot.sendMessage(chatId, '🔄 Verificando status do TopSapp...'); - - const status = this.topsapp.getConfigStatus(); - const instances = this.topsapp.getInstances(); - - let message = `🏢 **STATUS TOPSAPP**\n\n`; - - if (status.ready) { - message += `✅ **Configuração**: OK\n`; - message += `📊 **Instâncias configuradas**: ${instances.length}\n\n`; - - instances.forEach(instance => { - message += `🌐 **${instance.location}**\n`; - message += `└ ${instance.host}\n`; - message += `└ Status: ${status.instances.find(i => i.location === instance.location)?.configured ? '✅ Configurado' : '❌ Não configurado'}\n\n`; - }); - - message += `💡 Use os comandos:\n`; - message += `• "buscar cliente João"\n`; - message += `• "ver faturas 123.456.789-10"\n`; - message += `• "gerar boleto fatura123"\n`; - message += `• "bloquear cliente 456"`; - } else { - message += `❌ **Configuração**: Incompleta\n\n`; - message += `⚠️ **Problemas encontrados**:\n`; - - status.instances.forEach(instance => { - if (!instance.configured) { - message += `• ${instance.location}: Credenciais não configuradas\n`; - } - }); - - message += `\n📝 **Configure no arquivo .env**:\n`; - message += `• TOPSAPP_GUARAI_USER\n`; - message += `• TOPSAPP_GUARAI_PASS\n`; - message += `• TOPSAPP_PEDRO_AFONSO_USER\n`; - message += `• TOPSAPP_PEDRO_AFONSO_PASS`; - } - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - - } catch (error) { - console.error('❌ Erro ao verificar status TopSapp:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao verificar status TopSapp**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Buscar cliente no TopSapp - */ - async handleBuscarCliente(chatId, target) { - if (!target) { - await this.bot.sendMessage(chatId, - `🔍 **Buscar Cliente**\n\n` + - `💡 **Como usar:**\n` + - `• "buscar cliente João da Silva"\n` + - `• "buscar cliente 123.456.789-10"\n` + - `• "buscar cliente Guarai João" (específica instância)\n` + - `• "buscar cliente Pedro Afonso Maria"` - ); - return; - } - - try { - await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); - - // Detectar se especificou uma localização - let location = ''; - let query = target; - - if (target.toLowerCase().includes('guarai') || target.toLowerCase().includes('colmeia')) { - location = 'Guarai/Colmeia'; - query = target.replace(/(guarai|colmeia)/gi, '').trim(); - } else if (target.toLowerCase().includes('pedro afonso')) { - location = 'Pedro Afonso'; - query = target.replace(/pedro afonso/gi, '').trim(); - } - - const resultado = await this.topsapp.buscarCliente(query, location); - - if (!resultado.success) { - await this.bot.sendMessage(chatId, - `❌ **Erro na busca**\n\n` + - `⚠️ ${resultado.error}`, - { parse_mode: 'Markdown' } - ); - return; - } - - if (!resultado.clientes || resultado.clientes.length === 0) { - await this.bot.sendMessage(chatId, - `🔍 **Nenhum cliente encontrado**\n\n` + - `📝 Termo buscado: "${query}"\n` + - `🌐 Instância: ${resultado.instancia || 'Todas'}`, - { parse_mode: 'Markdown' } - ); - return; - } - - // Se encontrou mais de um cliente, mostrar lista para seleção - if (resultado.clientes.length > 1) { - let message = `🔍 **${resultado.clientes.length} clientes encontrados**\n\n`; - message += `🌐 **Instância**: ${resultado.instancia}\n\n`; - - const keyboard = []; - - resultado.clientes.slice(0, 10).forEach((cliente, index) => { - message += `👤 **${index + 1}. ${cliente.nome}**\n`; - message += `🆔 ID: \`${cliente.id}\`\n`; - if (cliente.cpf) { - message += `📄 CPF: \`${cliente.cpf}\`\n`; - } - message += `📞 ${cliente.telefone || 'Telefone não informado'}\n`; - message += `📊 Status: ${cliente.status === 1 ? '✅ Ativo' : '❌ Inativo'}\n\n`; - - // Adicionar botões para ações - keyboard.push([ - { text: `📋 Dados ${index + 1}`, callback_data: `dados_cliente_${cliente.id}_${location}` }, - { text: `📄 Faturas ${index + 1}`, callback_data: `faturas_cliente_${cliente.id}_${location}` } - ]); - }); - - if (resultado.clientes.length > 10) { - message += `📋 *Mostrando apenas os primeiros 10 resultados de ${resultado.clientes.length}*\n\n`; - } - - message += `💡 **Selecione uma opção abaixo:**`; - - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: keyboard - } - }); - return; - } - - // Se encontrou apenas um cliente, mostrar dados detalhados - // Se encontrou apenas um cliente, mostrar dados detalhados - const cliente = resultado.clientes[0]; - let message = `👤 **Cliente Encontrado**\n\n`; - message += `🆔 **ID**: \`${cliente.id}\`\n`; - message += `👤 **Nome**: ${cliente.nome}\n`; - - if (cliente.cpf) { - message += `📄 **CPF**: \`${cliente.cpf}\`\n`; - } - - message += `📧 **Email**: ${cliente.email || 'Não informado'}\n`; - message += `📞 **Telefone**: ${cliente.telefone || 'Não informado'}\n`; - message += `📍 **Endereço**: ${cliente.endereco || 'Não informado'}\n`; - message += `🏙️ **Cidade**: ${cliente.cidade || 'Não informado'}\n`; - message += `📊 **Status**: ${cliente.status === 1 ? '✅ Ativo' : '❌ Inativo'}\n`; - message += `🌐 **Instância**: ${resultado.instancia}\n\n`; - - // Botões de ação - const keyboard = [ - [ - { text: '📋 Dados Cadastrais', callback_data: `dados_cliente_${cliente.id}_${location}` }, - { text: '📄 Ver Faturas', callback_data: `faturas_cliente_${cliente.id}_${location}` } - ], - [ - { text: '🔒 Bloquear', callback_data: `confirm_block_${cliente.id}_${location}` }, - { text: '🔓 Liberar', callback_data: `confirm_unblock_${cliente.id}_${location}` } - ] - ]; - - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: keyboard - } - }); - - } catch (error) { - console.error('❌ Erro ao buscar cliente:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao buscar cliente**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Bloquear cliente - */ - async handleBloquearCliente(chatId, target) { - if (!target) { - await this.bot.sendMessage(chatId, - `🔒 **Bloquear Cliente**\n\n` + - `💡 **Como usar:**\n` + - `• "bloquear cliente João da Silva"\n` + - `• "bloquear cliente 123.456.789-10"\n` + - `• "bloquear Guarai João" (específica instância)` - ); - return; - } - - try { - await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); - - // Detectar se especificou uma localização - let location = ''; - let query = target; - - if (target.toLowerCase().includes('guarai') || target.toLowerCase().includes('colmeia')) { - location = 'Guarai/Colmeia'; - query = target.replace(/(guarai|colmeia)/gi, '').trim(); - } else if (target.toLowerCase().includes('pedro afonso')) { - location = 'Pedro Afonso'; - query = target.replace(/pedro afonso/gi, '').trim(); - } - - const resultado = await this.topsapp.buscarCliente(query, location); - - if (!resultado.success) { - await this.bot.sendMessage(chatId, - `❌ **Erro na busca**\n\n` + - `⚠️ ${resultado.error}`, - { parse_mode: 'Markdown' } - ); - return; - } - - if (!resultado.clientes || resultado.clientes.length === 0) { - await this.bot.sendMessage(chatId, - `🔍 **Nenhum cliente encontrado**\n\n` + - `📝 Termo buscado: "${query}"\n` + - `🌐 Instância: ${resultado.instancia || 'Todas'}`, - { parse_mode: 'Markdown' } - ); - return; - } - - const cliente = resultado.clientes[0]; - - // Obter serviços do cliente para bloquear - const servicosResult = await this.topsapp.obterServicos(cliente.id, location); - - if (!servicosResult.success) { - await this.bot.sendMessage(chatId, - `❌ **Erro ao buscar serviços**\n\n` + - `⚠️ ${servicosResult.error}`, - { parse_mode: 'Markdown' } - ); - return; - } - - const servicos = servicosResult.servicos || servicosResult.data?.dados || servicosResult.data?.servicos || []; - const servicosAtivos = servicos.filter(s => { - // id_status já vem normalizado da API - return s.id_status === 1 || (s.status && s.status.toLowerCase() === 'liberado'); - }); - - if (servicosAtivos.length === 0) { - await this.bot.sendMessage(chatId, - `⚠️ **Nenhum serviço ativo**\n\n` + - `O cliente **${cliente.nome}** não possui serviços ativos para bloquear.`, - { parse_mode: 'Markdown' } - ); - return; - } - - let message = `🔒 **Bloquear Cliente**\n\n`; - message += `👤 **Cliente**: ${cliente.nome}\n`; - message += `🆔 **ID**: ${cliente.id}\n`; - message += `📊 **Serviços ativos**: ${servicosAtivos.length}\n\n`; - message += `⚠️ **Confirma o bloqueio de todos os serviços?**`; - - const keyboard = [ - [ - { text: '✅ Confirmar Bloqueio', callback_data: `confirm_block_all_${cliente.id}_${location}` }, - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ]; - - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: keyboard - } - }); - - } catch (error) { - console.error('❌ Erro ao bloquear cliente:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao bloquear cliente**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Liberar cliente - */ - async handleLiberarCliente(chatId, target) { - if (!target) { - await this.bot.sendMessage(chatId, - `🔓 **Liberar Cliente**\n\n` + - `💡 **Como usar:**\n` + - `• "liberar cliente João da Silva"\n` + - `• "liberar cliente 123.456.789-10"\n` + - `• "liberar Guarai João" (específica instância)` - ); - return; - } - - try { - await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); - - // Detectar se especificou uma localização - let location = ''; - let query = target; - - if (target.toLowerCase().includes('guarai') || target.toLowerCase().includes('colmeia')) { - location = 'Guarai/Colmeia'; - query = target.replace(/(guarai|colmeia)/gi, '').trim(); - } else if (target.toLowerCase().includes('pedro afonso')) { - location = 'Pedro Afonso'; - query = target.replace(/pedro afonso/gi, '').trim(); - } - - const resultado = await this.topsapp.buscarCliente(query, location); - - if (!resultado.success) { - await this.bot.sendMessage(chatId, - `❌ **Erro na busca**\n\n` + - `⚠️ ${resultado.error}`, - { parse_mode: 'Markdown' } - ); - return; - } - - if (!resultado.clientes || resultado.clientes.length === 0) { - await this.bot.sendMessage(chatId, - `🔍 **Nenhum cliente encontrado**\n\n` + - `📝 Termo buscado: "${query}"\n` + - `🌐 Instância: ${resultado.instancia || 'Todas'}`, - { parse_mode: 'Markdown' } - ); - return; - } - - const cliente = resultado.clientes[0]; - - // Obter serviços do cliente para liberar - const servicosResult = await this.topsapp.obterServicos(cliente.id, location); - - if (!servicosResult.success) { - await this.bot.sendMessage(chatId, - `❌ **Erro ao buscar serviços**\n\n` + - `⚠️ ${servicosResult.error}`, - { parse_mode: 'Markdown' } - ); - return; - } - - const servicos = servicosResult.servicos || servicosResult.data?.dados || servicosResult.data?.servicos || []; - - // DEBUG: Verificar estrutura dos serviços - console.log('🔍 DEBUG - Total de serviços recebidos:', servicos.length); - if (servicos.length > 0) { - console.log('🔍 DEBUG - Primeiro serviço:', JSON.stringify(servicos[0], null, 2)); - } - - const servicosBloqueados = servicos.filter(s => { - // id_status já vem normalizado da API - const isBloqueado = s.id_status === 2 || (s.status && s.status.toLowerCase() === 'bloqueado'); - console.log(`🔍 DEBUG - Serviço ${s.id_servico || s.id}: id_status=${s.id_status}, status="${s.status}", bloqueado=${isBloqueado}`); - return isBloqueado; - }); - - console.log('🔍 DEBUG - Serviços bloqueados encontrados:', servicosBloqueados.length); - - if (servicosBloqueados.length === 0) { - await this.bot.sendMessage(chatId, - `⚠️ **Nenhum serviço bloqueado**\n\n` + - `O cliente **${cliente.nome}** não possui serviços bloqueados para liberar.`, - { parse_mode: 'Markdown' } - ); - return; - } - - let message = `🔓 **Liberar Cliente**\n\n`; - message += `👤 **Cliente**: ${cliente.nome}\n`; - message += `🆔 **ID**: ${cliente.id}\n`; - message += `📊 **Serviços bloqueados**: ${servicosBloqueados.length}\n\n`; - message += `⚠️ **Confirma a liberação de todos os serviços?**`; - - const keyboard = [ - [ - { text: '✅ Confirmar Liberação', callback_data: `confirm_unblock_all_${cliente.id}_${location}` }, - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ]; - - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: keyboard - } - }); - - } catch (error) { - console.error('❌ Erro ao liberar cliente:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao liberar cliente**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Listar serviços do cliente - */ - async handleListarServicos(chatId, target) { - if (!target) { - await this.bot.sendMessage(chatId, - `📋 **Listar Serviços**\n\n` + - `💡 **Como usar:**\n` + - `• "listar serviços João da Silva"\n` + - `• "serviços do cliente 123.456.789-10"\n` + - `• "serviços Guarai João" (específica instância)` - ); - return; - } - - try { - await this.bot.sendMessage(chatId, '🔍 Buscando serviços...'); - - // Detectar se especificou uma localização - let location = ''; - let query = target; - - if (target.toLowerCase().includes('guarai') || target.toLowerCase().includes('colmeia')) { - location = 'Guarai/Colmeia'; - query = target.replace(/(guarai|colmeia)/gi, '').trim(); - } else if (target.toLowerCase().includes('pedro afonso')) { - location = 'Pedro Afonso'; - query = target.replace(/pedro afonso/gi, '').trim(); - } - - const resultado = await this.topsapp.buscarCliente(query, location); - - if (!resultado.success || !resultado.clientes || resultado.clientes.length === 0) { - await this.bot.sendMessage(chatId, - `🔍 **Nenhum cliente encontrado**\n\n` + - `📝 Termo buscado: "${query}"`, - { parse_mode: 'Markdown' } - ); - return; - } - - const cliente = resultado.clientes[0]; - - // Obter serviços do cliente - const servicosResult = await this.topsapp.obterServicos(cliente.id, location); - - if (!servicosResult.success) { - await this.bot.sendMessage(chatId, - `❌ **Erro ao buscar serviços**\n\n` + - `⚠️ ${servicosResult.error}`, - { parse_mode: 'Markdown' } - ); - return; - } - - const servicos = servicosResult.servicos || servicosResult.data?.dados || servicosResult.data?.servicos || []; - - if (servicos.length === 0) { - await this.bot.sendMessage(chatId, - `📋 **Nenhum serviço encontrado**\n\n` + - `O cliente **${cliente.nome}** não possui serviços cadastrados.`, - { parse_mode: 'Markdown' } - ); - return; - } - - // Separar serviços por status (id_status já vem normalizado da API) - const servicosAtivos = servicos.filter(s => { - return s.id_status === 1 || (s.status && s.status.toLowerCase() === 'liberado'); - }); - - const servicosBloqueados = servicos.filter(s => { - return s.id_status === 2 || (s.status && s.status.toLowerCase() === 'bloqueado'); - }); - - const servicosDesativados = servicos.filter(s => { - return s.id_status === 3 || (s.status && s.status.toLowerCase() === 'desativado'); - }); - - let message = `📋 **SERVIÇOS DO CLIENTE**\n\n`; - message += `👤 **Cliente**: ${cliente.nome}\n`; - message += `🆔 **ID**: ${cliente.id}\n\n`; - - // Exibir serviços ativos - if (servicosAtivos.length > 0) { - message += `✅ **SERVIÇOS ATIVOS** (${servicosAtivos.length})\n\n`; - servicosAtivos.forEach((servico, index) => { - message += `${index + 1}. 🟢 **${servico.descricao || servico.plano || 'Serviço'}**\n`; - message += ` 🆔 ID: \`${servico.id_servico || servico.id}\`\n`; - - if (servico.usuario) { - message += ` 👤 Login: \`${servico.usuario}\`\n`; - } - - if (servico.servico_valor || servico.valor) { - const valor = parseFloat(servico.servico_valor || servico.valor); - message += ` 💰 Valor: R$ ${valor.toFixed(2)}\n`; - } - - if (servico.vencimento) { - message += ` 📅 Vencimento: dia ${servico.vencimento}\n`; - } - - message += `\n`; - }); - } - - // Exibir serviços bloqueados - if (servicosBloqueados.length > 0) { - message += `\n🔒 **SERVIÇOS BLOQUEADOS** (${servicosBloqueados.length})\n\n`; - servicosBloqueados.forEach((servico, index) => { - message += `${index + 1}. 🔴 **${servico.descricao || servico.plano || 'Serviço'}**\n`; - message += ` 🆔 ID: \`${servico.id_servico || servico.id}\`\n`; - - if (servico.usuario) { - message += ` 👤 Login: \`${servico.usuario}\`\n`; - } - - if (servico.servico_valor || servico.valor) { - const valor = parseFloat(servico.servico_valor || servico.valor); - message += ` 💰 Valor: R$ ${valor.toFixed(2)}\n`; - } - - if (servico.data_bloqueio && servico.data_bloqueio !== '0000-00-00') { - message += ` 🕒 Bloqueado em: ${servico.data_bloqueio}\n`; - } - - message += `\n`; - }); - } - - // Exibir serviços desativados - if (servicosDesativados.length > 0) { - message += `\n❌ **SERVIÇOS DESATIVADOS** (${servicosDesativados.length})\n\n`; - servicosDesativados.forEach((servico, index) => { - message += `${index + 1}. ⚫ **${servico.descricao || servico.plano || 'Serviço'}**\n`; - message += ` 🆔 ID: \`${servico.id_servico || servico.id}\`\n`; - - if (servico.data_desativacao && servico.data_desativacao !== '0000-00-00') { - message += ` 🕒 Desativado em: ${servico.data_desativacao}\n`; - } - - message += `\n`; - }); - } - - // Botões de ação - const keyboard = []; - - if (servicosAtivos.length > 0) { - keyboard.push([ - { text: '🔒 Bloquear Todos', callback_data: `confirm_block_all_${cliente.id}_${location}` } - ]); - } - - if (servicosBloqueados.length > 0) { - keyboard.push([ - { text: '🔓 Liberar Todos', callback_data: `confirm_unblock_all_${cliente.id}_${location}` } - ]); - } - - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: keyboard.length > 0 ? { - inline_keyboard: keyboard - } : undefined - }); - - } catch (error) { - console.error('❌ Erro ao listar serviços:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao listar serviços**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Ver faturas do cliente - */ - async handleVerFaturas(chatId, targetOrId, location = '', idServicoFilter = null) { - try { - let idCliente = targetOrId; - - // Se não for um número, buscar cliente primeiro - if (isNaN(targetOrId)) { - await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); - - const resultado = await this.topsapp.buscarCliente(targetOrId, location); - - if (!resultado.success || !resultado.clientes || resultado.clientes.length === 0) { - await this.bot.sendMessage(chatId, - `🔍 **Nenhum cliente encontrado**\n\n` + - `📝 Termo buscado: "${targetOrId}"`, - { parse_mode: 'Markdown' } - ); - return; - } - - idCliente = resultado.clientes[0].id; - location = location || resultado.instancia; - } - - await this.bot.sendMessage(chatId, '📄 Buscando faturas...'); - - const resultado = await this.topsapp.obterFaturasDetalhadas(idCliente, location); - - if (!resultado.success) { - await this.bot.sendMessage(chatId, - `❌ **Erro ao buscar faturas**\n\n` + - `⚠️ ${resultado.error}`, - { parse_mode: 'Markdown' } - ); - return; - } - - let faturasPorServico = resultado.faturasPorServico || []; - - if (faturasPorServico.length === 0) { - await this.bot.sendMessage(chatId, - `📄 **Nenhuma fatura encontrada**\n\n` + - `✅ O cliente não possui faturas cadastradas.`, - { parse_mode: 'Markdown' } - ); - return; - } - - // Se há mais de um serviço com faturas e nenhum filtro foi aplicado, - // perguntar qual serviço o usuário quer ver - if (faturasPorServico.length > 1 && !idServicoFilter) { - let message = `📄 **SELECIONE O SERVIÇO**\n\n`; - message += `🆔 **ID Cliente**: ${idCliente}\n`; - message += `📊 Este cliente possui **${faturasPorServico.length} serviços** com faturas.\n\n`; - - faturasPorServico.forEach((servicoInfo, index) => { - const s = servicoInfo.servico; - const rawStatus = String(s.status || '').toLowerCase(); - let statusText; - if (rawStatus === 'liberado' || rawStatus === '1' || parseInt(s.status) === 1) { - statusText = '🟢 Liberado'; - } else if (rawStatus === 'bloqueado' || rawStatus === '2' || parseInt(s.status) === 2) { - statusText = '🔴 Bloqueado'; - } else if (rawStatus === 'desativado' || rawStatus === '3' || parseInt(s.status) === 3) { - statusText = '⚫ Desativado'; - } else { - statusText = `❓ ${s.status || 'Desconhecido'}`; - } - - message += `*${index + 1}. ${s.nome || 'Serviço'}*\n`; - if (s.login) message += ` 👤 Login PPPoE: \`${s.login}\`\n`; - message += ` 📊 Status: ${statusText}\n`; - if (s.endereco) message += ` 📍 Endereço: ${s.endereco}\n`; - message += ` 🧾 ${servicoInfo.faturas.length} fatura(s)\n\n`; - }); - - message += `Escolha qual serviço deseja ver as faturas:`; - - const keyboard = faturasPorServico.map((servicoInfo, index) => [{ - text: `${index + 1}. ${servicoInfo.servico.login || servicoInfo.servico.nome || 'Serviço'} — ${servicoInfo.faturas.length} fatura(s)`, - callback_data: `faturas_servico_${idCliente}_${servicoInfo.servico.id}_${location}` - }]); - - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: { inline_keyboard: keyboard } - }); - return; - } - - // Filtrar pelo serviço selecionado, se houver - if (idServicoFilter) { - faturasPorServico = faturasPorServico.filter( - s => String(s.servico.id) === String(idServicoFilter) - ); - } - - // Contar total de faturas - let totalFaturas = 0; - faturasPorServico.forEach(s => { - totalFaturas += s.faturas.length; - }); - - let message = `📄 **FATURAS DO CLIENTE**\n\n`; - message += `🆔 **ID Cliente**: ${idCliente}\n`; - message += `📊 **Total de faturas**: ${totalFaturas}\n\n`; - - faturasPorServico.forEach((servicoInfo, sIndex) => { - if (servicoInfo.faturas.length > 0) { - message += `📋 **Serviço**: ${servicoInfo.servico.nome || servicoInfo.servico.descricao || 'Serviço'}\n`; - message += `🆔 **ID**: ${servicoInfo.servico.id}\n`; - if (servicoInfo.servico.login) { - message += `👤 **Login**: ${servicoInfo.servico.login}\n`; - } - if (servicoInfo.servico.endereco) { - message += `📍 **Endereço**: ${servicoInfo.servico.endereco}\n`; - } - message += `\n`; - - servicoInfo.faturas.forEach((fatura, fIndex) => { - const isPago = fatura.status === 'Pago'; - const statusEmoji = isPago ? '✅' : '🔴'; - - message += `${fIndex + 1}. ${statusEmoji} **Fatura #${fatura.id}**\n`; - - if (fatura.valor) { - const valor = parseFloat(fatura.valor); - message += ` 💰 Valor: R$ ${valor.toFixed(2)}\n`; - } - - if (fatura.vencimento) { - message += ` 📅 Vencimento: ${fatura.vencimento}\n`; - } - - message += ` 📊 Status: ${fatura.status}\n`; - - if (fatura.descricao) { - message += ` 📝 Descrição: ${fatura.descricao}\n`; - } - - if (fatura.periodo) { - message += ` 📆 Período: ${fatura.periodo}\n`; - } - - message += `\n`; - }); - - message += `\n`; - } - }); - - // Criar botões para cada fatura com linha digitável/PIX - // Inclui a data de vencimento para melhor identificação - const keyboard = []; - - faturasPorServico.forEach((servicoInfo) => { - servicoInfo.faturas.forEach((fatura) => { - if (fatura.status !== 'Pago') { - const vencLabel = fatura.vencimento ? ` (${fatura.vencimento})` : ''; - const row = []; - - if (fatura.linhaDigitavel) { - row.push({ - text: `📋 Boleto #${fatura.id}${vencLabel}`, - callback_data: `linha_${fatura.id}_${location}` - }); - } - - if (fatura.pixCopiaCola) { - row.push({ - text: `💳 PIX #${fatura.id}${vencLabel}`, - callback_data: `pix_${fatura.id}_${location}` - }); - } - - if (row.length > 0) { - keyboard.push(row); - } - } - }); - }); - - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: keyboard.length > 0 ? { - inline_keyboard: keyboard - } : undefined - }); - - } catch (error) { - console.error('❌ Erro ao buscar faturas:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao buscar faturas**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Gerar boleto - */ - async handleGerarBoleto(chatId, target) { - if (!target) { - await this.bot.sendMessage(chatId, - `💸 **Gerar Boleto**\n\n` + - `💡 **Como usar:**\n` + - `• "gerar boleto João da Silva"\n` + - `• "boleto do cliente 123.456.789-10"` - ); - return; - } - - try { - await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); - - let location = ''; - let query = target; - - if (target.toLowerCase().includes('guarai') || target.toLowerCase().includes('colmeia')) { - location = 'Guarai/Colmeia'; - query = target.replace(/(guarai|colmeia)/gi, '').trim(); - } else if (target.toLowerCase().includes('pedro afonso')) { - location = 'Pedro Afonso'; - query = target.replace(/pedro afonso/gi, '').trim(); - } - - const resultado = await this.topsapp.buscarCliente(query, location); - - if (!resultado.success || !resultado.clientes || resultado.clientes.length === 0) { - await this.bot.sendMessage(chatId, - `🔍 **Nenhum cliente encontrado**\n\n` + - `📝 Termo buscado: "${query}"`, - { parse_mode: 'Markdown' } - ); - return; - } - - const cliente = resultado.clientes[0]; - - await this.handleVerFaturas(chatId, cliente.id, location); - - } catch (error) { - console.error('❌ Erro ao gerar boleto:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao gerar boleto**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Callback para confirmar bloqueio de todos os serviços - */ - async handleConfirmBlockAllCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.replace('confirm_block_all_', '').split('_'); - const idCliente = parts[0]; - const location = parts.slice(1).join('_'); - - try { - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.bot.editMessageText('⏳ Bloqueando serviços...', { - chat_id: chatId, - message_id: callbackQuery.message.message_id - }); - - // Obter serviços do cliente - const servicosResult = await this.topsapp.obterServicos(idCliente, location); - - if (!servicosResult.success) { - await this.bot.sendMessage(chatId, - `❌ **Erro ao buscar serviços**\n\n` + - `⚠️ ${servicosResult.error}`, - { parse_mode: 'Markdown' } - ); - return; - } - - const servicos = servicosResult.servicos || servicosResult.data?.dados || servicosResult.data?.servicos || []; - const servicosAtivos = servicos.filter(s => { - // id_status já vem normalizado da API - return s.id_status === 1 || (s.status && s.status.toLowerCase() === 'liberado'); - }); - - let sucesso = 0; - let falhas = 0; - - for (const servico of servicosAtivos) { - const resultado = await this.topsapp.bloquearServico(servico.id_servico || servico.id, idCliente, location); - if (resultado.success) { - sucesso++; - } else { - falhas++; - } - } - - let message = `🔒 **Bloqueio Concluído**\n\n`; - message += `✅ **Bloqueados**: ${sucesso}\n`; - if (falhas > 0) { - message += `❌ **Falhas**: ${falhas}\n`; - } - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - - } catch (error) { - console.error('❌ Erro ao bloquear serviços:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao bloquear serviços**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Callback para confirmar liberação de todos os serviços - */ - async handleConfirmUnblockAllCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.replace('confirm_unblock_all_', '').split('_'); - const idCliente = parts[0]; - const location = parts.slice(1).join('_'); - - try { - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.bot.editMessageText('⏳ Liberando serviços...', { - chat_id: chatId, - message_id: callbackQuery.message.message_id - }); - - // Obter serviços do cliente - const servicosResult = await this.topsapp.obterServicos(idCliente, location); - - if (!servicosResult.success) { - await this.bot.sendMessage(chatId, - `❌ **Erro ao buscar serviços**\n\n` + - `⚠️ ${servicosResult.error}`, - { parse_mode: 'Markdown' } - ); - return; - } - - const servicos = servicosResult.servicos || servicosResult.data?.dados || servicosResult.data?.servicos || []; - const servicosBloqueados = servicos.filter(s => { - // id_status já vem normalizado da API - return s.id_status === 2 || (s.status && s.status.toLowerCase() === 'bloqueado'); - }); - - let sucesso = 0; - let falhas = 0; - - for (const servico of servicosBloqueados) { - const resultado = await this.topsapp.liberarServico(servico.id_servico || servico.id, idCliente, location); - if (resultado.success) { - sucesso++; - } else { - falhas++; - } - } - - let message = `🔓 **Liberação Concluída**\n\n`; - message += `✅ **Liberados**: ${sucesso}\n`; - if (falhas > 0) { - message += `❌ **Falhas**: ${falhas}\n`; - } - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - - } catch (error) { - console.error('❌ Erro ao liberar serviços:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao liberar serviços**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Callback para confirmar bloqueio de cliente individual - */ - async handleConfirmBlockCallback(callbackQuery, data) { - await this.handleConfirmBlockAllCallback(callbackQuery, data.replace('confirm_block_', 'confirm_block_all_')); - } - - /** - * Callback para confirmar liberação de cliente individual - */ - async handleConfirmUnblockCallback(callbackQuery, data) { - await this.handleConfirmUnblockAllCallback(callbackQuery, data.replace('confirm_unblock_', 'confirm_unblock_all_')); - } - - /** - * Callback para confirmar bloqueio de serviço individual - */ - async handleConfirmBlockServiceCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - // Formato: confirm_block_service_idServico_idCliente_location - const parts = data.replace('confirm_block_service_', '').split('_'); - const idServico = parts[0]; - const idCliente = parts[1]; - const location = parts.slice(2).join('_'); - - try { - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.bot.editMessageText('⏳ Bloqueando serviço...', { - chat_id: chatId, - message_id: callbackQuery.message.message_id - }); - - const resultado = await this.topsapp.bloquearServico(idServico, idCliente, location); - - if (resultado.success) { - await this.bot.sendMessage(chatId, - `✅ **Serviço bloqueado com sucesso!**\n\n` + - `🆔 **ID do Serviço**: ${idServico}`, - { parse_mode: 'Markdown' } - ); - } else { - await this.bot.sendMessage(chatId, - `❌ **Erro ao bloquear serviço**\n\n` + - `⚠️ ${resultado.error}`, - { parse_mode: 'Markdown' } - ); - } - - } catch (error) { - console.error('❌ Erro ao bloquear serviço:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao bloquear serviço**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Callback para confirmar liberação de serviço individual - */ - async handleConfirmUnblockServiceCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - // Formato: confirm_unblock_service_idServico_idCliente_location - const parts = data.replace('confirm_unblock_service_', '').split('_'); - const idServico = parts[0]; - const idCliente = parts[1]; - const location = parts.slice(2).join('_'); - - try { - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.bot.editMessageText('⏳ Liberando serviço...', { - chat_id: chatId, - message_id: callbackQuery.message.message_id - }); - - const resultado = await this.topsapp.liberarServico(idServico, idCliente, location); - - if (resultado.success) { - await this.bot.sendMessage(chatId, - `✅ **Serviço liberado com sucesso!**\n\n` + - `🆔 **ID do Serviço**: ${idServico}`, - { parse_mode: 'Markdown' } - ); - } else { - await this.bot.sendMessage(chatId, - `❌ **Erro ao liberar serviço**\n\n` + - `⚠️ ${resultado.error}`, - { parse_mode: 'Markdown' } - ); - } - - } catch (error) { - console.error('❌ Erro ao liberar serviço:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao liberar serviço**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Callback para dados do cliente - */ - async handleDadosClienteCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.replace('dados_cliente_', '').split('_'); - const idCliente = parts[0]; - const location = parts.slice(1).join('_'); - - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.handleDadosCliente(chatId, idCliente, location); - } - - /** - * Callback para faturas do cliente - */ - async handleFaturasClienteCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.replace('faturas_cliente_', '').split('_'); - const idCliente = parts[0]; - const location = parts.slice(1).join('_'); - - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.handleVerFaturas(chatId, idCliente, location); - } - - /** - * Callback para selecionar serviço específico ao ver faturas - */ - async handleFaturasServicoCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.replace('faturas_servico_', '').split('_'); - const idCliente = parts[0]; - const idServico = parts[1]; - const location = parts.slice(2).join('_'); - - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.handleVerFaturas(chatId, idCliente, location, idServico); - } - - /** - * Callback para serviços do cliente - */ - async handleServicosClienteCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.replace('servicos_cliente_', '').split('_'); - const idCliente = parts[0]; - const location = parts.slice(1).join('_'); - - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.handleListarServicos(chatId, idCliente); - } - - /** - * Callback para exibir linha digitável - */ - async handleLinhaDigitavelCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.replace('linha_', '').split('_'); - const idFatura = parts[0]; - const location = parts.slice(1).join('_'); - - try { - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.bot.sendMessage(chatId, '⏳ Buscando linha digitável...'); - - // Buscar dados da fatura - const resultado = await this.topsapp.obterDadosFatura(idFatura, location); - - if (!resultado.success) { - await this.bot.sendMessage(chatId, - `❌ **Erro ao buscar linha digitável**\n\n` + - `⚠️ ${resultado.error}`, - { parse_mode: 'Markdown' } - ); - return; - } - - const dadosFatura = resultado.data?.fatura || resultado.data?.dados || resultado.data; - - if (!dadosFatura || !dadosFatura.linha_digitavel) { - await this.bot.sendMessage(chatId, - `⚠️ **Linha digitável não disponível**\n\n` + - `A fatura #${idFatura} não possui linha digitável.`, - { parse_mode: 'Markdown' } - ); - return; - } - - let message = `📋 **LINHA DIGITÁVEL**\n\n`; - message += `🆔 **Fatura**: #${idFatura}\n`; - message += `💰 **Valor**: R$ ${parseFloat(dadosFatura.valor || dadosFatura.valor_atualizado || 0).toFixed(2)}\n`; - message += `📅 **Vencimento**: ${dadosFatura.vencimento || dadosFatura.vencimento_atualizado || 'N/A'}\n\n`; - message += `📋 **Linha Digitável**:\n\`${dadosFatura.linha_digitavel}\`\n\n`; - message += `💡 _Copie a linha digitável acima para pagar pelo app do banco_`; - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - - } catch (error) { - console.error('❌ Erro ao buscar linha digitável:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao buscar linha digitável**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Callback para exibir PIX copia e cola - */ - async handlePixCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.replace('pix_', '').split('_'); - const idFatura = parts[0]; - const location = parts.slice(1).join('_'); - - try { - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.bot.sendMessage(chatId, '⏳ Buscando código PIX...'); - - // Buscar dados da fatura - const resultado = await this.topsapp.obterDadosFatura(idFatura, location); - - if (!resultado.success) { - await this.bot.sendMessage(chatId, - `❌ **Erro ao buscar código PIX**\n\n` + - `⚠️ ${resultado.error}`, - { parse_mode: 'Markdown' } - ); - return; - } - - const dadosFatura = resultado.data?.fatura || resultado.data?.dados || resultado.data; - - const pixCode = dadosFatura?.qrcode || dadosFatura?.pix_copia_cola || dadosFatura?.codigo_pix; - - if (!dadosFatura || !pixCode) { - await this.bot.sendMessage(chatId, - `⚠️ **PIX não disponível**\n\n` + - `A fatura #${idFatura} não possui código PIX.`, - { parse_mode: 'Markdown' } - ); - return; - } - - let message = `💳 **PIX COPIA E COLA**\n\n`; - message += `🆔 **Fatura**: #${idFatura}\n`; - message += `💰 **Valor**: R$ ${parseFloat(dadosFatura.valor || dadosFatura.valor_atualizado || 0).toFixed(2)}\n`; - message += `📅 **Vencimento**: ${dadosFatura.vencimento || dadosFatura.vencimento_atualizado || 'N/A'}\n\n`; - message += `💳 **PIX Copia e Cola**:\n\`${pixCode}\`\n\n`; - message += `💡 _Copie o código acima e cole no app do banco na opção PIX Copia e Cola_`; - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - - } catch (error) { - console.error('❌ Erro ao buscar código PIX:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao buscar código PIX**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Escapar caracteres problemáticos para Markdown do Telegram - */ - escapeMarkdown(text) { - if (!text) return text; - - // Escapar caracteres especiais que podem causar problemas no Markdown - return text.toString() - .replace(/\\/g, '\\\\') // Escape backslash - .replace(/\*/g, '\\*') // Escape asterisco - .replace(/_/g, '\\_') // Escape underscore - .replace(/\[/g, '\\[') // Escape colchete - .replace(/\]/g, '\\]') // Escape colchete - .replace(/\(/g, '\\(') // Escape parêntese - .replace(/\)/g, '\\)') // Escape parêntese - .replace(/~/g, '\\~') // Escape til - .replace(/>/g, '\\>') // Escape maior que - .replace(/#/g, '\\#') // Escape hashtag - .replace(/\+/g, '\\+') // Escape mais - .replace(/-/g, '\\-') // Escape hífen - .replace(/=/g, '\\=') // Escape igual - .replace(/\|/g, '\\|') // Escape pipe - .replace(/\{/g, '\\{') // Escape chaves - .replace(/\}/g, '\\}') // Escape chaves - .replace(/\./g, '\\.') // Escape ponto - .replace(/!/g, '\\!') // Escape exclamação - .replace(/@/g, '\\@'); // Escape arroba - } - - /** - * Mostrar dados cadastrais completos do cliente - */ - async handleDadosCliente(chatId, idCliente, location = '') { - try { - await this.bot.sendMessage(chatId, '📋 Carregando dados cadastrais...'); - - const resultado = await this.topsapp.obterDadosCadastrais(idCliente, location); - - if (!resultado.success) { - await this.bot.sendMessage(chatId, - `❌ Erro ao buscar dados\n\n` + - `⚠️ ${resultado.error}` - ); - return; - } - - const cliente = resultado.cliente; - const servicos = resultado.servicos || []; - - let message = `📋 Dados Cadastrais Completos\n\n`; - - // Dados do cliente - message += `👤 CLIENTE\n`; - message += `🆔 ID: ${cliente.id}\n`; - message += `👤 Nome: ${cliente.nome || 'Não informado'}\n`; - - if (cliente.cpf) { - message += `📄 CPF: \`${cliente.cpf}\`\n`; - } - - if (cliente.rg) { - message += `📄 RG: ${cliente.rg}\n`; - } - - message += `📧 Email: ${cliente.email || 'Não informado'}\n`; - - if (cliente.telefone && cliente.telefone !== 'Não informado') { - message += `📞 Telefone: \`${cliente.telefone}\`\n`; - } else { - message += `📞 Telefone: Não informado\n`; - } - - if (cliente.celular && cliente.celular !== 'Não informado') { - message += `📱 Celular: \`${cliente.celular}\`\n`; - } else { - message += `📱 Celular: Não informado\n`; - } - - // Endereço - if (cliente.endereco) { - message += `📍 Endereço: ${cliente.endereco}`; - if (cliente.numero) { - message += `, ${cliente.numero}`; - } - message += `\n`; - } - - if (cliente.complemento) { - message += `📝 Complemento: ${cliente.complemento}\n`; - } - - message += `🏘️ Bairro: ${cliente.bairro || 'Não informado'}\n`; - message += `🏙️ Cidade: ${cliente.cidade || 'Não informado'}`; - - if (cliente.uf) { - message += ` - ${cliente.uf}`; - } - message += `\n`; - - message += `📮 CEP: ${cliente.cep || 'Não informado'}\n`; - message += `📊 Status Geral: ${cliente.status === 1 ? '✅ Ativo' : '❌ Inativo'}\n`; - - if (cliente.data_cadastro) { - message += `📅 Cadastro: ${this.formatarData(cliente.data_cadastro)}\n`; - } - - if (cliente.observacoes) { - message += `📝 Observações: ${cliente.observacoes}\n`; - } - - message += `\n🌐 Instância: ${resultado.instancia}\n\n`; - - // Dados dos serviços - if (servicos.length > 0) { - message += `⚙️ **SERVIÇOS CONTRATADOS**\n\n`; - - servicos.forEach((servico, index) => { - message += `📦 Serviço ${index + 1}\n`; - message += `🏷️ Plano: ${servico.planoNome}\n`; - message += `📊 Status: ${servico.statusFormatado}\n`; - - if (servico.login) { - message += `👤 Login: ${servico.login}\n`; - } - - if (servico.senha) { - message += `🔑 Senha: ${servico.senha}\n`; - } - - if (servico.ip) { - message += `🌐 IP: ${servico.ip}\n`; - } - - if (servico.vencimento) { - message += `📅 Vencimento: Dia ${servico.vencimento}\n`; - } - - if (servico.valor) { - message += `💰 Valor: R$ ${parseFloat(servico.valor).toFixed(2)}\n`; - } - - message += `🔧 Tecnologia: ${servico.tecnologia}\n`; - - if (servico.velocidade_down) { - message += `⬇️ Download: ${servico.velocidade_down} Mbps\n`; - } - - if (servico.velocidade_up) { - message += `⬆️ Upload: ${servico.velocidade_up} Mbps\n`; - } - - // Dados FTTH - usando campos reais da API - if (servico.tecnologia === 'FTTH' || servico.pon || servico.serial) { - message += `\n📡 Dados FTTH:\n`; - - if (servico.base || servico.id_ap) { - message += `🔌 Base/AP: ${servico.base || `AP ${servico.id_ap}`}\n`; - } - - if (servico.pon) { - message += `🔗 PON: ${servico.pon}\n`; - } - - if (servico.perfil) { - message += `⚙️ Perfil: ${servico.perfil}\n`; - } - - if (servico.serial) { - message += `📟 Serial ONU: \`${servico.serial}\`\n`; - } - - if (servico.serviceport) { - message += `🔌 Service Port: ${servico.serviceport}\n`; - } - - if (servico.ultimo_rx_power) { - message += `📶 Último RX: ${servico.ultimo_rx_power} dBm\n`; - } - - if (servico.ultimo_tx_power) { - message += `📡 Último TX: ${servico.ultimo_tx_power} dBm\n`; - } - - if (servico.data_ultimo_sinal) { - message += `📅 Último sinal: ${this.formatarData(servico.data_ultimo_sinal)} ${servico.hora_ultimo_sinal || ''}\n`; - } - - if (servico.conexao) { - const statusConexao = servico.conexao === 'online' ? '🟢 Online' : '🔴 Offline'; - message += `🔌 Conexão: ${statusConexao}\n`; - } - - if (servico.ultimo_acesso) { - message += `🕐 Último acesso: ${servico.ultimo_acesso}\n`; - } - - if (servico.ultimo_ip) { - message += `🌐 Último IP: ${servico.ultimo_ip}\n`; - } - } - - // Dados DHCP / IPoE - if (servico.dhcp_username || servico.dhcp_agent_circuit_id) { - message += `\n🔗 Dados IPoE (DHCP Option 82):\n`; - - if (servico.dhcp_username) { - message += `👤 DHCP Username: \`${servico.dhcp_username}\`\n`; - } - - if (servico.dhcp_agent_circuit_id) { - message += `🔌 Agent Circuit ID: \`${servico.dhcp_agent_circuit_id}\`\n`; - } - } - - // Dados de instalação - if (servico.instalacao_endereco) { - message += `\n📍 Endereço de Instalação:\n`; - message += `🏠 ${servico.instalacao_endereco}, ${servico.instalacao_numero || 'S/N'}\n`; - - if (servico.instalacao_complemento) { - message += `📝 Complemento: ${servico.instalacao_complemento}\n`; - } - - message += `🏘️ ${servico.instalacao_bairro}, ${servico.instalacao_cidade}/${servico.instalacao_uf}\n`; - - if (servico.instalacao_cep) { - message += `📮 CEP: ${servico.instalacao_cep}\n`; - } - - if (servico.latitude && servico.longitude) { - message += `🗺️ Coordenadas: ${servico.latitude}, ${servico.longitude}\n`; - } - } - - message += `\n`; - }); - } else { - message += `⚙️ SERVIÇOS: Nenhum serviço encontrado\n\n`; - } - - // Botões de ação - const keyboard = [ - [ - { text: '📄 Ver Faturas', callback_data: `faturas_cliente_${idCliente}_${location}` }, - { text: '⚙️ Listar Serviços', callback_data: `servicos_cliente_${idCliente}_${location}` } - ], - [ - { text: '🔒 Bloquear Cliente', callback_data: `confirm_block_${idCliente}_${location}` }, - { text: '🔓 Liberar Cliente', callback_data: `confirm_unblock_${idCliente}_${location}` } - ] - ]; - - // Adicionar botão para gerar PDF da primeira fatura (se houver) - if (resultado.faturasPorServico && - resultado.faturasPorServico.length > 0 && - resultado.faturasPorServico[0].faturas && - resultado.faturasPorServico[0].faturas.length > 0) { - const primeiraFatura = resultado.faturasPorServico[0].faturas[0]; - keyboard.push([ - { text: '📄 Gerar Boleto PDF', callback_data: `boleto_pdf_${primeiraFatura.id}_${location}` } - ]); - } - - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: keyboard - } - }); - - } catch (error) { - console.error('❌ Erro ao buscar faturas detalhadas:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao buscar faturas**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Buscar e mostrar dados cadastrais do cliente - */ - async handleDadosClientePorBusca(chatId, target) { - if (!target) { - await this.bot.sendMessage(chatId, - `📋 **Dados Cadastrais**\n\n` + - `💡 **Como usar:**\n` + - `• "dados cliente João da Silva"\n` + - `• "dados cadastrais 123.456.789-10"\n` + - `• "dados cliente 12345" (ID do cliente)\n` + - `• "dados cliente Guarai João" (específica instância)` - ); - return; - } - - try { - // Primeiro, buscar o cliente - let location = ''; - let query = target; - - if (target.toLowerCase().includes('guarai') || target.toLowerCase().includes('colmeia')) { - location = 'Guarai/Colmeia'; - query = target.replace(/(guarai|colmeia)/gi, '').trim(); - } else if (target.toLowerCase().includes('pedro afonso')) { - location = 'Pedro Afonso'; - query = target.replace(/pedro afonso/gi, '').trim(); - } - - const busca = await this.topsapp.buscarCliente(query, location); - - if (!busca.success) { - await this.bot.sendMessage(chatId, - `❌ **Erro na busca**\n\n` + - `⚠️ ${busca.error}`, - { parse_mode: 'Markdown' } - ); - return; - } - - if (!busca.clientes || busca.clientes.length === 0) { - await this.bot.sendMessage(chatId, - `🔍 **Nenhum cliente encontrado**\n\n` + - `📝 Termo buscado: "${query}"\n` + - `🌐 Instância: ${busca.instancia || 'Todas'}`, - { parse_mode: 'Markdown' } - ); - return; - } - - // Se encontrou mais de um, mostrar lista - if (busca.clientes.length > 1) { - let message = `📋 **${busca.clientes.length} clientes encontrados**\n\n`; - message += `🌐 **Instância**: ${busca.instancia}\n\n`; - message += `💡 **Selecione um cliente para ver os dados cadastrais:**\n\n`; - - const keyboard = []; - - busca.clientes.slice(0, 10).forEach((cliente, index) => { - message += `👤 **${index + 1}. ${cliente.nome}**\n`; - message += `🆔 ID: \`${cliente.id}\`\n`; - if (cliente.cpf) { - message += `📄 CPF: \`${cliente.cpf}\`\n`; - } - message += `📊 Status: ${cliente.status === 1 ? '✅ Ativo' : '❌ Inativo'}\n\n`; - - keyboard.push([ - { text: `📋 Ver Dados ${index + 1}`, callback_data: `dados_cliente_${cliente.id}_${location}` } - ]); - }); - - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: keyboard - } - }); - return; - } - - // Se encontrou apenas um, mostrar dados diretamente - const cliente = busca.clientes[0]; - await this.handleDadosCliente(chatId, cliente.id, location || busca.instancia); - - } catch (error) { - console.error('❌ Erro ao buscar dados do cliente:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro ao buscar dados do cliente**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Formatardata para DD/MM/YYYY - */ - formatarData(data) { - if (!data) return 'Data não informada'; - - // Se já está no formato DD/MM/YYYY, retornar como está - if (data.match(/^\d{2}\/\d{2}\/\d{4}$/)) { - return data; - } - - // Se está no formato YYYY-MM-DD, converter - if (data.match(/^\d{4}-\d{2}-\d{2}$/)) { - const [ano, mes, dia] = data.split('-'); - return `${dia}/${mes}/${ano}`; - } - - // Tentar converter outros formatos - try { - const dataObj = new Date(data); - if (!isNaN(dataObj.getTime())) { - const dia = String(dataObj.getDate()).padStart(2, '0'); - const mes = String(dataObj.getMonth() + 1).padStart(2, '0'); - const ano = dataObj.getFullYear(); - return `${dia}/${mes}/${ano}`; - } - } catch (error) { - console.warn('Erro ao formatar data:', data, error); - } - - // Se não conseguiu formatar, retornar original - return data; - } - - /** - * Verificar status de ONU Fiberhome - */ - async handleStatusONU(chatId, target) { - if (!target) { - await this.bot.sendMessage(chatId, - `📡 **Status ONU Fiberhome**\n\n` + - `💡 **Como usar:**\n` + - `• "status onu FHTT12345678"\n` + - `• "verificar onu FHTT12345678"\n` + - `• "onu status FHTT12345678"\n\n` + - `📝 **Nota:** Informe o serial da ONU (12 caracteres)`, - { parse_mode: 'Markdown' } - ); - return; - } - - try { - await this.bot.sendMessage(chatId, '📡 Conectando à OLT Fiberhome...'); - - logger.botCommand(chatId, 'status_onu', { target }); - - // Verificar se é serial ou device name - let resultado; - if (target.length === 12 && /^[A-Z0-9]+$/.test(target)) { - // É um serial - resultado = await this.fiberhome.obterStatusONU(target); - } else { - // Pode ser device name - resultado = await this.fiberhome.obterStatusONUPorNome(target); - } - - if (resultado.success) { - // Formatar informações da ONU - let message = `📡 **Status ONU Fiberhome**\n\n`; - message += `🆔 **Serial:** ${resultado.serial}\n`; - message += `📋 **Device Name:** ${resultado.deviceName}\n`; - message += `📍 **Posição:** Slot ${resultado.slot}, PON ${resultado.pon}, ONU ${resultado.onuId}\n`; - message += `🔌 **Status:** ${resultado.online ? '🟢 Online' : '🔴 Offline'}\n\n`; - - // Informações de sinal - message += `📶 **Informações de Sinal:**\n`; - message += `📡 **RX Power:** ${resultado.rxPower}\n`; - message += `📤 **TX Power:** ${resultado.txPower}\n`; - message += `📏 **Distância:** ${resultado.distance}\n\n`; - - // Status das portas Ethernet (resumido) - if (resultado.portasEthernet.length > 0) { - message += `🔗 **Portas Ethernet:**\n`; - resultado.portasEthernet.forEach(porta => { - const statusIcon = porta.status === 'up' ? '🟢' : '🔴'; - message += ` ${statusIcon} **Porta ${porta.porta}:** ${porta.status}\n`; - }); - message += '\n'; - } - - // Serviços WAN - if (resultado.servicosWAN.length > 0) { - message += `🌐 **Serviços WAN:**\n`; - resultado.servicosWAN.forEach(servico => { - const statusIcon = servico.status === 'active' ? '🟢' : '🔴'; - message += ` ${statusIcon} **Serviço ${servico.id}:** ${servico.status}\n`; - if (servico.ip !== 'N/A') { - message += ` 📍 **IP:** ${servico.ip}\n`; - } - }); - } else { - message += `🌐 **Serviços WAN:** Nenhum configurado\n`; - } - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - - } else { - await this.bot.sendMessage(chatId, `❌ ONU ${target} não encontrada na OLT Fiberhome.`); - - // Oferecer pesquisar em OLTs Datacom - const keyboard = { - inline_keyboard: [ - [ - { text: '🔍 Pesquisar em OLT Datacom', callback_data: `ver_sinal_${target}` }, - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ] - }; - - await this.bot.sendMessage(chatId, 'ONU não encontrada na Fiberhome. Deseja pesquisar nas OLTs Datacom?', { - reply_markup: keyboard - }); - } - } catch (error) { - logger.botError(chatId, error, 'handleStatusONU'); - - // Escapar caracteres especiais para evitar problemas no Telegram - const errorMsg = error.message ? error.message.replace(/[_*\[\]()~`>#+=|{}.!-]/g, '\\$&') : 'Erro desconhecido'; - - await this.bot.sendMessage(chatId, - `❌ **Erro ao verificar status da ONU**\n\n` + - `⚠️ ${errorMsg}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Verificar status das conexões Fiberhome - */ - async handleFiberhomeStatus(chatId) { - try { - await this.bot.sendMessage(chatId, '🔌 Diagnosticando conexões Fiberhome...'); - - logger.botCommand(chatId, 'fiberhome_status'); - - // Fazer diagnóstico completo - const diagnostico = await this.fiberhome.diagnosticarConectividade(); - - let message = `🔌 **Diagnóstico Fiberhome**\n\n`; - - // Status OLT - message += `📡 **OLT (${diagnostico.olt.host}:${diagnostico.olt.port}):**\n`; - message += ` 🌐 **Conectividade:** ${diagnostico.olt.conectividade ? '🟢 OK' : '🔴 Falhou'}\n`; - message += ` � **Telnet:** ${diagnostico.olt.telnet ? '🟢 Conectado' : '🔴 Falhou'}\n`; - if (diagnostico.olt.erro) { - message += ` ⚠️ **Erro:** ${diagnostico.olt.erro}\n`; - } - message += '\n'; - - // Status UNM2000 - message += `�️ **UNM2000 (${diagnostico.unm2000.host}:${diagnostico.unm2000.port}):**\n`; - message += ` 🌐 **Conectividade:** ${diagnostico.unm2000.conectividade ? '🟢 OK' : '🔴 Falhou'}\n`; - message += ` � **TL1:** ${diagnostico.unm2000.tl1 ? '🟢 Conectado' : '🔴 Falhou'}\n`; - if (diagnostico.unm2000.erro) { - message += ` ⚠️ **Erro:** ${diagnostico.unm2000.erro}\n`; - } - message += '\n'; - - // Configurações - message += `⚙️ **Configurações:**\n`; - message += ` 📡 **OLT:** GEPON/GEPON\n`; - message += ` 🖥️ **UNM2000:** 1/1\n\n`; - - // Comandos disponíveis - message += `💡 **Comandos disponíveis:**\n`; - message += `• \`status onu FHTT12345678\` - Status de ONU específica\n`; - message += `• \`verificar onu FHTT12345678\` - Verificar ONU\n`; - message += `• \`onu status DeviceName\` - Status por nome do dispositivo\n\n`; - - // Status geral - const oltOk = diagnostico.olt.conectividade && diagnostico.olt.telnet; - const unm2000Ok = diagnostico.unm2000.conectividade && diagnostico.unm2000.tl1; - - if (oltOk && unm2000Ok) { - message += `✅ **Status:** Sistema funcionando normalmente`; - } else if (oltOk || unm2000Ok) { - message += `⚠️ **Status:** Funcionamento parcial`; - } else { - message += `❌ **Status:** Sistema indisponível`; - } - - await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); - - } catch (error) { - logger.botError(chatId, error, 'handleFiberhomeStatus'); - await this.bot.sendMessage(chatId, - `❌ **Erro ao diagnosticar Fiberhome**\n\n` + - `⚠️ ${error.message}`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Iniciar processo interativo de abertura de suporte - */ - async iniciarProcessoSuporte(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; - } - - // Criar sessão de suporte - const sessionId = `${userId}_${chatId}_${Date.now()}`; - const suporteSession = { - id: sessionId, - chatId, - userId, - etapa: 'buscar_cliente', - dados: {}, - iniciado: new Date() - }; - - this.suporteSessions.set(sessionId, suporteSession); - - await this.bot.sendMessage(chatId, - `🎫 **Abertura de Suporte**\n\n` + - `🔍 **Etapa 1:** Identificar Cliente\n\n` + - `Por favor, digite o **nome** ou **CPF** do cliente para quem deseja abrir o suporte:`, - { parse_mode: 'Markdown' } - ); - - // Se foi fornecido um target, usar como busca inicial - if (target && target.trim()) { - await this.processarEtapaSuporteBuscarCliente(sessionId, target.trim()); - } - } - - /** - * Processar mensagem dentro do fluxo de suporte - */ - async processarMensagemSuporte(msg) { - const chatId = msg.chat.id; - const userId = msg.from.id; - const text = msg.text; - - // Encontrar sessão ativa para este usuário - const sessaoAtiva = Array.from(this.suporteSessions.values()).find( - session => session.chatId === chatId && session.userId === userId - ); - - if (!sessaoAtiva) { - return false; // Não há sessão ativa - } - - try { - switch (sessaoAtiva.etapa) { - case 'buscar_cliente': - await this.processarEtapaSuporteBuscarCliente(sessaoAtiva.id, text); - break; - - case 'selecionar_cliente': - await this.processarEtapaSelecionarCliente(sessaoAtiva.id, text); - break; - - case 'selecionar_servico': - await this.processarEtapaSelecionarServico(sessaoAtiva.id, text); - break; - - case 'selecionar_categoria': - await this.processarEtapaSelecionarCategoria(sessaoAtiva.id, text); - break; - - case 'selecionar_tecnico': - await this.processarEtapaSelecionarTecnico(sessaoAtiva.id, text); - break; - - case 'definir_data_hora': - await this.processarEtapaDefinirDataHora(sessaoAtiva.id, text); - break; - - case 'adicionar_descricao': - await this.processarEtapaAdicionarDescricao(sessaoAtiva.id, text); - break; - - 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...'); - this.suporteSessions.delete(sessaoAtiva.id); - break; - } - - return true; // Mensagem foi processada pelo fluxo de suporte - - } catch (error) { - logger.error('Erro no processamento de suporte:', error); - await this.bot.sendMessage(chatId, - `❌ **Erro no processamento**\n\n` + - `⚠️ ${error.message}\n\n` + - `🔄 Reiniciando processo de suporte...` - ); - this.suporteSessions.delete(sessaoAtiva.id); - return true; - } - } - - /** - * Etapa 1: Buscar cliente - */ - async processarEtapaSuporteBuscarCliente(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) { - await this.bot.sendMessage(session.chatId, - `❌ **Erro na busca**\n\n` + - `⚠️ ${resultado.error}\n\n` + - `Por favor, tente novamente com outro nome ou CPF:` - ); - return; - } - - if (!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) { - // Apenas um cliente encontrado, selecionar automaticamente - session.dados.clienteSelecionado = resultado.clientes[0]; - session.etapa = 'selecionar_servico'; - await this.processarEtapaSelecionarServico(sessionId); - } else { - // Múltiplos clientes, deixar usuário escolher - session.etapa = 'selecionar_cliente'; - await this.mostrarOpcoesClientes(session); - } - - } catch (error) { - await this.bot.sendMessage(session.chatId, - `❌ **Erro na busca**\n\n` + - `⚠️ ${error.message}\n\n` + - `Por favor, tente novamente:` - ); - } - } - - /** - * Mostrar opções de clientes encontrados - */ - async mostrarOpcoesClientes(session) { - let message = `👥 **Clientes encontrados:**\n\n`; - - const buttons = []; - - session.dados.clientesEncontrados.forEach((cliente, index) => { - const status = this.topsapp.interpretarStatusCliente ? - this.topsapp.interpretarStatusCliente(cliente) : - cliente.status; - const statusEmoji = status === 1 ? '🟢' : '🔴'; - - message += `**${index + 1}.** ${statusEmoji} ${cliente.nome}\n`; - message += ` 📄 CPF: ${cliente.cpf || 'N/A'}\n`; - message += ` 🆔 ID: ${cliente.id}\n\n`; - - // Adicionar botão para seleção direta - buttons.push([{ - text: `${index + 1}. ${cliente.nome}`, - callback_data: `suporte_cliente_${index}` - }]); - }); - - message += `Selecione o cliente clicando no botão ou digite o **número** (1-${session.dados.clientesEncontrados.length}):`; - - const keyboard = { - inline_keyboard: buttons - }; - - await this.bot.sendMessage(session.chatId, message, { - parse_mode: 'Markdown', - reply_markup: keyboard - }); - } - - /** - * Etapa 2: Selecionar cliente específico - */ - async processarEtapaSelecionarCliente(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**\n\n` + - `Por favor, digite um número entre 1 e ${session.dados.clientesEncontrados.length}:` - ); - return; - } - - session.dados.clienteSelecionado = session.dados.clientesEncontrados[numero - 1]; - session.etapa = 'selecionar_servico'; - await this.processarEtapaSelecionarServico(sessionId); - } - - /** - * Etapa 3: Selecionar serviço - */ - async processarEtapaSelecionarServico(sessionId, input = null) { - const session = this.suporteSessions.get(sessionId); - if (!session) return; - - const cliente = session.dados.clienteSelecionado; - - if (!input) { - // Primeira vez nesta etapa - buscar serviços - await this.bot.sendMessage(session.chatId, - `🔍 Buscando serviços do cliente **${cliente.nome}**...`, - { parse_mode: 'Markdown' } - ); - - try { - const resultado = await this.topsapp.obterServicosCliente(cliente.id); - - console.log(`🔍 Handler - Resultado completo:`, { - success: resultado.success, - servicosCount: resultado.servicos?.length, - total: resultado.total, - debug: resultado.debug - }); - - if (!resultado.success) { - await this.bot.sendMessage(session.chatId, - `❌ **Erro ao buscar serviços**\n\n` + - `⚠️ ${resultado.error}\n\n` + - `🔄 Reiniciando processo...` - ); - this.suporteSessions.delete(sessionId); - return; - } - - if (!resultado.servicos || resultado.servicos.length === 0) { - await this.bot.sendMessage(session.chatId, - `❌ **Nenhum serviço encontrado**\n\n` + - `O cliente não possui serviços cadastrados.\n\n` + - `🔄 Reiniciando processo...` - ); - this.suporteSessions.delete(sessionId); - return; - } - - session.dados.servicos = resultado.servicos; - await this.mostrarOpcoesServicos(session); - - } catch (error) { - await this.bot.sendMessage(session.chatId, - `❌ **Erro ao buscar serviços**\n\n` + - `⚠️ ${error.message}\n\n` + - `🔄 Reiniciando processo...` - ); - this.suporteSessions.delete(sessionId); - } - } else { - // Usuário selecionou um serviço - const numero = parseInt(input.trim()); - - if (isNaN(numero) || numero < 1 || numero > session.dados.servicos.length) { - await this.bot.sendMessage(session.chatId, - `❌ **Número inválido**\n\n` + - `Por favor, digite um número entre 1 e ${session.dados.servicos.length}:` - ); - return; - } - - session.dados.servicoSelecionado = session.dados.servicos[numero - 1]; - session.etapa = 'selecionar_categoria'; - await this.processarEtapaSelecionarCategoria(sessionId); - } - } - - /** - * Mostrar opções de serviços - */ - async mostrarOpcoesServicos(session) { - let message = `📋 **Serviços disponíveis:**\n\n`; - - const buttons = []; - - session.dados.servicos.forEach((servico, index) => { - const statusEmoji = servico.statusNumerico === 1 ? '🟢' : '🔴'; - - // Extrair usuário sem o domínio - const usuarioRaw = servico.usuario || servico.login || servico.nome || ''; - const usuario = usuarioRaw.includes('@') ? usuarioRaw.split('@')[0] : usuarioRaw; - - // Montar endereço de instalação - const endParts = [ - servico.instalacao_endereco, - servico.instalacao_numero - ].filter(Boolean); - const endereco = endParts.length > 0 ? endParts.join(', ') : null; - - message += `**${index + 1}.** ${statusEmoji} ${usuario}\n`; - if (endereco) message += ` 📍 ${endereco}\n`; - message += ` 🆔 ID: ${servico.id}\n`; - message += `\n`; - - // Adicionar botão para seleção direta - const labelBtn = endereco ? `${index + 1}. ${usuario} — ${endereco}` : `${index + 1}. ${usuario}`; - buttons.push([{ - text: labelBtn, - callback_data: `suporte_servico_${index}` - }]); - }); - - message += `Selecione o serviço clicando no botão ou digite o **número** (1-${session.dados.servicos.length}):`; - - const keyboard = { - inline_keyboard: buttons - }; - - await this.bot.sendMessage(session.chatId, message, { - parse_mode: 'Markdown', - reply_markup: keyboard - }); - } - - /** - * Etapa 4: Selecionar categoria - */ - async processarEtapaSelecionarCategoria(sessionId, input = null) { - const session = this.suporteSessions.get(sessionId); - if (!session) return; - - if (!input) { - // Primeira vez nesta etapa - buscar categorias - await this.bot.sendMessage(session.chatId, `🔍 Buscando categorias de suporte...`); - - try { - const resultado = await this.topsapp.obterSuporteCategorias(); - - if (!resultado.success) { - await this.bot.sendMessage(session.chatId, - `❌ **Erro ao buscar categorias**\n\n` + - `⚠️ ${resultado.error}\n\n` + - `🔄 Reiniciando processo...` - ); - this.suporteSessions.delete(sessionId); - return; - } - - if (!resultado.data?.dados || resultado.data.dados.length === 0) { - await this.bot.sendMessage(session.chatId, - `❌ **Nenhuma categoria encontrada**\n\n` + - `🔄 Reiniciando processo...` - ); - this.suporteSessions.delete(sessionId); - return; - } - - session.dados.categorias = resultado.data.dados; - await this.mostrarOpcoesCategorias(session); - - } catch (error) { - await this.bot.sendMessage(session.chatId, - `❌ **Erro ao buscar categorias**\n\n` + - `⚠️ ${error.message}\n\n` + - `🔄 Reiniciando processo...` - ); - this.suporteSessions.delete(sessionId); - } - } else { - // Usuário selecionou uma categoria - const numero = parseInt(input.trim()); - - if (isNaN(numero) || numero < 1 || numero > session.dados.categorias.length) { - await this.bot.sendMessage(session.chatId, - `❌ **Número inválido**\n\n` + - `Por favor, digite um número entre 1 e ${session.dados.categorias.length}:` - ); - return; - } - - session.dados.categoriaSelecionada = session.dados.categorias[numero - 1]; - session.etapa = 'selecionar_tecnico'; - await this.processarEtapaSelecionarTecnico(sessionId); - } - } - - /** - * Mostrar opções de categorias - */ - async mostrarOpcoesCategorias(session) { - let message = `📁 **Categorias de suporte:**\n\n`; - - // Verificar se categorias é um array válido - if (!Array.isArray(session.dados.categorias)) { - console.error(`❌ session.dados.categorias não é um array:`, { - type: typeof session.dados.categorias, - value: session.dados.categorias - }); - throw new Error(`Dados de categorias inválidos: ${typeof session.dados.categorias}`); - } - - session.dados.categorias.forEach((categoria, index) => { - message += `**${index + 1}.** ${categoria.descricao}\n`; - }); - - message += `\nDigite o **número** da categoria do suporte (1-${session.dados.categorias.length}):`; - - await this.bot.sendMessage(session.chatId, message, { parse_mode: 'Markdown' }); - } - - /** - * Etapa 5: Selecionar técnico - */ - async processarEtapaSelecionarTecnico(sessionId, input = null) { - const session = this.suporteSessions.get(sessionId); - if (!session) return; - - if (!input) { - // Primeira vez nesta etapa - buscar equipes/técnicos via operadores - await this.bot.sendMessage(session.chatId, `🔍 Buscando técnicos disponíveis...`); - - try { - const location = session.dados.location || ''; - - // ObterOperadores traz o campo id_tecnico/tecnico que mapeia as equipes cadastradas - const resultado = await this.topsapp.obterOperadores(location); - - if (!resultado.success) { - await this.bot.sendMessage(session.chatId, - `❌ **Erro ao buscar técnicos**\n\n` + - `⚠️ ${resultado.error}\n\n` + - `🔄 Reiniciando processo...` - ); - this.suporteSessions.delete(sessionId); - return; - } - - const operadores = resultado.data?.dados || []; - - // Extrair técnicos únicos pelo id_tecnico (cada operador aponta para uma equipe) - const tecnicosMap = new Map(); - operadores.forEach(op => { - if (op.id_tecnico > 0 && op.tecnico && op.tecnico.trim() !== '') { - if (!tecnicosMap.has(op.id_tecnico)) { - tecnicosMap.set(op.id_tecnico, { id: op.id_tecnico, nome: op.tecnico }); - } - } - }); - - const tecnicos = Array.from(tecnicosMap.values()).sort((a, b) => a.nome.localeCompare(b.nome)); - - if (tecnicos.length === 0) { - await this.bot.sendMessage(session.chatId, - `❌ **Nenhum técnico encontrado**\n\n` + - `🔄 Reiniciando processo...` - ); - this.suporteSessions.delete(sessionId); - return; - } - - session.dados.tecnicos = tecnicos; - await this.mostrarOpcoesTecnicos(session); - - } catch (error) { - await this.bot.sendMessage(session.chatId, - `❌ **Erro ao buscar técnicos**\n\n` + - `⚠️ ${error.message}\n\n` + - `🔄 Reiniciando processo...` - ); - this.suporteSessions.delete(sessionId); - } - } else { - // Usuário selecionou um técnico - const numero = parseInt(input.trim()); - - if (isNaN(numero) || numero < 1 || numero > session.dados.tecnicos.length) { - await this.bot.sendMessage(session.chatId, - `❌ **Número inválido**\n\n` + - `Por favor, digite um número entre 1 e ${session.dados.tecnicos.length}:` - ); - return; - } - - const tecnicoSelecionado = session.dados.tecnicos[numero - 1]; - session.dados.tecnicoSelecionado = tecnicoSelecionado; - session.etapa = 'definir_data_hora'; - await this.processarEtapaDefinirDataHora(sessionId); - } - } - - /** - * Mostrar opções de técnicos/tipos de atendimento - */ - async mostrarOpcoesTecnicos(session) { - let message = `👨‍🔧 **Técnicos disponíveis:**\n\n`; - - // Validar se técnicos é um array - if (!Array.isArray(session.dados.tecnicos)) { - console.error(`❌ session.dados.tecnicos não é um array:`, { - type: typeof session.dados.tecnicos, - value: session.dados.tecnicos - }); - throw new Error(`Dados de técnicos inválidos: ${typeof session.dados.tecnicos}`); - } - - const buttons = []; - session.dados.tecnicos.forEach((tecnico, index) => { - message += `**${index + 1}.** ${tecnico.nome}\n`; - buttons.push([{ text: `${index + 1}. ${tecnico.nome}`, callback_data: `suporte_tecnico_${index}` }]); - }); - - message += `\nSelecione o técnico clicando no botão ou digitando o número (1-${session.dados.tecnicos.length}):`; - - await this.bot.sendMessage(session.chatId, message, { - parse_mode: 'Markdown', - reply_markup: { inline_keyboard: buttons } - }); - } - - /** - * Etapa 6: Definir data e hora - */ - async processarEtapaDefinirDataHora(sessionId, input = null) { - const session = this.suporteSessions.get(sessionId); - if (!session) return; - - if (!input) { - // Primeira vez nesta etapa - await this.bot.sendMessage(session.chatId, - `📅 **Data e horário para visita**\n\n` + - `Digite quando você gostaria que o técnico fizesse a visita.\n\n` + - `**Exemplos:**\n` + - `• "amanhã às 14h"\n` + - `• "sexta-feira 09:00"\n` + - `• "15/08 às 16h30"\n` + - `• "hoje 18h"\n` + - `• "em 3 dias às 10h"\n\n` + - `📝 Digite a data e horário:`, - { parse_mode: 'Markdown' } - ); - } else { - // Processar texto de data/hora - const resultado = this.dateProcessor.processarTexto(input); - - if (!resultado.success) { - await this.bot.sendMessage(session.chatId, - `❌ **Não consegui entender a data/hora**\n\n` + - `⚠️ ${resultado.error}\n\n` + - `**Exemplos válidos:**\n` + - `• "amanhã às 14h"\n` + - `• "sexta-feira 09:00"\n` + - `• "15/08 às 16h30"\n\n` + - `Por favor, tente novamente:` - ); - return; - } - - // Verificar se a data não é no passado - if (resultado.data < new Date()) { - await this.bot.sendMessage(session.chatId, - `❌ **Data no passado**\n\n` + - `A data especificada (${this.dateProcessor.formatarData(resultado.data)}) já passou.\n\n` + - `Por favor, especifique uma data futura:` - ); - return; - } - - session.dados.dataHora = resultado.data; - session.dados.dataHoraTexto = input; - session.etapa = 'adicionar_descricao'; - - await this.bot.sendMessage(session.chatId, - `✅ **Data/hora confirmada:**\n${this.dateProcessor.formatarData(resultado.data)}\n\n` + - `📝 **Última etapa:** Descrição do problema\n\n` + - `Digite uma descrição detalhada do problema ou serviço que precisa ser realizado:`, - { parse_mode: 'Markdown' } - ); - } - } - - /** - * Etapa 7: Adicionar descrição - */ - async processarEtapaAdicionarDescricao(sessionId, descricao) { - const session = this.suporteSessions.get(sessionId); - if (!session) return; - - if (!descricao || descricao.trim().length < 10) { - await this.bot.sendMessage(session.chatId, - `❌ **Descrição muito curta**\n\n` + - `Por favor, forneça uma descrição mais detalhada (mínimo 10 caracteres):` - ); - return; - } - - session.dados.descricao = descricao.trim(); - - // Mostrar resumo e confirmar - await this.mostrarResumoSuporte(session); - } - - /** - * Mostrar resumo do suporte antes de criar - */ - async mostrarResumoSuporte(session) { - const dados = session.dados; - - let resumo = `📋 **Resumo do Suporte**\n\n`; - resumo += `👤 **Cliente:** ${dados.clienteSelecionado.nome}\n`; - resumo += `🆔 **ID Cliente:** ${dados.clienteSelecionado.id}\n`; - resumo += `📋 **Serviço:** ${dados.servicoSelecionado.descricao || dados.servicoSelecionado.plano}\n`; - resumo += `🆔 **ID Serviço:** ${dados.servicoSelecionado.id}\n`; - resumo += `📁 **Categoria:** ${dados.categoriaSelecionada.descricao}\n`; - resumo += `👨‍🔧 **Técnico:** ${dados.tecnicoSelecionado.nome}\n`; - resumo += `📅 **Data/Hora:** ${this.dateProcessor.formatarData(dados.dataHora)}\n`; - resumo += `📝 **Descrição:** ${dados.descricao}\n\n`; - resumo += `Confirma a abertura do suporte? (sim/não)`; - - await this.bot.sendMessage(session.chatId, resumo, { parse_mode: 'Markdown' }); - - session.etapa = 'confirmar'; - } - - /** - * Finalizar criação do suporte - */ - async finalizarCriacaoSuporte(sessionId, confirmacao) { - const session = this.suporteSessions.get(sessionId); - if (!session) return; - - const confirmar = confirmacao.toLowerCase().trim(); - - if (!['sim', 's', 'yes', 'y', 'confirmar', 'ok'].includes(confirmar)) { - await this.bot.sendMessage(session.chatId, - `❌ **Suporte cancelado**\n\n` + - `O processo foi cancelado. Use o comando "abrir suporte" para tentar novamente.` - ); - this.suporteSessions.delete(sessionId); - return; - } - - await this.bot.sendMessage(session.chatId, `⏳ Criando suporte...`); - - try { - const dados = session.dados; - const location = dados.location || ''; - const dadosSuporte = { - idCliente: dados.clienteSelecionado.id, - idServico: dados.servicoSelecionado.id, - idCategoria: dados.categoriaSelecionada.id, - descricao: `${dados.descricao}\n\nData/Hora agendada: ${this.dateProcessor.formatarData(dados.dataHora)}\nTécnico: ${dados.tecnicoSelecionado.nome}` - }; - - if (dados.clienteSelecionado?.nome) { - dadosSuporte.contato = dados.clienteSelecionado.nome; - } - - if (dados.clienteSelecionado?.telefone) { - dadosSuporte.contatoTelefone = dados.clienteSelecionado.telefone; - } - - dadosSuporte.outrasInformacoes = `Criado via Bot Telegram\nData original: ${dados.dataHoraTexto}`; - - const resultado = await this.topsapp.adicionarSuporte(dadosSuporte, location); - - if (resultado.success) { - const protocolo = resultado.data?.dados?.protocolo || resultado.data?.protocolo; - - // Encaminhar para o técnico selecionado - const idTecnico = dados.tecnicoSelecionado?.id; - if (protocolo && idTecnico) { - const resEnc = await this.topsapp.encaminharSuporte( - protocolo, - idTecnico, - `Técnico atribuído via Bot Telegram: ${dados.tecnicoSelecionado.nome}`, - location - ); - if (!resEnc.success) { - logger.warn('Suporte criado mas falha ao encaminhar técnico:', resEnc.error); - } - } - - await this.bot.sendMessage(session.chatId, - `✅ **Suporte criado com sucesso!**\n\n` + - `🎫 **Protocolo:** ${protocolo || 'N/A'}\n` + - `👤 **Cliente:** ${dados.clienteSelecionado.nome}\n` + - `📋 **Serviço:** ${dados.servicoSelecionado.descricao || dados.servicoSelecionado.plano}\n` + - `📁 **Categoria:** ${dados.categoriaSelecionada.descricao}\n` + - `👨‍🔧 **Técnico:** ${dados.tecnicoSelecionado.nome}\n` + - `📅 **Data/Hora:** ${this.dateProcessor.formatarData(dados.dataHora)}\n\n` + - `🎯 O suporte foi registrado no sistema!`, - { parse_mode: 'Markdown' } - ); - - logger.info('Suporte criado via bot', { - protocolo, - cliente: dados.clienteSelecionado.nome, - usuario: session.userId - }); - } else { - await this.bot.sendMessage(session.chatId, - `❌ **Erro ao criar suporte**\n\n` + - `⚠️ ${resultado.error}\n\n` + - `Tente novamente mais tarde.` - ); - } - - } catch (error) { - logger.error('Erro ao criar suporte:', error); - await this.bot.sendMessage(session.chatId, - `❌ **Erro ao criar suporte**\n\n` + - `⚠️ ${error.message}\n\n` + - `Tente novamente mais tarde.` - ); - } - - // Limpar sessão - 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 - // ───────────────────────────────────────────────────────────────────────── - - /** - * Roteador de callbacks topsapp_onu_* - */ - async handleTopSappOnuCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const userId = callbackQuery.from.id; - - await this.bot.answerCallbackQuery(callbackQuery.id); - - const session = this.userSessions.get(userId); - - if (data === 'topsapp_onu_sim') { - if (!session || !session.onuData) { - await this.bot.sendMessage(chatId, '❌ Sessão expirada. Tente novamente após adicionar a ONU.'); - return; - } - session.action = 'topsapp_onu_aguardando_nome'; - this.userSessions.set(userId, session); - await this.bot.sendMessage(chatId, - `🔍 *Buscar Cliente no TopSapp*\n\nDigite o nome ou CPF do cliente:`, - { parse_mode: 'Markdown' } - ); - return; - } - - if (data.startsWith('topsapp_onu_cli_')) { - const idCliente = data.replace('topsapp_onu_cli_', ''); - if (!session || !session.onuData) { - await this.bot.sendMessage(chatId, '❌ Sessão expirada. Tente novamente.'); - return; - } - await this.topsappOnuMostrarServicos(chatId, userId, idCliente, session.onuData); - return; - } - - if (data.startsWith('topsapp_onu_svc_')) { - // formato: topsapp_onu_svc__ - const parts = data.replace('topsapp_onu_svc_', '').split('_'); - const idServico = parts[0]; - const idCliente = parts[1]; - if (!session || !session.onuData) { - await this.bot.sendMessage(chatId, '❌ Sessão expirada. Tente novamente.'); - return; - } - await this.topsappOnuConfirmarVinculo(chatId, userId, idServico, idCliente, session.onuData); - return; - } - - if (data === 'topsapp_onu_ok') { - if (!session || !session.ftthPayload) { - await this.bot.sendMessage(chatId, '❌ Sessão expirada. Tente novamente.'); - return; - } - await this.topsappOnuExecutarVinculo(chatId, userId, session); - return; - } - - await this.bot.sendMessage(chatId, '❓ Ação desconhecida.'); - } - - /** - * Busca cliente no TopSapp e exibe lista para seleção - */ - async topsappOnuBuscarCliente(chatId, userId, query, onuData) { - try { - await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); - - const resultado = await this.topsapp.buscarCliente(query, onuData.location); - - if (!resultado.success) { - await this.bot.sendMessage(chatId, - `❌ *Erro na busca*\n\n${resultado.error}`, - { parse_mode: 'Markdown' } - ); - this.userSessions.delete(userId); - return; - } - - if (!resultado.clientes || resultado.clientes.length === 0) { - await this.bot.sendMessage(chatId, - `🔍 *Nenhum cliente encontrado*\n\nTermo: "${query}"\n\nDigite outro nome ou CPF:`, - { - parse_mode: 'Markdown', - reply_markup: { inline_keyboard: [[{ text: '❌ Cancelar', callback_data: 'cancel_action' }]] } - } - ); - // Mantém sessão ativa para nova tentativa - return; - } - - const keyboard = []; - let msg = `👥 *${resultado.clientes.length > 1 ? resultado.clientes.length + ' clientes encontrados' : 'Cliente encontrado'}*\n\nSelecione o cliente:\n\n`; - - resultado.clientes.slice(0, 8).forEach((cliente, i) => { - msg += `${i + 1}. *${cliente.nome}* (ID: ${cliente.id})\n`; - keyboard.push([{ text: `${i + 1}. ${cliente.nome}`, callback_data: `topsapp_onu_cli_${cliente.id}` }]); - }); - - keyboard.push([{ text: '❌ Cancelar', callback_data: 'cancel_action' }]); - - await this.bot.sendMessage(chatId, msg, { - parse_mode: 'Markdown', - reply_markup: { inline_keyboard: keyboard } - }); - - } catch (error) { - console.error('❌ Erro ao buscar cliente TopSapp:', error); - await this.bot.sendMessage(chatId, `❌ Erro ao buscar cliente: ${error.message}`); - this.userSessions.delete(userId); - } - } - - /** - * Lista serviços ativos do cliente para seleção - */ - async topsappOnuMostrarServicos(chatId, userId, idCliente, onuData) { - try { - await this.bot.sendMessage(chatId, '📋 Buscando serviços do cliente...'); - - const servicosResult = await this.topsapp.obterServicosCliente(idCliente, onuData.location); - - if (!servicosResult.success) { - await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviços: ${servicosResult.error}`); - this.userSessions.delete(userId); - return; - } - - const servicos = servicosResult.servicos || []; - const servicosAtivos = servicos.filter(s => - s.id_status === 1 || (s.status || '').toLowerCase() === 'liberado' - ); - - if (servicosAtivos.length === 0) { - await this.bot.sendMessage(chatId, - `⚠️ *Nenhum serviço ativo encontrado* para o cliente ID ${idCliente}.\n\nCancele e verifique o cliente.`, - { - parse_mode: 'Markdown', - reply_markup: { inline_keyboard: [[{ text: '❌ Cancelar', callback_data: 'cancel_action' }]] } - } - ); - return; - } - - const keyboard = []; - let msg = `📋 *Serviços ativos do cliente*\n\nSelecione o serviço FTTH:\n\n`; - - servicosAtivos.slice(0, 8).forEach((servico, i) => { - const idSvc = servico.id_servico || servico.id; - const nome = servico.descricao || servico.nome || 'Serviço'; - const login = servico.usuario ? ` (${servico.usuario})` : ''; - msg += `${i + 1}. *${nome}*${login} — ID: ${idSvc}\n`; - keyboard.push([{ text: `${i + 1}. ${nome}${login}`, callback_data: `topsapp_onu_svc_${idSvc}_${idCliente}` }]); - }); - - keyboard.push([{ text: '❌ Cancelar', callback_data: 'cancel_action' }]); - - await this.bot.sendMessage(chatId, msg, { - parse_mode: 'Markdown', - reply_markup: { inline_keyboard: keyboard } - }); - - } catch (error) { - console.error('❌ Erro ao listar serviços TopSapp:', error); - await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); - this.userSessions.delete(userId); - } - } - - /** - * Faz matching OLT/porta/perfil e mostra tela de confirmação - */ - async topsappOnuConfirmarVinculo(chatId, userId, idServico, idCliente, onuData) { - try { - await this.bot.sendMessage(chatId, '🔌 Buscando dados da OLT no TopSapp...'); - - const oltsResult = await this.topsapp.obterOlts(onuData.location); - - if (!oltsResult.success) { - await this.bot.sendMessage(chatId, `❌ Erro ao buscar OLTs: ${oltsResult.error}`); - this.userSessions.delete(userId); - return; - } - - const olts = oltsResult.data?.dados?.dados || oltsResult.data?.dados || []; - - // Mapa manual de keywords por oltId/tipo → para busca no campo ssid e nome do TopSapp - // Fonte: valores reais de ssid cadastrados no TopSapp - const SSID_KEYWORDS = { - 'fiberhome': ['fiberhome', '172.16.10.6'], - 'olt_colmeia': ['colmeia'], - 'olt_canaa': ['canaa'], - 'olt_canaa2': ['canaa 2', 'canaa2'], - 'olt_dito': ['dito'], - 'olt_pedro_afonso': ['pedro', 'afonso'], - 'olt_canavieiras': ['canavieiras'] - }; - - /** - * Busca uma OLT na lista pelo campo nome OU ssid usando lista de keywords. - * Tenta match com TODOS os tokens primeiro; se falhar, com QUALQUER token. - */ - const buscarPorKeywords = (keywords) => { - const found = olts.find(o => { - const haystack = `${o.nome || ''} ${o.ssid || ''}`.toLowerCase(); - return keywords.every(k => haystack.includes(k)); - }); - if (found) return found; - return olts.find(o => { - const haystack = `${o.nome || ''} ${o.ssid || ''}`.toLowerCase(); - return keywords.some(k => haystack.includes(k)); - }); - }; - - let oltEncontrada = null; - - // 1. Match manual pelo tipo (Fiberhome) ou oltId (Datacom) - const tipoKey = onuData.tipo === 'fiberhome' ? 'fiberhome' : onuData.oltId; - if (tipoKey && SSID_KEYWORDS[tipoKey]) { - oltEncontrada = buscarPorKeywords(SSID_KEYWORDS[tipoKey]); - if (oltEncontrada) { - console.log(`✅ OLT encontrada por mapa manual (${tipoKey}): "${oltEncontrada.nome}" / ssid: "${oltEncontrada.ssid}"`); - } - } - - // 2. Fallback: keyword livre do oltId (Datacom sem mapa definido) - if (!oltEncontrada && onuData.oltId) { - const keyword = onuData.oltId.replace(/^olt_/, '').replace(/_/g, ' ').toLowerCase(); - const keywords = keyword.split(' ').filter(k => k.length > 2); - oltEncontrada = buscarPorKeywords(keywords); - if (oltEncontrada) { - console.log(`✅ OLT encontrada por keyword livre (oltId): "${oltEncontrada.nome}" (keyword: "${keyword}")`); - } - } - - // 3. Fallback: keyword livre do oltHost (Fiberhome sem mapa definido) - if (!oltEncontrada && onuData.oltHost) { - const keywords = onuData.oltHost.toLowerCase().split(/[\s_\-]+/).filter(k => k.length > 1); - oltEncontrada = buscarPorKeywords(keywords); - if (oltEncontrada) { - console.log(`✅ OLT encontrada por keyword livre (oltHost): "${oltEncontrada.nome}" (keyword: "${onuData.oltHost}")`); - } - } - - // 4. Fallback: primeira OLT disponível - if (!oltEncontrada && olts.length > 0) { - oltEncontrada = olts[0]; - console.log(`⚠️ OLT não identificada — usando a primeira disponível: ${oltEncontrada.nome}`); - } - - if (!oltEncontrada) { - await this.bot.sendMessage(chatId, `❌ Nenhuma OLT encontrada no TopSapp para este servidor.`); - this.userSessions.delete(userId); - return; - } - - // Encontrar porta pelo alias - const portas = oltEncontrada.portas || []; - let portaEncontrada = portas.find(p => p.alias === onuData.portaAlias); - if (!portaEncontrada && portas.length > 0) { - portaEncontrada = portas[0]; - console.log(`⚠️ Porta não encontrada por alias "${onuData.portaAlias}", usando a primeira: ${portaEncontrada.alias}`); - } - - if (!portaEncontrada) { - await this.bot.sendMessage(chatId, `❌ Nenhuma porta encontrada na OLT "${oltEncontrada.nome}".`); - this.userSessions.delete(userId); - return; - } - - // Encontrar perfil por tipo de ONU - const perfis = oltEncontrada.perfis || []; - let perfilEncontrado; - if (onuData.tipo === 'datacom') { - perfilEncontrado = perfis.find(p => p.identificacao && p.identificacao.includes('DATACOM ONU_BRIDGE_')); - } else { - perfilEncontrado = perfis.find(p => p.identificacao === 'ONU-BRIDGE-AN5506-02-B'); - } - if (!perfilEncontrado && perfis.length > 0) { - perfilEncontrado = perfis[0]; - console.log(`⚠️ Perfil não encontrado por tipo, usando o primeiro: ${perfilEncontrado.identificacao}`); - } - - if (!perfilEncontrado) { - await this.bot.sendMessage(chatId, `❌ Nenhum perfil encontrado na OLT "${oltEncontrada.nome}".`); - this.userSessions.delete(userId); - return; - } - - const ftthData = { - serial: onuData.serial, - idAP: oltEncontrada.id, - idPorta: portaEncontrada.id, - idPerfil: perfilEncontrado.id - }; - - if (onuData.onuId !== undefined && onuData.onuId !== null) { - ftthData.ftth_onuid = parseInt(onuData.onuId); - ftthData.onuid = parseInt(onuData.onuId); - } - if (onuData.servicePort !== null && onuData.servicePort !== undefined) { - ftthData.ftth_servport = onuData.servicePort; - ftthData.ftth_serviceport = onuData.servicePort; - ftthData.serviceport = onuData.servicePort; - } - - console.log('🔧 TopSapp ftthData completo:', JSON.stringify(ftthData, null, 2)); - - // Salvar payload de confirmação na sessão - const session = this.userSessions.get(userId); - session.action = 'topsapp_onu_confirmando'; - session.ftthPayload = { - idCliente, - idServico, - ftthData, - location: onuData.location - }; - this.userSessions.set(userId, session); - - const onuIdInfo = onuData.onuId !== undefined && onuData.onuId !== null - ? `\n🔢 *ONU ID:* ${onuData.onuId}` : ''; - const spInfo = onuData.servicePort !== null && onuData.servicePort !== undefined - ? `\n🔌 *Service-port:* ${onuData.servicePort}` : ''; - - await this.bot.sendMessage(chatId, - `🔌 *Confirmar Vinculação no TopSapp*\n\n` + - `📱 *Serial:* \`${onuData.serial}\`\n` + - `🏢 *OLT:* ${oltEncontrada.nome}\n` + - `📡 *Porta:* ${portaEncontrada.alias}` + - onuIdInfo + spInfo + `\n` + - `⚙️ *Perfil:* ${perfilEncontrado.identificacao}\n` + - `🆔 *Serviço ID:* ${idServico}\n` + - `👤 *Cliente ID:* ${idCliente}\n\n` + - `Confirmar os dados acima?`, - { - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: [[ - { text: '✅ Confirmar', callback_data: 'topsapp_onu_ok' }, - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ]] - } - } - ); - - } catch (error) { - console.error('❌ Erro ao confirmar vínculo TopSapp:', error); - await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); - this.userSessions.delete(userId); - } - } - - /** - * Executa a atualização FTTH no TopSapp via AlterarServico - */ - async topsappOnuExecutarVinculo(chatId, userId, session) { - try { - const { idCliente, idServico, ftthData, location } = session.ftthPayload; - - await this.bot.sendMessage(chatId, '⏳ Salvando dados no TopSapp...'); - - const resultado = await this.topsapp.atualizarFTTH(idCliente, idServico, ftthData, location); - - if (resultado.success) { - await this.bot.sendMessage(chatId, - `✅ *ONU vinculada com sucesso no TopSapp!*\n\n` + - `📱 *Serial:* \`${ftthData.serial}\`\n` + - `🆔 *Serviço ID:* ${idServico}\n` + - `👤 *Cliente ID:* ${idCliente}`, - { parse_mode: 'Markdown' } - ); - } else { - await this.bot.sendMessage(chatId, - `❌ *Erro ao vincular no TopSapp*\n\n${resultado.error}`, - { parse_mode: 'Markdown' } - ); - } - - } catch (error) { - console.error('❌ Erro ao executar vínculo TopSapp:', error); - await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); - } finally { - this.userSessions.delete(userId); - } - } - - /** - * Callback: Seleção de cliente via botão inline no fluxo de suporte - */ - async handleSuporteClienteCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const userId = callbackQuery.from.id; - - await this.bot.answerCallbackQuery(callbackQuery.id); - - const index = parseInt(data.replace('suporte_cliente_', '')); - - const sessaoAtiva = Array.from(this.suporteSessions.values()).find( - session => session.chatId === chatId && session.userId === userId - ); - - if (!sessaoAtiva) { - await this.bot.sendMessage(chatId, - `❌ **Sessão expirada**\n\nUse o comando "abrir suporte" para iniciar novamente.`, - { parse_mode: 'Markdown' } - ); - return; - } - - const clientes = sessaoAtiva.dados.clientesEncontrados; - if (!clientes || index < 0 || index >= clientes.length) { - await this.bot.sendMessage(chatId, `❌ **Cliente inválido.** Por favor, tente novamente.`, { parse_mode: 'Markdown' }); - return; - } - - sessaoAtiva.dados.clienteSelecionado = clientes[index]; - sessaoAtiva.etapa = 'selecionar_servico'; - await this.processarEtapaSelecionarServico(sessaoAtiva.id); - } - - /** - * Callback: Seleção de serviço via botão inline no fluxo de suporte - */ - async handleSuporteServicoCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const userId = callbackQuery.from.id; - - await this.bot.answerCallbackQuery(callbackQuery.id); - - const index = parseInt(data.replace('suporte_servico_', '')); - - const sessaoAtiva = Array.from(this.suporteSessions.values()).find( - session => session.chatId === chatId && session.userId === userId - ); - - if (!sessaoAtiva) { - await this.bot.sendMessage(chatId, - `❌ **Sessão expirada**\n\nUse o comando "abrir suporte" para iniciar novamente.`, - { parse_mode: 'Markdown' } - ); - return; - } - - const servicos = sessaoAtiva.dados.servicos; - if (!servicos || index < 0 || index >= servicos.length) { - await this.bot.sendMessage(chatId, `❌ **Serviço inválido.** Por favor, tente novamente.`, { parse_mode: 'Markdown' }); - return; - } - - sessaoAtiva.dados.servicoSelecionado = servicos[index]; - sessaoAtiva.etapa = 'selecionar_categoria'; - await this.processarEtapaSelecionarCategoria(sessaoAtiva.id); - } - - /** - * Callback: Seleção de técnico via botão inline no fluxo de suporte - */ - async handleSuporteTecnicoCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const userId = callbackQuery.from.id; - - const index = parseInt(data.replace('suporte_tecnico_', '')); - - const sessaoAtiva = Array.from(this.suporteSessions.values()).find( - session => session.chatId === chatId && session.userId === userId - ); - - if (!sessaoAtiva) { - await this.bot.sendMessage(chatId, - `❌ **Sessão expirada**\n\nUse o comando "abrir suporte" para iniciar novamente.`, - { parse_mode: 'Markdown' } - ); - return; - } - - const tecnicos = sessaoAtiva.dados.tecnicos; - if (!tecnicos || index < 0 || index >= tecnicos.length) { - await this.bot.sendMessage(chatId, `❌ **Técnico inválido.** Por favor, tente novamente.`, { parse_mode: 'Markdown' }); - return; - } - - sessaoAtiva.dados.tecnicoSelecionado = tecnicos[index]; - sessaoAtiva.etapa = 'definir_data_hora'; - await this.processarEtapaDefinirDataHora(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 " - */ - async handleVerRoteador(chatId, termo) { - if (!termo) { - await this.bot.sendMessage(chatId, - '📡 *Ver Roteador - GenieACS*\n\n' + - '❓ Informe o serial ou a TAG (login TopSapp) do dispositivo.\n' + - 'Exemplos:\n' + - '• `ver roteador ABCD12345678`\n' + - '• `ver roteador joao.silva`', - { parse_mode: 'Markdown' } - ); - return; - } - - // Escapa caracteres especiais do Markdown V1 em valores dinâmicos - const esc = (v) => { - if (v === null || v === undefined || v === '') return 'N/A'; - return String(v).replace(/[_*`\[]/g, '\\$&'); - }; - - await this.bot.sendMessage(chatId, `🔍 Consultando dispositivo \`${esc(termo)}\` no GenieACS...`, { parse_mode: 'Markdown' }); - - try { - const resultado = await this.genieacs.buscarRoteador(termo); - - if (!resultado.success) { - await this.bot.sendMessage(chatId, - `❌ *Dispositivo não encontrado*\n\n` + - `🔍 Busca: \`${esc(termo)}\`\n` + - `⚠️ ${esc(resultado.error)}`, - { parse_mode: 'Markdown' } - ); - return; - } - - const d = resultado.device; - const tagLabel = d.tags && d.tags.length > 0 ? d.tags.map(esc).join(', ') : 'N/A'; - const buscadoPor = resultado.searchedBy === 'tag' ? 'TAG' : 'Serial'; - - let msg = - `📡 *Informações do Roteador*\n` + - `📌 Instância: ${esc(resultado.instance)}\n` + - `🔎 Encontrado por: ${buscadoPor}\n\n` + - `🖥️ *Dispositivo*\n` + - `├ Nome: ${esc(d.productClass)}\n` + - `├ Fabricante: ${esc(d.manufacturer)}\n` + - `├ OUI: ${esc(d.oui)}\n` + - `├ Serial: \`${esc(d.serial)}\`\n` + - `├ TAG (TopSapp): \`${tagLabel}\`\n` + - `├ HW: ${esc(d.hardwareVersion)}\n` + - `└ SW: ${esc(d.softwareVersion)}\n\n` + - `📶 *Wi-Fi 2.4 GHz*\n` + - `├ SSID: ${esc(d.ssid24)}\n` + - `├ Senha: \`${esc(d.senha24)}\`\n` + - `├ Canal: ${esc(d.channel24)}\n` + - `├ Auto-Canal: ${esc(d.autoChannel24)}\n` + - `└ Clientes: ${d.clients24 !== null && d.clients24 !== undefined ? esc(d.clients24) : 'N/A'}\n\n` + - `📶 *Wi-Fi 5 GHz*\n` + - `├ SSID: ${esc(d.ssid5)}\n` + - `├ Senha: \`${esc(d.senha5)}\`\n` + - `├ Canal: ${esc(d.channel5)}\n` + - `├ Auto-Canal: ${esc(d.autoChannel5)}\n` + - `└ Clientes: ${d.clients5 !== null && d.clients5 !== undefined ? esc(d.clients5) : 'N/A'}`; - - // PPPoE WAN - msg += `\n\n🌐 *PPPoE WAN*\n`; - msg += `├ Username: ${esc(d.pppoeUsername)}\n`; - msg += `├ Status: ${esc(d.pppoeStatus)}\n`; - msg += `├ IP: \`${esc(d.pppoeIP)}\`\n`; - msg += `├ Uptime: ${esc(d.pppoeUptime)}\n`; - msg += `├ Gateway: ${esc(d.gatewayWAN)}\n`; - msg += `├ DNS WAN: ${esc(d.dnsWAN)}\n`; - msg += `├ Link Físico: ${esc(d.linkFisicoWAN)}\n`; - msg += `├ Recebido: ${esc(d.trafegoRecebido)}\n`; - msg += `├ Enviado: ${esc(d.trafegoEnviado)}\n`; - if (d.taxaDownload) msg += `├ ↓ Download: ${esc(d.taxaDownload)}\n`; - if (d.taxaUpload) msg += `├ ↑ Upload: ${esc(d.taxaUpload)}\n`; - msg += `└ Último Erro: ${esc(d.pppoeLastError)}`; - - // DHCP LAN - msg += `\n\n🏠 *LAN/DHCP*\n`; - msg += `├ IP LAN: \`${esc(d.ipLAN)}\`\n`; - msg += `├ Máscara: ${esc(d.mascaraLAN)}\n`; - msg += `├ Início DHCP: ${esc(d.dhcpInicio)}\n`; - msg += `├ Fim DHCP: ${esc(d.dhcpFim)}\n`; - msg += `└ DNS LAN: ${esc(d.dnsLAN)}`; - - await this.bot.sendMessage(chatId, msg, { parse_mode: 'Markdown' }); - - } catch (error) { - logger.error('Erro em handleVerRoteador:', error); - await this.bot.sendMessage(chatId, `❌ Erro ao consultar o GenieACS: ${error.message}`); - } - } - - // ───────────────────────────────────────────────────────────────────────── - // CONFIGURAR ROTEADOR VIA GENIEACS - // ───────────────────────────────────────────────────────────────────────── - - /** - * Mapa de campos configuráveis: chave curta → detalhes do parâmetro TR-069. - * Centralizado aqui para ser usado tanto no menu quanto nos callbacks. - */ - _routerParamMap() { - return { - ssid24: { label: 'SSID 2.4GHz', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID', type: 'xsd:string', toggle: false }, - ssid5: { label: 'SSID 5GHz', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.SSID', type: 'xsd:string', toggle: false }, - senha24: { label: 'Senha Wi-Fi 2.4', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.KeyPassphrase', type: 'xsd:string', toggle: false }, - senha5: { label: 'Senha Wi-Fi 5', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.PreSharedKey.1.KeyPassphrase', type: 'xsd:string', toggle: false }, - ch24: { label: 'Canal 2.4GHz', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.Channel', type: 'xsd:unsignedInt', toggle: false }, - ch5: { label: 'Canal 5GHz', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.Channel', type: 'xsd:unsignedInt', toggle: false }, - auto24: { label: 'Auto-Canal 2.4', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.AutoChannelEnable', type: 'xsd:boolean', toggle: true }, - auto5: { label: 'Auto-Canal 5', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.AutoChannelEnable', type: 'xsd:boolean', toggle: true }, - dnswan: { label: 'DNS WAN', param: 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.DNSServers', type: 'xsd:string', toggle: false }, - dnslan: { label: 'DNS LAN', param: 'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.DNSServers', type: 'xsd:string', toggle: false }, - dhcpstart: { label: 'DHCP Início', param: 'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MinAddress', type: 'xsd:string', toggle: false }, - dhcpend: { label: 'DHCP Fim', param: 'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MaxAddress', type: 'xsd:string', toggle: false }, - iplan: { label: 'IP LAN', param: 'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceIPAddress', type: 'xsd:string', toggle: false }, - mascaralan: { label: 'Máscara LAN', param: 'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceSubnetMask', type: 'xsd:string', toggle: false }, - }; - } - - /** - * Comando "configurar roteador ". - * - * Fluxo: - * 1. Se o termo parece serial/tag GenieACS (alphanumérico sem espaços) → busca direto no GenieACS. - * 2. Caso contrário, busca no TopSapp por nome/CPF: - * a. Seleciona o cliente (se houver múltiplos) - * b. Seleciona o serviço com login PPPoE - * c. Usa o login PPPoE como TAG para buscar no GenieACS - * d. Se não encontrar TAG no GenieACS → informa que não é possível configurar - */ - async handleConfigurarRoteador(chatId, userId, termo) { - if (!termo) { - await this.bot.sendMessage(chatId, - '⚙️ *Configurar Roteador*\n\n' + - 'Informe o serial, TAG, nome do cliente ou CPF.\n' + - 'Exemplos:\n' + - '• `configurar roteador ABCD12345678`\n' + - '• `configurar roteador joao.silva`\n' + - '• `configurar roteador João da Silva`\n' + - '• `configurar roteador 123.456.789-10`', - { parse_mode: 'Markdown' } - ); - return; - } - - // CPF sem formatação: 11 dígitos numéricos → formata e encaminha para TopSapp - if (/^\d{11}$/.test(termo)) { - const cpfFormatado = `${termo.slice(0,3)}.${termo.slice(3,6)}.${termo.slice(6,9)}-${termo.slice(9,11)}`; - return this._configurarRoteadorBuscarTopSapp(chatId, userId, cpfFormatado); - } - - // Heurística: serial/tag GenieACS não tem espaço e não é CPF formatado - const pareceGenieACS = !/\s/.test(termo) && !/^\d{3}\.\d{3}\.\d{3}-\d{2}$/.test(termo); - - if (pareceGenieACS) { - // Caminho direto: buscar no GenieACS por serial ou TAG - await this._configurarRoteadorPorGenieACS(chatId, userId, termo); - } else { - // Caminho via TopSapp: buscar cliente por nome/CPF - await this._configurarRoteadorBuscarTopSapp(chatId, userId, termo); - } - } - - /** - * Busca o dispositivo diretamente no GenieACS e abre o menu de configuração. - */ - async _configurarRoteadorPorGenieACS(chatId, userId, termo) { - await this.bot.sendMessage(chatId, `🔍 Buscando dispositivo \`${termo}\` no GenieACS...`, { parse_mode: 'Markdown' }); - try { - const resultado = await this.genieacs.buscarRoteador(termo); - if (!resultado.success) { - await this.bot.sendMessage(chatId, - `❌ *Dispositivo não encontrado no GenieACS*\n\n` + - `🔍 Busca: \`${termo}\`\n` + - `⚠️ ${resultado.error || 'Nenhum resultado'}\n\n` + - `_Tente buscar pelo nome ou CPF do cliente._`, - { parse_mode: 'Markdown' } - ); - return; - } - this.userSessions.set(userId, { - action: 'router_cfg', - deviceId: resultado.device.deviceId, - device: resultado.device, - instance: resultado.instance || '', - timestamp: Date.now() - }); - await this._enviarMenuConfigurarRoteador(chatId, resultado.device, resultado.instance || ''); - } catch (error) { - logger.error('Erro em _configurarRoteadorPorGenieACS:', error); - await this.bot.sendMessage(chatId, `❌ Erro ao consultar o GenieACS: ${error.message}`); - } - } - - /** - * Busca o cliente no TopSapp por nome/CPF e apresenta seleção. - */ - async _configurarRoteadorBuscarTopSapp(chatId, userId, query) { - await this.bot.sendMessage(chatId, `🔍 Buscando cliente "${query}" no TopSapp...`); - try { - const resultado = await this.topsapp.buscarCliente(query); - if (!resultado.success) { - await this.bot.sendMessage(chatId, `❌ Erro na busca: ${resultado.error}`); - return; - } - if (!resultado.clientes || resultado.clientes.length === 0) { - await this.bot.sendMessage(chatId, `🔍 Nenhum cliente encontrado para: "${query}"`); - return; - } - - const instancia = resultado.instancia || ''; - - if (resultado.clientes.length === 1) { - await this._configurarRoteadorSelecionarServico(chatId, userId, resultado.clientes[0].id, instancia); - return; - } - - // Múltiplos clientes → seleção - let msg = `🔍 *${resultado.clientes.length} clientes encontrados:*\n\n`; - const keyboard = []; - resultado.clientes.slice(0, 8).forEach((c, i) => { - msg += `*${i + 1}.* ${c.nome}`; - if (c.cpf) msg += ` — \`${c.cpf}\``; - msg += '\n'; - keyboard.push([{ - text: `⚙️ ${c.nome.substring(0, 35)}`, - callback_data: `cfg_roteador_cli_${c.id}_${instancia}` - }]); - }); - - await this.bot.sendMessage(chatId, msg, { - parse_mode: 'Markdown', - reply_markup: { inline_keyboard: keyboard } - }); - } catch (error) { - logger.error('Erro em _configurarRoteadorBuscarTopSapp:', error); - await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); - } - } - - /** - * Busca os serviços do cliente e apresenta seleção (ou vai direto se só há 1). - */ - async _configurarRoteadorSelecionarServico(chatId, userId, idCliente, location) { - const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); - if (!servicosResult.success) { - await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviços: ${servicosResult.error}`); - return; - } - - const servicos = (servicosResult.servicos || []).filter(s => { - const st = s.id_status || s.statusNumerico; - return st === 1 || st === '1' || - String(s.status).toLowerCase() === 'liberado' || - String(s.status).toLowerCase() === 'ativo'; - }); - - if (servicos.length === 0) { - await this.bot.sendMessage(chatId, `⚠️ Cliente sem serviços ativos.`); - return; - } - - if (servicos.length === 1) { - await this._configurarRoteadorPorServico(chatId, userId, servicos[0], location); - return; - } - - let msg = `⚙️ *Selecione o serviço:*\n\n`; - const keyboard = []; - servicos.forEach((s, i) => { - const nome = s.descricao || s.nome || `Serviço ${i + 1}`; - const login = s.usuario || s.pppoe_login || s.login || ''; - msg += `*${i + 1}.* ${nome}${login ? ` — \`${login}\`` : ''}\n`; - keyboard.push([{ - text: `⚙️ ${nome.substring(0, 35)}`, - callback_data: `cfg_roteador_srv_${idCliente}_${s.id_servico || s.id}_${location}` - }]); - }); - - await this.bot.sendMessage(chatId, msg, { - parse_mode: 'Markdown', - reply_markup: { inline_keyboard: keyboard } - }); - } - - /** - * A partir de um serviço TopSapp, extrai o login PPPoE, busca a TAG no GenieACS e abre o menu. - */ - async _configurarRoteadorPorServico(chatId, userId, servico, location) { - const pppoeLogin = servico.usuario || servico.pppoe_login || servico.login || null; - - if (!pppoeLogin) { - await this.bot.sendMessage(chatId, - `⚠️ O serviço selecionado não possui login PPPoE cadastrado.\n` + - `Não é possível localizar o roteador no GenieACS sem esse dado.` - ); - return; - } - - await this.bot.sendMessage(chatId, `🔍 Localizando roteador pela TAG \`${pppoeLogin}\` no GenieACS...`, { parse_mode: 'Markdown' }); - - try { - const resultado = await this.genieacs.buscarRoteador(pppoeLogin, location); - - if (!resultado.success) { - await this.bot.sendMessage(chatId, - `⚠️ *Roteador não encontrado no GenieACS*\n\n` + - `🏷️ TAG pesquisada: \`${pppoeLogin}\`\n\n` + - `Não é possível realizar alterações no roteador deste cliente pelo bot.\n` + - `_O dispositivo pode não estar cadastrado no GenieACS ou a TAG ainda não foi sincronizada._`, - { parse_mode: 'Markdown' } - ); - return; - } - - this.userSessions.set(userId, { - action: 'router_cfg', - deviceId: resultado.device.deviceId, - device: resultado.device, - instance: resultado.instance || '', - timestamp: Date.now() - }); - - await this._enviarMenuConfigurarRoteador(chatId, resultado.device, resultado.instance || ''); - } catch (error) { - logger.error('Erro em _configurarRoteadorPorServico:', error); - await this.bot.sendMessage(chatId, `❌ Erro ao consultar o GenieACS: ${error.message}`); - } - } - - /** - * Envia/atualiza a mensagem de menu de configuração do roteador. - */ - async _enviarMenuConfigurarRoteador(chatId, device, instance) { - const esc = (v) => (v === null || v === undefined || v === '') ? 'N/A' : String(v).replace(/[_*`\[]/g, '\\$&'); - - const auto24On = device.autoChannel24 === 'Sim'; - const auto5On = device.autoChannel5 === 'Sim'; - - const sumario = - `⚙️ *Configurar Roteador*\n` + - `📌 ${esc(instance)} — \`${esc(device.serial)}\`\n\n` + - `📶 *Wi-Fi 2.4 GHz*\n` + - `├ SSID: ${esc(device.ssid24)}\n` + - `├ Senha: \`${esc(device.senha24)}\`\n` + - `├ Canal: ${esc(device.channel24)} (Auto: ${esc(device.autoChannel24)})\n\n` + - `📶 *Wi-Fi 5 GHz*\n` + - `├ SSID: ${esc(device.ssid5)}\n` + - `├ Senha: \`${esc(device.senha5)}\`\n` + - `├ Canal: ${esc(device.channel5)} (Auto: ${esc(device.autoChannel5)})\n\n` + - `🌐 *WAN/PPPoE*\n` + - `├ DNS WAN: ${esc(device.dnsWAN)}\n\n` + - `🏠 *LAN/DHCP*\n` + - `├ IP LAN: ${esc(device.ipLAN)}\n` + - `├ Máscara: ${esc(device.mascaraLAN)}\n` + - `├ DNS LAN: ${esc(device.dnsLAN)}\n` + - `├ DHCP Início: ${esc(device.dhcpInicio)}\n` + - `└ DHCP Fim: ${esc(device.dhcpFim)}\n\n` + - `_Selecione o que deseja alterar:_`; - - const keyboard = { - inline_keyboard: [ - [ - { text: '📶 SSID 2.4GHz', callback_data: 'router_cfg_ssid24' }, - { text: '📶 SSID 5GHz', callback_data: 'router_cfg_ssid5' }, - ], - [ - { text: '🔑 Senha 2.4GHz', callback_data: 'router_cfg_senha24' }, - { text: '🔑 Senha 5GHz', callback_data: 'router_cfg_senha5' }, - ], - [ - { text: `${auto24On ? '✅' : '⬜'} Auto-Canal 2.4`, callback_data: 'router_cfg_auto24' }, - { text: `${auto5On ? '✅' : '⬜'} Auto-Canal 5`, callback_data: 'router_cfg_auto5' }, - ], - [ - { text: '📡 Canal 2.4GHz', callback_data: 'router_cfg_ch24' }, - { text: '📡 Canal 5GHz', callback_data: 'router_cfg_ch5' }, - ], - [ - { text: '🌐 DNS WAN', callback_data: 'router_cfg_dnswan' }, - { text: '🏠 DNS LAN', callback_data: 'router_cfg_dnslan' }, - ], - [ - { text: '▶ DHCP Início', callback_data: 'router_cfg_dhcpstart' }, - { text: '◀ DHCP Fim', callback_data: 'router_cfg_dhcpend' }, - ], - [ - { text: '🖥️ IP LAN', callback_data: 'router_cfg_iplan' }, - { text: '🔢 Máscara LAN', callback_data: 'router_cfg_mascaralan'}, - ], - [ - { text: '🔄 Atualizar leitura', callback_data: 'router_cfg_refresh' }, - { text: '❌ Fechar', callback_data: 'cancel_action' }, - ], - ] - }; - - await this.bot.sendMessage(chatId, sumario, { parse_mode: 'Markdown', reply_markup: keyboard }); - } - - /** - * Dispatcher de todos os callbacks com prefixo "router_cfg_". - */ - async handleRouterCfgCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const userId = callbackQuery.from.id; - const msgId = callbackQuery.message.message_id; - - await this.bot.answerCallbackQuery(callbackQuery.id); - - const session = this.userSessions.get(userId); - if (!session || session.action !== 'router_cfg') { - await this.bot.sendMessage(chatId, '⚠️ Sessão expirada. Use `configurar roteador ` novamente.', { parse_mode: 'Markdown' }); - return; - } - - const field = data.replace('router_cfg_', ''); // ex.: "ssid24", "auto24", "ch24_6", "refresh" - - // ── Atualizar leitura ──────────────────────────────────────────────── - if (field === 'refresh') { - await this.bot.sendMessage(chatId, '🔄 Relendo dados do dispositivo...'); - const fresh = await this.genieacs.buscarRoteadorPorId(session.deviceId, session.instance); - if (!fresh.success) { - await this.bot.sendMessage(chatId, `❌ Falha ao reler dispositivo: ${fresh.error}`); - return; - } - session.device = fresh.device; - this.userSessions.set(userId, session); - await this._enviarMenuConfigurarRoteador(chatId, fresh.device, session.instance); - return; - } - - // ── Seleção de canal vinda do picker inline (ch24_N ou ch5_N) ─────── - const chPickMatch = field.match(/^(ch24|ch5)_(\d+)$/); - if (chPickMatch) { - const band = chPickMatch[1]; // 'ch24' ou 'ch5' - const canal = parseInt(chPickMatch[2], 10); - const paramMap = this._routerParamMap(); - const meta = paramMap[band]; - - await this.bot.sendMessage(chatId, `⏳ Aplicando ${meta.label} → canal ${canal}...`); - const res = await this.genieacs.configurarRoteador( - session.deviceId, - [[meta.param, canal, 'xsd:unsignedInt']], - session.instance - ); - if (!res.success) { - await this.bot.sendMessage(chatId, `❌ Falha: ${res.error}`); - } else { - if (band === 'ch24') session.device.channel24 = String(canal); - else session.device.channel5 = String(canal); - this.userSessions.set(userId, session); - const okMsg = res.queued - ? `⏳ *${meta.label}* → canal *${canal}* enfileirado. O roteador aplicará na próxima conexão.` - : `✅ *${meta.label}* definido para o canal *${canal}*.`; - await this.bot.sendMessage(chatId, okMsg, { parse_mode: 'Markdown' }); - } - await this._enviarMenuConfigurarRoteador(chatId, session.device, session.instance); - return; - } - - const paramMap = this._routerParamMap(); - - // ── Toggle de auto-canal ───────────────────────────────────────────── - if (field === 'auto24' || field === 'auto5') { - const meta = paramMap[field]; - const atualSim = field === 'auto24' - ? session.device.autoChannel24 === 'Sim' - : session.device.autoChannel5 === 'Sim'; - const novoValor = !atualSim; // inverte - - await this.bot.sendMessage(chatId, `⏳ ${atualSim ? 'Desativando' : 'Ativando'} ${meta.label}...`); - const res = await this.genieacs.configurarRoteador( - session.deviceId, - [[meta.param, novoValor, 'xsd:boolean']], - session.instance - ); - - if (!res.success) { - await this.bot.sendMessage(chatId, `❌ Falha: ${res.error}`); - return; - } - // Atualiza o device local - if (field === 'auto24') session.device.autoChannel24 = novoValor ? 'Sim' : 'Não'; - else session.device.autoChannel5 = novoValor ? 'Sim' : 'Não'; - this.userSessions.set(userId, session); - - const autoMsg = res.queued - ? `⏳ ${meta.label} ${novoValor ? 'ativação' : 'desativação'} enfileirada. O roteador aplicará na próxima conexão.` - : `✅ ${meta.label} ${novoValor ? 'ativado' : 'desativado'} com sucesso.`; - await this.bot.sendMessage(chatId, autoMsg); - await this._enviarMenuConfigurarRoteador(chatId, session.device, session.instance); - return; - } - - // ── Picker de canal com botões inline ──────────────────────────────── - if (field === 'ch24' || field === 'ch5') { - const meta = paramMap[field]; - const canalAtual = field === 'ch24' ? (session.device.channel24 || '') : (session.device.channel5 || ''); - const is24 = field === 'ch24'; - - const canais24 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; - const canais5 = [36, 40, 44, 48, 52, 60, 64, 149, 153, 157, 161]; - const canais = is24 ? canais24 : canais5; - - // Monta rows de 4 botões por linha - const rows = []; - for (let i = 0; i < canais.length; i += 4) { - rows.push( - canais.slice(i, i + 4).map(ch => ({ - text: String(ch) === String(canalAtual) ? `✅ ${ch}` : String(ch), - callback_data: `router_cfg_${field}_${ch}` - })) - ); - } - rows.push([{ text: '↩️ Voltar', callback_data: 'router_cfg_refresh' }]); - - await this.bot.sendMessage(chatId, - `📡 *${meta.label}*\nCanal atual: \`${canalAtual || 'N/A'}\`\n\nEscolha o novo canal:`, - { parse_mode: 'Markdown', reply_markup: { inline_keyboard: rows } } - ); - return; - } - - // ── Campos de texto/número: solicita input ─────────────────────────── - const meta = paramMap[field]; - if (!meta) { - await this.bot.sendMessage(chatId, `❓ Campo desconhecido: \`${field}\``, { parse_mode: 'Markdown' }); - return; - } - - // Valor atual para mostrar ao usuário - const fieldToCurrent = { - ssid24: 'ssid24', ssid5: 'ssid5', - senha24: 'senha24', senha5: 'senha5', - dnswan: 'dnsWAN', dnslan: 'dnsLAN', - dhcpstart: 'dhcpInicio', dhcpend: 'dhcpFim', - iplan: 'ipLAN', mascaralan: 'mascaraLAN', - }; - const atualKey = fieldToCurrent[field]; - const valorAtual = atualKey ? (session.device[atualKey] || 'N/A') : 'N/A'; - - let hint = ''; - if (field.startsWith('dns')) hint = ' (ex.: 8.8.8.8,8.8.4.4)'; - else if (field.startsWith('dhcp')) hint = ' (ex.: 192.168.1.100)'; - else if (field === 'iplan') hint = ' (ex.: 192.168.1.1)'; - else if (field === 'mascaralan') hint = ' (ex.: 255.255.255.0)'; - - // Promove o estado de sessão para aguardar o valor - session.action = `router_cfg_wait_${field}`; - this.userSessions.set(userId, session); - - await this.bot.sendMessage(chatId, - `⚙️ *Alterar ${meta.label}*\n\n` + - `Valor atual: \`${valorAtual}\`\n\n` + - `Digite o novo valor${hint}:\n` + - `_(ou envie /cancelar para abortar)_`, - { parse_mode: 'Markdown' } - ); - } - - /** - * Processa o valor digitado pelo usuário para alterar um parâmetro do roteador. - * Chamado por handleUserSessionInput quando session.action começa com "router_cfg_wait_". - */ - async _processarInputConfigurarRoteador(chatId, userId, session, text) { - const field = session.action.replace('router_cfg_wait_', ''); // ex.: "ssid24" - const paramMap = this._routerParamMap(); - const meta = paramMap[field]; - - if (!meta) { - this.userSessions.delete(userId); - await this.bot.sendMessage(chatId, '❌ Campo desconhecido. Operação cancelada.'); - return; - } - - // Cancela se o usuário enviou /cancelar - if (text.trim().toLowerCase() === '/cancelar') { - session.action = 'router_cfg'; - this.userSessions.set(userId, session); - await this.bot.sendMessage(chatId, '🚫 Alteração cancelada.'); - await this._enviarMenuConfigurarRoteador(chatId, session.device, session.instance); - return; - } - - // Converte para o tipo correto - let valorFinal; - if (meta.type === 'xsd:unsignedInt') { - const n = parseInt(text.trim(), 10); - if (isNaN(n) || n < 0) { - await this.bot.sendMessage(chatId, `❌ Valor inválido. Informe um número inteiro positivo.`); - return; // mantém sessão aguardando nova tentativa - } - valorFinal = n; - } else { - valorFinal = text.trim(); - if (!valorFinal) { - await this.bot.sendMessage(chatId, `❌ Valor não pode ser vazio.`); - return; - } - } - - await this.bot.sendMessage(chatId, `⏳ Aplicando ${meta.label}...`); - - const res = await this.genieacs.configurarRoteador( - session.deviceId, - [[meta.param, valorFinal, meta.type]], - session.instance - ); - - if (!res.success) { - await this.bot.sendMessage(chatId, `❌ Falha ao aplicar: ${res.error}`); - session.action = 'router_cfg'; - this.userSessions.set(userId, session); - return; - } - - const statusMsg = res.queued - ? `⏳ *${meta.label}* → \`${valorFinal}\`\n_Task enfileirada. O roteador aplicará na próxima conexão com o servidor._` - : `✅ *${meta.label}* atualizado para: \`${valorFinal}\``; - await this.bot.sendMessage(chatId, statusMsg, { parse_mode: 'Markdown' }); - - // Atualiza o device local com o novo valor para o menu refletir - const fieldToDeviceKey = { - ssid24: 'ssid24', ssid5: 'ssid5', - senha24: 'senha24', senha5: 'senha5', - ch24: 'channel24', ch5: 'channel5', - dnswan: 'dnsWAN', dnslan: 'dnsLAN', - dhcpstart: 'dhcpInicio', dhcpend: 'dhcpFim', - iplan: 'ipLAN', mascaralan: 'mascaraLAN', - }; - const devKey = fieldToDeviceKey[field]; - if (devKey) session.device[devKey] = String(valorFinal); - session.action = 'router_cfg'; - this.userSessions.set(userId, session); - - await this._enviarMenuConfigurarRoteador(chatId, session.device, session.instance); - } - - // ========== IPoE ========== - - /** - * Handler para comando "mudar para ipoe" - * Busca o cliente e inicia fluxo de seleção de serviço - */ - async handleMudarIPoE(chatId, target) { - if (!target) { - await this.bot.sendMessage(chatId, - `🔗 *Mudar para IPoE*\n\n` + - `💡 *Como usar:*\n` + - `• "mudar para ipoe João da Silva"\n` + - `• "ipoe 123.456.789-10"\n` + - `• "mudar ipoe 12345" (ID do cliente)\n\n` + - `O comando configura DHCP Option 82 (Agent Circuit ID) e habilita o DHCP no serviço do cliente no TopSapp.`, - { parse_mode: 'Markdown' } - ); - return; - } - - try { - await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); - - let location = ''; - let query = target; - - if (/guarai|colmeia/i.test(target)) { - location = 'Guarai/Colmeia'; - query = target.replace(/(guarai|colmeia)/gi, '').trim(); - } else if (/pedro afonso/i.test(target)) { - location = 'Pedro Afonso'; - query = target.replace(/pedro afonso/gi, '').trim(); - } - - const resultado = await this.topsapp.buscarCliente(query, location); - - if (!resultado.success) { - await this.bot.sendMessage(chatId, `❌ Erro na busca: ${resultado.error}`); - return; - } - - if (!resultado.clientes || resultado.clientes.length === 0) { - await this.bot.sendMessage(chatId, `🔍 Nenhum cliente encontrado para: "${query}"`); - return; - } - - const instancia = resultado.instancia || location; - - if (resultado.clientes.length === 1) { - await this._mostrarServicosParaIPoE(chatId, resultado.clientes[0].id, instancia); - return; - } - - // Múltiplos clientes → teclado de seleção - let message = `🔗 *Mudar para IPoE — Selecione o cliente:*\n\n`; - const keyboard = []; - - resultado.clientes.slice(0, 8).forEach((cliente, index) => { - message += `*${index + 1}.* ${cliente.nome}`; - if (cliente.cpf) message += ` — CPF: \`${cliente.cpf}\``; - message += `\n`; - keyboard.push([{ - text: `🔗 ${cliente.nome.substring(0, 30)}`, - callback_data: `ipoe_cliente_${cliente.id}_${instancia}` - }]); - }); - - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: { inline_keyboard: keyboard } - }); - - } catch (error) { - console.error('❌ Erro em handleMudarIPoE:', error); - await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); - } - } - - /** - * Callback quando o usuário seleciona um cliente para IPoE - */ - async handleIPoEClienteCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.replace('ipoe_cliente_', '').split('_'); - const idCliente = parts[0]; - const location = parts.slice(1).join('_'); - - await this.bot.answerCallbackQuery(callbackQuery.id); - await this._mostrarServicosParaIPoE(chatId, idCliente, location); - } - - /** - * Exibe serviços ativos do cliente com endereço e login para seleção (IPoE) - */ - async _mostrarServicosParaIPoE(chatId, idCliente, location) { - await this.bot.sendMessage(chatId, '📋 Carregando serviços do cliente...'); - - const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); - - if (!servicosResult.success) { - await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviços: ${servicosResult.error}`); - return; - } - - const servicos = (servicosResult.servicos || []).filter(s => { - const st = s.id_status || s.statusNumerico; - return st === 1 || st === '1' || String(s.status).toLowerCase() === 'liberado'; - }); - - if (servicos.length === 0) { - await this.bot.sendMessage(chatId, `⚠️ Cliente sem serviços ativos para configurar IPoE.`); - return; - } - - let message = `🔗 *Mudar para IPoE — Serviços do cliente:*\n\n`; - const keyboard = []; - const esc = (v) => String(v).replace(/_/g, '\\_'); - - servicos.forEach((s, i) => { - const nome = s.descricao || s.nome || `Serviço ${i + 1}`; - const serial = s.serial || s.mac || 'sem serial'; - const login = s.usuario || s.pppoe_login || s.login || 'sem login'; - const endereco = s.instalacao_endereco || ''; - const numero = s.instalacao_numero || ''; - const bairro = s.instalacao_bairro || ''; - const cidade = s.instalacao_cidade || ''; - - message += `📦 *${i + 1}. ${esc(nome)}*\n`; - message += ` 👤 Login: \`${login}\`\n`; - message += ` 📟 Serial: \`${serial}\`\n`; - if (endereco) { - message += ` 📍 ${endereco}${numero ? `, ${numero}` : ''}`; - if (bairro) message += ` — ${bairro}`; - if (cidade) message += `, ${cidade}`; - message += `\n`; - } - message += `\n`; - - keyboard.push([{ - text: `🔗 ${(i + 1)}. ${nome.substring(0, 30)}`, - callback_data: `ipoe_servico_${idCliente}_${s.id_servico || s.id}_${location}` - }]); - }); - - message += `💡 Selecione o serviço para configurar IPoE:`; - - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: { inline_keyboard: keyboard } - }); - } - - /** - * Callback quando o usuário seleciona um serviço para IPoE - */ - async handleIPoEServicoCallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.replace('ipoe_servico_', '').split('_'); - const idCliente = parts[0]; - const idServico = parts[1]; - const location = parts.slice(2).join('_'); - - await this.bot.answerCallbackQuery(callbackQuery.id); - - // Recarregar serviço para ter dados atualizados - const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); - if (!servicosResult.success) { - await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviço: ${servicosResult.error}`); - return; - } - - const servico = (servicosResult.servicos || []).find(s => - String(s.id_servico || s.id) === String(idServico) - ); - - if (!servico) { - await this.bot.sendMessage(chatId, `❌ Serviço #${idServico} não encontrado.`); - return; - } - - await this._confirmarMudarIPoE(chatId, idCliente, servico, location); - } - - /** - * Mostra preview do que será alterado e pede confirmação - */ - async _confirmarMudarIPoE(chatId, idCliente, servico, location) { - const serial = servico.serial || servico.mac || ''; - const baseInfo = servico.base || ''; - const prefixo = this.topsapp._resolverPrefixoIPoE - ? this.topsapp._resolverPrefixoIPoE(baseInfo) - : null; - - if (!serial) { - await this.bot.sendMessage(chatId, - `❌ *Serviço sem serial de ONU cadastrado.*\n\n` + - `Não é possível gerar o Circuit-ID sem o serial.\n` + - `Verifique o cadastro do serviço no TopSapp.`, - { parse_mode: 'Markdown' } - ); - return; - } - - if (!prefixo) { - await this.bot.sendMessage(chatId, - `❌ *Não foi possível identificar a OLT/base deste serviço.*\n\n` + - `Base cadastrada: "${baseInfo || 'vazio'}"\n` + - `Verifique o cadastro do serviço no TopSapp.`, - { parse_mode: 'Markdown' } - ); - return; - } - - const circuitId = `${prefixo}:${serial}`; - const nome = servico.descricao || servico.nome || 'N/A'; - const idServico = servico.id_servico || servico.id; - - // Escapar underscores para Telegram Markdown - const esc = (v) => String(v).replace(/_/g, '\\_'); - - let message = `🔗 *Confirmar mudança para IPoE*\n\n`; - message += `📦 *Serviço:* ${esc(nome)}\n`; - message += `🆔 *ID Serviço:* ${idServico}\n`; - message += `📟 *Serial ONU:* \`${serial}\`\n`; - message += `🏢 *Base/OLT:* ${esc(baseInfo)}\n`; - message += `🏷️ *Prefixo:* ${esc(prefixo)}\n\n`; - message += `*Será configurado:*\n`; - message += `� DHCP Username: \`${circuitId}\`\n`; - message += `�🔗 Agent Circuit ID: \`${circuitId}\`\n`; - message += `✅ DHCP Liberar: *Sim*\n\n`; - message += `Deseja confirmar?`; - - const keyboard = [ - [ - { text: '✅ Confirmar', callback_data: `confirm_ipoe_${idCliente}_${idServico}_${location}` }, - { text: '❌ Cancelar', callback_data: 'cancel_action' } - ] - ]; - - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: { inline_keyboard: keyboard } - }); - } - - /** - * Callback de confirmação: executa a mudança para IPoE - */ - async handleConfirmIPoECallback(callbackQuery, data) { - const chatId = callbackQuery.message.chat.id; - const parts = data.replace('confirm_ipoe_', '').split('_'); - const idCliente = parts[0]; - const idServico = parts[1]; - const location = parts.slice(2).join('_'); - - await this.bot.answerCallbackQuery(callbackQuery.id); - await this.bot.sendMessage(chatId, '⏳ Configurando IPoE no TopSapp...'); - - const resultado = await this.topsapp.mudarParaIPoE(idCliente, idServico, location); - - if (resultado.success) { - const esc = (v) => String(v).replace(/_/g, '\\_'); - let msg = `✅ *IPoE configurado com sucesso!*\n\n`; - msg += `📦 Serviço: ${esc(resultado.servico)}\n`; - msg += `📟 Serial: \`${resultado.serial}\`\n`; - msg += `🏢 OLT: ${esc(resultado.baseInfo)} (${esc(resultado.prefixo)})\n`; - msg += `🔗 Agent Circuit ID: \`${resultado.circuitId}\`\n`; - msg += `✅ DHCP Liberar: *Ativado*`; - - await this.bot.sendMessage(chatId, msg, { parse_mode: 'Markdown' }); - } else { - await this.bot.sendMessage(chatId, - `❌ *Erro ao configurar IPoE*\n\n⚠️ ${resultado.error}`, - { parse_mode: 'Markdown' } - ); - } - } -} - -module.exports = BotHandler; - +const NLPProcessor = require('../nlp/processor'); +const SSHManager = require('../ssh/manager'); +const { TopSappManager } = require('../topsapp'); +const FiberhomeManager = require('../fiberhome/manager'); +const FiberhomeTL1Manager = require('../fiberhome/manager_tl1'); +const { GenieACSManager } = require('../genieacs'); +const config = require('../config/config'); +const logger = require('../utils/logger'); +const DateProcessor = require('../utils/dateProcessor'); + +class BotHandler { + constructor(bot) { + this.bot = bot; + this.nlp = new NLPProcessor(); + this.ssh = new SSHManager(); + this.topsapp = new TopSappManager(); + this.fiberhome = new FiberhomeManager(); + this.fiberhomeTL1 = new FiberhomeTL1Manager(); + this.genieacs = new GenieACSManager(); + this.dateProcessor = new DateProcessor(); + this.userSessions = new Map(); // Armazena sessões de usuário + this.suporteSessions = new Map(); // Armazena sessões de suporte em andamento + + logger.info('BotHandler inicializado'); + console.log('🎯 BotHandler inicializado'); + } + + /** + * Verifica se o usuário tem permissão + */ + isAuthorized(userId) { + if (config.bot.allowedUsers.length === 0) { + return true; // Se não há lista de usuários, permite todos + } + return config.bot.allowedUsers.includes(userId); + } + + /** + * Processa mensagens recebidas + */ + async handleMessage(msg) { + const chatId = msg.chat.id; + const userId = msg.from.id; + const text = msg.text; + + // Log da mensagem + if (config.bot.debug) { + console.log(`📨 Mensagem de ${msg.from.first_name} (${userId}): ${text}`); + } + + // Verificar autorização + if (!this.isAuthorized(userId)) { + await this.bot.sendMessage(chatId, + `🚫 **Acesso Negado**\n\n` + + `❌ Você não tem permissão para usar este bot.\n` + + `🆔 Seu ID: \`${userId}\`\n\n` + + `📞 Entre em contato com o administrador para solicitar acesso.`, + { parse_mode: 'Markdown' } + ); + console.log(`🚫 Acesso negado para usuário ${msg.from.first_name} (${userId})`); + return; + } + + try { + // Verificar se há uma sessão de suporte ativa + const processouSuporte = await this.processarMensagemSuporte(msg); + if (processouSuporte) { + return; // Mensagem foi processada pelo fluxo de suporte + } + + // Verificar se o usuário está em uma sessão esperando input + const userSession = this.userSessions.get(userId); + if (userSession) { + await this.handleUserSessionInput(chatId, text, userId, userSession); + return; + } + + // Processar a mensagem com NLP + const intent = this.nlp.processMessage(text); + + if (config.bot.debug) { + console.log('🧠 Intent detectado:', intent); + } + + // Executar ação baseada na intenção + await this.executeAction(chatId, intent, msg.from); + + } catch (error) { + console.error('❌ Erro ao processar mensagem:', error); + await this.bot.sendMessage(chatId, '❌ Ocorreu um erro ao processar sua mensagem. Tente novamente.'); + } + } + + /** + * Executa ação baseada na intenção detectada + */ + async executeAction(chatId, intent, user) { + const { action, target, confidence, type } = intent; + const userId = user.id; // Extrair userId do objeto user + + // Se a confiança é muito baixa, pedir esclarecimento + if (confidence < 0.3 && action) { + const suggestions = this.nlp.suggestCorrection(action); + let message = '🤔 Não entendi muito bem. '; + + if (suggestions.length > 0) { + message += `Você quis dizer: ${suggestions.join(', ')}?`; + } else { + message += 'Digite /help para ver os comandos disponíveis.'; + } + + await this.bot.sendMessage(chatId, message); + return; + } + + // Executar ação específica + try { + switch (action) { + case 'start': + await this.handleStart(chatId, user); + break; + + case 'help': + await this.handleHelp(chatId); + break; + + case 'ssh': + await this.handleSSH(chatId, target); + break; + + case 'status': + await this.handleStatus(chatId, target); + break; + + case 'lista': + case 'equipamentos': + await this.handleListEquipments(chatId); + break; + + case 'listar_usuarios': + await this.handleListUsers(chatId, target); + break; + + case 'consultar_usuario': + console.log(`🔍 Executando handleConsultarUsuario com target: ${target}`); + await this.handleConsultarUsuario(chatId, target); + break; + + case 'admin': + case 'administracao': + await this.handleAdmin(chatId); + break; + + case 'logs': + await this.handleViewLogs(chatId); + break; + + case 'limpar_logs': + await this.handleClearLogs(chatId); + break; + + case 'usuarios_autorizados': + await this.handleListAuthorizedUsers(chatId); + break; + + case 'meu_id': + case 'meu_user_id': + await this.handleMyId(chatId, userId); + break; + + case 'ver_sinal': + console.log(`🔍 Executando handleVerSinal com target: ${target}`); + await this.handleVerSinal(chatId, target); + break; + + case 'remover_onu': + console.log(`🗑️ Executando handleRemoverOnu com target: ${target}`); + await this.handleRemoverOnu(chatId, target); + break; + + case 'adicionar_onu': + console.log(`➕ Executando handleAdicionarOnu com target: ${target}`); + await this.handleAdicionarOnu(chatId, target); + break; + + case 'adicionar_onu_vlan': + console.log(`➕ Executando handleAdicionarOnuVlan com target: ${target}, vlanId: ${intent.vlanId}`); + await this.handleAdicionarOnuVlan(chatId, target, intent.vlanId); + break; + + case 'adicionar_onu_fiberhome': + console.log(`➕ Executando handleAdicionarOnuFiberhome com target: ${target}`); + await this.handleAdicionarOnuFiberhome(chatId, target, userId); + break; + + case 'remover_onu_fiberhome': + console.log(`🗑️ Executando handleRemoverOnuFiberhome com target: ${target}`); + await this.handleRemoverOnuFiberhome(chatId, target, userId); + break; + + case 'buscar_cliente': + console.log(`🔍 Executando handleBuscarCliente com target: ${target}`); + await this.handleBuscarCliente(chatId, target); + break; + + case 'ver_faturas': + console.log(`📄 Executando handleVerFaturas com target: ${target}`); + await this.handleVerFaturas(chatId, target); + break; + + case 'gerar_boleto': + console.log(`💸 Executando handleGerarBoleto com target: ${target}`); + await this.handleGerarBoleto(chatId, target); + break; + + case 'bloquear_cliente': + console.log(`🔒 Executando handleBloquearCliente com target: ${target}`); + await this.handleBloquearCliente(chatId, target); + break; + + case 'liberar_cliente': + console.log(`🔓 Executando handleLiberarCliente com target: ${target}`); + await this.handleLiberarCliente(chatId, target); + break; + + case 'listar_servicos': + console.log(`📋 Executando handleListarServicos com target: ${target}`); + await this.handleListarServicos(chatId, target); + break; + + case 'diagnosticar': + console.log(`🩺 Executando handleDiagnosticarCliente com target: ${target}`); + await this.handleDiagnosticarCliente(chatId, target); + break; + + case 'dados_cliente': + case 'dados_cadastrais': + console.log(`📋 Executando handleDadosClientePorBusca com target: ${target}`); + await this.handleDadosClientePorBusca(chatId, target); + break; + + case 'status_topsapp': + console.log(`🏢 Executando handleStatusTopSapp`); + await this.handleStatusTopSapp(chatId); + break; + + case 'status_onu': + console.log(`📡 Executando handleStatusONU com target: ${target}`); + await this.handleStatusONU(chatId, target); + break; + + case 'fiberhome_status': + console.log(`🔌 Executando handleFiberhomeStatus`); + await this.handleFiberhomeStatus(chatId); + break; + + case 'ver_roteador': + console.log(`📡 Executando handleVerRoteador com target: ${target}`); + await this.handleVerRoteador(chatId, target); + break; + + case 'configurar_roteador': + console.log(`⚙️ Executando handleConfigurarRoteador com target: ${target}`); + await this.handleConfigurarRoteador(chatId, userId, target); + break; + + case 'abrir_suporte': + case 'adicionar_suporte': + case 'criar_suporte': + 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 'mudar_ipoe': + console.log(`🔗 Executando handleMudarIPoE com target: ${target}`); + await this.handleMudarIPoE(chatId, target); + break; + + case 'conectar': + await this.handleConnect(chatId, target); + break; + + case 'desconectar': + await this.handleDisconnect(chatId, target); + break; + + case 'desconectar_usuario': + await this.handleDisconnectUser(chatId, target); + break; + + case 'reiniciar': + await this.handleRestart(chatId, target); + break; + + case 'ping': + await this.handlePing(chatId, target); + break; + + default: + if (!action) { + await this.handleUnknown(chatId, intent.originalText || ''); + } else { + await this.bot.sendMessage(chatId, `❓ Ação '${action}' não implementada ainda.`); + } + } + } catch (actionError) { + console.error(`❌ Erro ao executar ação '${action}':`, actionError); + await this.bot.sendMessage(chatId, `❌ Erro ao executar ação: ${actionError.message}`); + } + } + + /** + * Comando /start + */ + async handleStart(chatId, user) { + const welcomeMessage = ` +🤖 *Bem-vindo ao Bot de Ajuda!* + +Olá ${user.first_name}! Agora você não precisa mandar mensagem pro Rodrigo ou no grupo da ID pra ficar vendo coisas triviais: + +🔐 *Conexões SSH* - Acesso remoto a equipamentos +🌐 *Status de rede* - Verificação de conectividade +⚙️ *Comandos remotos* - Execução de tarefas + +*Exemplos do que você pode dizer:* +• "Conectar no router1" +• "Verificar status do switch1" +• "Reiniciar equipamento" +• "Lista de equipamentos" + +Digite /help para ver todos os comandos disponíveis! + `; + + await this.bot.sendMessage(chatId, welcomeMessage, { parse_mode: 'Markdown' }); + } + + /** + * Comando /help + */ + async handleHelp(chatId) { + const helpMessage = `🤖 **BOT DE AJUDA TÉCNICA** + +🔧 **COMANDOS PRINCIPAIS:** +• /start - Inicia o bot +• /help - Esta ajuda +• /equipamentos - Lista equipamentos + +🏢 **GESTÃO DE CLIENTES:** +• "buscar cliente João Silva" +• "dados cliente 123.456.789-10" +• "ver faturas cliente" +• "abrir suporte" + +📡 **OLT/ONU:** +• "ver sinal DACM91569513" +• "adicionar onu SERIAL" +• "adicionar onu fiberhome SERIAL" +• "remover onu SERIAL" + +🌐 **GENIEACS (CPE/ROTEADOR):** +• "ver roteador SERIAL" +• "configurar roteador SERIAL" +• "configurar roteador João Silva" + +👥 **JUNIPER:** +• "consultar usuario USER" +• "desconectar cliente USER" +• "listar usuários conectados" + +🔐 **SSH:** +• "ssh juniper" +• "acessar concentrador" + +📋 **Para ver comandos detalhados, use:**`; + + const keyboard = [ + [ + { text: '🏢 TopSapp', callback_data: 'help_topsapp' }, + { text: '📡 OLT/ONU', callback_data: 'help_olt' } + ], + [ + { text: '🌐 GenieACS', callback_data: 'help_genieacs' }, + { text: '👥 Juniper', callback_data: 'help_juniper' } + ], + [ + { text: '🔐 SSH', callback_data: 'help_ssh' }, + { text: '🎫 Suporte', callback_data: 'help_suporte' } + ], + [ + { text: '🛡️ Admin', callback_data: 'help_admin' } + ] + ]; + + await this.bot.sendMessage(chatId, helpMessage, { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: keyboard + } + }); + } + + /** + * Handler para callbacks de help detalhado + */ + async handleHelpDetailCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const helpType = data.replace('help_', ''); + + await this.bot.answerCallbackQuery(callbackQuery.id); + + let helpMessage = ''; + let backButton = { text: '⬅️ Voltar', callback_data: 'help_main' }; + + switch (helpType) { + case 'main': + await this.handleHelp(chatId); + return; + + case 'topsapp': + helpMessage = `🏢 **TOPSAPP - GESTÃO DE CLIENTES** + +**🔍 BUSCAR CLIENTES:** +• "buscar cliente João Silva" +• "buscar cliente 123.456.789-10" +• "buscar cliente Guarai João" + +**📋 DADOS CADASTRAIS:** +• "dados cliente João Silva" +• "dados cadastrais 123.456.789-10" + +**📄 FATURAS:** +• "ver faturas 123.456.789-10" +• "gerar boleto fatura123" + +**⚙️ SERVIÇOS:** +• "listar serviços 123.456.789-10" + +**🔒 BLOQUEIO/LIBERAÇÃO:** +• "bloquear 123.456.789-10" +• "liberar 123.456.789-10" + +**📊 STATUS:** +• "status topsapp"`; + break; + + case 'olt': + helpMessage = `📡 **OLT/ONU - COMANDOS** + +**📶 VERIFICAR SINAL:** +• "ver sinal DACM91569513" +• "verificar sinal ABCD12345678" +• "sinal SERIAL_NUMBER" + +**➕ ADICIONAR ONU:** +• "adicionar onu DACM91569513" +• "cadastrar ABCD12345678" +• "add onu SERIAL_NUMBER" + +**➕ ADICIONAR ONU COM VLAN CUSTOMIZADA:** +• "adicionar onu vlan 603 DACM91569513" +• "add onu vlan 700 ABCD12345678" +• "adicionar vlan 603 SERIAL_NUMBER" + +**➕ ADICIONAR ONU FIBERHOME VIA TL1:** +• "adicionar onu fiberhome FHTT12345678" +• "fiberhome onu FHTT12345678" +• Processo interativo com: + - Nome/alias do usuário + - Nome da OLT + - Porta PON (ex: 7-1) + - Tipo da ONU (AN5506-02-B ou AN5506-01-A) + - VLAN (padrão: 1002) +• Configura automaticamente: + - WAN Services + - Login Web (admin/clicknet10) + - Reinicia a ONU + +**🗑️ REMOVER ONU:** +• "remover onu DACM91569513" +• "deletar ABCD12345678" +• "remover SERIAL_NUMBER" + +**🔄 REINICIAR ONU:** +• Após verificar sinal, use botão "🔌 Reiniciar ONU" + +**⬆️ ATUALIZAR FIRMWARE:** +• Após verificar sinal, use botão "⬆️ Atualizar ONU" + +**🏢 OLTs DISPONÍVEIS:** +• OLT FIBERHOME (131.0.192.9) +• OLT DITO (192.168.190.7) +• OLT COLMEIA (192.168.190.8) +• OLT CANAA (192.168.190.9) +• OLT CANAA 2 (192.168.190.10) +• OLT PEDRO AFONSO 1 (192.168.100.2) +• OLT CANAVIEIRAS (192.168.100.4)`; + break; + + case 'juniper': + helpMessage = `👥 **JUNIPER - GERENCIAR USUÁRIOS** + +**� CONSULTAR USUÁRIO:** +• "consultar usuario USUARIO" +• "ver usuario USUARIO" +• "status usuario USUARIO" + +**�🚫 DESCONECTAR:** +• "desconectar cliente USUARIO" +• "kick usuário USUARIO" +• "remover cliente USUARIO" + +**📊 LISTAR:** +• "listar usuários conectados" + +**🔧 STATUS:** +• "verificar concentrador" +• "status do juniper" + +**🌐 PING:** +• "ping 131.0.192.254" + +**🔄 REINICIAR:** +• "reiniciar juniper"`; + break; + + case 'ssh': + helpMessage = `🔐 **SSH E CONEXÕES** + +**📡 CONECTAR:** +• /ssh +• "ssh juniper" +• "acessar concentrador" + +**📋 LISTAR:** +• /equipamentos + +**🔧 EXEMPLOS:** +• "conectar no concentrador" +• "acessar juniper" +• "ssh olt"`; + break; + + case 'suporte': + helpMessage = `🎫 **SISTEMA DE SUPORTE** + +**🆕 ABRIR SUPORTE:** +• "abrir suporte" +• "criar suporte para João Silva" +• "novo chamado" +• "suporte cliente 123.456.789-10" + +**� 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 +4️⃣ Seleção do técnico responsável +5️⃣ Agendamento de data/hora +6️⃣ Descrição detalhada +7️⃣ Confirmação e protocolo + +**📅 EXEMPLOS DE DATA/HORA:** +• "amanhã às 14h" +• "sexta-feira 09:00" +• "15/08 às 16h30" +• "hoje 18h" +• "em 3 dias às 10h" +• "segunda-feira 08:30"`; + break; + + case 'genieacs': + helpMessage = `🌐 **GENIEACS - CPE / ROTEADOR** + +**🔍 VER ROTEADOR:** +• "ver roteador SERIAL" +• "ver roteador TAG" +• Exibe: SSID, senha, canais, clientes Wi-Fi, PPPoE, velocidade, IP LAN, DHCP + +**⚙️ CONFIGURAR ROTEADOR:** +• "configurar roteador SERIAL" +• "configurar roteador TAG" +• "configurar roteador João Silva" (busca no TopSapp) +• "configurar roteador 123.456.789-10" (CPF) +• Abre menu interativo para alterar: + - SSID e senha (2.4 GHz e 5 GHz) + - Canal (fixo ou automático) + - DNS WAN e DNS LAN + - DHCP início/fim + - IP LAN e máscara + +**📊 DIAGNÓSTICO:** +• Disponível durante o diagnóstico de serviço +• Exibe: taxa de download/upload, tráfego total, uptime PPPoE, estado do link físico + +**📌 OBSERVAÇÕES:** +• Alterações com CPE online: ✅ aplicadas imediatamente +• Alterações com CPE offline: ⏳ aguardam próximo contato do equipamento`; + break; + + case 'admin': + helpMessage = `🛡️ **COMANDOS ADMINISTRATIVOS** + +**👥 USUÁRIOS:** +• "admin" +• "usuarios autorizados" +• "meu id" + +**📊 STATUS:** +• "fiberhome status" +• "status topsapp" + +**🔧 FIBERHOME ESPECÍFICOS:** +• "status onu FHTT12345678" +• "verificar onu FHTT12345678" +• "onu status DeviceName" + +**🔒 SEGURANÇA:** +• Bot restrito a usuários autorizados +• Para solicitar acesso, forneça seu ID`; + break; + + default: + helpMessage = '❌ Seção de ajuda não encontrada.'; + } + + await this.bot.sendMessage(chatId, helpMessage, { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [[backButton]] + } + }); + } + + /** + * Listar equipamentos + */ + async handleListEquipments(chatId) { + const equipments = this.ssh.listEquipments(); + + if (equipments.length === 0) { + await this.bot.sendMessage(chatId, '📋 Nenhum equipamento configurado.'); + return; + } + + let message = '📋 *Equipamentos Disponíveis:*\n\n'; + + equipments.forEach((eq, index) => { + message += `${index + 1}. *${eq.name}*\n`; + message += ` 📍 ${eq.host}:${eq.port}\n`; + message += ` 🔑 ID: \`${eq.id}\`\n\n`; + }); + + message += '💡 *Como usar:* Digite "conectar " ou "ssh "'; + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + } + + /** + * Conexão SSH + */ + async handleSSH(chatId, target) { + if (!target) { + await this.bot.sendMessage(chatId, '❓ Especifique o equipamento. Exemplo: "ssh router1"'); + return; + } + + await this.bot.sendMessage(chatId, `🔄 Conectando em ${target}...`); + + try { + const result = await this.ssh.connect(target); + + if (result.success) { + await this.bot.sendMessage(chatId, `✅ ${result.message}`); + + // Oferecer opções de comandos + const keyboard = { + inline_keyboard: [ + [ + { text: '📊 Status', callback_data: `cmd_status_${result.connectionId}` }, + { text: '🔄 Reiniciar', callback_data: `cmd_reboot_${result.connectionId}` } + ], + [ + { text: '📋 Comando personalizado', callback_data: `cmd_custom_${result.connectionId}` }, + { text: '❌ Desconectar', callback_data: `cmd_disconnect_${result.connectionId}` } + ] + ] + }; + + await this.bot.sendMessage(chatId, '🎛️ O que você gostaria de fazer?', { + reply_markup: keyboard + }); + } else { + await this.bot.sendMessage(chatId, `❌ Falha na conexão: ${result.message}`); + } + } catch (error) { + await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); + } + } + + /** + * Verificar status + */ + async handleStatus(chatId, target) { + if (!target) { + // Mostrar status geral + const connections = this.ssh.getActiveConnections(); + + if (connections.length === 0) { + await this.bot.sendMessage(chatId, '📊 Nenhuma conexão ativa no momento.'); + return; + } + + let message = '📊 *Conexões Ativas:*\n\n'; + connections.forEach((conn, index) => { + message += `${index + 1}. ${conn.equipment} (${conn.host})\n`; + message += ` ⏰ Conectado: ${conn.connectedAt.toLocaleString()}\n\n`; + }); + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + return; + } + + await this.bot.sendMessage(chatId, `🔍 Verificando status de ${target}...`); + + try { + const result = await this.ssh.testConnection(target); + + if (result.success) { + await this.bot.sendMessage(chatId, `✅ ${result.message}`); + } else { + await this.bot.sendMessage(chatId, `❌ ${result.message}`); + } + } catch (error) { + await this.bot.sendMessage(chatId, `❌ Erro ao verificar status: ${error.message}`); + } + } + + /** + * Conectar a um equipamento + */ + async handleConnect(chatId, target) { + await this.handleSSH(chatId, target); + } + + /** + * Desconectar + */ + async handleDisconnect(chatId, target) { + if (!target || target === 'todas' || target === 'all') { + // Desconectar todas + this.ssh.closeAllConnections(); + await this.bot.sendMessage(chatId, '🔌 Todas as conexões foram encerradas.'); + return; + } + + // Implementar lógica para desconectar equipamento específico + await this.bot.sendMessage(chatId, `🔌 Desconectando ${target}...`); + } + + /** + * Reiniciar equipamento + */ + async handleRestart(chatId, target) { + if (!target) { + await this.bot.sendMessage(chatId, '❓ Especifique qual equipamento reiniciar.'); + return; + } + + await this.bot.sendMessage(chatId, `⚠️ Reiniciar ${target}? Esta ação pode causar indisponibilidade.`, { + reply_markup: { + inline_keyboard: [ + [ + { text: '✅ Confirmar', callback_data: `confirm_restart_${target}` }, + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ] + } + }); + } + + /** + * Ping + */ + async handlePing(chatId, target) { + if (!target) { + await this.bot.sendMessage(chatId, '❓ Especifique o IP ou hostname para fazer ping.'); + return; + } + + await this.bot.sendMessage(chatId, `🏓 Fazendo ping para ${target}...`); + + // Implementar ping usando comandos do sistema + // Por enquanto, simular resposta + setTimeout(async () => { + await this.bot.sendMessage(chatId, `🏓 Ping para ${target}: 12ms - Conexão OK`); + }, 2000); + } + + /** + * Desconectar usuário específico no concentrador Juniper + */ + async handleDisconnectUser(chatId, username) { + if (!username) { + await this.bot.sendMessage(chatId, '❓ Especifique o usuário a ser desconectado. Exemplo: "desconectar cliente joao123"'); + return; + } + + await this.bot.sendMessage(chatId, `🔄 Desconectando usuário ${username} do concentrador...`); + + try { + // Usar concentrador Juniper como padrão + const result = await this.ssh.disconnectJuniperUser('juniper', username); + + if (result.success) { + await this.bot.sendMessage(chatId, `✅ ${result.message}`); + + if (result.output && result.output.trim()) { + const outputMessage = `📋 *Saída do comando:*\n\`\`\`\n${result.output}\n\`\`\``; + await this.bot.sendMessage(chatId, outputMessage, { parse_mode: 'Markdown' }); + } + } else { + await this.bot.sendMessage(chatId, `❌ ${result.message}`); + + if (result.error && result.error.trim()) { + const errorMessage = `⚠️ *Detalhes do erro:*\n\`\`\`\n${result.error}\n\`\`\``; + await this.bot.sendMessage(chatId, errorMessage, { parse_mode: 'Markdown' }); + } + } + } catch (error) { + console.error('Erro ao desconectar usuário:', error); + await this.bot.sendMessage(chatId, `❌ Erro interno: ${error.message}`); + } + } + + /** + * Listar usuários conectados no concentrador + */ + async handleListUsers(chatId, target = 'juniper') { + await this.bot.sendMessage(chatId, '🔍 Listando usuários conectados no concentrador...'); + + try { + const result = await this.ssh.listJuniperUsers(target); + + if (result.success) { + let message = '📋 *Usuários Conectados:*\n\n'; + + if (result.output && result.output.trim()) { + // Processar saída para extrair informações úteis + const lines = result.output.split('\n'); + let userCount = 0; + + for (const line of lines) { + if (line.trim() && !line.includes('Total') && !line.includes('---')) { + userCount++; + message += `${userCount}. \`${line.trim()}\`\n`; + } + } + + if (userCount === 0) { + message += '🟢 Nenhum usuário conectado no momento.\n'; + } else { + message += `\n👥 Total: ${userCount} usuário(s) conectado(s)`; + } + } else { + message += '🟢 Nenhum usuário conectado no momento.'; + } + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + + // Oferecer opções para desconectar usuários se houver algum conectado + if (result.output && result.output.trim() && !result.output.includes('Total: 0')) { + const keyboard = { + inline_keyboard: [ + [ + { text: '🔄 Atualizar lista', callback_data: 'refresh_users' }, + { text: '❌ Desconectar usuário', callback_data: 'disconnect_user_prompt' } + ] + ] + }; + + await this.bot.sendMessage(chatId, '🎛️ O que você gostaria de fazer?', { + reply_markup: keyboard + }); + } + + } else { + await this.bot.sendMessage(chatId, `❌ ${result.message}`); + } + } catch (error) { + console.error('Erro ao listar usuários:', error); + await this.bot.sendMessage(chatId, `❌ Erro interno: ${error.message}`); + } + } + + /** + * Consultar status de um assinante específico no concentrador Juniper + */ + async handleConsultarUsuario(chatId, username) { + if (!username) { + await this.bot.sendMessage(chatId, '❓ Especifique o login do usuário. Exemplo: "consultar usuario joao@provedor"'); + return; + } + + await this.bot.sendMessage(chatId, `🔍 Consultando usuário *${username}* no concentrador...`, { parse_mode: 'Markdown' }); + + try { + const result = await this.ssh.getJuniperSubscriberInfo('juniper', username); + + if (!result.success) { + await this.bot.sendMessage(chatId, `❌ ${result.message}`); + return; + } + + if (!result.connected) { + await this.bot.sendMessage(chatId, `🔴 *${username}* não está conectado no concentrador.`, { parse_mode: 'Markdown' }); + return; + } + + const message = + `✅ *Usuário conectado!*\n\n` + + `👤 *Login:* \`${result.username}\`\n` + + `🌐 *Endereço IP:* \`${result.ip || 'N/A'}\`\n` + + `🔌 *Interface PPPoE:* \`${result.interface || 'N/A'}\`\n` + + `\n📋 *Linha bruta:*\n\`\`\`\n${result.rawLine}\n\`\`\``; + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + + } catch (error) { + console.error('Erro ao consultar usuário:', error); + await this.bot.sendMessage(chatId, `❌ Erro interno: ${error.message}`); + } + } + + /** + * Diagnóstico completo de um cliente: TopSapp + Juniper PPPoE + ONU + */ + async handleDiagnosticarCliente(chatId, query) { + if (!query) { + await this.bot.sendMessage(chatId, + `🩺 *Diagnóstico de Cliente*\n\n` + + `💡 *Como usar:*\n` + + `• "diagnosticar João da Silva"\n` + + `• "diagnosticar 123.456.789-10"\n\n` + + `O diagnóstico verifica: status no TopSapp, sessão PPPoE no concentrador e status da ONU.`, + { parse_mode: 'Markdown' } + ); + return; + } + + try { + await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); + + let location = ''; + let searchQuery = query; + + if (/guarai|colmeia/i.test(query)) { + location = 'Guarai/Colmeia'; + searchQuery = query.replace(/(guarai|colmeia)/gi, '').trim(); + } else if (/pedro afonso/i.test(query)) { + location = 'Pedro Afonso'; + searchQuery = query.replace(/pedro afonso/gi, '').trim(); + } + + const resultado = await this.topsapp.buscarCliente(searchQuery, location); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, `❌ Erro na busca: ${resultado.error}`); + return; + } + + if (!resultado.clientes || resultado.clientes.length === 0) { + await this.bot.sendMessage(chatId, `🔍 Nenhum cliente encontrado para: "${searchQuery}"`); + return; + } + + const instancia = resultado.instancia || location; + + if (resultado.clientes.length === 1) { + await this._mostrarServicosParaDiagnostico(chatId, resultado.clientes[0].id, instancia); + return; + } + + // Múltiplos clientes → exibir teclado de seleção + let message = `🔍 *${resultado.clientes.length} clientes encontrados*\n\n`; + const keyboard = []; + + resultado.clientes.slice(0, 8).forEach((cliente, index) => { + message += `*${index + 1}.* ${cliente.nome}`; + if (cliente.cpf) message += ` — CPF: \`${cliente.cpf}\``; + message += `\n`; + + keyboard.push([{ + text: `🩺 Diagnosticar: ${cliente.nome.substring(0, 30)}`, + callback_data: `diag_cliente_${cliente.id}_${instancia}` + }]); + }); + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + + } catch (error) { + console.error('❌ Erro em handleDiagnosticarCliente:', error); + await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); + } + } + + async _mostrarServicosParaDiagnostico(chatId, idCliente, location) { + const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); + + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviços: ${servicosResult.error}`); + return; + } + + const servicos = (servicosResult.servicos || []).filter(s => { + const st = (s.id_status || s.statusNumerico); + return st === 1 || st === '1' || String(s.status).toLowerCase() === 'liberado' || String(s.status).toLowerCase() === 'ativo'; + }); + + if (servicos.length === 0) { + await this.bot.sendMessage(chatId, `⚠️ Cliente sem serviços ativos para diagnosticar.`); + return; + } + + if (servicos.length === 1) { + await this.runDiagnosticoServico(chatId, idCliente, servicos[0].id_servico || servicos[0].id, location, servicos[0]); + return; + } + + let message = `🩺 *Selecione o serviço para diagnosticar:*\n\n`; + const keyboard = []; + + servicos.forEach((s, i) => { + const nome = s.descricao || s.nome || `Serviço ${i + 1}`; + const login = s.usuario || s.pppoe_login || s.login || ''; + message += `*${i + 1}.* ${nome}${login ? ` — \`${login}\`` : ''}\n`; + keyboard.push([{ + text: `🔍 ${nome.substring(0, 35)}`, + callback_data: `diag_servico_${idCliente}_${s.id_servico || s.id}_${location}` + }]); + }); + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + } + + async handleDiagClienteCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('diag_cliente_', '').split('_'); + const idCliente = parts[0]; + const location = parts.slice(1).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this._mostrarServicosParaDiagnostico(chatId, idCliente, location); + } + + async handleDiagServicoCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('diag_servico_', '').split('_'); + const idCliente = parts[0]; + const idServico = parts[1]; + const location = parts.slice(2).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.runDiagnosticoServico(chatId, idCliente, idServico, location, null); + } + + async runDiagnosticoServico(chatId, idCliente, idServico, location, servicoPreCarregado) { + try { + await this.bot.sendMessage(chatId, '⏳ Executando diagnóstico completo...'); + + // Buscar serviços caso não tenhamos o objeto do serviço + let servico = servicoPreCarregado; + if (!servico) { + const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviços: ${servicosResult.error}`); + return; + } + const idNum = parseInt(idServico, 10); + servico = (servicosResult.servicos || []).find(s => + (s.id_servico || s.id) === idNum || String(s.id_servico || s.id) === String(idServico) + ); + } + + if (!servico) { + await this.bot.sendMessage(chatId, `❌ Serviço #${idServico} não encontrado.`); + return; + } + + const pppoeLogin = servico.usuario || servico.pppoe_login || servico.login || null; + const serial = servico.serial || servico.comodato_serial || servico.mac || null; + const statusServico = this.topsapp.getStatusServicoDetalhado + ? this.topsapp.getStatusServicoDetalhado(servico.id_status) + : (servico.status || 'N/A'); + const nomePlano = servico.descricao || servico.nome || 'N/A'; + const endereco = servico.instalacao_endereco || servico.end_rua || null; + const cidade = servico.instalacao_cidade || null; + // Campo base/AP usado para rotear para a OLT correta + const baseInfo = servico.base || (servico.id_ap ? `AP ${servico.id_ap}` : null); + + // Executar consultas em paralelo: Juniper + ONU + GenieACS (por TAG/login) + // No concentrador Juniper usa-se só o usuário (antes do @) + const pppoeUser = pppoeLogin ? pppoeLogin.split('@')[0] : null; + const [juniperResult, onuResult, genieResult] = await Promise.all([ + pppoeUser + ? this.ssh.getJuniperSubscriberInfo('juniper', pppoeUser).catch(e => ({ success: false, error: e.message })) + : Promise.resolve({ success: false, skipped: true }), + serial + ? this._diagnosticarONU(serial, baseInfo).catch(e => ({ success: false, error: e.message })) + : Promise.resolve({ success: false, skipped: true }), + pppoeLogin && this.genieacs.isReady() + ? this.genieacs.buscarRoteador(pppoeLogin).catch(() => ({ success: false })) + : Promise.resolve({ success: false, skipped: true }) + ]); + + // Sincronização TAG GenieACS ↔ PPPoE TopSapp (fire-and-forget, silencioso) + // Garante que o login PPPoE do cliente fica registrado como TAG no GenieACS, + // permitindo buscas futuras por "ver roteador ". + if (serial && pppoeLogin && this.genieacs.isReady()) { + this.genieacs.sincronizarTagPPPoE(serial, pppoeLogin) + .then(r => { + if (r.success && !r.already) { + logger.info(`[GenieACS] TAG "${r.tag}" sincronizada em ${r.deviceId}`); + console.log(`🏷️ [GenieACS] TAG "${r.tag}" adicionada ao device ${r.deviceId}`); + } + }) + .catch(e => logger.warn(`[GenieACS] Falha ao sincronizar TAG: ${e.message}`)); + } + + // Montar relatório + let msg = `🩺 *Diagnóstico do Cliente*\n`; + msg += `━━━━━━━━━━━━━━━━━━━━━\n\n`; + + // Bloco 1: TopSapp + msg += `📋 *Serviço TopSapp*\n`; + msg += ` 📦 Plano: ${nomePlano}\n`; + msg += ` 🔌 Status: ${statusServico}\n`; + if (pppoeLogin) msg += ` 👤 Login PPPoE: \`${pppoeLogin}\`\n`; + if (servico.dhcp_username) msg += ` 👤 DHCP Username: \`${servico.dhcp_username}\`\n`; + if (servico.dhcp_agent_circuit_id) msg += ` 🔗 Agent Circuit ID: \`${servico.dhcp_agent_circuit_id}\`\n`; + if (endereco) msg += ` 📍 Endereço: ${endereco}${cidade ? `, ${cidade}` : ''}\n`; + msg += '\n'; + + // Bloco 2: Concentrador Juniper + msg += `🌐 *Concentrador Juniper*\n`; + if (juniperResult.skipped) { + msg += ` ⚫ Login PPPoE não informado no cadastro\n`; + } else if (!juniperResult.success) { + msg += ` ⚠️ Erro ao consultar: ${juniperResult.error || 'falha na consulta'}\n`; + } else if (juniperResult.connected) { + msg += ` 🟢 *Conectado*\n`; + msg += ` 🌐 IP: \`${juniperResult.ip || 'N/A'}\`\n`; + msg += ` 🔌 Interface: \`${juniperResult.interface || 'N/A'}\`\n`; + } else { + msg += ` 🔴 *Desconectado* (sem sessão PPPoE ativa)\n`; + } + msg += '\n'; + + // Bloco 3: ONU + msg += `📡 *ONU / ONT*\n`; + if (onuResult.skipped) { + msg += ` ⚫ Serial não informado no cadastro\n`; + } else if (!onuResult.success) { + msg += ` ❌ ${onuResult.error || 'ONU não encontrada'}\n`; + if (serial) msg += ` 📋 Serial buscado: \`${serial}\`\n`; + } else { + const rxLabel = onuResult.rxPower || 'N/A'; + const rxNum = parseFloat(rxLabel); + const rxEmoji = isNaN(rxNum) ? '⚫' : rxNum >= -27 ? '🟢' : rxNum >= -30 ? '🟡' : '🔴'; + + msg += ` 📋 Serial: \`${serial}\`\n`; + msg += ` 🏢 OLT: ${onuResult.oltName || onuResult.oltType || 'N/A'}\n`; + if (onuResult.slot || onuResult.pon) { + msg += ` 📍 Slot/PON: ${onuResult.slot || '?'}/${onuResult.pon || '?'}\n`; + } + msg += ` 🔌 Estado: ${onuResult.online ? '🟢 Online' : '🔴 Offline'}\n`; + msg += ` ${rxEmoji} Rx Power: *${rxLabel}*\n`; + + if (onuResult.portaEthernet) { + const ethOk = /forward|up|active|1gbit|100mbit/i.test(onuResult.portaEthernet); + msg += ` ${ethOk ? '🟢' : '🔴'} Porta Ethernet: ${onuResult.portaEthernet}\n`; + } + } + + await this.bot.sendMessage(chatId, msg, { parse_mode: 'Markdown' }); + + // Bloco 4: Roteador/CPE via GenieACS (exibido apenas se encontrado pela TAG) + if (genieResult && genieResult.success) { + const esc = (v) => { + if (v === null || v === undefined || v === '') return 'N/A'; + return String(v).replace(/[_*`\[]/g, '\\$&'); + }; + const d = genieResult.device; + const tagLabel = d.tags && d.tags.length > 0 ? d.tags.map(esc).join(', ') : null; + + let msgCPE = `📡 *Roteador / CPE (GenieACS)*\n`; + msgCPE += ` 🖥️ Nome: ${esc(d.productClass)}\n`; + msgCPE += ` 🏭 Fabricante: ${esc(d.manufacturer)}\n`; + msgCPE += ` 🔧 SW: ${esc(d.softwareVersion)}\n`; + if (tagLabel) msgCPE += ` 🏷️ TAG: \`${tagLabel}\`\n`; + if (d.ssid24) msgCPE += ` 📶 SSID 2.4GHz: ${esc(d.ssid24)} (Canal ${esc(d.channel24)}, Auto: ${esc(d.autoChannel24)})\n`; + if (d.senha24) msgCPE += ` 🔑 Senha 2.4GHz: \`${esc(d.senha24)}\`\n`; + if (d.clients24 !== null && d.clients24 !== undefined) msgCPE += ` 👥 Clientes 2.4GHz: ${esc(d.clients24)}\n`; + if (d.ssid5) msgCPE += ` 📶 SSID 5GHz: ${esc(d.ssid5)} (Canal ${esc(d.channel5)}, Auto: ${esc(d.autoChannel5)})\n`; + if (d.senha5) msgCPE += ` 🔑 Senha 5GHz: \`${esc(d.senha5)}\`\n`; + if (d.clients5 !== null && d.clients5 !== undefined) msgCPE += ` 👥 Clientes 5GHz: ${esc(d.clients5)}\n`; + + // PPPoE WAN + if (d.pppoeStatus || d.pppoeIP) { + msgCPE += `\n 🌐 *PPPoE WAN*\n`; + if (d.pppoeStatus) msgCPE += ` ├ Status: ${esc(d.pppoeStatus)}\n`; + if (d.pppoeIP) msgCPE += ` ├ IP: \`${esc(d.pppoeIP)}\`\n`; + if (d.pppoeUptime) msgCPE += ` ├ Uptime: ${esc(d.pppoeUptime)}\n`; + if (d.gatewayWAN) msgCPE += ` ├ Gateway: ${esc(d.gatewayWAN)}\n`; + if (d.dnsWAN) msgCPE += ` ├ DNS WAN: ${esc(d.dnsWAN)}\n`; + if (d.linkFisicoWAN) msgCPE += ` ├ Link Físico: ${esc(d.linkFisicoWAN)}\n`; + if (d.trafegoRecebido) msgCPE += ` ├ Recebido: ${esc(d.trafegoRecebido)}\n`; + if (d.trafegoEnviado) msgCPE += ` ├ Enviado: ${esc(d.trafegoEnviado)}\n`; + if (d.taxaDownload) msgCPE += ` ├ ↓ Download: ${esc(d.taxaDownload)}\n`; + if (d.taxaUpload) msgCPE += ` ├ ↑ Upload: ${esc(d.taxaUpload)}\n`; + if (d.pppoeLastError && d.pppoeLastError !== 'ERROR_NONE') msgCPE += ` └ Último Erro: ${esc(d.pppoeLastError)}\n`; + } + + // DHCP LAN + if (d.dhcpInicio || d.dhcpFim || d.dnsLAN || d.ipLAN || d.mascaraLAN) { + msgCPE += `\n 🏠 *LAN/DHCP*\n`; + if (d.ipLAN) msgCPE += ` ├ IP LAN: \`${esc(d.ipLAN)}\`\n`; + if (d.mascaraLAN) msgCPE += ` ├ Máscara: ${esc(d.mascaraLAN)}\n`; + if (d.dhcpInicio) msgCPE += ` ├ DHCP Início: ${esc(d.dhcpInicio)}\n`; + if (d.dhcpFim) msgCPE += ` ├ DHCP Fim: ${esc(d.dhcpFim)}\n`; + if (d.dnsLAN) msgCPE += ` └ DNS LAN: ${esc(d.dnsLAN)}\n`; + } + + await this.bot.sendMessage(chatId, msgCPE, { parse_mode: 'Markdown' }); + } + + } catch (error) { + console.error('❌ Erro em runDiagnosticoServico:', error); + await this.bot.sendMessage(chatId, `❌ Erro no diagnóstico: ${error.message}`); + } + } + + /** + * Mapeia o campo base/AP do TopSapp para o ID da OLT Datacom (ou 'fiberhome') + */ + _resolverOLTporBase(baseInfo) { + if (!baseInfo) return null; + const b = String(baseInfo).toUpperCase(); + if (/INTERNET\s+FIBRA\s+OPTICA/i.test(b)) return 'fiberhome'; + if (/COLMEIA/i.test(b)) return 'olt_colmeia'; + if (/DITO/i.test(b)) return 'olt_dito'; + if (/CANAA\s*2/i.test(b)) return 'olt_canaa2'; + if (/CANAA/i.test(b)) return 'olt_canaa'; + return null; + } + + /** + * Diagnóstico de ONU: detecta tipo (Datacom/FiberHome) e retorna dados estruturados + * @param {string} serial - Serial da ONU + * @param {string|null} baseInfo - Campo base/AP do TopSapp para rotear para OLT correta + */ + async _diagnosticarONU(serial, baseInfo) { + const serialUpper = serial.toUpperCase(); + const oltHint = this._resolverOLTporBase(baseInfo); + + // Usar base description para decidir se é FiberHome + if (oltHint === 'fiberhome' || + (!serialUpper.startsWith('DACM') && (serialUpper.startsWith('FHTT') || serialUpper.startsWith('FHGT')))) { + return await this._diagnosticarONUFiberhome(serial); + } + + return await this._diagnosticarONUDatacom(serial, oltHint); + } + + async _diagnosticarONUDatacom(serial, oltIdHint) { + let found = null; + + // 1. Tentar na OLT indicada pelo campo base/AP do cadastro + if (oltIdHint && this.ssh.equipamentos.has(oltIdHint)) { + const r = await this.ssh.findOnuBySerial(oltIdHint, serial).catch(() => null); + if (r && r.success) { + const olt = this.ssh.equipamentos.get(oltIdHint); + found = { ...r, oltId: oltIdHint, oltName: olt.name }; + } + } + + // 2. Fallback: buscar em todas as OLTs Datacom + if (!found) { + const olts = this.ssh.listOLTs(); + if (olts.length === 0) { + return { success: false, error: 'Nenhuma OLT Datacom configurada' }; + } + const searches = olts.map(olt => + this.ssh.findOnuBySerial(olt.id, serial) + .then(r => ({ ...r, oltId: olt.id, oltName: olt.name })) + .catch(() => ({ success: false })) + ); + const results = await Promise.allSettled(searches); + for (const r of results) { + if (r.status === 'fulfilled' && r.value.success) { + found = r.value; + break; + } + } + } + + if (!found) { + return { success: false, error: `ONU ${serial} não encontrada em nenhuma OLT Datacom` }; + } + + const { oltId, oltName, onuInfo } = found; + const { slot, onuId } = onuInfo; + + // Buscar sinal e porta ethernet em paralelo + const [signalResult, ethResult] = await Promise.all([ + this.ssh.checkOnuSignal(oltId, slot, onuId).catch(e => ({ success: false, error: e.message })), + this.ssh.checkOnuEthernetDatacom(oltId, slot, onuId).catch(e => ({ success: false })) + ]); + + // Parsear sinal + const rawSignal = signalResult.output || ''; + const stateMatch = rawSignal.match(/Operational\s+state\s*:\s*(\S+)/i); + const rxMatch = rawSignal.match(/Rx\s+Optical\s+Power\s*\[dBm\]\s*:\s*([-\d.]+)/i); + + const operationalState = stateMatch ? stateMatch[1] : null; + const rxPower = rxMatch ? `${rxMatch[1]} dBm` : null; + const online = operationalState ? /up|active|online/i.test(operationalState) : false; + + // Parsear ethernet + let portaEthernet = null; + if (ethResult.success) { + const ethRaw = ethResult.rawOutput || ''; + const ethMatch = ethRaw.match(/\b(Forwarding|Down|Up|Blocking)\b[^\n]*([\d]+(?:Gbit|Mbit)[^\n]*|N\/A)?/i); + if (ethMatch) { + portaEthernet = ethMatch[0].trim().replace(/\s+/g, ' '); + } + } + + return { + success: true, + type: 'datacom', + oltName, + slot, + pon: slot ? slot.split('/')[2] || slot : null, + onuId, + online, + rxPower, + operationalState, + portaEthernet + }; + } + + async _diagnosticarONUFiberhome(serial) { + const resultado = await this.fiberhome.obterStatusONU(serial); + + if (!resultado.success) { + return { success: false, error: resultado.error || 'ONU não encontrada na FiberHome' }; + } + + // Preferir porta com dados reais de velocidade; cair para primeira disponível + const portas = resultado.portasEthernet || []; + const portaComDados = portas.find(p => p.status !== 'unknown' && p.speed); + const portaAtiva = portas.find(p => p.status === 'active'); + const portaBase = portaComDados || portaAtiva || (portas.length > 0 ? portas[0] : null); + + let portaEthernetStr = null; + if (portaBase && portaBase.status !== 'unknown') { + const statusLabel = portaBase.status === 'active' ? 'up' : 'down'; + portaEthernetStr = portaBase.speed + ? `${statusLabel} — ${portaBase.speed}` + : statusLabel; + } + + // rxPower: retornar null se for 'N/A' para que a UI mostre 'N/A' via fallback + const rxPower = resultado.rxPower && resultado.rxPower !== 'N/A' ? resultado.rxPower : null; + + return { + success: true, + type: 'fiberhome', + oltName: 'FiberHome AN5516', + slot: resultado.slot, + pon: resultado.pon, + onuId: resultado.onuId, + online: resultado.online, + rxPower, + txPower: resultado.txPower !== 'N/A' ? resultado.txPower : null, + portaEthernet: portaEthernetStr + }; + } + + /** + * Verificar sinal de ONU + */ + async handleVerSinal(chatId, serialNumber) { + console.log(`🎯 handleVerSinal chamado - chatId: ${chatId}, serialNumber: ${serialNumber}`); + + if (!serialNumber) { + console.log('❌ Serial number não fornecido'); + await this.bot.sendMessage(chatId, '❓ Especifique o serial number da ONU. Exemplo: "ver sinal DACM91569513"'); + return; + } + + // Obter lista de OLTs disponíveis + const olts = this.ssh.listOLTs(); + console.log(`📋 OLTs Datacom encontradas: ${olts.length}`); + + // Criar menu de seleção de OLT + const keyboard = { + inline_keyboard: [] + }; + + // Adicionar OLT Fiberhome como primeira opção + keyboard.inline_keyboard.push([ + { text: '🔶 OLT FIBERHOME', callback_data: `check_signal_fiberhome_${serialNumber}` } + ]); + + // Adicionar separador visual + if (olts.length > 0) { + keyboard.inline_keyboard.push([ + { text: '➖ OLTs DATACOM ➖', callback_data: 'separator' } + ]); + + // Adicionar botões para cada OLT Datacom (2 por linha) + for (let i = 0; i < olts.length; i += 2) { + const row = []; + + row.push({ + text: olts[i].name, + callback_data: `check_signal_${olts[i].id}_${serialNumber}` + }); + + if (i + 1 < olts.length) { + row.push({ + text: olts[i + 1].name, + callback_data: `check_signal_${olts[i + 1].id}_${serialNumber}` + }); + } + + keyboard.inline_keyboard.push(row); + } + } + + // Adicionar botão de cancelar + keyboard.inline_keyboard.push([ + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ]); + + console.log(`📤 Enviando mensagem com Fiberhome + ${olts.length} OLTs Datacom para seleção`); + await this.bot.sendMessage(chatId, `🔍 *Verificar sinal da ONU*\nSerial: \`${serialNumber}\`\n\nSelecione onde deseja pesquisar:`, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + console.log(`✅ Mensagem de seleção de OLT enviada com sucesso`); + } + + /** + * Executa verificação de sinal em uma OLT específica + */ + async handleCheckSignalOLT(chatId, oltId, serialNumber) { + const olt = this.ssh.equipamentos.get(oltId); + if (!olt) { + await this.bot.sendMessage(chatId, `❌ OLT ${oltId} não encontrada.`); + return; + } + + await this.bot.sendMessage(chatId, `🔍 Procurando ONU ${serialNumber} na ${olt.name}...`); + + try { + const result = await this.ssh.checkOnuSignalBySerial(oltId, serialNumber); + + if (result.success) { + // Escapar caracteres especiais do Markdown + const oltNameEscaped = olt.name.replace(/_/g, '\\_'); + const serialEscaped = serialNumber.replace(/_/g, '\\_'); + + let message = `📡 *Sinal da ONU ${serialEscaped}*\n\n`; + message += `🏢 *OLT:* ${oltNameEscaped} (${olt.host})\n`; + message += `📍 *Localização:* Slot ${result.slot}, ONU ${result.onuId}\n\n`; + message += `📊 *Informações do Sinal:*\n`; + message += `\`\`\`\n${result.signalOutput}\n\`\`\``; + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + + // Oferecer opções adicionais + const keyboard = { + inline_keyboard: [ + [ + { text: '🔄 Atualizar sinal', callback_data: `check_signal_${oltId}_${serialNumber}` }, + { text: '📋 Ver configuração', callback_data: `show_config_${oltId}_${result.slot}_${result.onuId}` } + ], + [ + { text: '🔌 Reiniciar ONU', callback_data: `reset_onu_${oltId}_${result.slot}_${result.onuId}` }, + { text: '⬆️ Atualizar ONU', callback_data: `update_onu_${oltId}_${result.slot}_${result.onuId}` } + ], + [ + { text: '🔍 Pesquisar em outra OLT', callback_data: `ver_sinal_${serialNumber}` } + ] + ] + }; + + await this.bot.sendMessage(chatId, '🎛️ O que você gostaria de fazer?', { + reply_markup: keyboard + }); + + } else { + await this.bot.sendMessage(chatId, `❌ ${result.message}`); + + // Oferecer pesquisar em outras OLTs + const keyboard = { + inline_keyboard: [ + [ + { text: '🔍 Pesquisar em outra OLT', callback_data: `ver_sinal_${serialNumber}` }, + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ] + }; + + await this.bot.sendMessage(chatId, 'ONU não encontrada nesta OLT. Deseja pesquisar em outra?', { + reply_markup: keyboard + }); + } + } catch (error) { + console.error('Erro ao verificar sinal:', error); + await this.bot.sendMessage(chatId, `❌ Erro interno: ${error.message}`); + } + } + + /** + * Comando para remover ONU + */ + async handleRemoverOnu(chatId, serialNumber) { + if (!serialNumber) { + await this.bot.sendMessage(chatId, '❌ Por favor, informe o serial da ONU que deseja remover.\n\nExemplo: `remover FHTT12345678`', { parse_mode: 'Markdown' }); + return; + } + + // Perguntar em qual OLT buscar + const oltOptions = this.ssh.listOLTs(); + + if (oltOptions.length === 0) { + await this.bot.sendMessage(chatId, '❌ Nenhuma OLT disponível configurada.'); + return; + } + + let keyboard = { + inline_keyboard: oltOptions.map(olt => [ + { text: `🏢 ${olt.name}`, callback_data: `remove_onu_${olt.id}_${serialNumber}` } + ]) + }; + + // Adicionar opção de cancelar + keyboard.inline_keyboard.push([ + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ]); + + // Escapar caracteres especiais do serial + const serialEscaped = serialNumber.replace(/_/g, '\\_'); + + await this.bot.sendMessage(chatId, + `🗑️ *Remover ONU ${serialEscaped}*\n\n` + + '🏢 Selecione a OLT onde a ONU está configurada:', + { + reply_markup: keyboard, + parse_mode: 'Markdown' + } + ); + } + + /** + * Comando para adicionar ONU + */ + async handleAdicionarOnu(chatId, serialNumber) { + if (!serialNumber) { + await this.bot.sendMessage(chatId, '❌ Por favor, informe o serial da ONU que deseja adicionar.\n\nExemplo: `adicionar onu FHTT12345678`', { parse_mode: 'Markdown' }); + return; + } + + // Perguntar em qual OLT adicionar + const oltOptions = this.ssh.listOLTs(); + + if (oltOptions.length === 0) { + await this.bot.sendMessage(chatId, '❌ Nenhuma OLT disponível configurada.'); + return; + } + + let keyboard = { + inline_keyboard: oltOptions.map(olt => [ + { text: `🏢 ${olt.name}`, callback_data: `add_onu_${olt.id}_${serialNumber}` } + ]) + }; + + // Adicionar opção de cancelar + keyboard.inline_keyboard.push([ + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ]); + + // Escapar caracteres especiais + const serialEscaped = serialNumber.replace(/_/g, '\\_'); + + await this.bot.sendMessage(chatId, + `➕ *Adicionar ONU ${serialEscaped}*\n\n` + + '🏢 Selecione a OLT onde deseja adicionar a ONU:', + { + reply_markup: keyboard, + parse_mode: 'Markdown' + } + ); + } + + /** + * Comando para adicionar ONU com VLAN customizada + */ + async handleAdicionarOnuVlan(chatId, serialNumber, vlanId) { + if (!serialNumber) { + await this.bot.sendMessage(chatId, '❌ Por favor, informe o serial da ONU.\n\nExemplo: `adicionar onu vlan 603 FHTT12345678`', { parse_mode: 'Markdown' }); + return; + } + + if (!vlanId || isNaN(vlanId) || vlanId < 1 || vlanId > 4094) { + await this.bot.sendMessage(chatId, '❌ VLAN ID inválida. Deve ser um número entre 1 e 4094.\n\nExemplo: `adicionar onu vlan 603 FHTT12345678`', { parse_mode: 'Markdown' }); + return; + } + + // Perguntar em qual OLT adicionar + const oltOptions = this.ssh.listOLTs(); + + if (oltOptions.length === 0) { + await this.bot.sendMessage(chatId, '❌ Nenhuma OLT disponível configurada.'); + return; + } + + let keyboard = { + inline_keyboard: oltOptions.map(olt => [ + { text: `🏢 ${olt.name}`, callback_data: `add_onu_vlan_${olt.id}_${vlanId}_${serialNumber}` } + ]) + }; + + // Adicionar opção de cancelar + keyboard.inline_keyboard.push([ + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ]); + + // Escapar caracteres especiais + const serialEscaped = serialNumber.replace(/_/g, '\\_'); + + await this.bot.sendMessage(chatId, + `➕ *Adicionar ONU com VLAN Customizada*\n\n` + + `📱 *Serial:* ${serialEscaped}\n` + + `🏷️ *VLAN:* ${vlanId}\n\n` + + '🏢 Selecione a OLT onde deseja adicionar a ONU:', + { + reply_markup: keyboard, + parse_mode: 'Markdown' + } + ); + } + + /** + * Comando para adicionar ONU Fiberhome via TL1 + */ + async handleAdicionarOnuFiberhome(chatId, serialNumber, userId) { + if (!serialNumber) { + await this.bot.sendMessage(chatId, + '❌ Por favor, informe o serial da ONU Fiberhome.\n\n' + + 'Exemplo: `adicionar onu fiberhome FHTT12345678`', + { parse_mode: 'Markdown' } + ); + return; + } + + console.log(`📝 Criando sessão para usuário ${userId}, serial: ${serialNumber}`); + + // Criar sessão para coletar informações + this.userSessions.set(userId, { + action: 'adicionar_onu_fiberhome_step1', + serial: serialNumber, + timestamp: Date.now() + }); + + console.log(`✅ Sessão criada. Sessões ativas:`, this.userSessions.size); + + // Escapar caracteres especiais + const serialEscaped = serialNumber.replace(/_/g, '\\_'); + + // Perguntar o alias/nome do usuário + await this.bot.sendMessage(chatId, + `➕ *Adicionar ONU Fiberhome*\n\n` + + `📱 *Serial:* ${serialEscaped}\n` + + `🔍 A porta PON será descoberta automaticamente\n\n` + + '👤 Digite o *nome/alias* do usuário:', + { parse_mode: 'Markdown' } + ); + } + + /** + * Processar input do usuário durante sessões + */ + async handleUserSessionInput(chatId, text, userId, session) { + try { + console.log(`🔄 Processando input de sessão. Action: ${session.action}, Type: ${session.type}, Text: ${text}`); + + // Verificar se é sessão baseada em action (Fiberhome ONU) + if (session.action) { + // Sessões de configuração de roteador (ação dinâmica com prefixo) + if (session.action.startsWith('router_cfg_wait_')) { + await this._processarInputConfigurarRoteador(chatId, userId, session, text); + return; + } + + switch (session.action) { + case 'adicionar_onu_fiberhome_step1': + // Recebeu o nome do usuário, adicionar sufixo e perguntar pelo tipo da ONU + const usuarioBase = text.trim(); + session.usuario = usuarioBase.includes('@') ? usuarioBase : `${usuarioBase}@clicknetguarai.com.br`; + session.action = 'adicionar_onu_fiberhome_step2'; + this.userSessions.set(userId, session); + + const keyboard = { + inline_keyboard: [ + [{ text: '🔌 AN5506-02-B (2 portas)', callback_data: 'fh_onu_type_AN5506-02-B' }], + [{ text: '🔌 AN5506-01-A (1 porta)', callback_data: 'fh_onu_type_AN5506-01-A' }], + [{ text: '❌ Cancelar', callback_data: 'cancel_action' }] + ] + }; + + await this.bot.sendMessage(chatId, + `👤 **Usuário:** ${session.usuario}\n\n` + + '🔌 Selecione o **tipo da ONU**:', + { reply_markup: keyboard, parse_mode: 'Markdown' } + ); + return; + + case 'adicionar_onu_fiberhome_step3': + // Recebeu confirmação de VLAN customizada + const vlanInput = text.trim(); + if (vlanInput && !isNaN(vlanInput)) { + const vlan = parseInt(vlanInput); + if (vlan >= 1 && vlan <= 4094) { + session.vlan = vlan; + } else { + await this.bot.sendMessage(chatId, '❌ VLAN inválida. Usando VLAN padrão 1002.'); + session.vlan = 1002; + } + } else { + session.vlan = 1002; + } + + // Limpar sessão + this.userSessions.delete(userId); + + // Executar adição da ONU + await this.executarAdicaoOnuFiberhome(chatId, session, userId); + return; + + case 'topsapp_onu_aguardando_nome': + await this.topsappOnuBuscarCliente(chatId, userId, text.trim(), session.onuData); + return; + } + } + + // Verificar se é sessão baseada em type (outros fluxos) + if (session.type) { + switch (session.type) { + case 'waiting_custom_command': + await this.executeCustomCommand(chatId, session.connectionId, text); + this.userSessions.delete(userId); + return; + + case 'waiting_username_disconnect': + await this.handleDisconnectUser(chatId, text.trim()); + this.userSessions.delete(userId); + return; + } + } + + // Estado desconhecido + console.log(`❓ Estado de sessão desconhecido: action=${session.action}, type=${session.type}`); + this.userSessions.delete(userId); + await this.bot.sendMessage(chatId, '❓ Estado de sessão desconhecido. Tente novamente.'); + } catch (error) { + console.error('❌ Erro ao processar input da sessão:', error); + this.userSessions.delete(userId); + await this.bot.sendMessage(chatId, '❌ Erro ao processar sua resposta. Tente novamente.'); + } + } + + /** + * Executar adição da ONU Fiberhome + */ + async executarAdicaoOnuFiberhome(chatId, session, userId = null) { + try { + const { serial, usuario, vlan, onuType } = session; + + // Escapar caracteres especiais + const serialEscaped = serial.replace(/_/g, '\\_'); + + await this.bot.sendMessage(chatId, + `⏳ Adicionando ONU Fiberhome...\n\n` + + `🔍 Descobrindo porta PON automaticamente...\n` + + `👤 *Usuário:* ${usuario}\n` + + `📱 *Serial:* ${serialEscaped}\n` + + `🔌 *Tipo:* ${onuType}\n` + + `🏷️ *VLAN:* ${vlan}`, + { parse_mode: 'Markdown' } + ); + + // Executar comando TL1 via Fiberhome Manager + const resultado = await this.fiberhomeTL1.adicionarOnuFiberhome( + serial, + usuario, + vlan, + onuType + ); + + if (resultado.success) { + // Escapar caracteres especiais do Markdown + const oltEscaped = resultado.details.olt.replace(/_/g, '\\_'); + const serialEscaped = serial.replace(/_/g, '\\_'); + + await this.bot.sendMessage(chatId, + `✅ *ONU Fiberhome Adicionada com Sucesso!*\n\n` + + `👤 *Usuário:* ${usuario}\n` + + `🏢 *OLT:* ${oltEscaped}\n` + + `📡 *Porta descoberta:* Slot ${resultado.details.slot}, PON ${resultado.details.ponPort}\n` + + `📱 *Serial:* ${serialEscaped}\n` + + `🔌 *Tipo:* ${onuType}\n` + + `🏷️ *VLAN:* ${vlan}\n` + + `⚙️ *Portas configuradas:* ${resultado.details.portas}\n\n` + + `🔐 Login Web: \`admin / clicknet10\`\n\n` + + `⚡ A ONU foi reiniciada e estará disponível em alguns instantes.`, + { parse_mode: 'Markdown' } + ); + + // Convidar para registrar no TopSapp + if (userId) { + this.userSessions.set(userId, { + action: 'topsapp_onu_pre_busca', + onuData: { + tipo: 'fiberhome', + location: 'guarai_colmeia', + oltHost: resultado.details.olt, + serial, + portaAlias: `${resultado.details.slot}-${resultado.details.ponPort}` + } + }); + await this.bot.sendMessage(chatId, + `📋 *Registrar no TopSapp?*\n\nDeseja vincular esta ONU a um cliente no sistema TopSapp?`, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [[ + { text: '✅ Sim, vincular', callback_data: 'topsapp_onu_sim' }, + { text: '❌ Não', callback_data: 'cancel_action' } + ]] + } + } + ); + } + + } else { + await this.bot.sendMessage(chatId, + `❌ *Erro ao adicionar ONU Fiberhome*\n\n` + + `⚠️ ${resultado.error}`, + { parse_mode: 'Markdown' } + ); + } + + } catch (error) { + console.error('❌ Erro ao executar adição de ONU Fiberhome:', error); + await this.bot.sendMessage(chatId, + `❌ *Erro ao adicionar ONU Fiberhome*\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Handler para remover ONU Fiberhome via TL1 + */ + async handleRemoverOnuFiberhome(chatId, serialNumber, userId) { + try { + // Validar serial + if (!serialNumber || serialNumber.length !== 12) { + await this.bot.sendMessage(chatId, + '❌ Serial inválido. O serial deve conter 12 caracteres.\n\n' + + 'Exemplo: `FHTT199cf3c8`' + ); + return; + } + + // Escapar caracteres especiais + const serialEscaped = serialNumber.replace(/_/g, '\\_'); + + // Solicitar confirmação + const keyboard = { + inline_keyboard: [ + [ + { text: '✅ Sim, remover', callback_data: `confirm_remove_fh_${serialNumber}` }, + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ] + }; + + await this.bot.sendMessage(chatId, + `🗑️ *Remover ONU Fiberhome*\n\n` + + `📱 *Serial:* ${serialEscaped}\n\n` + + `⚠️ Esta ação irá remover a ONU e todas as suas configurações.\n\n` + + `Deseja confirmar a remoção?`, + { + reply_markup: keyboard, + parse_mode: 'Markdown' + } + ); + + } catch (error) { + console.error('❌ Erro ao preparar remoção de ONU Fiberhome:', error); + await this.bot.sendMessage(chatId, + `❌ *Erro ao preparar remoção*\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Comando não reconhecido + */ + async handleUnknown(chatId, text) { + const suggestions = this.nlp.suggestCorrection(text); + + let message = '❓ Não entendi o comando. '; + + if (suggestions.length > 0) { + message += `Talvez você quis dizer: ${suggestions.join(', ')}?\n\n`; + } + + message += 'Digite /help para ver todos os comandos disponíveis.'; + + await this.bot.sendMessage(chatId, message); + } + + /** + * Processa callbacks de botões inline + */ + async handleCallbackQuery(callbackQuery) { + const chatId = callbackQuery.message.chat.id; + const data = callbackQuery.data; + const userId = callbackQuery.from.id; + + // Verificar autorização + if (!this.isAuthorized(userId)) { + await this.bot.answerCallbackQuery(callbackQuery.id, '🚫 Acesso negado'); + console.log(`🚫 Callback negado para usuário (${userId})`); + return; + } + + try { + console.log(`🔍 Callback recebido: ${data}`); + console.log(`🔍 Verifica confirm_remove_: ${data.startsWith('confirm_remove_')}`); + + if (data.startsWith('cmd_')) { + console.log(`🔍 Condição cmd_ atendida`); + await this.handleCommandCallback(callbackQuery, data); + } else if (data.startsWith('confirm_remove_fh_')) { + console.log(`🔍 Chamando handleConfirmRemoveFiberhomeCallback`); + try { + await this.handleConfirmRemoveFiberhomeCallback(callbackQuery, data); + } catch (confirmError) { + console.error(`❌ Erro em handleConfirmRemoveFiberhomeCallback:`, confirmError); + await this.bot.answerCallbackQuery(callbackQuery.id, 'Erro na confirmação'); + } + } else if (data.startsWith('confirm_remove_')) { + console.log(`🔍 Chamando handleConfirmRemoveCallback`); + try { + await this.handleConfirmRemoveCallback(callbackQuery, data); + } catch (confirmError) { + console.error(`❌ Erro em handleConfirmRemoveCallback:`, confirmError); + await this.bot.answerCallbackQuery(callbackQuery.id, 'Erro na confirmação'); + } + } else if (data.startsWith('confirm_add_')) { + console.log(`🔍 Chamando handleConfirmAddCallback`); + try { + await this.handleConfirmAddCallback(callbackQuery, data); + } catch (confirmError) { + console.error(`❌ Erro em handleConfirmAddCallback:`, confirmError); + await this.bot.answerCallbackQuery(callbackQuery.id, 'Erro na confirmação'); + } + } else if (data.startsWith('confirm_block_service_')) { + console.log(`🔍 Chamando handleConfirmBlockServiceCallback`); + await this.handleConfirmBlockServiceCallback(callbackQuery, data); + } else if (data.startsWith('confirm_unblock_service_')) { + console.log(`🔍 Chamando handleConfirmUnblockServiceCallback`); + await this.handleConfirmUnblockServiceCallback(callbackQuery, data); + } else if (data.startsWith('confirm_block_all_')) { + console.log(`🔍 Chamando handleConfirmBlockAllCallback`); + await this.handleConfirmBlockAllCallback(callbackQuery, data); + } else if (data.startsWith('confirm_unblock_all_')) { + console.log(`🔍 Chamando handleConfirmUnblockAllCallback`); + await this.handleConfirmUnblockAllCallback(callbackQuery, data); + } else if (data.startsWith('confirm_block_')) { + console.log(`🔍 Chamando handleConfirmBlockCallback`); + await this.handleConfirmBlockCallback(callbackQuery, data); + } else if (data.startsWith('confirm_unblock_')) { + console.log(`🔍 Chamando handleConfirmUnblockCallback`); + await this.handleConfirmUnblockCallback(callbackQuery, data); + } else if (data.startsWith('ipoe_cliente_')) { + console.log(`🔍 Condição ipoe_cliente_ atendida`); + await this.handleIPoEClienteCallback(callbackQuery, data); + } else if (data.startsWith('ipoe_servico_')) { + console.log(`🔍 Condição ipoe_servico_ atendida`); + await this.handleIPoEServicoCallback(callbackQuery, data); + } else if (data.startsWith('confirm_ipoe_')) { + console.log(`🔍 Condição confirm_ipoe_ atendida`); + await this.handleConfirmIPoECallback(callbackQuery, data); + } else if (data.startsWith('block_service_')) { + console.log(`🔍 Chamando handleBlockServiceSelection`); + await this.handleBlockServiceSelection(callbackQuery, data); + } else if (data.startsWith('unblock_service_')) { + console.log(`🔍 Chamando handleUnblockServiceCallback`); + await this.handleUnblockServiceCallback(callbackQuery, data); + } else if (data.startsWith('confirm_')) { + console.log(`🔍 Condição confirm_ atendida`); + await this.handleConfirmCallback(callbackQuery, data); + } else if (data.startsWith('check_signal_')) { + console.log(`🔍 Condição check_signal_ atendida`); + await this.handleCheckSignalCallback(callbackQuery, data); + } else if (data.startsWith('ver_sinal_')) { + console.log(`🔍 Condição ver_sinal_ atendida`); + const serialNumber = data.replace('ver_sinal_', ''); + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.handleVerSinal(chatId, serialNumber); + } else if (data.startsWith('show_config_')) { + console.log(`🔍 Condição show_config_ atendida`); + await this.handleShowConfigCallback(callbackQuery, data); + } else if (data.startsWith('reset_onu_')) { + console.log(`🔍 Condição reset_onu_ atendida`); + await this.handleResetOnuCallback(callbackQuery, data); + } else if (data.startsWith('update_onu_')) { + console.log(`🔍 Condição update_onu_ atendida`); + await this.handleUpdateOnuCallback(callbackQuery, data); + } else if (data.startsWith('firmware_')) { + console.log(`🔍 Condição firmware_ atendida`); + await this.handleFirmwareCallback(callbackQuery, data); + } else if (data.startsWith('remove_onu_')) { + console.log(`🔍 Chamando handleRemoveOnuCallback`); + await this.handleRemoveOnuCallback(callbackQuery, data); + } else if (data.startsWith('remove_readd_vlan_')) { + console.log(`🔍 Chamando handleRemoveAndReAddCallback (vlan)`); + await this.handleRemoveAndReAddCallback(callbackQuery, data); + } else if (data.startsWith('remove_readd_')) { + console.log(`🔍 Chamando handleRemoveAndReAddCallback`); + await this.handleRemoveAndReAddCallback(callbackQuery, data); + } else if (data.startsWith('topsapp_onu_')) { + console.log(`🔍 Condição topsapp_onu_ atendida`); + await this.handleTopSappOnuCallback(callbackQuery, data); + } else if (data.startsWith('add_onu_vlan_')) { + console.log(`🔍 Chamando handleAddOnuVlanCallback`); + await this.handleAddOnuVlanCallback(callbackQuery, data); + } else if (data.startsWith('add_onu_')) { + console.log(`🔍 Chamando handleAddOnuCallback`); + await this.handleAddOnuCallback(callbackQuery, data); + } else if (data.startsWith('fh_onu_type_')) { + console.log(`🔍 Chamando handleFiberhomeOnuTypeCallback`); + await this.handleFiberhomeOnuTypeCallback(callbackQuery, data); + } else if (data.startsWith('fh_vlan_')) { + console.log(`🔍 Chamando handleFiberhomeVlanCallback`); + await this.handleFiberhomeVlanCallback(callbackQuery, data); + } else if (data.startsWith('fiberhome_details_')) { + console.log(`🔍 Condição fiberhome_details_ atendida`); + const serialNumber = data.replace('fiberhome_details_', ''); + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.handleStatusONU(chatId, serialNumber); + } else if (data.startsWith('router_cfg_')) { + console.log(`🔍 Condição router_cfg_ atendida`); + await this.handleRouterCfgCallback(callbackQuery, data); + } else if (data.startsWith('cfg_roteador_cli_')) { + console.log(`🔍 Condição cfg_roteador_cli_ atendida`); + await this.bot.answerCallbackQuery(callbackQuery.id); + const parts = data.replace('cfg_roteador_cli_', '').split('_'); + const idCliente = parts[0]; + const location = parts.slice(1).join('_'); + await this._configurarRoteadorSelecionarServico(chatId, userId, idCliente, location); + } else if (data.startsWith('cfg_roteador_srv_')) { + console.log(`🔍 Condição cfg_roteador_srv_ atendida`); + await this.bot.answerCallbackQuery(callbackQuery.id); + // formato: cfg_roteador_srv___ + const rest = data.replace('cfg_roteador_srv_', ''); + const parts = rest.split('_'); + const idCliente = parts[0]; + const idServico = parts[1]; + const location = parts.slice(2).join('_'); + // Recarregar o serviço para ter o objeto completo com pppoeLogin + const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviço: ${servicosResult.error}`); + } else { + const servico = (servicosResult.servicos || []).find(s => + String(s.id_servico || s.id) === String(idServico) + ); + if (!servico) { + await this.bot.sendMessage(chatId, `❌ Serviço #${idServico} não encontrado.`); + } else { + await this._configurarRoteadorPorServico(chatId, userId, servico, location); + } + } + } else if (data.startsWith('diag_cliente_')) { + console.log(`🔍 Condição diag_cliente_ atendida`); + await this.handleDiagClienteCallback(callbackQuery, data); + } else if (data.startsWith('diag_servico_')) { + console.log(`🔍 Condição diag_servico_ atendida`); + await this.handleDiagServicoCallback(callbackQuery, data); + } else if (data.startsWith('dados_cliente_')) { + console.log(`🔍 Condição dados_cliente_ atendida`); + await this.handleDadosClienteCallback(callbackQuery, data); + } else if (data.startsWith('faturas_cliente_')) { + console.log(`🔍 Condição faturas_cliente_ atendida`); + await this.handleFaturasClienteCallback(callbackQuery, data); + } else if (data.startsWith('faturas_servico_')) { + console.log(`🔍 Condição faturas_servico_ atendida`); + await this.handleFaturasServicoCallback(callbackQuery, data); + } else if (data.startsWith('servicos_cliente_')) { + console.log(`🔍 Condição servicos_cliente_ atendida`); + await this.handleServicosClienteCallback(callbackQuery, data); + } else if (data.startsWith('help_')) { + console.log(`🔍 Condição help_ atendida`); + await this.handleHelpDetailCallback(callbackQuery, data); + } else if (data === 'separator') { + // Ignorar clique no separador visual + await this.bot.answerCallbackQuery(callbackQuery.id); + } else if (data === 'cancel_action') { + await this.bot.answerCallbackQuery(callbackQuery.id, 'Ação cancelada'); + + // Limpar sessão do usuário se existir + if (this.userSessions.has(userId)) { + this.userSessions.delete(userId); + } + + await this.bot.editMessageText('❌ Ação cancelada.', { + chat_id: chatId, + message_id: callbackQuery.message.message_id + }); + } else if (data === 'refresh_users') { + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.handleListUsers(chatId); + } else if (data === 'disconnect_user_prompt') { + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.editMessageText('👤 Digite o nome do usuário que deseja desconectar:', { + chat_id: chatId, + message_id: callbackQuery.message.message_id + }); + + // Armazenar estado para próxima mensagem + this.userSessions.set(callbackQuery.from.id, { + type: 'waiting_username_disconnect' + }); + } else if (data.startsWith('suporte_cliente_')) { + console.log(`🔍 Condição suporte_cliente_ atendida`); + await this.handleSuporteClienteCallback(callbackQuery, data); + } else if (data.startsWith('suporte_servico_')) { + console.log(`🔍 Condição suporte_servico_ atendida`); + await this.handleSuporteServicoCallback(callbackQuery, data); + } else if (data.startsWith('suporte_tecnico_')) { + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.handleSuporteTecnicoCallback(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); + } else if (data.startsWith('pix_')) { + console.log(`🔍 Condição pix_ atendida`); + await this.handlePixCallback(callbackQuery, data); + } else { + console.log(`❓ Callback não reconhecido: ${data}`); + await this.bot.answerCallbackQuery(callbackQuery.id, 'Comando não reconhecido'); + } + } catch (error) { + console.error('❌ Erro no callback:', error); + await this.bot.answerCallbackQuery(callbackQuery.id, 'Erro interno'); + } + } + + /** + * Processa callbacks de comandos + */ + async handleCommandCallback(callbackQuery, data) { + const parts = data.split('_'); + const action = parts[1]; + const connectionId = parts.slice(2).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + + switch (action) { + case 'status': + await this.executeSSHCommand(callbackQuery, connectionId, 'uptime'); + break; + case 'reboot': + await this.confirmRebootCallback(callbackQuery, connectionId); + break; + case 'disconnect': + await this.disconnectCallback(callbackQuery, connectionId); + break; + case 'custom': + await this.requestCustomCommand(callbackQuery, connectionId); + break; + } + } + + /** + * Executa comando SSH e mostra resultado + */ + async executeSSHCommand(callbackQuery, connectionId, command) { + const chatId = callbackQuery.message.chat.id; + + try { + const result = await this.ssh.executeCommand(connectionId, command); + + let message = `💻 *Comando:* \`${command}\`\n\n`; + + if (result.success) { + message += `✅ *Resultado:*\n\`\`\`\n${result.output}\`\`\``; + } else { + message += `❌ *Erro:*\n\`\`\`\n${result.error}\`\`\``; + } + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + } catch (error) { + await this.bot.sendMessage(chatId, `❌ Erro ao executar comando: ${error.message}`); + } + } + + /** + * Confirma reinicialização + */ + async confirmRebootCallback(callbackQuery, connectionId) { + const chatId = callbackQuery.message.chat.id; + + await this.bot.editMessageText('⚠️ Tem certeza que deseja reiniciar o equipamento?', { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + reply_markup: { + inline_keyboard: [ + [ + { text: '✅ SIM, reiniciar', callback_data: `confirm_reboot_${connectionId}` }, + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ] + } + }); + } + + /** + * Desconecta conexão SSH + */ + async disconnectCallback(callbackQuery, connectionId) { + const chatId = callbackQuery.message.chat.id; + + const success = this.ssh.disconnect(connectionId); + + if (success) { + await this.bot.editMessageText('🔌 Conexão encerrada com sucesso.', { + chat_id: chatId, + message_id: callbackQuery.message.message_id + }); + } else { + await this.bot.editMessageText('❌ Conexão não encontrada ou já encerrada.', { + chat_id: chatId, + message_id: callbackQuery.message.message_id + }); + } + } + + /** + * Solicita comando personalizado + */ + async requestCustomCommand(callbackQuery, connectionId) { + const chatId = callbackQuery.message.chat.id; + + // Armazenar estado para próxima mensagem + this.userSessions.set(callbackQuery.from.id, { + type: 'waiting_custom_command', + connectionId: connectionId + }); + + await this.bot.editMessageText('💻 Digite o comando que deseja executar:', { + chat_id: chatId, + message_id: callbackQuery.message.message_id + }); + } + + /** + * Processa callbacks de confirmação + */ + async handleConfirmCallback(callbackQuery, data) { + const parts = data.split('_'); + const action = parts[1]; + + if (action === 'restart' || action === 'reboot') { + const target = parts.slice(2).join('_'); + await this.executeRestart(callbackQuery, target); + } else if (action === 'reset' && parts[2] === 'onu') { + // Formato: confirm_reset_onu_oltid_slot_onuid + await this.handleConfirmResetOnu(callbackQuery, data); + } else if (action === 'firmware') { + // Formato: confirm_firmware_filename_oltid_slot_onuid + await this.handleConfirmFirmware(callbackQuery, data); + } + } + + /** + * Executa reinicialização confirmada + */ + async executeRestart(callbackQuery, target) { + const chatId = callbackQuery.message.chat.id; + + await this.bot.editMessageText(`🔄 Reiniciando ${target}...`, { + chat_id: chatId, + message_id: callbackQuery.message.message_id + }); + + // Simular reinicialização (implementar comando real aqui) + setTimeout(async () => { + await this.bot.sendMessage(chatId, `✅ ${target} foi reiniciado com sucesso.`); + }, 3000); + } + + /** + * MÉTODO REMOVIDO - handleUserSessionInput duplicado foi mesclado acima + */ + + /** + * Executa comando personalizado em uma conexão SSH + */ + async executeCustomCommand(chatId, connectionId, command) { + await this.bot.sendMessage(chatId, `💻 Executando: \`${command}\``, { parse_mode: 'Markdown' }); + + try { + const result = await this.ssh.executeCommand(connectionId, command); + + let message = `💻 *Comando:* \`${command}\`\n\n`; + + if (result.success) { + message += `✅ *Resultado:*\n\`\`\`\n${result.output || 'Comando executado com sucesso'}\`\`\``; + } else { + message += `❌ *Erro:*\n\`\`\`\n${result.error || 'Erro desconhecido'}\`\`\``; + } + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + } catch (error) { + await this.bot.sendMessage(chatId, `❌ Erro ao executar comando: ${error.message}`); + } + } + + /** + * Processa callback de verificação de sinal + */ + async handleCheckSignalCallback(callbackQuery, data) { + const parts = data.split('_'); + const chatId = callbackQuery.message.chat.id; + + // Verificar se é Fiberhome (formato: check_signal_fiberhome_SERIAL) + if (parts.length === 4 && parts[2] === 'fiberhome') { + const serialNumber = parts[3]; + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.handleCheckSignalFiberhome(chatId, serialNumber); + return; + } + + // Formato padrão Datacom: check_signal_olt_dito_SERIAL ou check_signal_olt_pedro_afonso_SERIAL + let oltId, serialNumber; + + if (parts.length === 6 && parts[2] === 'olt' && parts[3] === 'pedro' && parts[4] === 'afonso') { + // Caso: check_signal_olt_pedro_afonso_SERIAL + oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" + serialNumber = parts[5]; + } else if (parts.length === 5) { + // Caso: check_signal_olt_dito_SERIAL + oltId = parts[2] + '_' + parts[3]; // "olt_dito" + serialNumber = parts[4]; + } else if (parts.length === 4) { + // Caso: check_signal_oltid_SERIAL (se existir formato mais simples) + oltId = parts[2]; + serialNumber = parts[3]; + } else { + // Tentar reconstruir dinamicamente + oltId = parts.slice(2, -1).join('_'); // Tudo exceto o último (que é o serial) + serialNumber = parts[parts.length - 1]; // Último elemento + } + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.handleCheckSignalOLT(chatId, oltId, serialNumber); + } + + /** + * Executa verificação de sinal na OLT Fiberhome + */ + async handleCheckSignalFiberhome(chatId, serialNumber) { + // Escapar caracteres especiais + const serialEscaped = serialNumber.replace(/_/g, '\\_'); + + await this.bot.sendMessage(chatId, `🔍 Procurando ONU ${serialEscaped} na OLT Fiberhome...`); + + try { + // Usar a mesma lógica do handleStatusONU + let resultado; + if (serialNumber.length === 12 && /^[A-Z0-9]+$/.test(serialNumber)) { + // É um serial + resultado = await this.fiberhome.obterStatusONU(serialNumber); + } else { + // Pode ser device name + resultado = await this.fiberhome.obterStatusONUPorNome(serialNumber); + } + + if (resultado.success) { + // Formatar resposta similar às OLTs Datacom + let message = `📡 *Sinal da ONU ${serialEscaped}*\n\n`; + message += `🏢 *OLT:* Fiberhome UNM2000 (131.0.192.9)\n`; + message += `📍 *Localização:* Slot ${resultado.slot}, PON ${resultado.pon}, ONU ${resultado.onuId}\n`; + message += `🔌 *Status:* ${resultado.online ? '🟢 Online' : '🔴 Offline'}\n\n`; + + message += `📊 *Informações do Sinal:*\n`; + message += `📡 *RX Power:* ${resultado.rxPower}\n`; + message += `📤 *TX Power:* ${resultado.txPower}\n`; + message += `📏 *Distância:* ${resultado.distance}\n\n`; + + // Nota sobre leitura de potência + message += `_💡 Valores medidos pela OLT via protocolo TL1_\n\n`; + + // Status das portas Ethernet (resumido) + if (resultado.portasEthernet.length > 0) { + const portasAtivas = resultado.portasEthernet.filter(p => p.status === 'active' || p.status === 'up').length; + message += `🔗 *Portas Ethernet:* ${portasAtivas}/${resultado.portasEthernet.length} ativas\n`; + } + + // Serviços WAN + if (resultado.servicosWAN.length > 0) { + const servicosAtivos = resultado.servicosWAN.filter(s => s.status === 'active').length; + message += `🌐 *Serviços WAN:* ${servicosAtivos} ativo(s)\n`; + } + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + + // Oferecer opções adicionais (similar às OLTs Datacom) + const keyboard = { + inline_keyboard: [ + [ + { text: '🔄 Atualizar sinal', callback_data: `check_signal_fiberhome_${serialNumber}` }, + { text: '📋 Detalhes completos', callback_data: `fiberhome_details_${serialNumber}` } + ], + [ + { text: '🔍 Pesquisar em OLT Datacom', callback_data: `ver_sinal_${serialNumber}` } + ] + ] + }; + + await this.bot.sendMessage(chatId, '🎛️ O que você gostaria de fazer?', { + reply_markup: keyboard + }); + + } else { + await this.bot.sendMessage(chatId, `❌ ONU ${serialNumber} não encontrada na OLT Fiberhome.`); + + // Oferecer pesquisar em OLTs Datacom + const keyboard = { + inline_keyboard: [ + [ + { text: '🔍 Pesquisar em OLT Datacom', callback_data: `ver_sinal_${serialNumber}` }, + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ] + }; + + await this.bot.sendMessage(chatId, 'ONU não encontrada na Fiberhome. Deseja pesquisar nas OLTs Datacom?', { + reply_markup: keyboard + }); + } + } catch (error) { + console.error('Erro ao verificar sinal Fiberhome:', error); + await this.bot.sendMessage(chatId, `❌ Erro interno: ${error.message}`); + } + } + + /** + * Processa callback de mostrar configuração + */ + async handleShowConfigCallback(callbackQuery, data) { + const parts = data.split('_'); + // Formato: show_config_olt_dito_slot_onuid ou show_config_olt_pedro_afonso_slot_onuid + let oltId, slot, onuId; + + if (parts.length === 7 && parts[2] === 'olt' && parts[3] === 'pedro' && parts[4] === 'afonso') { + // Caso: show_config_olt_pedro_afonso_slot_onuid + oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" + slot = parts[5]; + onuId = parts[6]; + } else if (parts.length === 6) { + // Caso: show_config_olt_dito_slot_onuid + oltId = parts[2] + '_' + parts[3]; // "olt_dito" + slot = parts[4]; + onuId = parts[5]; + } else if (parts.length === 5) { + // Caso: show_config_oltid_slot_onuid (se existir formato mais simples) + oltId = parts[2]; + slot = parts[3]; + onuId = parts[4]; + } else { + // Tentar reconstruir dinamicamente + // Os dois últimos são sempre slot e onuId + onuId = parts[parts.length - 1]; + slot = parts[parts.length - 2]; + oltId = parts.slice(2, -2).join('_'); // Tudo entre o terceiro e os dois últimos + } + + const chatId = callbackQuery.message.chat.id; + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.sendMessage(chatId, `📋 Buscando configuração da ONU ${onuId} no slot ${slot}...`); + + try { + const connection = await this.ssh.connect(oltId); + + if (!connection.success) { + await this.bot.sendMessage(chatId, `❌ Erro na conexão: ${connection.message}`); + return; + } + + const configCommand = `show running-config interface gpon ${slot} onu ${onuId}`; + const spCommand = `show running-config service-port | context-match "gpon ${slot} onu ${onuId}"`; + + const [configResult, spResult] = await Promise.all([ + this.ssh.executeCommand(connection.connectionId, configCommand), + this.ssh.executeCommand(connection.connectionId, spCommand) + ]); + + this.ssh.disconnect(connection.connectionId); + + if (configResult.success) { + // Extrair service-ports + const servicePorts = spResult.success + ? this.ssh.parseServicePorts(spResult.output) + : []; + + let message = `📋 *Configuração da ONU*\n`; + message += `📍 Slot: ${slot}, ONU: ${onuId}\n`; + + if (servicePorts.length > 0) { + message += `🔌 *Service-port${servicePorts.length > 1 ? 's' : ''}:* ${servicePorts.join(', ')}\n`; + } else { + message += `🔌 *Service-port:* não encontrado\n`; + } + + message += `\n\`\`\`\n${configResult.output}\n\`\`\``; + + if (spResult.success && spResult.output && spResult.output.trim()) { + message += `\n\n*Service-port(s):*\n\`\`\`\n${spResult.output.trim()}\n\`\`\``; + } + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + } else { + await this.bot.sendMessage(chatId, `❌ Erro ao obter configuração: ${configResult.error}`); + } + } catch (error) { + console.error('Erro ao mostrar configuração:', error); + await this.bot.sendMessage(chatId, `❌ Erro interno: ${error.message}`); + } + } + + /** + * Processa callback de reiniciar ONU + */ + async handleResetOnuCallback(callbackQuery, data) { + console.log(`🔌 handleResetOnuCallback chamado com data: ${data}`); + + const parts = data.split('_'); + console.log(`📝 Parts: ${JSON.stringify(parts)}`); + + // Formato: reset_onu_olt_dito_slot_onuid ou reset_onu_olt_pedro_afonso_slot_onuid + let oltId, slot, onuId; + + if (parts.length === 7 && parts[2] === 'olt' && parts[3] === 'pedro' && parts[4] === 'afonso') { + // Caso: reset_onu_olt_pedro_afonso_slot_onuid + oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" + slot = parts[5]; + onuId = parts[6]; + } else if (parts.length === 6) { + // Caso: reset_onu_olt_dito_slot_onuid + oltId = parts[2] + '_' + parts[3]; // "olt_dito" + slot = parts[4]; + onuId = parts[5]; + } else if (parts.length === 5) { + // Caso: reset_onu_oltid_slot_onuid (se existir formato mais simples) + oltId = parts[2]; + slot = parts[3]; + onuId = parts[4]; + } else { + // Tentar reconstruir dinamicamente + // Os dois últimos são sempre slot e onuId + onuId = parts[parts.length - 1]; + slot = parts[parts.length - 2]; + oltId = parts.slice(2, -2).join('_'); // Tudo entre o terceiro e os dois últimos + } + + console.log(`🎯 Parsed - OLT: ${oltId}, Slot: ${slot}, ONU: ${onuId}`); + + const chatId = callbackQuery.message.chat.id; + + // Primeiro mostrar confirmação + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.editMessageText( + `⚠️ **Confirmação de Reinicialização**\n\n` + + `Tem certeza que deseja reiniciar a ONU?\n\n` + + `📍 **Slot:** ${slot}\n` + + `🔌 **ONU ID:** ${onuId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `⚠️ Esta ação irá desconectar temporariamente o cliente.`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [ + { text: '✅ SIM, Reiniciar', callback_data: `confirm_reset_onu_${oltId}_${slot}_${onuId}` }, + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ] + } + } + ); + } + + /** + * Processa confirmação de reinicialização de ONU + */ + async handleConfirmResetOnu(callbackQuery, data) { + console.log(`🔄 handleConfirmResetOnu chamado com data: ${data}`); + + const parts = data.replace('confirm_reset_onu_', '').split('_'); + console.log(`📝 Parts após parsing: ${JSON.stringify(parts)}`); + + // Formato após remoção do prefixo: olt_dito_slot_onuid + let oltId, slot, onuId; + + if (parts.length === 4) { + // Caso: olt_dito_slot_onuid + oltId = parts[0] + '_' + parts[1]; // "olt_dito" + slot = parts[2]; + onuId = parts[3]; + } else if (parts.length === 3) { + // Caso: oltid_slot_onuid + oltId = parts[0]; + slot = parts[1]; + onuId = parts[2]; + } else { + // Tentar reconstruir dinamicamente + onuId = parts[parts.length - 1]; + slot = parts[parts.length - 2]; + oltId = parts.slice(0, -2).join('_'); + } + + console.log(`🎯 Parsed - OLT: ${oltId}, Slot: ${slot}, ONU: ${onuId}`); + + const chatId = callbackQuery.message.chat.id; + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.editMessageText( + `🔄 **Reiniciando ONU...**\n\n` + + `📍 Slot: ${slot}\n` + + `🔌 ONU ID: ${onuId}\n` + + `🏢 OLT: ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `⏳ Aguarde, isso pode levar alguns segundos...`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + + try { + const result = await this.ssh.resetOnuAlternative(oltId, slot, onuId); + + if (result.success) { + await this.bot.editMessageText( + `✅ **ONU Reiniciada com Sucesso!**\n\n` + + `📍 Slot: ${slot}\n` + + `🔌 ONU ID: ${onuId}\n` + + `🏢 OLT: ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `🔄 A ONU deve voltar a funcionar em alguns instantes.\n` + + `📊 Recomenda-se verificar o sinal novamente em 1-2 minutos.`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } else { + await this.bot.editMessageText( + `❌ **Erro ao Reiniciar ONU**\n\n` + + `📍 Slot: ${slot}\n` + + `🔌 ONU ID: ${onuId}\n` + + `🏢 OLT: ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `❌ Erro: ${result.message}`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } catch (error) { + console.error('Erro ao reiniciar ONU:', error); + await this.bot.editMessageText( + `❌ **Erro Interno**\n\n` + + `Não foi possível reiniciar a ONU.\n` + + `Erro: ${error.message}`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } + + /** + * Processa callback de atualização de firmware da ONU + */ + async handleUpdateOnuCallback(callbackQuery, data) { + const parts = data.split('_'); + // Formato: update_onu_olt_dito_slot_onuid ou update_onu_olt_pedro_afonso_slot_onuid + let oltId, slot, onuId; + + if (parts.length === 7 && parts[2] === 'olt' && parts[3] === 'pedro' && parts[4] === 'afonso') { + // Caso: update_onu_olt_pedro_afonso_slot_onuid + oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" + slot = parts[5]; + onuId = parts[6]; + } else if (parts.length === 6) { + // Caso: update_onu_olt_dito_slot_onuid + oltId = parts[2] + '_' + parts[3]; // "olt_dito" + slot = parts[4]; + onuId = parts[5]; + } else if (parts.length === 5) { + // Caso: update_onu_oltid_slot_onuid (se existir formato mais simples) + oltId = parts[2]; + slot = parts[3]; + onuId = parts[4]; + } else { + // Tentar reconstruir dinamicamente + // Os dois últimos são sempre slot e onuId + onuId = parts[parts.length - 1]; + slot = parts[parts.length - 2]; + oltId = parts.slice(2, -2).join('_'); // Tudo entre o terceiro e os dois últimos + } + + const chatId = callbackQuery.message.chat.id; + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.sendMessage(chatId, `🔍 Identificando modelo da ONU ${onuId} no slot ${slot}...`); + + try { + // Obter detalhes da ONU para identificar o modelo + const details = await this.ssh.getOnuDetails(oltId, slot, onuId); + + if (!details.success) { + await this.bot.sendMessage(chatId, `❌ Erro ao obter detalhes da ONU: ${details.message}`); + return; + } + + const equipmentId = details.equipmentId; + console.log(`🔍 Equipment ID detectado: ${equipmentId}`); + + // Determinar opções de firmware baseado no modelo + let firmwareOptions = []; + + if (equipmentId.includes('DM986-100')) { + firmwareOptions = [ + { text: '📦 FW INTERMEDIÁRIA', callback_data: `firmware_DM986-100-inter.tar_${oltId}_${slot}_${onuId}` }, + { text: '📦 FW ATUAL (v56)', callback_data: `firmware_DM986-100-v56.tar_${oltId}_${slot}_${onuId}` }, + { text: '📦 FW ROUTER (R300)', callback_data: `firmware_DM986-100-R300.tar_${oltId}_${slot}_${onuId}` } + ]; + } else if (equipmentId.includes('DM986-414')) { + firmwareOptions = [ + { text: '📦 FW INTERMEDIÁRIA', callback_data: `firmware_DM986-414-inter.tar_${oltId}_${slot}_${onuId}` }, + { text: '📦 FW ATUAL (v70)', callback_data: `firmware_DM986-414-v70.tar_${oltId}_${slot}_${onuId}` } + ]; + } else { + await this.bot.sendMessage(chatId, + `❌ **Modelo não suportado**\n\n` + + `📍 Slot: ${slot}, ONU: ${onuId}\n` + + `🔍 Equipment ID: ${equipmentId}\n\n` + + `⚠️ Apenas DM986-100 e DM986-414 são suportados para atualização.` + ); + return; + } + + // Criar teclado com opções de firmware + const keyboard = { + inline_keyboard: [ + ...firmwareOptions.map(option => [option]), + [{ text: '❌ Cancelar', callback_data: 'cancel_action' }] + ] + }; + + await this.bot.sendMessage(chatId, + `⬆️ **Atualização de Firmware**\n\n` + + `📍 **Slot:** ${slot}\n` + + `🔌 **ONU ID:** ${onuId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n` + + `🔍 **Modelo:** ${equipmentId}\n\n` + + `📦 Selecione a versão do firmware:`, + { + parse_mode: 'Markdown', + reply_markup: keyboard + } + ); + + } catch (error) { + console.error('Erro ao processar atualização de ONU:', error); + await this.bot.sendMessage(chatId, `❌ Erro interno: ${error.message}`); + } + } + + /** + * Processa callback de seleção de firmware + */ + async handleFirmwareCallback(callbackQuery, data) { + // Formato: firmware_DM986-100-v56.tar_olt_dito_slot_onuid + const parts = data.replace('firmware_', '').split('_'); + + let firmwareFile, oltId, slot, onuId; + + // O arquivo de firmware é sempre o primeiro elemento + firmwareFile = parts[0]; + + // Processar o resto baseado no número de partes + if (parts.length === 5) { + // Caso: DM986-100-v56.tar_olt_dito_slot_onuid + oltId = parts[1] + '_' + parts[2]; // "olt_dito" + slot = parts[3]; + onuId = parts[4]; + } else if (parts.length === 4) { + // Caso: DM986-100-v56.tar_oltid_slot_onuid + oltId = parts[1]; + slot = parts[2]; + onuId = parts[3]; + } else { + // Tentar reconstruir dinamicamente + onuId = parts[parts.length - 1]; + slot = parts[parts.length - 2]; + oltId = parts.slice(1, -2).join('_'); + } + + const chatId = callbackQuery.message.chat.id; + + // Mostrar confirmação + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.editMessageText( + `⚠️ **Confirmação de Atualização de Firmware**\n\n` + + `📦 **Firmware:** ${firmwareFile}\n` + + `📍 **Slot:** ${slot}\n` + + `🔌 **ONU ID:** ${onuId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `⚠️ **ATENÇÃO:**\n` + + `• A atualização pode demorar alguns minutos\n` + + `• A ONU ficará offline durante o processo\n` + + `• Não interrompa a operação\n\n` + + `Tem certeza que deseja continuar?`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [ + { text: '✅ SIM, Atualizar', callback_data: `confirm_firmware_${firmwareFile}_${oltId}_${slot}_${onuId}` }, + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ] + } + } + ); + } + + /** + * Processa confirmação de atualização de firmware + */ + async handleConfirmFirmware(callbackQuery, data) { + // Formato: confirm_firmware_DM986-100-v56.tar_olt_dito_slot_onuid + const parts = data.replace('confirm_firmware_', '').split('_'); + + let firmwareFile, oltId, slot, onuId; + + firmwareFile = parts[0]; + + if (parts.length === 5) { + oltId = parts[1] + '_' + parts[2]; + slot = parts[3]; + onuId = parts[4]; + } else if (parts.length === 4) { + oltId = parts[1]; + slot = parts[2]; + onuId = parts[3]; + } else { + onuId = parts[parts.length - 1]; + slot = parts[parts.length - 2]; + oltId = parts.slice(1, -2).join('_'); + } + + const chatId = callbackQuery.message.chat.id; + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.editMessageText( + `⬆️ **Atualizando Firmware...**\n\n` + + `📦 Firmware: ${firmwareFile}\n` + + `📍 Slot: ${slot}\n` + + `🔌 ONU ID: ${onuId}\n` + + `🏢 OLT: ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `⏳ Enviando comando de atualização...`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + + try { + const result = await this.ssh.updateOnuFirmware(oltId, slot, onuId, firmwareFile); + + if (result.success) { + await this.bot.editMessageText( + `✅ **Comando de Atualização Enviado!**\n\n` + + `📦 **Firmware:** ${firmwareFile}\n` + + `📍 **Slot:** ${slot}\n` + + `🔌 **ONU ID:** ${onuId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `✅ **Status:** Comando enviado com sucesso\n` + + `⏳ **Processamento:** A atualização está sendo processada pela OLT\n` + + `🕒 **Tempo estimado:** 5-15 minutos\n` + + `📡 **Importante:** Monitore o status da ONU\n\n` + + `💡 *A ONU ficará offline durante a atualização*`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } else { + await this.bot.editMessageText( + `❌ **Erro ao Enviar Comando**\n\n` + + `📦 **Firmware:** ${firmwareFile}\n` + + `📍 **Slot:** ${slot}\n` + + `🔌 **ONU ID:** ${onuId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `❌ **Erro:** ${result.message}\n\n` + + `🔄 Tente novamente ou verifique a conexão`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } catch (error) { + console.error('Erro ao atualizar firmware:', error); + await this.bot.editMessageText( + `❌ **Erro Interno**\n\n` + + `Não foi possível atualizar o firmware.\n` + + `Erro: ${error.message}`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } + + /** + * Handle callback para iniciar processo de remoção de ONU + */ + async handleRemoveOnuCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.split('_'); // remove_onu_olt_dito_[serialNumber] ou remove_onu_olt_pedro_afonso_[serialNumber] + + let oltId, serialNumber; + + if (parts.length >= 6 && parts[3] === 'pedro' && parts[4] === 'afonso') { + // Caso: remove_onu_olt_pedro_afonso_SERIAL + oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" + serialNumber = parts.slice(5).join('_'); // Serial pode ter underscores + } else if (parts.length >= 5) { + // Caso: remove_onu_olt_dito_SERIAL ou outras OLTs de duas palavras + oltId = parts[2] + '_' + parts[3]; // "olt_dito" + serialNumber = parts.slice(4).join('_'); // Serial pode ter underscores + } else { + // Fallback - caso não siga o padrão esperado + oltId = parts[2]; + serialNumber = parts.slice(3).join('_'); + } + + await this.bot.answerCallbackQuery(callbackQuery.id); + + await this.bot.editMessageText( + `🔍 **Buscando ONU ${serialNumber}**\n\n` + + `🏢 OLT: ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n` + + `📱 Serial: ${serialNumber}\n\n` + + `⏳ Localizando ONU e service-port...`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + + try { + const result = await this.ssh.removeOnuBySerial(oltId, serialNumber); + + if (result.success && result.readyForRemoval) { + // ONU encontrada - mostrar informações e pedir confirmação + // O service-port NÃO é incluído no callback_data pois removeOnu busca todos automaticamente + const keyboard = { + inline_keyboard: [ + [ + { text: '✅ CONFIRMAR REMOÇÃO', callback_data: `confirm_remove_${oltId}_${result.slot.replace(/\//g, '-')}_${result.onuId}_${serialNumber}` } + ], + [ + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ] + }; + + const servicePortsInfo = result.servicePorts && result.servicePorts.length > 1 + ? `🔌 **Service-ports:** ${result.servicePorts.join(', ')} *(${result.servicePorts.length} encontrados — todos serão removidos)*` + : `🔌 **Service-port:** ${result.servicePort}`; + + await this.bot.editMessageText( + `⚠️ **CONFIRMAÇÃO DE REMOÇÃO**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `📍 **Localização:** Slot ${result.slot}, ONU ${result.onuId}\n` + + `${servicePortsInfo}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + + `⚠️ **ATENÇÃO:** Esta ação não pode ser desfeita!\n` + + `A ONU será completamente removida da OLT.\n\n` + + `🔴 **Confirme que deseja remover esta ONU:**`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown', + reply_markup: keyboard + } + ); + } else { + await this.bot.editMessageText( + `❌ **ONU não encontrada**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + + `❌ **Erro:** ${result.message}\n\n` + + `💡 Verifique se o serial está correto ou se a ONU existe nesta OLT`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } catch (error) { + console.error('Erro ao buscar ONU para remoção:', error); + await this.bot.editMessageText( + `❌ **Erro interno**\n\n` + + `Não foi possível buscar a ONU.\n` + + `Erro: ${error.message}`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } + + /** + * Handle callback para confirmar remoção de ONU + */ + async handleConfirmRemoveCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + console.log(`🔍 handleConfirmRemoveCallback chamado com data: ${data}`); + + // Novo formato (sem servicePort): confirm_remove_olt_dito_[slot]_[onuId]_[serialNumber] + const parts = data.split('_'); + console.log(`🔍 Parts: ${JSON.stringify(parts)}`); + + let oltId, slot, onuId, serialNumber; + + if (parts.length >= 8 && parts[3] === 'pedro' && parts[4] === 'afonso') { + // Caso: confirm_remove_olt_pedro_afonso_slot_onuId_serialNumber + oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" + slot = parts[5].replace(/-/g, '/'); + onuId = parts[6]; + serialNumber = parts.slice(7).join('_'); + } else if (parts.length >= 7) { + // Caso: confirm_remove_olt_dito_slot_onuId_serialNumber + oltId = parts[2] + '_' + parts[3]; // "olt_dito" + slot = parts[4].replace(/-/g, '/'); + onuId = parts[5]; + serialNumber = parts.slice(6).join('_'); + } else { + // Fallback + oltId = parts[2]; + slot = parts[3].replace(/-/g, '/'); + onuId = parts[4]; + serialNumber = parts.slice(5).join('_'); + } + + console.log(`🔍 Parsed - oltId: ${oltId}, slot: ${slot}, onuId: ${onuId}, serialNumber: ${serialNumber}`); + + await this.bot.answerCallbackQuery(callbackQuery.id); + + await this.bot.editMessageText( + `🗑️ **Removendo ONU...**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `📍 **Localização:** Slot ${slot}, ONU ${onuId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + + `⏳ Buscando e removendo todos os service-ports...`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + + try { + const result = await this.ssh.removeOnu(oltId, slot, onuId); + + if (result.success) { + const portsRemovidos = result.servicePorts && result.servicePorts.length > 0 + ? result.servicePorts.join(', ') + : 'N/A'; + const portsLabel = result.servicePorts && result.servicePorts.length > 1 + ? `• Removidos ${result.servicePorts.length} service-ports (${portsRemovidos})` + : `• Removido service-port ${portsRemovidos}`; + + await this.bot.editMessageText( + `✅ **ONU Removida com Sucesso!**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `📍 **Localização:** Slot ${slot}, ONU ${onuId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + + `✅ **Status:** ONU removida completamente\n` + + `🗑️ **Ações realizadas:**\n` + + `• Removida interface ONU\n` + + `${portsLabel}\n` + + `• Configuração commitada\n\n` + + `💡 A ONU pode ser reconfigurada quando necessário`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } else { + await this.bot.editMessageText( + `❌ **Erro na Remoção**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `📍 **Localização:** Slot ${slot}, ONU ${onuId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + + `❌ **Erro:** ${result.message}\n\n` + + `🔄 Verifique o status da ONU ou tente novamente`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } catch (error) { + console.error('Erro ao remover ONU:', error); + await this.bot.editMessageText( + `❌ **Erro Interno**\n\n` + + `Não foi possível remover a ONU.\n` + + `Erro: ${error.message}`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } + + /** + * Handle callback para confirmar remoção de ONU Fiberhome via TL1 + */ + async handleConfirmRemoveFiberhomeCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + console.log(`🔍 handleConfirmRemoveFiberhomeCallback chamado com data: ${data}`); + + // Formato: confirm_remove_fh_SERIAL + const serialNumber = data.replace('confirm_remove_fh_', '').toUpperCase(); + console.log(`🔍 Serial a remover: ${serialNumber}`); + + await this.bot.answerCallbackQuery(callbackQuery.id); + + // Escapar caracteres especiais + const serialEscaped = serialNumber.replace(/_/g, '\\_'); + + await this.bot.editMessageText( + `🗑️ *Removendo ONU Fiberhome...*\n\n` + + `📱 *Serial:* ${serialEscaped}\n\n` + + `⏳ Aguarde...`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + + try { + // Executar comando TL1 de remoção + const resultado = await this.fiberhomeTL1.removerOnuFiberhome(serialNumber); + + if (resultado.success) { + await this.bot.editMessageText( + `✅ *ONU Fiberhome Removida com Sucesso!*\n\n` + + `📱 *Serial:* ${serialEscaped}\n` + + `🏢 *OLT:* ${resultado.details.olt.replace(/_/g, '\\_')}\n` + + `📡 *Porta:* ${resultado.details.porta}\n\n` + + `🔄 A ONU foi removida do sistema.`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } else { + await this.bot.editMessageText( + `❌ *Erro na Remoção*\n\n` + + `📱 *Serial:* ${serialEscaped}\n\n` + + `⚠️ *Erro:* ${resultado.error}\n\n` + + `🔄 Verifique o status da ONU ou tente novamente`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } catch (error) { + console.error('❌ Erro ao remover ONU Fiberhome:', error); + await this.bot.editMessageText( + `❌ *Erro Interno*\n\n` + + `📱 *Serial:* ${serialEscaped}\n\n` + + `⚠️ ${error.message}`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } + + /** + * Handle callback para iniciar processo de adição de ONU + */ + /** + * Handler: remove ONU já cadastrada e tenta readicioná-la automaticamente. + * Formatos de callback_data: + * remove_readd____ + * remove_readd_vlan_____ + */ + async handleRemoveAndReAddCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const hasVlan = data.startsWith('remove_readd_vlan_'); + const parts = data.split('_'); + + let oltId, slot, onuId, vlanId, serialNumber; + + if (hasVlan) { + // remove_readd_vlan_olt_pedro_afonso_slot_onuId_vlanId_serial + // remove_readd_vlan_olt_dito_slot_onuId_vlanId_serial + // parts[0]=remove [1]=readd [2]=vlan [3]=olt [4]=dito|pedro [5]=afonso|slot ... + if (parts[4] === 'pedro' && parts[5] === 'afonso') { + oltId = parts[3] + '_' + parts[4] + '_' + parts[5]; + slot = parts[6].replace(/-/g, '/'); + onuId = parts[7]; + vlanId = parts[8]; + serialNumber = parts.slice(9).join('_'); + } else { + oltId = parts[3] + '_' + parts[4]; + slot = parts[5].replace(/-/g, '/'); + onuId = parts[6]; + vlanId = parts[7]; + serialNumber = parts.slice(8).join('_'); + } + } else { + // remove_readd_olt_pedro_afonso_slot_onuId_serial + // remove_readd_olt_dito_slot_onuId_serial + // parts[0]=remove [1]=readd [2]=olt [3]=dito|pedro [4]=afonso|slot ... + if (parts[3] === 'pedro' && parts[4] === 'afonso') { + oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; + slot = parts[5].replace(/-/g, '/'); + onuId = parts[6]; + serialNumber = parts.slice(7).join('_'); + } else { + oltId = parts[2] + '_' + parts[3]; + slot = parts[4].replace(/-/g, '/'); + onuId = parts[5]; + serialNumber = parts.slice(6).join('_'); + } + } + + console.log(`🗑️➕ remove_readd - oltId: ${oltId}, slot: ${slot}, onuId: ${onuId}, vlanId: ${vlanId || 'auto'}, serial: ${serialNumber}`); + + await this.bot.answerCallbackQuery(callbackQuery.id); + + await this.bot.editMessageText( + `🗑️ **Removendo ONU cadastrada...**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `📍 **Slot:** ${slot}, ONU ${onuId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + + `⏳ Aguarde...`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + + try { + const removeResult = await this.ssh.removeOnu(oltId, slot, onuId); + + if (!removeResult.success) { + await this.bot.editMessageText( + `❌ **Erro ao remover ONU**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + + `❌ **Erro:** ${removeResult.message}\n\n` + + `🔄 Verifique o status da ONU e tente novamente.`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + return; + } + + // ONU removida — aguardar brevemente e tentar redescobrir + await this.bot.editMessageText( + `✅ **ONU removida!** Aguardando redescoberta...\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + + `⏳ Verificando discovered-onus...`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + + // Aguardar 4 segundos para a ONU voltar à lista de discovered + await new Promise(resolve => setTimeout(resolve, 4000)); + + const addResult = await this.ssh.addOnuBySerial(oltId, serialNumber, vlanId || undefined); + + if (addResult.success && addResult.readyForAddition) { + const effectiveVlan = vlanId || addResult.vlanId; + const keyboard = { + inline_keyboard: [ + [ + { text: '✅ CONFIRMAR ADIÇÃO', callback_data: `confirm_add_${oltId}_${addResult.slot.replace(/\//g, '-')}_${addResult.onuId}_${effectiveVlan}_${serialNumber}` } + ], + [ + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ] + }; + + await this.bot.editMessageText( + `✅ **ONU pronta para adição!**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `📍 **Slot descoberto:** ${addResult.slot}\n` + + `🔢 **ONU ID a usar:** ${addResult.onuId}\n` + + `🏷️ **VLAN ID:** ${effectiveVlan}${vlanId ? ' (customizada)' : ''}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + + `📋 **ONU IDs em uso no slot:** ${addResult.usedIds.join(', ')}\n\n` + + `✅ **Confirme que deseja adicionar esta ONU:**`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown', + reply_markup: keyboard + } + ); + } else { + // ONU não apareceu em discovered-onus ainda + await this.bot.editMessageText( + `✅ **ONU removida com sucesso!**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + + `⚠️ A ONU ainda não apareceu como disponível para adição.\n\n` + + `💡 Aguarde alguns instantes e tente:\n` + + `\`adicionar onu ${serialNumber}\``, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } catch (error) { + console.error('Erro em handleRemoveAndReAddCallback:', error); + await this.bot.editMessageText( + `❌ **Erro interno**\n\n` + + `Não foi possível concluir a operação.\n` + + `Erro: ${error.message}`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } + + /** + * Handle callback para iniciar processo de adição de ONU + */ + async handleAddOnuCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.split('_'); // add_onu_olt_dito_[serialNumber] ou add_onu_olt_pedro_afonso_[serialNumber] + + let oltId, serialNumber; + + if (parts.length >= 6 && parts[3] === 'pedro' && parts[4] === 'afonso') { + // Caso: add_onu_olt_pedro_afonso_SERIAL + oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" + serialNumber = parts.slice(5).join('_'); // Serial pode ter underscores + } else if (parts.length >= 5) { + // Caso: add_onu_olt_dito_SERIAL ou outras OLTs de duas palavras + oltId = parts[2] + '_' + parts[3]; // "olt_dito" + serialNumber = parts.slice(4).join('_'); // Serial pode ter underscores + } else { + // Fallback - caso não siga o padrão esperado + oltId = parts[2]; + serialNumber = parts.slice(3).join('_'); + } + + await this.bot.answerCallbackQuery(callbackQuery.id); + + await this.bot.editMessageText( + `🔍 **Analisando ONU ${serialNumber}**\n\n` + + `🏢 OLT: ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n` + + `📱 Serial: ${serialNumber}\n\n` + + `⏳ Descobrindo slot e próximo ONU ID disponível...`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + + try { + const result = await this.ssh.addOnuBySerial(oltId, serialNumber); + + if (result.success && result.readyForAddition) { + // ONU analisada - mostrar informações e pedir confirmação + const keyboard = { + inline_keyboard: [ + [ + { text: '✅ CONFIRMAR ADIÇÃO', callback_data: `confirm_add_${oltId}_${result.slot.replace(/\//g, '-')}_${result.onuId}_${result.vlanId}_${serialNumber}` } + ], + [ + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ] + }; + + await this.bot.editMessageText( + `✅ **CONFIRMAÇÃO DE ADIÇÃO**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `📍 **Slot descoberto:** ${result.slot}\n` + + `🔢 **ONU ID a usar:** ${result.onuId}\n` + + `🏷️ **VLAN ID:** ${result.vlanId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `📋 **ONU IDs em uso no slot:** ${result.usedIds.join(', ')}\n\n` + + `⚡ **Configurações que serão aplicadas:**\n` + + `• Line-profile: DM986-100\n` + + `• Ethernet: porta 1\n` + + `• Native VLAN: ${result.vlanId}\n` + + `• Service-port: novo (auto)\n\n` + + `✅ **Confirme que deseja adicionar esta ONU:**`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown', + reply_markup: keyboard + } + ); + } else { + // ONU não encontrada em discovered-onus — verificar se já está cadastrada no running-config + if (result.message && result.message.includes('discovered-onus')) { + await this.bot.editMessageText( + `🔍 **ONU não disponível para adição**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `⏳ Verificando se a ONU já está cadastrada...`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + + const findResult = await this.ssh.findOnuBySerial(oltId, serialNumber); + + if (findResult.success && findResult.onuInfo && findResult.onuInfo.found) { + const { slot, onuId } = findResult.onuInfo; + const keyboard = { + inline_keyboard: [ + [ + { text: '🗑️ Remover e adicionar novamente', callback_data: `remove_readd_${oltId}_${slot.replace(/\//g, '-')}_${onuId}_${serialNumber}` } + ], + [ + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ] + }; + await this.bot.editMessageText( + `⚠️ **ONU já cadastrada na OLT**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `📍 **Localização atual:** Slot ${slot}, ONU ${onuId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `A ONU já está cadastrada e não aparece como disponível para adição.\n\n` + + `Deseja **remover** o cadastro atual e adicioná-la novamente?`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown', + reply_markup: keyboard + } + ); + } else { + await this.bot.editMessageText( + `❌ **ONU não disponível**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `A ONU não foi encontrada em discovered-onus nem no running-config.\n\n` + + `💡 **Dica:** Verifique se a ONU está ligada e conectada à OLT.\n` + + `Use o comando: \`show interface gpon discovered-onus\``, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } else { + let errorMessage = `❌ **Não foi possível adicionar a ONU**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n`; + + if (result.message) errorMessage += `❌ **Erro:** ${result.message}\n\n`; + + if (result.message && result.message.includes('todos os ONU IDs')) { + errorMessage += `💡 **Dica:** O slot está lotado. Considere usar outro slot ou remover ONUs inutilizadas.`; + } else { + errorMessage += `💡 Verifique a conectividade com a OLT ou se a ONU está descoberta.`; + } + + await this.bot.editMessageText(errorMessage, { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + }); + } + } + } catch (error) { + console.error('Erro ao analisar ONU para adição:', error); + await this.bot.editMessageText( + `❌ **Erro interno**\n\n` + + `Não foi possível analisar a ONU.\n` + + `Erro: ${error.message}`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } + + /** + * Handle callback para iniciar processo de adição de ONU com VLAN customizada + */ + async handleAddOnuVlanCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + // Formato: add_onu_vlan_olt_dito_603_SERIAL ou add_onu_vlan_olt_pedro_afonso_603_SERIAL + // Índices: [0]_[1]_[2]_[3]_[4]_[5]_[6] + const parts = data.split('_'); + + let oltId, vlanId, serialNumber; + + if (parts.length >= 9 && parts[4] === 'pedro' && parts[5] === 'afonso') { + // Caso: add_onu_vlan_olt_pedro_afonso_VLANID_SERIAL + oltId = parts[3] + '_' + parts[4] + '_' + parts[5]; // "olt_pedro_afonso" + vlanId = parts[6]; + serialNumber = parts.slice(7).join('_'); // Serial pode ter underscores + } else if (parts.length >= 7) { + // Caso: add_onu_vlan_olt_dito_VLANID_SERIAL ou add_onu_vlan_olt_colmeia_VLANID_SERIAL + oltId = parts[3] + '_' + parts[4]; // "olt_dito" ou "olt_colmeia" + vlanId = parts[5]; + serialNumber = parts.slice(6).join('_'); // Serial pode ter underscores + } else { + // Fallback + oltId = parts[3]; + vlanId = parts[4]; + serialNumber = parts.slice(5).join('_'); + } + + await this.bot.answerCallbackQuery(callbackQuery.id); + + await this.bot.editMessageText( + `🔍 **Analisando ONU ${serialNumber}**\n\n` + + `🏢 OLT: ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n` + + `📱 Serial: ${serialNumber}\n` + + `🏷️ VLAN customizada: ${vlanId}\n\n` + + `⏳ Descobrindo slot e próximo ONU ID disponível...`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + + try { + // Usar a mesma função addOnuBySerial mas sem a vlanId (vamos usar a customizada) + const result = await this.ssh.addOnuBySerial(oltId, serialNumber); + + if (result.success && result.readyForAddition) { + // ONU analisada - mostrar informações e pedir confirmação + const keyboard = { + inline_keyboard: [ + [ + { text: '✅ CONFIRMAR ADIÇÃO', callback_data: `confirm_add_${oltId}_${result.slot.replace(/\//g, '-')}_${result.onuId}_${vlanId}_${serialNumber}` } + ], + [ + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ] + }; + + await this.bot.editMessageText( + `✅ **CONFIRMAÇÃO DE ADIÇÃO COM VLAN CUSTOMIZADA**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `📍 **Slot descoberto:** ${result.slot}\n` + + `🔢 **ONU ID a usar:** ${result.onuId}\n` + + `🏷️ **VLAN ID customizada:** ${vlanId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + + `📋 **ONU IDs em uso no slot:** ${result.usedIds.join(', ')}\n\n` + + `⚡ **Configurações que serão aplicadas:**\n` + + `• Line-profile: DM986-100\n` + + `• Ethernet: porta 1\n` + + `• Native VLAN: ${vlanId} (customizada)\n` + + `• Service-port: novo (auto) com VLAN ${vlanId}\n\n` + + `✅ **Confirme que deseja adicionar esta ONU:**`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown', + reply_markup: keyboard + } + ); + } else { + // ONU não encontrada em discovered-onus — verificar se já está cadastrada no running-config + if (result.message && result.message.includes('discovered-onus')) { + await this.bot.editMessageText( + `🔍 **ONU não disponível para adição**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `🏷️ **VLAN:** ${vlanId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + + `⏳ Verificando se a ONU já está cadastrada...`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + + const findResult = await this.ssh.findOnuBySerial(oltId, serialNumber); + + if (findResult.success && findResult.onuInfo && findResult.onuInfo.found) { + const { slot, onuId } = findResult.onuInfo; + const keyboard = { + inline_keyboard: [ + [ + { text: '🗑️ Remover e adicionar novamente', callback_data: `remove_readd_vlan_${oltId}_${slot.replace(/\//g, '-')}_${onuId}_${vlanId}_${serialNumber}` } + ], + [ + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ] + }; + await this.bot.editMessageText( + `⚠️ **ONU já cadastrada na OLT**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `📍 **Localização atual:** Slot ${slot}, ONU ${onuId}\n` + + `🏷️ **VLAN a usar:** ${vlanId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + + `A ONU já está cadastrada e não aparece como disponível para adição.\n\n` + + `Deseja **remover** o cadastro atual e adicioná-la novamente com VLAN ${vlanId}?`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown', + reply_markup: keyboard + } + ); + } else { + await this.bot.editMessageText( + `❌ **ONU não disponível**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `🏷️ **VLAN:** ${vlanId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n` + + `A ONU não foi encontrada em discovered-onus nem no running-config.\n\n` + + `💡 **Dica:** Verifique se a ONU está ligada e conectada à OLT.\n` + + `Use o comando: \`show interface gpon discovered-onus\``, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } else { + let errorMessage = `❌ **Não foi possível adicionar a ONU**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `🏷️ **VLAN:** ${vlanId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').replace(/_/g, '\\_').toUpperCase()}\n\n`; + + if (result.message) errorMessage += `❌ **Erro:** ${result.message}\n\n`; + + if (result.message && result.message.includes('todos os ONU IDs')) { + errorMessage += `💡 **Dica:** O slot está lotado. Considere usar outro slot ou remover ONUs inutilizadas.`; + } else { + errorMessage += `💡 Verifique a conectividade com a OLT ou se a ONU está descoberta.`; + } + + await this.bot.editMessageText(errorMessage, { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + }); + } + } + } catch (error) { + console.error('Erro ao analisar ONU para adição com VLAN customizada:', error); + await this.bot.editMessageText( + `❌ **Erro interno**\n\n` + + `Não foi possível analisar a ONU.\n` + + `Erro: ${error.message}`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } + + /** + * Handle callback para tipo de ONU Fiberhome + */ + async handleFiberhomeOnuTypeCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const userId = callbackQuery.from.id; + + // Formato: fh_onu_type_AN5506-02-B ou fh_onu_type_AN5506-01-A + const onuType = data.replace('fh_onu_type_', ''); + + // Recuperar sessão + const session = this.userSessions.get(userId); + if (!session || session.action !== 'adicionar_onu_fiberhome_step2') { + await this.bot.answerCallbackQuery(callbackQuery.id, '❌ Sessão expirada'); + await this.bot.sendMessage(chatId, '❌ Sessão expirada. Por favor, comece novamente.'); + this.userSessions.delete(userId); + return; + } + + session.onuType = onuType; + session.action = 'adicionar_onu_fiberhome_step3'; + this.userSessions.set(userId, session); + + await this.bot.answerCallbackQuery(callbackQuery.id); + + // Perguntar sobre VLAN + const keyboard = { + inline_keyboard: [ + [{ text: '✅ Usar VLAN 1002 (padrão)', callback_data: 'fh_vlan_1002' }], + [{ text: '🔧 Usar outra VLAN', callback_data: 'fh_vlan_custom' }], + [{ text: '❌ Cancelar', callback_data: 'cancel_action' }] + ] + }; + + await this.bot.editMessageText( + `🔌 **Tipo da ONU:** ${onuType}\n\n` + + `🏷️ **VLAN:**\n` + + `A VLAN padrão é 1002. Deseja usar esta VLAN ou configurar outra?`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + reply_markup: keyboard, + parse_mode: 'Markdown' + } + ); + } + + /** + * Handle callback para VLAN Fiberhome + */ + async handleFiberhomeVlanCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const userId = callbackQuery.from.id; + + // Recuperar sessão + const session = this.userSessions.get(userId); + if (!session || session.action !== 'adicionar_onu_fiberhome_step3') { + await this.bot.answerCallbackQuery(callbackQuery.id, '❌ Sessão expirada'); + await this.bot.sendMessage(chatId, '❌ Sessão expirada. Por favor, comece novamente.'); + this.userSessions.delete(userId); + return; + } + + await this.bot.answerCallbackQuery(callbackQuery.id); + + if (data === 'fh_vlan_1002') { + // Usar VLAN padrão 1002 + session.vlan = 1002; + + // Limpar sessão + this.userSessions.delete(userId); + + // Executar adição da ONU + await this.executarAdicaoOnuFiberhome(chatId, session, userId); + + } else if (data === 'fh_vlan_custom') { + // Pedir VLAN customizada - manter sessão no step3 para receber input do usuário + await this.bot.editMessageText( + `🏷️ **VLAN Customizada**\n\n` + + `Digite o número da VLAN que deseja usar (1-4094):`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } + + /** + * Handle callback para confirmar adição de ONU + */ + async handleConfirmAddCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const userId = callbackQuery.from.id; + console.log(`🔍 handleConfirmAddCallback chamado com data: ${data}`); + + // Parse do formato: confirm_add_olt_dito_1-1-3_0_603_DACM915E0F76 + const parts = data.split('_'); + console.log(`🔍 Parts: ${JSON.stringify(parts)}`); + + let oltId, slot, onuId, vlanId, serialNumber; + + if (parts.length >= 8 && parts[2] === 'olt' && parts[3] === 'pedro' && parts[4] === 'afonso') { + // Caso: confirm_add_olt_pedro_afonso_1-1-3_0_603_DACM915E0F76 + oltId = parts[2] + '_' + parts[3] + '_' + parts[4]; // "olt_pedro_afonso" + slot = parts[5].replace(/-/g, '/'); // Converter de volta: "1-1-3" -> "1/1/3" + onuId = parts[6]; + vlanId = parts[7]; + serialNumber = parts.slice(8).join('_'); // Serial pode ter underscores + } else if (parts.length >= 7) { + // Caso padrão: confirm_add_olt_dito_1-1-3_0_603_DACM915E0F76 + oltId = parts[2] + '_' + parts[3]; // "olt_dito" + slot = parts[4].replace(/-/g, '/'); // Converter de volta: "1-1-3" -> "1/1/3" + onuId = parts[5]; + vlanId = parts[6]; + serialNumber = parts.slice(7).join('_'); // Serial pode ter underscores + } else if (parts.length >= 6) { + // Fallback para OLTs com nome simples + oltId = parts[2]; // "olt" + slot = parts[3].replace(/-/g, '/'); // Converter de volta + onuId = parts[4]; + vlanId = parts[5]; + serialNumber = parts.slice(6).join('_'); + } else { + console.error(`❌ Formato de callback inválido: ${data}`); + await this.bot.answerCallbackQuery(callbackQuery.id, { text: 'Erro no formato do callback' }); + return; + } + + console.log(`🔍 Parsed - oltId: ${oltId}, slot: ${slot}, onuId: ${onuId}, vlanId: ${vlanId}, serialNumber: ${serialNumber}`); + + await this.bot.answerCallbackQuery(callbackQuery.id); + + await this.bot.editMessageText( + `➕ **Adicionando ONU...**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `📍 **Localização:** Slot ${slot}, ONU ${onuId}\n` + + `🏷️ **VLAN:** ${vlanId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `⏳ Executando comandos de configuração...\n` + + `⚡ Este processo pode levar até 90 segundos`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + + try { + const result = await this.ssh.addOnu(oltId, slot, onuId, serialNumber, parseInt(vlanId)); + + if (result.success) { + const servicePortInfo = result.servicePortId ? + `🔌 **Service-port:** ${result.servicePortId} (Auto-gerado)` : + `🔌 **Service-port:** Criado automaticamente`; + + await this.bot.editMessageText( + `✅ **ONU Adicionada com Sucesso!**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `📍 **Localização:** Slot ${slot}, ONU ${onuId}\n` + + `🏷️ **VLAN:** ${vlanId}\n` + + `${servicePortInfo}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `✅ **Configurações aplicadas:**\n` + + `• Line-profile: DM986-100\n` + + `• Serial number: ${serialNumber}\n` + + `• Ethernet: porta 1\n` + + `• Native VLAN: ${vlanId}\n` + + `• Service-port: ${result.servicePortId || 'auto-gerado'} criado e configurado\n` + + `• Configuração commitada\n\n` + + `🎉 A ONU está pronta para uso!`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + + // Convidar para registrar no TopSapp + const tsLocation = oltId.includes('pedro_afonso') ? 'pedro_afonso' : 'guarai_colmeia'; + const oltEquipment = this.ssh.equipamentos.get(oltId); + this.userSessions.set(userId, { + action: 'topsapp_onu_pre_busca', + onuData: { + tipo: 'datacom', + location: tsLocation, + oltId, + oltHost: oltEquipment ? oltEquipment.host : '', + serial: serialNumber, + onuId, + servicePort: result.servicePortId ? parseInt(result.servicePortId) : null, + portaAlias: slot + } + }); + await this.bot.sendMessage(chatId, + `📋 *Registrar no TopSapp?*\n\nDeseja vincular esta ONU a um cliente no sistema TopSapp?`, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [[ + { text: '✅ Sim, vincular', callback_data: 'topsapp_onu_sim' }, + { text: '❌ Não', callback_data: 'cancel_action' } + ]] + } + } + ); + + } else { + await this.bot.editMessageText( + `❌ **Erro na Adição**\n\n` + + `📱 **Serial:** ${serialNumber}\n` + + `📍 **Localização:** Slot ${slot}, ONU ${onuId}\n` + + `🏷️ **VLAN:** ${vlanId}\n` + + `🏢 **OLT:** ${oltId.replace('olt_', '').toUpperCase()}\n\n` + + `❌ **Erro:** ${result.message}\n\n` + + `🔄 Verifique a conectividade com a OLT ou tente novamente`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } catch (error) { + console.error('Erro ao adicionar ONU:', error); + await this.bot.editMessageText( + `❌ **Erro Interno**\n\n` + + `Não foi possível adicionar a ONU.\n` + + `Erro: ${error.message}`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown' + } + ); + } + } + + /** + * Painel administrativo + */ + async handleAdmin(chatId) { + const userId = chatId; // Para chats individuais, chatId = userId + + // Verificar se é usuário autorizado + if (!this.isAuthorized(userId)) { + await this.bot.sendMessage(chatId, '🚫 Acesso negado ao painel administrativo.'); + return; + } + + await this.bot.sendMessage(chatId, + `🛡️ **Painel Administrativo**\n\n` + + `👥 **Gestão de Usuários:**\n` + + `• \`usuarios autorizados\` - Ver lista de usuários\n` + + `• \`meu id\` - Ver seu ID do Telegram\n\n` + + `📊 **Sistema:**\n` + + `• \`status\` - Status do bot\n` + + `• \`equipamentos\` - Lista de equipamentos\n` + + `• \`logs\` - Ver logs recentes\n` + + `• \`limpar logs\` - Limpar logs do sistema\n\n` + + `🔧 **Comandos disponíveis:**\n` + + `• Ver sinal, adicionar ONU, remover ONU\n` + + `• Resetar ONU, atualizar firmware\n` + + `• Gerenciar usuários Juniper`, + { parse_mode: 'Markdown' } + ); + } + + /** + * Listar usuários autorizados + */ + async handleListAuthorizedUsers(chatId) { + const userId = chatId; + + if (!this.isAuthorized(userId)) { + await this.bot.sendMessage(chatId, '🚫 Acesso negado.'); + return; + } + + const authorizedUsers = config.bot.allowedUsers; + + if (authorizedUsers.length === 0) { + await this.bot.sendMessage(chatId, + `👥 **Usuários Autorizados**\n\n` + + `⚠️ Nenhuma restrição configurada\n` + + `🌐 Bot está aberto para todos os usuários\n\n` + + `💡 Configure ALLOWED_USERS no .env para restringir` + ); + } else { + const userList = authorizedUsers.map((id, index) => + `${index + 1}. ID: \`${id}\`` + ).join('\n'); + + await this.bot.sendMessage(chatId, + `👥 **Usuários Autorizados**\n\n` + + `📊 Total: ${authorizedUsers.length} usuário(s)\n\n` + + `${userList}\n\n` + + `🆔 Seu ID: \`${userId}\``, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Exibir ID do usuário + */ + async handleMyId(chatId, userId) { + await this.bot.sendMessage(chatId, + `🆔 **Sua Identificação**\n\n` + + `👤 User ID: \`${userId}\`\n` + + `💬 Chat ID: \`${chatId}\`\n\n` + + `📋 Use estes IDs para configurar permissões`, + { parse_mode: 'Markdown' } + ); + } + + /** + * Status do TopSapp + */ + async handleStatusTopSapp(chatId) { + try { + await this.bot.sendMessage(chatId, '🔄 Verificando status do TopSapp...'); + + const status = this.topsapp.getConfigStatus(); + const instances = this.topsapp.getInstances(); + + let message = `🏢 **STATUS TOPSAPP**\n\n`; + + if (status.ready) { + message += `✅ **Configuração**: OK\n`; + message += `📊 **Instâncias configuradas**: ${instances.length}\n\n`; + + instances.forEach(instance => { + message += `🌐 **${instance.location}**\n`; + message += `└ ${instance.host}\n`; + message += `└ Status: ${status.instances.find(i => i.location === instance.location)?.configured ? '✅ Configurado' : '❌ Não configurado'}\n\n`; + }); + + message += `💡 Use os comandos:\n`; + message += `• "buscar cliente João"\n`; + message += `• "ver faturas 123.456.789-10"\n`; + message += `• "gerar boleto fatura123"\n`; + message += `• "bloquear cliente 456"`; + } else { + message += `❌ **Configuração**: Incompleta\n\n`; + message += `⚠️ **Problemas encontrados**:\n`; + + status.instances.forEach(instance => { + if (!instance.configured) { + message += `• ${instance.location}: Credenciais não configuradas\n`; + } + }); + + message += `\n📝 **Configure no arquivo .env**:\n`; + message += `• TOPSAPP_GUARAI_USER\n`; + message += `• TOPSAPP_GUARAI_PASS\n`; + message += `• TOPSAPP_PEDRO_AFONSO_USER\n`; + message += `• TOPSAPP_PEDRO_AFONSO_PASS`; + } + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + + } catch (error) { + console.error('❌ Erro ao verificar status TopSapp:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao verificar status TopSapp**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Buscar cliente no TopSapp + */ + async handleBuscarCliente(chatId, target) { + if (!target) { + await this.bot.sendMessage(chatId, + `🔍 **Buscar Cliente**\n\n` + + `💡 **Como usar:**\n` + + `• "buscar cliente João da Silva"\n` + + `• "buscar cliente 123.456.789-10"\n` + + `• "buscar cliente Guarai João" (específica instância)\n` + + `• "buscar cliente Pedro Afonso Maria"` + ); + return; + } + + try { + await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); + + // Detectar se especificou uma localização + let location = ''; + let query = target; + + if (target.toLowerCase().includes('guarai') || target.toLowerCase().includes('colmeia')) { + location = 'Guarai/Colmeia'; + query = target.replace(/(guarai|colmeia)/gi, '').trim(); + } else if (target.toLowerCase().includes('pedro afonso')) { + location = 'Pedro Afonso'; + query = target.replace(/pedro afonso/gi, '').trim(); + } + + const resultado = await this.topsapp.buscarCliente(query, location); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, + `❌ **Erro na busca**\n\n` + + `⚠️ ${resultado.error}`, + { parse_mode: 'Markdown' } + ); + return; + } + + if (!resultado.clientes || resultado.clientes.length === 0) { + await this.bot.sendMessage(chatId, + `🔍 **Nenhum cliente encontrado**\n\n` + + `📝 Termo buscado: "${query}"\n` + + `🌐 Instância: ${resultado.instancia || 'Todas'}`, + { parse_mode: 'Markdown' } + ); + return; + } + + // Se encontrou mais de um cliente, mostrar lista para seleção + if (resultado.clientes.length > 1) { + let message = `🔍 **${resultado.clientes.length} clientes encontrados**\n\n`; + message += `🌐 **Instância**: ${resultado.instancia}\n\n`; + + const keyboard = []; + + resultado.clientes.slice(0, 10).forEach((cliente, index) => { + message += `👤 **${index + 1}. ${cliente.nome}**\n`; + message += `🆔 ID: \`${cliente.id}\`\n`; + if (cliente.cpf) { + message += `📄 CPF: \`${cliente.cpf}\`\n`; + } + message += `📞 ${cliente.telefone || 'Telefone não informado'}\n`; + message += `📊 Status: ${cliente.status === 1 ? '✅ Ativo' : '❌ Inativo'}\n\n`; + + // Adicionar botões para ações + keyboard.push([ + { text: `📋 Dados ${index + 1}`, callback_data: `dados_cliente_${cliente.id}_${location}` }, + { text: `📄 Faturas ${index + 1}`, callback_data: `faturas_cliente_${cliente.id}_${location}` } + ]); + }); + + if (resultado.clientes.length > 10) { + message += `📋 *Mostrando apenas os primeiros 10 resultados de ${resultado.clientes.length}*\n\n`; + } + + message += `💡 **Selecione uma opção abaixo:**`; + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: keyboard + } + }); + return; + } + + // Se encontrou apenas um cliente, mostrar dados detalhados + // Se encontrou apenas um cliente, mostrar dados detalhados + const cliente = resultado.clientes[0]; + let message = `👤 **Cliente Encontrado**\n\n`; + message += `🆔 **ID**: \`${cliente.id}\`\n`; + message += `👤 **Nome**: ${cliente.nome}\n`; + + if (cliente.cpf) { + message += `📄 **CPF**: \`${cliente.cpf}\`\n`; + } + + message += `📧 **Email**: ${cliente.email || 'Não informado'}\n`; + message += `📞 **Telefone**: ${cliente.telefone || 'Não informado'}\n`; + message += `📍 **Endereço**: ${cliente.endereco || 'Não informado'}\n`; + message += `🏙️ **Cidade**: ${cliente.cidade || 'Não informado'}\n`; + message += `📊 **Status**: ${cliente.status === 1 ? '✅ Ativo' : '❌ Inativo'}\n`; + message += `🌐 **Instância**: ${resultado.instancia}\n\n`; + + // Botões de ação + const keyboard = [ + [ + { text: '📋 Dados Cadastrais', callback_data: `dados_cliente_${cliente.id}_${location}` }, + { text: '📄 Ver Faturas', callback_data: `faturas_cliente_${cliente.id}_${location}` } + ], + [ + { text: '🔒 Bloquear', callback_data: `confirm_block_${cliente.id}_${location}` }, + { text: '🔓 Liberar', callback_data: `confirm_unblock_${cliente.id}_${location}` } + ] + ]; + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: keyboard + } + }); + + } catch (error) { + console.error('❌ Erro ao buscar cliente:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao buscar cliente**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Bloquear cliente + */ + async handleBloquearCliente(chatId, target) { + if (!target) { + await this.bot.sendMessage(chatId, + `🔒 **Bloquear Cliente**\n\n` + + `💡 **Como usar:**\n` + + `• "bloquear cliente João da Silva"\n` + + `• "bloquear cliente 123.456.789-10"\n` + + `• "bloquear Guarai João" (específica instância)` + ); + return; + } + + try { + await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); + + // Detectar se especificou uma localização + let location = ''; + let query = target; + + if (target.toLowerCase().includes('guarai') || target.toLowerCase().includes('colmeia')) { + location = 'Guarai/Colmeia'; + query = target.replace(/(guarai|colmeia)/gi, '').trim(); + } else if (target.toLowerCase().includes('pedro afonso')) { + location = 'Pedro Afonso'; + query = target.replace(/pedro afonso/gi, '').trim(); + } + + const resultado = await this.topsapp.buscarCliente(query, location); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, + `❌ **Erro na busca**\n\n` + + `⚠️ ${resultado.error}`, + { parse_mode: 'Markdown' } + ); + return; + } + + if (!resultado.clientes || resultado.clientes.length === 0) { + await this.bot.sendMessage(chatId, + `🔍 **Nenhum cliente encontrado**\n\n` + + `📝 Termo buscado: "${query}"\n` + + `🌐 Instância: ${resultado.instancia || 'Todas'}`, + { parse_mode: 'Markdown' } + ); + return; + } + + const cliente = resultado.clientes[0]; + + // Obter serviços do cliente para bloquear + const servicosResult = await this.topsapp.obterServicos(cliente.id, location); + + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, + `❌ **Erro ao buscar serviços**\n\n` + + `⚠️ ${servicosResult.error}`, + { parse_mode: 'Markdown' } + ); + return; + } + + const servicos = servicosResult.servicos || servicosResult.data?.dados || servicosResult.data?.servicos || []; + const servicosAtivos = servicos.filter(s => { + // id_status já vem normalizado da API + return s.id_status === 1 || (s.status && s.status.toLowerCase() === 'liberado'); + }); + + if (servicosAtivos.length === 0) { + await this.bot.sendMessage(chatId, + `⚠️ **Nenhum serviço ativo**\n\n` + + `O cliente **${cliente.nome}** não possui serviços ativos para bloquear.`, + { parse_mode: 'Markdown' } + ); + return; + } + + let message = `🔒 **Bloquear Cliente**\n\n`; + message += `👤 **Cliente**: ${cliente.nome}\n`; + message += `🆔 **ID**: ${cliente.id}\n`; + message += `📊 **Serviços ativos**: ${servicosAtivos.length}\n\n`; + message += `⚠️ **Confirma o bloqueio de todos os serviços?**`; + + const keyboard = [ + [ + { text: '✅ Confirmar Bloqueio', callback_data: `confirm_block_all_${cliente.id}_${location}` }, + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ]; + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: keyboard + } + }); + + } catch (error) { + console.error('❌ Erro ao bloquear cliente:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao bloquear cliente**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Liberar cliente + */ + async handleLiberarCliente(chatId, target) { + if (!target) { + await this.bot.sendMessage(chatId, + `🔓 **Liberar Cliente**\n\n` + + `💡 **Como usar:**\n` + + `• "liberar cliente João da Silva"\n` + + `• "liberar cliente 123.456.789-10"\n` + + `• "liberar Guarai João" (específica instância)` + ); + return; + } + + try { + await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); + + // Detectar se especificou uma localização + let location = ''; + let query = target; + + if (target.toLowerCase().includes('guarai') || target.toLowerCase().includes('colmeia')) { + location = 'Guarai/Colmeia'; + query = target.replace(/(guarai|colmeia)/gi, '').trim(); + } else if (target.toLowerCase().includes('pedro afonso')) { + location = 'Pedro Afonso'; + query = target.replace(/pedro afonso/gi, '').trim(); + } + + const resultado = await this.topsapp.buscarCliente(query, location); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, + `❌ **Erro na busca**\n\n` + + `⚠️ ${resultado.error}`, + { parse_mode: 'Markdown' } + ); + return; + } + + if (!resultado.clientes || resultado.clientes.length === 0) { + await this.bot.sendMessage(chatId, + `🔍 **Nenhum cliente encontrado**\n\n` + + `📝 Termo buscado: "${query}"\n` + + `🌐 Instância: ${resultado.instancia || 'Todas'}`, + { parse_mode: 'Markdown' } + ); + return; + } + + const cliente = resultado.clientes[0]; + + // Obter serviços do cliente para liberar + const servicosResult = await this.topsapp.obterServicos(cliente.id, location); + + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, + `❌ **Erro ao buscar serviços**\n\n` + + `⚠️ ${servicosResult.error}`, + { parse_mode: 'Markdown' } + ); + return; + } + + const servicos = servicosResult.servicos || servicosResult.data?.dados || servicosResult.data?.servicos || []; + + // DEBUG: Verificar estrutura dos serviços + console.log('🔍 DEBUG - Total de serviços recebidos:', servicos.length); + if (servicos.length > 0) { + console.log('🔍 DEBUG - Primeiro serviço:', JSON.stringify(servicos[0], null, 2)); + } + + const servicosBloqueados = servicos.filter(s => { + // id_status já vem normalizado da API + const isBloqueado = s.id_status === 2 || (s.status && s.status.toLowerCase() === 'bloqueado'); + console.log(`🔍 DEBUG - Serviço ${s.id_servico || s.id}: id_status=${s.id_status}, status="${s.status}", bloqueado=${isBloqueado}`); + return isBloqueado; + }); + + console.log('🔍 DEBUG - Serviços bloqueados encontrados:', servicosBloqueados.length); + + if (servicosBloqueados.length === 0) { + await this.bot.sendMessage(chatId, + `⚠️ **Nenhum serviço bloqueado**\n\n` + + `O cliente **${cliente.nome}** não possui serviços bloqueados para liberar.`, + { parse_mode: 'Markdown' } + ); + return; + } + + let message = `🔓 **Liberar Cliente**\n\n`; + message += `👤 **Cliente**: ${cliente.nome}\n`; + message += `🆔 **ID**: ${cliente.id}\n`; + message += `📊 **Serviços bloqueados**: ${servicosBloqueados.length}\n\n`; + message += `⚠️ **Confirma a liberação de todos os serviços?**`; + + const keyboard = [ + [ + { text: '✅ Confirmar Liberação', callback_data: `confirm_unblock_all_${cliente.id}_${location}` }, + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ]; + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: keyboard + } + }); + + } catch (error) { + console.error('❌ Erro ao liberar cliente:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao liberar cliente**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Listar serviços do cliente + */ + async handleListarServicos(chatId, target) { + if (!target) { + await this.bot.sendMessage(chatId, + `📋 **Listar Serviços**\n\n` + + `💡 **Como usar:**\n` + + `• "listar serviços João da Silva"\n` + + `• "serviços do cliente 123.456.789-10"\n` + + `• "serviços Guarai João" (específica instância)` + ); + return; + } + + try { + await this.bot.sendMessage(chatId, '🔍 Buscando serviços...'); + + // Detectar se especificou uma localização + let location = ''; + let query = target; + + if (target.toLowerCase().includes('guarai') || target.toLowerCase().includes('colmeia')) { + location = 'Guarai/Colmeia'; + query = target.replace(/(guarai|colmeia)/gi, '').trim(); + } else if (target.toLowerCase().includes('pedro afonso')) { + location = 'Pedro Afonso'; + query = target.replace(/pedro afonso/gi, '').trim(); + } + + const resultado = await this.topsapp.buscarCliente(query, location); + + if (!resultado.success || !resultado.clientes || resultado.clientes.length === 0) { + await this.bot.sendMessage(chatId, + `🔍 **Nenhum cliente encontrado**\n\n` + + `📝 Termo buscado: "${query}"`, + { parse_mode: 'Markdown' } + ); + return; + } + + const cliente = resultado.clientes[0]; + + // Obter serviços do cliente + const servicosResult = await this.topsapp.obterServicos(cliente.id, location); + + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, + `❌ **Erro ao buscar serviços**\n\n` + + `⚠️ ${servicosResult.error}`, + { parse_mode: 'Markdown' } + ); + return; + } + + const servicos = servicosResult.servicos || servicosResult.data?.dados || servicosResult.data?.servicos || []; + + if (servicos.length === 0) { + await this.bot.sendMessage(chatId, + `📋 **Nenhum serviço encontrado**\n\n` + + `O cliente **${cliente.nome}** não possui serviços cadastrados.`, + { parse_mode: 'Markdown' } + ); + return; + } + + // Separar serviços por status (id_status já vem normalizado da API) + const servicosAtivos = servicos.filter(s => { + return s.id_status === 1 || (s.status && s.status.toLowerCase() === 'liberado'); + }); + + const servicosBloqueados = servicos.filter(s => { + return s.id_status === 2 || (s.status && s.status.toLowerCase() === 'bloqueado'); + }); + + const servicosDesativados = servicos.filter(s => { + return s.id_status === 3 || (s.status && s.status.toLowerCase() === 'desativado'); + }); + + let message = `📋 **SERVIÇOS DO CLIENTE**\n\n`; + message += `👤 **Cliente**: ${cliente.nome}\n`; + message += `🆔 **ID**: ${cliente.id}\n\n`; + + // Exibir serviços ativos + if (servicosAtivos.length > 0) { + message += `✅ **SERVIÇOS ATIVOS** (${servicosAtivos.length})\n\n`; + servicosAtivos.forEach((servico, index) => { + message += `${index + 1}. 🟢 **${servico.descricao || servico.plano || 'Serviço'}**\n`; + message += ` 🆔 ID: \`${servico.id_servico || servico.id}\`\n`; + + if (servico.usuario) { + message += ` 👤 Login: \`${servico.usuario}\`\n`; + } + + if (servico.servico_valor || servico.valor) { + const valor = parseFloat(servico.servico_valor || servico.valor); + message += ` 💰 Valor: R$ ${valor.toFixed(2)}\n`; + } + + if (servico.vencimento) { + message += ` 📅 Vencimento: dia ${servico.vencimento}\n`; + } + + message += `\n`; + }); + } + + // Exibir serviços bloqueados + if (servicosBloqueados.length > 0) { + message += `\n🔒 **SERVIÇOS BLOQUEADOS** (${servicosBloqueados.length})\n\n`; + servicosBloqueados.forEach((servico, index) => { + message += `${index + 1}. 🔴 **${servico.descricao || servico.plano || 'Serviço'}**\n`; + message += ` 🆔 ID: \`${servico.id_servico || servico.id}\`\n`; + + if (servico.usuario) { + message += ` 👤 Login: \`${servico.usuario}\`\n`; + } + + if (servico.servico_valor || servico.valor) { + const valor = parseFloat(servico.servico_valor || servico.valor); + message += ` 💰 Valor: R$ ${valor.toFixed(2)}\n`; + } + + if (servico.data_bloqueio && servico.data_bloqueio !== '0000-00-00') { + message += ` 🕒 Bloqueado em: ${servico.data_bloqueio}\n`; + } + + message += `\n`; + }); + } + + // Exibir serviços desativados + if (servicosDesativados.length > 0) { + message += `\n❌ **SERVIÇOS DESATIVADOS** (${servicosDesativados.length})\n\n`; + servicosDesativados.forEach((servico, index) => { + message += `${index + 1}. ⚫ **${servico.descricao || servico.plano || 'Serviço'}**\n`; + message += ` 🆔 ID: \`${servico.id_servico || servico.id}\`\n`; + + if (servico.data_desativacao && servico.data_desativacao !== '0000-00-00') { + message += ` 🕒 Desativado em: ${servico.data_desativacao}\n`; + } + + message += `\n`; + }); + } + + // Botões de ação + const keyboard = []; + + if (servicosAtivos.length > 0) { + keyboard.push([ + { text: '🔒 Bloquear Todos', callback_data: `confirm_block_all_${cliente.id}_${location}` } + ]); + } + + if (servicosBloqueados.length > 0) { + keyboard.push([ + { text: '🔓 Liberar Todos', callback_data: `confirm_unblock_all_${cliente.id}_${location}` } + ]); + } + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: keyboard.length > 0 ? { + inline_keyboard: keyboard + } : undefined + }); + + } catch (error) { + console.error('❌ Erro ao listar serviços:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao listar serviços**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Ver faturas do cliente + */ + async handleVerFaturas(chatId, targetOrId, location = '', idServicoFilter = null) { + try { + let idCliente = targetOrId; + + // Se não for um número, buscar cliente primeiro + if (isNaN(targetOrId)) { + await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); + + const resultado = await this.topsapp.buscarCliente(targetOrId, location); + + if (!resultado.success || !resultado.clientes || resultado.clientes.length === 0) { + await this.bot.sendMessage(chatId, + `🔍 **Nenhum cliente encontrado**\n\n` + + `📝 Termo buscado: "${targetOrId}"`, + { parse_mode: 'Markdown' } + ); + return; + } + + idCliente = resultado.clientes[0].id; + location = location || resultado.instancia; + } + + await this.bot.sendMessage(chatId, '📄 Buscando faturas...'); + + const resultado = await this.topsapp.obterFaturasDetalhadas(idCliente, location); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, + `❌ **Erro ao buscar faturas**\n\n` + + `⚠️ ${resultado.error}`, + { parse_mode: 'Markdown' } + ); + return; + } + + let faturasPorServico = resultado.faturasPorServico || []; + + if (faturasPorServico.length === 0) { + await this.bot.sendMessage(chatId, + `📄 **Nenhuma fatura encontrada**\n\n` + + `✅ O cliente não possui faturas cadastradas.`, + { parse_mode: 'Markdown' } + ); + return; + } + + // Se há mais de um serviço com faturas e nenhum filtro foi aplicado, + // perguntar qual serviço o usuário quer ver + if (faturasPorServico.length > 1 && !idServicoFilter) { + let message = `📄 **SELECIONE O SERVIÇO**\n\n`; + message += `🆔 **ID Cliente**: ${idCliente}\n`; + message += `📊 Este cliente possui **${faturasPorServico.length} serviços** com faturas.\n\n`; + + faturasPorServico.forEach((servicoInfo, index) => { + const s = servicoInfo.servico; + const rawStatus = String(s.status || '').toLowerCase(); + let statusText; + if (rawStatus === 'liberado' || rawStatus === '1' || parseInt(s.status) === 1) { + statusText = '🟢 Liberado'; + } else if (rawStatus === 'bloqueado' || rawStatus === '2' || parseInt(s.status) === 2) { + statusText = '🔴 Bloqueado'; + } else if (rawStatus === 'desativado' || rawStatus === '3' || parseInt(s.status) === 3) { + statusText = '⚫ Desativado'; + } else { + statusText = `❓ ${s.status || 'Desconhecido'}`; + } + + message += `*${index + 1}. ${s.nome || 'Serviço'}*\n`; + if (s.login) message += ` 👤 Login PPPoE: \`${s.login}\`\n`; + message += ` 📊 Status: ${statusText}\n`; + if (s.endereco) message += ` 📍 Endereço: ${s.endereco}\n`; + message += ` 🧾 ${servicoInfo.faturas.length} fatura(s)\n\n`; + }); + + message += `Escolha qual serviço deseja ver as faturas:`; + + const keyboard = faturasPorServico.map((servicoInfo, index) => [{ + text: `${index + 1}. ${servicoInfo.servico.login || servicoInfo.servico.nome || 'Serviço'} — ${servicoInfo.faturas.length} fatura(s)`, + callback_data: `faturas_servico_${idCliente}_${servicoInfo.servico.id}_${location}` + }]); + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + return; + } + + // Filtrar pelo serviço selecionado, se houver + if (idServicoFilter) { + faturasPorServico = faturasPorServico.filter( + s => String(s.servico.id) === String(idServicoFilter) + ); + } + + // Contar total de faturas + let totalFaturas = 0; + faturasPorServico.forEach(s => { + totalFaturas += s.faturas.length; + }); + + let message = `📄 **FATURAS DO CLIENTE**\n\n`; + message += `🆔 **ID Cliente**: ${idCliente}\n`; + message += `📊 **Total de faturas**: ${totalFaturas}\n\n`; + + faturasPorServico.forEach((servicoInfo, sIndex) => { + if (servicoInfo.faturas.length > 0) { + message += `📋 **Serviço**: ${servicoInfo.servico.nome || servicoInfo.servico.descricao || 'Serviço'}\n`; + message += `🆔 **ID**: ${servicoInfo.servico.id}\n`; + if (servicoInfo.servico.login) { + message += `👤 **Login**: ${servicoInfo.servico.login}\n`; + } + if (servicoInfo.servico.endereco) { + message += `📍 **Endereço**: ${servicoInfo.servico.endereco}\n`; + } + message += `\n`; + + servicoInfo.faturas.forEach((fatura, fIndex) => { + const isPago = fatura.status === 'Pago'; + const statusEmoji = isPago ? '✅' : '🔴'; + + message += `${fIndex + 1}. ${statusEmoji} **Fatura #${fatura.id}**\n`; + + if (fatura.valor) { + const valor = parseFloat(fatura.valor); + message += ` 💰 Valor: R$ ${valor.toFixed(2)}\n`; + } + + if (fatura.vencimento) { + message += ` 📅 Vencimento: ${fatura.vencimento}\n`; + } + + message += ` 📊 Status: ${fatura.status}\n`; + + if (fatura.descricao) { + message += ` 📝 Descrição: ${fatura.descricao}\n`; + } + + if (fatura.periodo) { + message += ` 📆 Período: ${fatura.periodo}\n`; + } + + message += `\n`; + }); + + message += `\n`; + } + }); + + // Criar botões para cada fatura com linha digitável/PIX + // Inclui a data de vencimento para melhor identificação + const keyboard = []; + + faturasPorServico.forEach((servicoInfo) => { + servicoInfo.faturas.forEach((fatura) => { + if (fatura.status !== 'Pago') { + const vencLabel = fatura.vencimento ? ` (${fatura.vencimento})` : ''; + const row = []; + + if (fatura.linhaDigitavel) { + row.push({ + text: `📋 Boleto #${fatura.id}${vencLabel}`, + callback_data: `linha_${fatura.id}_${location}` + }); + } + + if (fatura.pixCopiaCola) { + row.push({ + text: `💳 PIX #${fatura.id}${vencLabel}`, + callback_data: `pix_${fatura.id}_${location}` + }); + } + + if (row.length > 0) { + keyboard.push(row); + } + } + }); + }); + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: keyboard.length > 0 ? { + inline_keyboard: keyboard + } : undefined + }); + + } catch (error) { + console.error('❌ Erro ao buscar faturas:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao buscar faturas**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Gerar boleto + */ + async handleGerarBoleto(chatId, target) { + if (!target) { + await this.bot.sendMessage(chatId, + `💸 **Gerar Boleto**\n\n` + + `💡 **Como usar:**\n` + + `• "gerar boleto João da Silva"\n` + + `• "boleto do cliente 123.456.789-10"` + ); + return; + } + + try { + await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); + + let location = ''; + let query = target; + + if (target.toLowerCase().includes('guarai') || target.toLowerCase().includes('colmeia')) { + location = 'Guarai/Colmeia'; + query = target.replace(/(guarai|colmeia)/gi, '').trim(); + } else if (target.toLowerCase().includes('pedro afonso')) { + location = 'Pedro Afonso'; + query = target.replace(/pedro afonso/gi, '').trim(); + } + + const resultado = await this.topsapp.buscarCliente(query, location); + + if (!resultado.success || !resultado.clientes || resultado.clientes.length === 0) { + await this.bot.sendMessage(chatId, + `🔍 **Nenhum cliente encontrado**\n\n` + + `📝 Termo buscado: "${query}"`, + { parse_mode: 'Markdown' } + ); + return; + } + + const cliente = resultado.clientes[0]; + + await this.handleVerFaturas(chatId, cliente.id, location); + + } catch (error) { + console.error('❌ Erro ao gerar boleto:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao gerar boleto**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Callback para confirmar bloqueio de todos os serviços + */ + async handleConfirmBlockAllCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('confirm_block_all_', '').split('_'); + const idCliente = parts[0]; + const location = parts.slice(1).join('_'); + + try { + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.editMessageText('⏳ Bloqueando serviços...', { + chat_id: chatId, + message_id: callbackQuery.message.message_id + }); + + // Obter serviços do cliente + const servicosResult = await this.topsapp.obterServicos(idCliente, location); + + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, + `❌ **Erro ao buscar serviços**\n\n` + + `⚠️ ${servicosResult.error}`, + { parse_mode: 'Markdown' } + ); + return; + } + + const servicos = servicosResult.servicos || servicosResult.data?.dados || servicosResult.data?.servicos || []; + const servicosAtivos = servicos.filter(s => { + // id_status já vem normalizado da API + return s.id_status === 1 || (s.status && s.status.toLowerCase() === 'liberado'); + }); + + let sucesso = 0; + let falhas = 0; + + for (const servico of servicosAtivos) { + const resultado = await this.topsapp.bloquearServico(servico.id_servico || servico.id, idCliente, location); + if (resultado.success) { + sucesso++; + } else { + falhas++; + } + } + + let message = `🔒 **Bloqueio Concluído**\n\n`; + message += `✅ **Bloqueados**: ${sucesso}\n`; + if (falhas > 0) { + message += `❌ **Falhas**: ${falhas}\n`; + } + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + + } catch (error) { + console.error('❌ Erro ao bloquear serviços:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao bloquear serviços**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Callback para confirmar liberação de todos os serviços + */ + async handleConfirmUnblockAllCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('confirm_unblock_all_', '').split('_'); + const idCliente = parts[0]; + const location = parts.slice(1).join('_'); + + try { + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.editMessageText('⏳ Liberando serviços...', { + chat_id: chatId, + message_id: callbackQuery.message.message_id + }); + + // Obter serviços do cliente + const servicosResult = await this.topsapp.obterServicos(idCliente, location); + + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, + `❌ **Erro ao buscar serviços**\n\n` + + `⚠️ ${servicosResult.error}`, + { parse_mode: 'Markdown' } + ); + return; + } + + const servicos = servicosResult.servicos || servicosResult.data?.dados || servicosResult.data?.servicos || []; + const servicosBloqueados = servicos.filter(s => { + // id_status já vem normalizado da API + return s.id_status === 2 || (s.status && s.status.toLowerCase() === 'bloqueado'); + }); + + let sucesso = 0; + let falhas = 0; + + for (const servico of servicosBloqueados) { + const resultado = await this.topsapp.liberarServico(servico.id_servico || servico.id, idCliente, location); + if (resultado.success) { + sucesso++; + } else { + falhas++; + } + } + + let message = `🔓 **Liberação Concluída**\n\n`; + message += `✅ **Liberados**: ${sucesso}\n`; + if (falhas > 0) { + message += `❌ **Falhas**: ${falhas}\n`; + } + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + + } catch (error) { + console.error('❌ Erro ao liberar serviços:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao liberar serviços**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Callback para confirmar bloqueio de cliente individual + */ + async handleConfirmBlockCallback(callbackQuery, data) { + await this.handleConfirmBlockAllCallback(callbackQuery, data.replace('confirm_block_', 'confirm_block_all_')); + } + + /** + * Callback para confirmar liberação de cliente individual + */ + async handleConfirmUnblockCallback(callbackQuery, data) { + await this.handleConfirmUnblockAllCallback(callbackQuery, data.replace('confirm_unblock_', 'confirm_unblock_all_')); + } + + /** + * Callback para confirmar bloqueio de serviço individual + */ + async handleConfirmBlockServiceCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + // Formato: confirm_block_service_idServico_idCliente_location + const parts = data.replace('confirm_block_service_', '').split('_'); + const idServico = parts[0]; + const idCliente = parts[1]; + const location = parts.slice(2).join('_'); + + try { + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.editMessageText('⏳ Bloqueando serviço...', { + chat_id: chatId, + message_id: callbackQuery.message.message_id + }); + + const resultado = await this.topsapp.bloquearServico(idServico, idCliente, location); + + if (resultado.success) { + await this.bot.sendMessage(chatId, + `✅ **Serviço bloqueado com sucesso!**\n\n` + + `🆔 **ID do Serviço**: ${idServico}`, + { parse_mode: 'Markdown' } + ); + } else { + await this.bot.sendMessage(chatId, + `❌ **Erro ao bloquear serviço**\n\n` + + `⚠️ ${resultado.error}`, + { parse_mode: 'Markdown' } + ); + } + + } catch (error) { + console.error('❌ Erro ao bloquear serviço:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao bloquear serviço**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Callback para confirmar liberação de serviço individual + */ + async handleConfirmUnblockServiceCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + // Formato: confirm_unblock_service_idServico_idCliente_location + const parts = data.replace('confirm_unblock_service_', '').split('_'); + const idServico = parts[0]; + const idCliente = parts[1]; + const location = parts.slice(2).join('_'); + + try { + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.editMessageText('⏳ Liberando serviço...', { + chat_id: chatId, + message_id: callbackQuery.message.message_id + }); + + const resultado = await this.topsapp.liberarServico(idServico, idCliente, location); + + if (resultado.success) { + await this.bot.sendMessage(chatId, + `✅ **Serviço liberado com sucesso!**\n\n` + + `🆔 **ID do Serviço**: ${idServico}`, + { parse_mode: 'Markdown' } + ); + } else { + await this.bot.sendMessage(chatId, + `❌ **Erro ao liberar serviço**\n\n` + + `⚠️ ${resultado.error}`, + { parse_mode: 'Markdown' } + ); + } + + } catch (error) { + console.error('❌ Erro ao liberar serviço:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao liberar serviço**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Callback para dados do cliente + */ + async handleDadosClienteCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('dados_cliente_', '').split('_'); + const idCliente = parts[0]; + const location = parts.slice(1).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.handleDadosCliente(chatId, idCliente, location); + } + + /** + * Callback para faturas do cliente + */ + async handleFaturasClienteCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('faturas_cliente_', '').split('_'); + const idCliente = parts[0]; + const location = parts.slice(1).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.handleVerFaturas(chatId, idCliente, location); + } + + /** + * Callback para selecionar serviço específico ao ver faturas + */ + async handleFaturasServicoCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('faturas_servico_', '').split('_'); + const idCliente = parts[0]; + const idServico = parts[1]; + const location = parts.slice(2).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.handleVerFaturas(chatId, idCliente, location, idServico); + } + + /** + * Callback para serviços do cliente + */ + async handleServicosClienteCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('servicos_cliente_', '').split('_'); + const idCliente = parts[0]; + const location = parts.slice(1).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.handleListarServicos(chatId, idCliente); + } + + /** + * Callback para exibir linha digitável + */ + async handleLinhaDigitavelCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('linha_', '').split('_'); + const idFatura = parts[0]; + const location = parts.slice(1).join('_'); + + try { + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.sendMessage(chatId, '⏳ Buscando linha digitável...'); + + // Buscar dados da fatura + const resultado = await this.topsapp.obterDadosFatura(idFatura, location); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, + `❌ **Erro ao buscar linha digitável**\n\n` + + `⚠️ ${resultado.error}`, + { parse_mode: 'Markdown' } + ); + return; + } + + const dadosFatura = resultado.data?.fatura || resultado.data?.dados || resultado.data; + + if (!dadosFatura || !dadosFatura.linha_digitavel) { + await this.bot.sendMessage(chatId, + `⚠️ **Linha digitável não disponível**\n\n` + + `A fatura #${idFatura} não possui linha digitável.`, + { parse_mode: 'Markdown' } + ); + return; + } + + let message = `📋 **LINHA DIGITÁVEL**\n\n`; + message += `🆔 **Fatura**: #${idFatura}\n`; + message += `💰 **Valor**: R$ ${parseFloat(dadosFatura.valor || dadosFatura.valor_atualizado || 0).toFixed(2)}\n`; + message += `📅 **Vencimento**: ${dadosFatura.vencimento || dadosFatura.vencimento_atualizado || 'N/A'}\n\n`; + message += `📋 **Linha Digitável**:\n\`${dadosFatura.linha_digitavel}\`\n\n`; + message += `💡 _Copie a linha digitável acima para pagar pelo app do banco_`; + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + + } catch (error) { + console.error('❌ Erro ao buscar linha digitável:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao buscar linha digitável**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Callback para exibir PIX copia e cola + */ + async handlePixCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('pix_', '').split('_'); + const idFatura = parts[0]; + const location = parts.slice(1).join('_'); + + try { + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.sendMessage(chatId, '⏳ Buscando código PIX...'); + + // Buscar dados da fatura + const resultado = await this.topsapp.obterDadosFatura(idFatura, location); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, + `❌ **Erro ao buscar código PIX**\n\n` + + `⚠️ ${resultado.error}`, + { parse_mode: 'Markdown' } + ); + return; + } + + const dadosFatura = resultado.data?.fatura || resultado.data?.dados || resultado.data; + + const pixCode = dadosFatura?.qrcode || dadosFatura?.pix_copia_cola || dadosFatura?.codigo_pix; + + if (!dadosFatura || !pixCode) { + await this.bot.sendMessage(chatId, + `⚠️ **PIX não disponível**\n\n` + + `A fatura #${idFatura} não possui código PIX.`, + { parse_mode: 'Markdown' } + ); + return; + } + + let message = `💳 **PIX COPIA E COLA**\n\n`; + message += `🆔 **Fatura**: #${idFatura}\n`; + message += `💰 **Valor**: R$ ${parseFloat(dadosFatura.valor || dadosFatura.valor_atualizado || 0).toFixed(2)}\n`; + message += `📅 **Vencimento**: ${dadosFatura.vencimento || dadosFatura.vencimento_atualizado || 'N/A'}\n\n`; + message += `💳 **PIX Copia e Cola**:\n\`${pixCode}\`\n\n`; + message += `💡 _Copie o código acima e cole no app do banco na opção PIX Copia e Cola_`; + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + + } catch (error) { + console.error('❌ Erro ao buscar código PIX:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao buscar código PIX**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Escapar caracteres problemáticos para Markdown do Telegram + */ + escapeMarkdown(text) { + if (!text) return text; + + // Escapar caracteres especiais que podem causar problemas no Markdown + return text.toString() + .replace(/\\/g, '\\\\') // Escape backslash + .replace(/\*/g, '\\*') // Escape asterisco + .replace(/_/g, '\\_') // Escape underscore + .replace(/\[/g, '\\[') // Escape colchete + .replace(/\]/g, '\\]') // Escape colchete + .replace(/\(/g, '\\(') // Escape parêntese + .replace(/\)/g, '\\)') // Escape parêntese + .replace(/~/g, '\\~') // Escape til + .replace(/>/g, '\\>') // Escape maior que + .replace(/#/g, '\\#') // Escape hashtag + .replace(/\+/g, '\\+') // Escape mais + .replace(/-/g, '\\-') // Escape hífen + .replace(/=/g, '\\=') // Escape igual + .replace(/\|/g, '\\|') // Escape pipe + .replace(/\{/g, '\\{') // Escape chaves + .replace(/\}/g, '\\}') // Escape chaves + .replace(/\./g, '\\.') // Escape ponto + .replace(/!/g, '\\!') // Escape exclamação + .replace(/@/g, '\\@'); // Escape arroba + } + + /** + * Mostrar dados cadastrais completos do cliente + */ + async handleDadosCliente(chatId, idCliente, location = '') { + try { + await this.bot.sendMessage(chatId, '📋 Carregando dados cadastrais...'); + + const resultado = await this.topsapp.obterDadosCadastrais(idCliente, location); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, + `❌ Erro ao buscar dados\n\n` + + `⚠️ ${resultado.error}` + ); + return; + } + + const cliente = resultado.cliente; + const servicos = resultado.servicos || []; + + let message = `📋 Dados Cadastrais Completos\n\n`; + + // Dados do cliente + message += `👤 CLIENTE\n`; + message += `🆔 ID: ${cliente.id}\n`; + message += `👤 Nome: ${cliente.nome || 'Não informado'}\n`; + + if (cliente.cpf) { + message += `📄 CPF: \`${cliente.cpf}\`\n`; + } + + if (cliente.rg) { + message += `📄 RG: ${cliente.rg}\n`; + } + + message += `📧 Email: ${cliente.email || 'Não informado'}\n`; + + if (cliente.telefone && cliente.telefone !== 'Não informado') { + message += `📞 Telefone: \`${cliente.telefone}\`\n`; + } else { + message += `📞 Telefone: Não informado\n`; + } + + if (cliente.celular && cliente.celular !== 'Não informado') { + message += `📱 Celular: \`${cliente.celular}\`\n`; + } else { + message += `📱 Celular: Não informado\n`; + } + + // Endereço + if (cliente.endereco) { + message += `📍 Endereço: ${cliente.endereco}`; + if (cliente.numero) { + message += `, ${cliente.numero}`; + } + message += `\n`; + } + + if (cliente.complemento) { + message += `📝 Complemento: ${cliente.complemento}\n`; + } + + message += `🏘️ Bairro: ${cliente.bairro || 'Não informado'}\n`; + message += `🏙️ Cidade: ${cliente.cidade || 'Não informado'}`; + + if (cliente.uf) { + message += ` - ${cliente.uf}`; + } + message += `\n`; + + message += `📮 CEP: ${cliente.cep || 'Não informado'}\n`; + message += `📊 Status Geral: ${cliente.status === 1 ? '✅ Ativo' : '❌ Inativo'}\n`; + + if (cliente.data_cadastro) { + message += `📅 Cadastro: ${this.formatarData(cliente.data_cadastro)}\n`; + } + + if (cliente.observacoes) { + message += `📝 Observações: ${cliente.observacoes}\n`; + } + + message += `\n🌐 Instância: ${resultado.instancia}\n\n`; + + // Dados dos serviços + if (servicos.length > 0) { + message += `⚙️ **SERVIÇOS CONTRATADOS**\n\n`; + + servicos.forEach((servico, index) => { + message += `📦 Serviço ${index + 1}\n`; + message += `🏷️ Plano: ${servico.planoNome}\n`; + message += `📊 Status: ${servico.statusFormatado}\n`; + + if (servico.login) { + message += `👤 Login: ${servico.login}\n`; + } + + if (servico.senha) { + message += `🔑 Senha: ${servico.senha}\n`; + } + + if (servico.ip) { + message += `🌐 IP: ${servico.ip}\n`; + } + + if (servico.vencimento) { + message += `📅 Vencimento: Dia ${servico.vencimento}\n`; + } + + if (servico.valor) { + message += `💰 Valor: R$ ${parseFloat(servico.valor).toFixed(2)}\n`; + } + + message += `🔧 Tecnologia: ${servico.tecnologia}\n`; + + if (servico.velocidade_down) { + message += `⬇️ Download: ${servico.velocidade_down} Mbps\n`; + } + + if (servico.velocidade_up) { + message += `⬆️ Upload: ${servico.velocidade_up} Mbps\n`; + } + + // Dados FTTH - usando campos reais da API + if (servico.tecnologia === 'FTTH' || servico.pon || servico.serial) { + message += `\n📡 Dados FTTH:\n`; + + if (servico.base || servico.id_ap) { + message += `🔌 Base/AP: ${servico.base || `AP ${servico.id_ap}`}\n`; + } + + if (servico.pon) { + message += `🔗 PON: ${servico.pon}\n`; + } + + if (servico.perfil) { + message += `⚙️ Perfil: ${servico.perfil}\n`; + } + + if (servico.serial) { + message += `📟 Serial ONU: \`${servico.serial}\`\n`; + } + + if (servico.serviceport) { + message += `🔌 Service Port: ${servico.serviceport}\n`; + } + + if (servico.ultimo_rx_power) { + message += `📶 Último RX: ${servico.ultimo_rx_power} dBm\n`; + } + + if (servico.ultimo_tx_power) { + message += `📡 Último TX: ${servico.ultimo_tx_power} dBm\n`; + } + + if (servico.data_ultimo_sinal) { + message += `📅 Último sinal: ${this.formatarData(servico.data_ultimo_sinal)} ${servico.hora_ultimo_sinal || ''}\n`; + } + + if (servico.conexao) { + const statusConexao = servico.conexao === 'online' ? '🟢 Online' : '🔴 Offline'; + message += `🔌 Conexão: ${statusConexao}\n`; + } + + if (servico.ultimo_acesso) { + message += `🕐 Último acesso: ${servico.ultimo_acesso}\n`; + } + + if (servico.ultimo_ip) { + message += `🌐 Último IP: ${servico.ultimo_ip}\n`; + } + } + + // Dados DHCP / IPoE + if (servico.dhcp_username || servico.dhcp_agent_circuit_id) { + message += `\n🔗 Dados IPoE (DHCP Option 82):\n`; + + if (servico.dhcp_username) { + message += `👤 DHCP Username: \`${servico.dhcp_username}\`\n`; + } + + if (servico.dhcp_agent_circuit_id) { + message += `🔌 Agent Circuit ID: \`${servico.dhcp_agent_circuit_id}\`\n`; + } + } + + // Dados de instalação + if (servico.instalacao_endereco) { + message += `\n📍 Endereço de Instalação:\n`; + message += `🏠 ${servico.instalacao_endereco}, ${servico.instalacao_numero || 'S/N'}\n`; + + if (servico.instalacao_complemento) { + message += `📝 Complemento: ${servico.instalacao_complemento}\n`; + } + + message += `🏘️ ${servico.instalacao_bairro}, ${servico.instalacao_cidade}/${servico.instalacao_uf}\n`; + + if (servico.instalacao_cep) { + message += `📮 CEP: ${servico.instalacao_cep}\n`; + } + + if (servico.latitude && servico.longitude) { + message += `🗺️ Coordenadas: ${servico.latitude}, ${servico.longitude}\n`; + } + } + + message += `\n`; + }); + } else { + message += `⚙️ SERVIÇOS: Nenhum serviço encontrado\n\n`; + } + + // Botões de ação + const keyboard = [ + [ + { text: '📄 Ver Faturas', callback_data: `faturas_cliente_${idCliente}_${location}` }, + { text: '⚙️ Listar Serviços', callback_data: `servicos_cliente_${idCliente}_${location}` } + ], + [ + { text: '🔒 Bloquear Cliente', callback_data: `confirm_block_${idCliente}_${location}` }, + { text: '🔓 Liberar Cliente', callback_data: `confirm_unblock_${idCliente}_${location}` } + ] + ]; + + // Adicionar botão para gerar PDF da primeira fatura (se houver) + if (resultado.faturasPorServico && + resultado.faturasPorServico.length > 0 && + resultado.faturasPorServico[0].faturas && + resultado.faturasPorServico[0].faturas.length > 0) { + const primeiraFatura = resultado.faturasPorServico[0].faturas[0]; + keyboard.push([ + { text: '📄 Gerar Boleto PDF', callback_data: `boleto_pdf_${primeiraFatura.id}_${location}` } + ]); + } + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: keyboard + } + }); + + } catch (error) { + console.error('❌ Erro ao buscar faturas detalhadas:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao buscar faturas**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Buscar e mostrar dados cadastrais do cliente + */ + async handleDadosClientePorBusca(chatId, target) { + if (!target) { + await this.bot.sendMessage(chatId, + `📋 **Dados Cadastrais**\n\n` + + `💡 **Como usar:**\n` + + `• "dados cliente João da Silva"\n` + + `• "dados cadastrais 123.456.789-10"\n` + + `• "dados cliente 12345" (ID do cliente)\n` + + `• "dados cliente Guarai João" (específica instância)` + ); + return; + } + + try { + // Primeiro, buscar o cliente + let location = ''; + let query = target; + + if (target.toLowerCase().includes('guarai') || target.toLowerCase().includes('colmeia')) { + location = 'Guarai/Colmeia'; + query = target.replace(/(guarai|colmeia)/gi, '').trim(); + } else if (target.toLowerCase().includes('pedro afonso')) { + location = 'Pedro Afonso'; + query = target.replace(/pedro afonso/gi, '').trim(); + } + + const busca = await this.topsapp.buscarCliente(query, location); + + if (!busca.success) { + await this.bot.sendMessage(chatId, + `❌ **Erro na busca**\n\n` + + `⚠️ ${busca.error}`, + { parse_mode: 'Markdown' } + ); + return; + } + + if (!busca.clientes || busca.clientes.length === 0) { + await this.bot.sendMessage(chatId, + `🔍 **Nenhum cliente encontrado**\n\n` + + `📝 Termo buscado: "${query}"\n` + + `🌐 Instância: ${busca.instancia || 'Todas'}`, + { parse_mode: 'Markdown' } + ); + return; + } + + // Se encontrou mais de um, mostrar lista + if (busca.clientes.length > 1) { + let message = `📋 **${busca.clientes.length} clientes encontrados**\n\n`; + message += `🌐 **Instância**: ${busca.instancia}\n\n`; + message += `💡 **Selecione um cliente para ver os dados cadastrais:**\n\n`; + + const keyboard = []; + + busca.clientes.slice(0, 10).forEach((cliente, index) => { + message += `👤 **${index + 1}. ${cliente.nome}**\n`; + message += `🆔 ID: \`${cliente.id}\`\n`; + if (cliente.cpf) { + message += `📄 CPF: \`${cliente.cpf}\`\n`; + } + message += `📊 Status: ${cliente.status === 1 ? '✅ Ativo' : '❌ Inativo'}\n\n`; + + keyboard.push([ + { text: `📋 Ver Dados ${index + 1}`, callback_data: `dados_cliente_${cliente.id}_${location}` } + ]); + }); + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: keyboard + } + }); + return; + } + + // Se encontrou apenas um, mostrar dados diretamente + const cliente = busca.clientes[0]; + await this.handleDadosCliente(chatId, cliente.id, location || busca.instancia); + + } catch (error) { + console.error('❌ Erro ao buscar dados do cliente:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro ao buscar dados do cliente**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Formatardata para DD/MM/YYYY + */ + formatarData(data) { + if (!data) return 'Data não informada'; + + // Se já está no formato DD/MM/YYYY, retornar como está + if (data.match(/^\d{2}\/\d{2}\/\d{4}$/)) { + return data; + } + + // Se está no formato YYYY-MM-DD, converter + if (data.match(/^\d{4}-\d{2}-\d{2}$/)) { + const [ano, mes, dia] = data.split('-'); + return `${dia}/${mes}/${ano}`; + } + + // Tentar converter outros formatos + try { + const dataObj = new Date(data); + if (!isNaN(dataObj.getTime())) { + const dia = String(dataObj.getDate()).padStart(2, '0'); + const mes = String(dataObj.getMonth() + 1).padStart(2, '0'); + const ano = dataObj.getFullYear(); + return `${dia}/${mes}/${ano}`; + } + } catch (error) { + console.warn('Erro ao formatar data:', data, error); + } + + // Se não conseguiu formatar, retornar original + return data; + } + + /** + * Verificar status de ONU Fiberhome + */ + async handleStatusONU(chatId, target) { + if (!target) { + await this.bot.sendMessage(chatId, + `📡 **Status ONU Fiberhome**\n\n` + + `💡 **Como usar:**\n` + + `• "status onu FHTT12345678"\n` + + `• "verificar onu FHTT12345678"\n` + + `• "onu status FHTT12345678"\n\n` + + `📝 **Nota:** Informe o serial da ONU (12 caracteres)`, + { parse_mode: 'Markdown' } + ); + return; + } + + try { + await this.bot.sendMessage(chatId, '📡 Conectando à OLT Fiberhome...'); + + logger.botCommand(chatId, 'status_onu', { target }); + + // Verificar se é serial ou device name + let resultado; + if (target.length === 12 && /^[A-Z0-9]+$/.test(target)) { + // É um serial + resultado = await this.fiberhome.obterStatusONU(target); + } else { + // Pode ser device name + resultado = await this.fiberhome.obterStatusONUPorNome(target); + } + + if (resultado.success) { + // Formatar informações da ONU + let message = `📡 **Status ONU Fiberhome**\n\n`; + message += `🆔 **Serial:** ${resultado.serial}\n`; + message += `📋 **Device Name:** ${resultado.deviceName}\n`; + message += `📍 **Posição:** Slot ${resultado.slot}, PON ${resultado.pon}, ONU ${resultado.onuId}\n`; + message += `🔌 **Status:** ${resultado.online ? '🟢 Online' : '🔴 Offline'}\n\n`; + + // Informações de sinal + message += `📶 **Informações de Sinal:**\n`; + message += `📡 **RX Power:** ${resultado.rxPower}\n`; + message += `📤 **TX Power:** ${resultado.txPower}\n`; + message += `📏 **Distância:** ${resultado.distance}\n\n`; + + // Status das portas Ethernet (resumido) + if (resultado.portasEthernet.length > 0) { + message += `🔗 **Portas Ethernet:**\n`; + resultado.portasEthernet.forEach(porta => { + const statusIcon = porta.status === 'up' ? '🟢' : '🔴'; + message += ` ${statusIcon} **Porta ${porta.porta}:** ${porta.status}\n`; + }); + message += '\n'; + } + + // Serviços WAN + if (resultado.servicosWAN.length > 0) { + message += `🌐 **Serviços WAN:**\n`; + resultado.servicosWAN.forEach(servico => { + const statusIcon = servico.status === 'active' ? '🟢' : '🔴'; + message += ` ${statusIcon} **Serviço ${servico.id}:** ${servico.status}\n`; + if (servico.ip !== 'N/A') { + message += ` 📍 **IP:** ${servico.ip}\n`; + } + }); + } else { + message += `🌐 **Serviços WAN:** Nenhum configurado\n`; + } + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + + } else { + await this.bot.sendMessage(chatId, `❌ ONU ${target} não encontrada na OLT Fiberhome.`); + + // Oferecer pesquisar em OLTs Datacom + const keyboard = { + inline_keyboard: [ + [ + { text: '🔍 Pesquisar em OLT Datacom', callback_data: `ver_sinal_${target}` }, + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ] + }; + + await this.bot.sendMessage(chatId, 'ONU não encontrada na Fiberhome. Deseja pesquisar nas OLTs Datacom?', { + reply_markup: keyboard + }); + } + } catch (error) { + logger.botError(chatId, error, 'handleStatusONU'); + + // Escapar caracteres especiais para evitar problemas no Telegram + const errorMsg = error.message ? error.message.replace(/[_*\[\]()~`>#+=|{}.!-]/g, '\\$&') : 'Erro desconhecido'; + + await this.bot.sendMessage(chatId, + `❌ **Erro ao verificar status da ONU**\n\n` + + `⚠️ ${errorMsg}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Verificar status das conexões Fiberhome + */ + async handleFiberhomeStatus(chatId) { + try { + await this.bot.sendMessage(chatId, '🔌 Diagnosticando conexões Fiberhome...'); + + logger.botCommand(chatId, 'fiberhome_status'); + + // Fazer diagnóstico completo + const diagnostico = await this.fiberhome.diagnosticarConectividade(); + + let message = `🔌 **Diagnóstico Fiberhome**\n\n`; + + // Status OLT + message += `📡 **OLT (${diagnostico.olt.host}:${diagnostico.olt.port}):**\n`; + message += ` 🌐 **Conectividade:** ${diagnostico.olt.conectividade ? '🟢 OK' : '🔴 Falhou'}\n`; + message += ` � **Telnet:** ${diagnostico.olt.telnet ? '🟢 Conectado' : '🔴 Falhou'}\n`; + if (diagnostico.olt.erro) { + message += ` ⚠️ **Erro:** ${diagnostico.olt.erro}\n`; + } + message += '\n'; + + // Status UNM2000 + message += `�️ **UNM2000 (${diagnostico.unm2000.host}:${diagnostico.unm2000.port}):**\n`; + message += ` 🌐 **Conectividade:** ${diagnostico.unm2000.conectividade ? '🟢 OK' : '🔴 Falhou'}\n`; + message += ` � **TL1:** ${diagnostico.unm2000.tl1 ? '🟢 Conectado' : '🔴 Falhou'}\n`; + if (diagnostico.unm2000.erro) { + message += ` ⚠️ **Erro:** ${diagnostico.unm2000.erro}\n`; + } + message += '\n'; + + // Configurações + message += `⚙️ **Configurações:**\n`; + message += ` 📡 **OLT:** GEPON/GEPON\n`; + message += ` 🖥️ **UNM2000:** 1/1\n\n`; + + // Comandos disponíveis + message += `💡 **Comandos disponíveis:**\n`; + message += `• \`status onu FHTT12345678\` - Status de ONU específica\n`; + message += `• \`verificar onu FHTT12345678\` - Verificar ONU\n`; + message += `• \`onu status DeviceName\` - Status por nome do dispositivo\n\n`; + + // Status geral + const oltOk = diagnostico.olt.conectividade && diagnostico.olt.telnet; + const unm2000Ok = diagnostico.unm2000.conectividade && diagnostico.unm2000.tl1; + + if (oltOk && unm2000Ok) { + message += `✅ **Status:** Sistema funcionando normalmente`; + } else if (oltOk || unm2000Ok) { + message += `⚠️ **Status:** Funcionamento parcial`; + } else { + message += `❌ **Status:** Sistema indisponível`; + } + + await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }); + + } catch (error) { + logger.botError(chatId, error, 'handleFiberhomeStatus'); + await this.bot.sendMessage(chatId, + `❌ **Erro ao diagnosticar Fiberhome**\n\n` + + `⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Iniciar processo interativo de abertura de suporte + */ + async iniciarProcessoSuporte(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; + } + + // Criar sessão de suporte + const sessionId = `${userId}_${chatId}_${Date.now()}`; + const suporteSession = { + id: sessionId, + chatId, + userId, + etapa: 'buscar_cliente', + dados: {}, + iniciado: new Date() + }; + + this.suporteSessions.set(sessionId, suporteSession); + + await this.bot.sendMessage(chatId, + `🎫 **Abertura de Suporte**\n\n` + + `🔍 **Etapa 1:** Identificar Cliente\n\n` + + `Por favor, digite o **nome** ou **CPF** do cliente para quem deseja abrir o suporte:`, + { parse_mode: 'Markdown' } + ); + + // Se foi fornecido um target, usar como busca inicial + if (target && target.trim()) { + await this.processarEtapaSuporteBuscarCliente(sessionId, target.trim()); + } + } + + /** + * Processar mensagem dentro do fluxo de suporte + */ + async processarMensagemSuporte(msg) { + const chatId = msg.chat.id; + const userId = msg.from.id; + const text = msg.text; + + // Encontrar sessão ativa para este usuário + const sessaoAtiva = Array.from(this.suporteSessions.values()).find( + session => session.chatId === chatId && session.userId === userId + ); + + if (!sessaoAtiva) { + return false; // Não há sessão ativa + } + + try { + switch (sessaoAtiva.etapa) { + case 'buscar_cliente': + await this.processarEtapaSuporteBuscarCliente(sessaoAtiva.id, text); + break; + + case 'selecionar_cliente': + await this.processarEtapaSelecionarCliente(sessaoAtiva.id, text); + break; + + case 'selecionar_servico': + await this.processarEtapaSelecionarServico(sessaoAtiva.id, text); + break; + + case 'selecionar_categoria': + await this.processarEtapaSelecionarCategoria(sessaoAtiva.id, text); + break; + + case 'selecionar_tecnico': + await this.processarEtapaSelecionarTecnico(sessaoAtiva.id, text); + break; + + case 'definir_data_hora': + await this.processarEtapaDefinirDataHora(sessaoAtiva.id, text); + break; + + case 'adicionar_descricao': + await this.processarEtapaAdicionarDescricao(sessaoAtiva.id, text); + break; + + 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...'); + this.suporteSessions.delete(sessaoAtiva.id); + break; + } + + return true; // Mensagem foi processada pelo fluxo de suporte + + } catch (error) { + logger.error('Erro no processamento de suporte:', error); + await this.bot.sendMessage(chatId, + `❌ **Erro no processamento**\n\n` + + `⚠️ ${error.message}\n\n` + + `🔄 Reiniciando processo de suporte...` + ); + this.suporteSessions.delete(sessaoAtiva.id); + return true; + } + } + + /** + * Etapa 1: Buscar cliente + */ + async processarEtapaSuporteBuscarCliente(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) { + await this.bot.sendMessage(session.chatId, + `❌ **Erro na busca**\n\n` + + `⚠️ ${resultado.error}\n\n` + + `Por favor, tente novamente com outro nome ou CPF:` + ); + return; + } + + if (!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) { + // Apenas um cliente encontrado, selecionar automaticamente + session.dados.clienteSelecionado = resultado.clientes[0]; + session.etapa = 'selecionar_servico'; + await this.processarEtapaSelecionarServico(sessionId); + } else { + // Múltiplos clientes, deixar usuário escolher + session.etapa = 'selecionar_cliente'; + await this.mostrarOpcoesClientes(session); + } + + } catch (error) { + await this.bot.sendMessage(session.chatId, + `❌ **Erro na busca**\n\n` + + `⚠️ ${error.message}\n\n` + + `Por favor, tente novamente:` + ); + } + } + + /** + * Mostrar opções de clientes encontrados + */ + async mostrarOpcoesClientes(session) { + let message = `👥 **Clientes encontrados:**\n\n`; + + const buttons = []; + + session.dados.clientesEncontrados.forEach((cliente, index) => { + const status = this.topsapp.interpretarStatusCliente ? + this.topsapp.interpretarStatusCliente(cliente) : + cliente.status; + const statusEmoji = status === 1 ? '🟢' : '🔴'; + + message += `**${index + 1}.** ${statusEmoji} ${cliente.nome}\n`; + message += ` 📄 CPF: ${cliente.cpf || 'N/A'}\n`; + message += ` 🆔 ID: ${cliente.id}\n\n`; + + // Adicionar botão para seleção direta + buttons.push([{ + text: `${index + 1}. ${cliente.nome}`, + callback_data: `suporte_cliente_${index}` + }]); + }); + + message += `Selecione o cliente clicando no botão ou digite o **número** (1-${session.dados.clientesEncontrados.length}):`; + + const keyboard = { + inline_keyboard: buttons + }; + + await this.bot.sendMessage(session.chatId, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + } + + /** + * Etapa 2: Selecionar cliente específico + */ + async processarEtapaSelecionarCliente(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**\n\n` + + `Por favor, digite um número entre 1 e ${session.dados.clientesEncontrados.length}:` + ); + return; + } + + session.dados.clienteSelecionado = session.dados.clientesEncontrados[numero - 1]; + session.etapa = 'selecionar_servico'; + await this.processarEtapaSelecionarServico(sessionId); + } + + /** + * Etapa 3: Selecionar serviço + */ + async processarEtapaSelecionarServico(sessionId, input = null) { + const session = this.suporteSessions.get(sessionId); + if (!session) return; + + const cliente = session.dados.clienteSelecionado; + + if (!input) { + // Primeira vez nesta etapa - buscar serviços + await this.bot.sendMessage(session.chatId, + `🔍 Buscando serviços do cliente **${cliente.nome}**...`, + { parse_mode: 'Markdown' } + ); + + try { + const resultado = await this.topsapp.obterServicosCliente(cliente.id); + + console.log(`🔍 Handler - Resultado completo:`, { + success: resultado.success, + servicosCount: resultado.servicos?.length, + total: resultado.total, + debug: resultado.debug + }); + + if (!resultado.success) { + await this.bot.sendMessage(session.chatId, + `❌ **Erro ao buscar serviços**\n\n` + + `⚠️ ${resultado.error}\n\n` + + `🔄 Reiniciando processo...` + ); + this.suporteSessions.delete(sessionId); + return; + } + + if (!resultado.servicos || resultado.servicos.length === 0) { + await this.bot.sendMessage(session.chatId, + `❌ **Nenhum serviço encontrado**\n\n` + + `O cliente não possui serviços cadastrados.\n\n` + + `🔄 Reiniciando processo...` + ); + this.suporteSessions.delete(sessionId); + return; + } + + session.dados.servicos = resultado.servicos; + await this.mostrarOpcoesServicos(session); + + } catch (error) { + await this.bot.sendMessage(session.chatId, + `❌ **Erro ao buscar serviços**\n\n` + + `⚠️ ${error.message}\n\n` + + `🔄 Reiniciando processo...` + ); + this.suporteSessions.delete(sessionId); + } + } else { + // Usuário selecionou um serviço + const numero = parseInt(input.trim()); + + if (isNaN(numero) || numero < 1 || numero > session.dados.servicos.length) { + await this.bot.sendMessage(session.chatId, + `❌ **Número inválido**\n\n` + + `Por favor, digite um número entre 1 e ${session.dados.servicos.length}:` + ); + return; + } + + session.dados.servicoSelecionado = session.dados.servicos[numero - 1]; + session.etapa = 'selecionar_categoria'; + await this.processarEtapaSelecionarCategoria(sessionId); + } + } + + /** + * Mostrar opções de serviços + */ + async mostrarOpcoesServicos(session) { + let message = `📋 **Serviços disponíveis:**\n\n`; + + const buttons = []; + + session.dados.servicos.forEach((servico, index) => { + const statusEmoji = servico.statusNumerico === 1 ? '🟢' : '🔴'; + + // Extrair usuário sem o domínio + const usuarioRaw = servico.usuario || servico.login || servico.nome || ''; + const usuario = usuarioRaw.includes('@') ? usuarioRaw.split('@')[0] : usuarioRaw; + + // Montar endereço de instalação + const endParts = [ + servico.instalacao_endereco, + servico.instalacao_numero + ].filter(Boolean); + const endereco = endParts.length > 0 ? endParts.join(', ') : null; + + message += `**${index + 1}.** ${statusEmoji} ${usuario}\n`; + if (endereco) message += ` 📍 ${endereco}\n`; + message += ` 🆔 ID: ${servico.id}\n`; + message += `\n`; + + // Adicionar botão para seleção direta + const labelBtn = endereco ? `${index + 1}. ${usuario} — ${endereco}` : `${index + 1}. ${usuario}`; + buttons.push([{ + text: labelBtn, + callback_data: `suporte_servico_${index}` + }]); + }); + + message += `Selecione o serviço clicando no botão ou digite o **número** (1-${session.dados.servicos.length}):`; + + const keyboard = { + inline_keyboard: buttons + }; + + await this.bot.sendMessage(session.chatId, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + } + + /** + * Etapa 4: Selecionar categoria + */ + async processarEtapaSelecionarCategoria(sessionId, input = null) { + const session = this.suporteSessions.get(sessionId); + if (!session) return; + + if (!input) { + // Primeira vez nesta etapa - buscar categorias + await this.bot.sendMessage(session.chatId, `🔍 Buscando categorias de suporte...`); + + try { + const resultado = await this.topsapp.obterSuporteCategorias(); + + if (!resultado.success) { + await this.bot.sendMessage(session.chatId, + `❌ **Erro ao buscar categorias**\n\n` + + `⚠️ ${resultado.error}\n\n` + + `🔄 Reiniciando processo...` + ); + this.suporteSessions.delete(sessionId); + return; + } + + if (!resultado.data?.dados || resultado.data.dados.length === 0) { + await this.bot.sendMessage(session.chatId, + `❌ **Nenhuma categoria encontrada**\n\n` + + `🔄 Reiniciando processo...` + ); + this.suporteSessions.delete(sessionId); + return; + } + + session.dados.categorias = resultado.data.dados; + await this.mostrarOpcoesCategorias(session); + + } catch (error) { + await this.bot.sendMessage(session.chatId, + `❌ **Erro ao buscar categorias**\n\n` + + `⚠️ ${error.message}\n\n` + + `🔄 Reiniciando processo...` + ); + this.suporteSessions.delete(sessionId); + } + } else { + // Usuário selecionou uma categoria + const numero = parseInt(input.trim()); + + if (isNaN(numero) || numero < 1 || numero > session.dados.categorias.length) { + await this.bot.sendMessage(session.chatId, + `❌ **Número inválido**\n\n` + + `Por favor, digite um número entre 1 e ${session.dados.categorias.length}:` + ); + return; + } + + session.dados.categoriaSelecionada = session.dados.categorias[numero - 1]; + session.etapa = 'selecionar_tecnico'; + await this.processarEtapaSelecionarTecnico(sessionId); + } + } + + /** + * Mostrar opções de categorias + */ + async mostrarOpcoesCategorias(session) { + let message = `📁 **Categorias de suporte:**\n\n`; + + // Verificar se categorias é um array válido + if (!Array.isArray(session.dados.categorias)) { + console.error(`❌ session.dados.categorias não é um array:`, { + type: typeof session.dados.categorias, + value: session.dados.categorias + }); + throw new Error(`Dados de categorias inválidos: ${typeof session.dados.categorias}`); + } + + session.dados.categorias.forEach((categoria, index) => { + message += `**${index + 1}.** ${categoria.descricao}\n`; + }); + + message += `\nDigite o **número** da categoria do suporte (1-${session.dados.categorias.length}):`; + + await this.bot.sendMessage(session.chatId, message, { parse_mode: 'Markdown' }); + } + + /** + * Etapa 5: Selecionar técnico + */ + async processarEtapaSelecionarTecnico(sessionId, input = null) { + const session = this.suporteSessions.get(sessionId); + if (!session) return; + + if (!input) { + // Primeira vez nesta etapa - buscar equipes/técnicos via operadores + await this.bot.sendMessage(session.chatId, `🔍 Buscando técnicos disponíveis...`); + + try { + const location = session.dados.location || ''; + + // ObterOperadores traz o campo id_tecnico/tecnico que mapeia as equipes cadastradas + const resultado = await this.topsapp.obterOperadores(location); + + if (!resultado.success) { + await this.bot.sendMessage(session.chatId, + `❌ **Erro ao buscar técnicos**\n\n` + + `⚠️ ${resultado.error}\n\n` + + `🔄 Reiniciando processo...` + ); + this.suporteSessions.delete(sessionId); + return; + } + + const operadores = resultado.data?.dados || []; + + // Extrair técnicos únicos pelo id_tecnico (cada operador aponta para uma equipe) + const tecnicosMap = new Map(); + operadores.forEach(op => { + if (op.id_tecnico > 0 && op.tecnico && op.tecnico.trim() !== '') { + if (!tecnicosMap.has(op.id_tecnico)) { + tecnicosMap.set(op.id_tecnico, { id: op.id_tecnico, nome: op.tecnico }); + } + } + }); + + const tecnicos = Array.from(tecnicosMap.values()).sort((a, b) => a.nome.localeCompare(b.nome)); + + if (tecnicos.length === 0) { + await this.bot.sendMessage(session.chatId, + `❌ **Nenhum técnico encontrado**\n\n` + + `🔄 Reiniciando processo...` + ); + this.suporteSessions.delete(sessionId); + return; + } + + session.dados.tecnicos = tecnicos; + await this.mostrarOpcoesTecnicos(session); + + } catch (error) { + await this.bot.sendMessage(session.chatId, + `❌ **Erro ao buscar técnicos**\n\n` + + `⚠️ ${error.message}\n\n` + + `🔄 Reiniciando processo...` + ); + this.suporteSessions.delete(sessionId); + } + } else { + // Usuário selecionou um técnico + const numero = parseInt(input.trim()); + + if (isNaN(numero) || numero < 1 || numero > session.dados.tecnicos.length) { + await this.bot.sendMessage(session.chatId, + `❌ **Número inválido**\n\n` + + `Por favor, digite um número entre 1 e ${session.dados.tecnicos.length}:` + ); + return; + } + + const tecnicoSelecionado = session.dados.tecnicos[numero - 1]; + session.dados.tecnicoSelecionado = tecnicoSelecionado; + session.etapa = 'definir_data_hora'; + await this.processarEtapaDefinirDataHora(sessionId); + } + } + + /** + * Mostrar opções de técnicos/tipos de atendimento + */ + async mostrarOpcoesTecnicos(session) { + let message = `👨‍🔧 **Técnicos disponíveis:**\n\n`; + + // Validar se técnicos é um array + if (!Array.isArray(session.dados.tecnicos)) { + console.error(`❌ session.dados.tecnicos não é um array:`, { + type: typeof session.dados.tecnicos, + value: session.dados.tecnicos + }); + throw new Error(`Dados de técnicos inválidos: ${typeof session.dados.tecnicos}`); + } + + const buttons = []; + session.dados.tecnicos.forEach((tecnico, index) => { + message += `**${index + 1}.** ${tecnico.nome}\n`; + buttons.push([{ text: `${index + 1}. ${tecnico.nome}`, callback_data: `suporte_tecnico_${index}` }]); + }); + + message += `\nSelecione o técnico clicando no botão ou digitando o número (1-${session.dados.tecnicos.length}):`; + + await this.bot.sendMessage(session.chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: buttons } + }); + } + + /** + * Etapa 6: Definir data e hora + */ + async processarEtapaDefinirDataHora(sessionId, input = null) { + const session = this.suporteSessions.get(sessionId); + if (!session) return; + + if (!input) { + // Primeira vez nesta etapa + await this.bot.sendMessage(session.chatId, + `📅 **Data e horário para visita**\n\n` + + `Digite quando você gostaria que o técnico fizesse a visita.\n\n` + + `**Exemplos:**\n` + + `• "amanhã às 14h"\n` + + `• "sexta-feira 09:00"\n` + + `• "15/08 às 16h30"\n` + + `• "hoje 18h"\n` + + `• "em 3 dias às 10h"\n\n` + + `📝 Digite a data e horário:`, + { parse_mode: 'Markdown' } + ); + } else { + // Processar texto de data/hora + const resultado = this.dateProcessor.processarTexto(input); + + if (!resultado.success) { + await this.bot.sendMessage(session.chatId, + `❌ **Não consegui entender a data/hora**\n\n` + + `⚠️ ${resultado.error}\n\n` + + `**Exemplos válidos:**\n` + + `• "amanhã às 14h"\n` + + `• "sexta-feira 09:00"\n` + + `• "15/08 às 16h30"\n\n` + + `Por favor, tente novamente:` + ); + return; + } + + // Verificar se a data não é no passado + if (resultado.data < new Date()) { + await this.bot.sendMessage(session.chatId, + `❌ **Data no passado**\n\n` + + `A data especificada (${this.dateProcessor.formatarData(resultado.data)}) já passou.\n\n` + + `Por favor, especifique uma data futura:` + ); + return; + } + + session.dados.dataHora = resultado.data; + session.dados.dataHoraTexto = input; + session.etapa = 'adicionar_descricao'; + + await this.bot.sendMessage(session.chatId, + `✅ **Data/hora confirmada:**\n${this.dateProcessor.formatarData(resultado.data)}\n\n` + + `📝 **Última etapa:** Descrição do problema\n\n` + + `Digite uma descrição detalhada do problema ou serviço que precisa ser realizado:`, + { parse_mode: 'Markdown' } + ); + } + } + + /** + * Etapa 7: Adicionar descrição + */ + async processarEtapaAdicionarDescricao(sessionId, descricao) { + const session = this.suporteSessions.get(sessionId); + if (!session) return; + + if (!descricao || descricao.trim().length < 10) { + await this.bot.sendMessage(session.chatId, + `❌ **Descrição muito curta**\n\n` + + `Por favor, forneça uma descrição mais detalhada (mínimo 10 caracteres):` + ); + return; + } + + session.dados.descricao = descricao.trim(); + + // Mostrar resumo e confirmar + await this.mostrarResumoSuporte(session); + } + + /** + * Mostrar resumo do suporte antes de criar + */ + async mostrarResumoSuporte(session) { + const dados = session.dados; + + let resumo = `📋 **Resumo do Suporte**\n\n`; + resumo += `👤 **Cliente:** ${dados.clienteSelecionado.nome}\n`; + resumo += `🆔 **ID Cliente:** ${dados.clienteSelecionado.id}\n`; + resumo += `📋 **Serviço:** ${dados.servicoSelecionado.descricao || dados.servicoSelecionado.plano}\n`; + resumo += `🆔 **ID Serviço:** ${dados.servicoSelecionado.id}\n`; + resumo += `📁 **Categoria:** ${dados.categoriaSelecionada.descricao}\n`; + resumo += `👨‍🔧 **Técnico:** ${dados.tecnicoSelecionado.nome}\n`; + resumo += `📅 **Data/Hora:** ${this.dateProcessor.formatarData(dados.dataHora)}\n`; + resumo += `📝 **Descrição:** ${dados.descricao}\n\n`; + resumo += `Confirma a abertura do suporte? (sim/não)`; + + await this.bot.sendMessage(session.chatId, resumo, { parse_mode: 'Markdown' }); + + session.etapa = 'confirmar'; + } + + /** + * Finalizar criação do suporte + */ + async finalizarCriacaoSuporte(sessionId, confirmacao) { + const session = this.suporteSessions.get(sessionId); + if (!session) return; + + const confirmar = confirmacao.toLowerCase().trim(); + + if (!['sim', 's', 'yes', 'y', 'confirmar', 'ok'].includes(confirmar)) { + await this.bot.sendMessage(session.chatId, + `❌ **Suporte cancelado**\n\n` + + `O processo foi cancelado. Use o comando "abrir suporte" para tentar novamente.` + ); + this.suporteSessions.delete(sessionId); + return; + } + + await this.bot.sendMessage(session.chatId, `⏳ Criando suporte...`); + + try { + const dados = session.dados; + const location = dados.location || ''; + const dadosSuporte = { + idCliente: dados.clienteSelecionado.id, + idServico: dados.servicoSelecionado.id, + idCategoria: dados.categoriaSelecionada.id, + descricao: `${dados.descricao}\n\nData/Hora agendada: ${this.dateProcessor.formatarData(dados.dataHora)}\nTécnico: ${dados.tecnicoSelecionado.nome}` + }; + + if (dados.clienteSelecionado?.nome) { + dadosSuporte.contato = dados.clienteSelecionado.nome; + } + + if (dados.clienteSelecionado?.telefone) { + dadosSuporte.contatoTelefone = dados.clienteSelecionado.telefone; + } + + dadosSuporte.outrasInformacoes = `Criado via Bot Telegram\nData original: ${dados.dataHoraTexto}`; + + const resultado = await this.topsapp.adicionarSuporte(dadosSuporte, location); + + if (resultado.success) { + const protocolo = resultado.data?.dados?.protocolo || resultado.data?.protocolo; + + // Encaminhar para o técnico selecionado + const idTecnico = dados.tecnicoSelecionado?.id; + if (protocolo && idTecnico) { + const resEnc = await this.topsapp.encaminharSuporte( + protocolo, + idTecnico, + `Técnico atribuído via Bot Telegram: ${dados.tecnicoSelecionado.nome}`, + location + ); + if (!resEnc.success) { + logger.warn('Suporte criado mas falha ao encaminhar técnico:', resEnc.error); + } + } + + await this.bot.sendMessage(session.chatId, + `✅ **Suporte criado com sucesso!**\n\n` + + `🎫 **Protocolo:** ${protocolo || 'N/A'}\n` + + `👤 **Cliente:** ${dados.clienteSelecionado.nome}\n` + + `📋 **Serviço:** ${dados.servicoSelecionado.descricao || dados.servicoSelecionado.plano}\n` + + `📁 **Categoria:** ${dados.categoriaSelecionada.descricao}\n` + + `👨‍🔧 **Técnico:** ${dados.tecnicoSelecionado.nome}\n` + + `📅 **Data/Hora:** ${this.dateProcessor.formatarData(dados.dataHora)}\n\n` + + `🎯 O suporte foi registrado no sistema!`, + { parse_mode: 'Markdown' } + ); + + logger.info('Suporte criado via bot', { + protocolo, + cliente: dados.clienteSelecionado.nome, + usuario: session.userId + }); + } else { + await this.bot.sendMessage(session.chatId, + `❌ **Erro ao criar suporte**\n\n` + + `⚠️ ${resultado.error}\n\n` + + `Tente novamente mais tarde.` + ); + } + + } catch (error) { + logger.error('Erro ao criar suporte:', error); + await this.bot.sendMessage(session.chatId, + `❌ **Erro ao criar suporte**\n\n` + + `⚠️ ${error.message}\n\n` + + `Tente novamente mais tarde.` + ); + } + + // Limpar sessão + 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 + // ───────────────────────────────────────────────────────────────────────── + + /** + * Roteador de callbacks topsapp_onu_* + */ + async handleTopSappOnuCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const userId = callbackQuery.from.id; + + await this.bot.answerCallbackQuery(callbackQuery.id); + + const session = this.userSessions.get(userId); + + if (data === 'topsapp_onu_sim') { + if (!session || !session.onuData) { + await this.bot.sendMessage(chatId, '❌ Sessão expirada. Tente novamente após adicionar a ONU.'); + return; + } + session.action = 'topsapp_onu_aguardando_nome'; + this.userSessions.set(userId, session); + await this.bot.sendMessage(chatId, + `🔍 *Buscar Cliente no TopSapp*\n\nDigite o nome ou CPF do cliente:`, + { parse_mode: 'Markdown' } + ); + return; + } + + if (data.startsWith('topsapp_onu_cli_')) { + const idCliente = data.replace('topsapp_onu_cli_', ''); + if (!session || !session.onuData) { + await this.bot.sendMessage(chatId, '❌ Sessão expirada. Tente novamente.'); + return; + } + await this.topsappOnuMostrarServicos(chatId, userId, idCliente, session.onuData); + return; + } + + if (data.startsWith('topsapp_onu_svc_')) { + // formato: topsapp_onu_svc__ + const parts = data.replace('topsapp_onu_svc_', '').split('_'); + const idServico = parts[0]; + const idCliente = parts[1]; + if (!session || !session.onuData) { + await this.bot.sendMessage(chatId, '❌ Sessão expirada. Tente novamente.'); + return; + } + await this.topsappOnuConfirmarVinculo(chatId, userId, idServico, idCliente, session.onuData); + return; + } + + if (data === 'topsapp_onu_ok') { + if (!session || !session.ftthPayload) { + await this.bot.sendMessage(chatId, '❌ Sessão expirada. Tente novamente.'); + return; + } + await this.topsappOnuExecutarVinculo(chatId, userId, session); + return; + } + + await this.bot.sendMessage(chatId, '❓ Ação desconhecida.'); + } + + /** + * Busca cliente no TopSapp e exibe lista para seleção + */ + async topsappOnuBuscarCliente(chatId, userId, query, onuData) { + try { + await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); + + const resultado = await this.topsapp.buscarCliente(query, onuData.location); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, + `❌ *Erro na busca*\n\n${resultado.error}`, + { parse_mode: 'Markdown' } + ); + this.userSessions.delete(userId); + return; + } + + if (!resultado.clientes || resultado.clientes.length === 0) { + await this.bot.sendMessage(chatId, + `🔍 *Nenhum cliente encontrado*\n\nTermo: "${query}"\n\nDigite outro nome ou CPF:`, + { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: [[{ text: '❌ Cancelar', callback_data: 'cancel_action' }]] } + } + ); + // Mantém sessão ativa para nova tentativa + return; + } + + const keyboard = []; + let msg = `👥 *${resultado.clientes.length > 1 ? resultado.clientes.length + ' clientes encontrados' : 'Cliente encontrado'}*\n\nSelecione o cliente:\n\n`; + + resultado.clientes.slice(0, 8).forEach((cliente, i) => { + msg += `${i + 1}. *${cliente.nome}* (ID: ${cliente.id})\n`; + keyboard.push([{ text: `${i + 1}. ${cliente.nome}`, callback_data: `topsapp_onu_cli_${cliente.id}` }]); + }); + + keyboard.push([{ text: '❌ Cancelar', callback_data: 'cancel_action' }]); + + await this.bot.sendMessage(chatId, msg, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + + } catch (error) { + console.error('❌ Erro ao buscar cliente TopSapp:', error); + await this.bot.sendMessage(chatId, `❌ Erro ao buscar cliente: ${error.message}`); + this.userSessions.delete(userId); + } + } + + /** + * Lista serviços ativos do cliente para seleção + */ + async topsappOnuMostrarServicos(chatId, userId, idCliente, onuData) { + try { + await this.bot.sendMessage(chatId, '📋 Buscando serviços do cliente...'); + + const servicosResult = await this.topsapp.obterServicosCliente(idCliente, onuData.location); + + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviços: ${servicosResult.error}`); + this.userSessions.delete(userId); + return; + } + + const servicos = servicosResult.servicos || []; + const servicosAtivos = servicos.filter(s => + s.id_status === 1 || (s.status || '').toLowerCase() === 'liberado' + ); + + if (servicosAtivos.length === 0) { + await this.bot.sendMessage(chatId, + `⚠️ *Nenhum serviço ativo encontrado* para o cliente ID ${idCliente}.\n\nCancele e verifique o cliente.`, + { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: [[{ text: '❌ Cancelar', callback_data: 'cancel_action' }]] } + } + ); + return; + } + + const keyboard = []; + let msg = `📋 *Serviços ativos do cliente*\n\nSelecione o serviço FTTH:\n\n`; + + servicosAtivos.slice(0, 8).forEach((servico, i) => { + const idSvc = servico.id_servico || servico.id; + const nome = servico.descricao || servico.nome || 'Serviço'; + const login = servico.usuario ? ` (${servico.usuario})` : ''; + msg += `${i + 1}. *${nome}*${login} — ID: ${idSvc}\n`; + keyboard.push([{ text: `${i + 1}. ${nome}${login}`, callback_data: `topsapp_onu_svc_${idSvc}_${idCliente}` }]); + }); + + keyboard.push([{ text: '❌ Cancelar', callback_data: 'cancel_action' }]); + + await this.bot.sendMessage(chatId, msg, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + + } catch (error) { + console.error('❌ Erro ao listar serviços TopSapp:', error); + await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); + this.userSessions.delete(userId); + } + } + + /** + * Faz matching OLT/porta/perfil e mostra tela de confirmação + */ + async topsappOnuConfirmarVinculo(chatId, userId, idServico, idCliente, onuData) { + try { + await this.bot.sendMessage(chatId, '🔌 Buscando dados da OLT no TopSapp...'); + + const oltsResult = await this.topsapp.obterOlts(onuData.location); + + if (!oltsResult.success) { + await this.bot.sendMessage(chatId, `❌ Erro ao buscar OLTs: ${oltsResult.error}`); + this.userSessions.delete(userId); + return; + } + + const olts = oltsResult.data?.dados?.dados || oltsResult.data?.dados || []; + + // Mapa manual de keywords por oltId/tipo → para busca no campo ssid e nome do TopSapp + // Fonte: valores reais de ssid cadastrados no TopSapp + const SSID_KEYWORDS = { + 'fiberhome': ['fiberhome', '172.16.10.6'], + 'olt_colmeia': ['colmeia'], + 'olt_canaa': ['canaa'], + 'olt_canaa2': ['canaa 2', 'canaa2'], + 'olt_dito': ['dito'], + 'olt_pedro_afonso': ['pedro', 'afonso'], + 'olt_canavieiras': ['canavieiras'] + }; + + /** + * Busca uma OLT na lista pelo campo nome OU ssid usando lista de keywords. + * Tenta match com TODOS os tokens primeiro; se falhar, com QUALQUER token. + */ + const buscarPorKeywords = (keywords) => { + const found = olts.find(o => { + const haystack = `${o.nome || ''} ${o.ssid || ''}`.toLowerCase(); + return keywords.every(k => haystack.includes(k)); + }); + if (found) return found; + return olts.find(o => { + const haystack = `${o.nome || ''} ${o.ssid || ''}`.toLowerCase(); + return keywords.some(k => haystack.includes(k)); + }); + }; + + let oltEncontrada = null; + + // 1. Match manual pelo tipo (Fiberhome) ou oltId (Datacom) + const tipoKey = onuData.tipo === 'fiberhome' ? 'fiberhome' : onuData.oltId; + if (tipoKey && SSID_KEYWORDS[tipoKey]) { + oltEncontrada = buscarPorKeywords(SSID_KEYWORDS[tipoKey]); + if (oltEncontrada) { + console.log(`✅ OLT encontrada por mapa manual (${tipoKey}): "${oltEncontrada.nome}" / ssid: "${oltEncontrada.ssid}"`); + } + } + + // 2. Fallback: keyword livre do oltId (Datacom sem mapa definido) + if (!oltEncontrada && onuData.oltId) { + const keyword = onuData.oltId.replace(/^olt_/, '').replace(/_/g, ' ').toLowerCase(); + const keywords = keyword.split(' ').filter(k => k.length > 2); + oltEncontrada = buscarPorKeywords(keywords); + if (oltEncontrada) { + console.log(`✅ OLT encontrada por keyword livre (oltId): "${oltEncontrada.nome}" (keyword: "${keyword}")`); + } + } + + // 3. Fallback: keyword livre do oltHost (Fiberhome sem mapa definido) + if (!oltEncontrada && onuData.oltHost) { + const keywords = onuData.oltHost.toLowerCase().split(/[\s_\-]+/).filter(k => k.length > 1); + oltEncontrada = buscarPorKeywords(keywords); + if (oltEncontrada) { + console.log(`✅ OLT encontrada por keyword livre (oltHost): "${oltEncontrada.nome}" (keyword: "${onuData.oltHost}")`); + } + } + + // 4. Fallback: primeira OLT disponível + if (!oltEncontrada && olts.length > 0) { + oltEncontrada = olts[0]; + console.log(`⚠️ OLT não identificada — usando a primeira disponível: ${oltEncontrada.nome}`); + } + + if (!oltEncontrada) { + await this.bot.sendMessage(chatId, `❌ Nenhuma OLT encontrada no TopSapp para este servidor.`); + this.userSessions.delete(userId); + return; + } + + // Encontrar porta pelo alias + const portas = oltEncontrada.portas || []; + let portaEncontrada = portas.find(p => p.alias === onuData.portaAlias); + if (!portaEncontrada && portas.length > 0) { + portaEncontrada = portas[0]; + console.log(`⚠️ Porta não encontrada por alias "${onuData.portaAlias}", usando a primeira: ${portaEncontrada.alias}`); + } + + if (!portaEncontrada) { + await this.bot.sendMessage(chatId, `❌ Nenhuma porta encontrada na OLT "${oltEncontrada.nome}".`); + this.userSessions.delete(userId); + return; + } + + // Encontrar perfil por tipo de ONU + const perfis = oltEncontrada.perfis || []; + let perfilEncontrado; + if (onuData.tipo === 'datacom') { + perfilEncontrado = perfis.find(p => p.identificacao && p.identificacao.includes('DATACOM ONU_BRIDGE_')); + } else { + perfilEncontrado = perfis.find(p => p.identificacao === 'ONU-BRIDGE-AN5506-02-B'); + } + if (!perfilEncontrado && perfis.length > 0) { + perfilEncontrado = perfis[0]; + console.log(`⚠️ Perfil não encontrado por tipo, usando o primeiro: ${perfilEncontrado.identificacao}`); + } + + if (!perfilEncontrado) { + await this.bot.sendMessage(chatId, `❌ Nenhum perfil encontrado na OLT "${oltEncontrada.nome}".`); + this.userSessions.delete(userId); + return; + } + + const ftthData = { + serial: onuData.serial, + idAP: oltEncontrada.id, + idPorta: portaEncontrada.id, + idPerfil: perfilEncontrado.id + }; + + if (onuData.onuId !== undefined && onuData.onuId !== null) { + ftthData.ftth_onuid = parseInt(onuData.onuId); + ftthData.onuid = parseInt(onuData.onuId); + } + if (onuData.servicePort !== null && onuData.servicePort !== undefined) { + ftthData.ftth_servport = onuData.servicePort; + ftthData.ftth_serviceport = onuData.servicePort; + ftthData.serviceport = onuData.servicePort; + } + + console.log('🔧 TopSapp ftthData completo:', JSON.stringify(ftthData, null, 2)); + + // Salvar payload de confirmação na sessão + const session = this.userSessions.get(userId); + session.action = 'topsapp_onu_confirmando'; + session.ftthPayload = { + idCliente, + idServico, + ftthData, + location: onuData.location + }; + this.userSessions.set(userId, session); + + const onuIdInfo = onuData.onuId !== undefined && onuData.onuId !== null + ? `\n🔢 *ONU ID:* ${onuData.onuId}` : ''; + const spInfo = onuData.servicePort !== null && onuData.servicePort !== undefined + ? `\n🔌 *Service-port:* ${onuData.servicePort}` : ''; + + await this.bot.sendMessage(chatId, + `🔌 *Confirmar Vinculação no TopSapp*\n\n` + + `📱 *Serial:* \`${onuData.serial}\`\n` + + `🏢 *OLT:* ${oltEncontrada.nome}\n` + + `📡 *Porta:* ${portaEncontrada.alias}` + + onuIdInfo + spInfo + `\n` + + `⚙️ *Perfil:* ${perfilEncontrado.identificacao}\n` + + `🆔 *Serviço ID:* ${idServico}\n` + + `👤 *Cliente ID:* ${idCliente}\n\n` + + `Confirmar os dados acima?`, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [[ + { text: '✅ Confirmar', callback_data: 'topsapp_onu_ok' }, + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ]] + } + } + ); + + } catch (error) { + console.error('❌ Erro ao confirmar vínculo TopSapp:', error); + await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); + this.userSessions.delete(userId); + } + } + + /** + * Executa a atualização FTTH no TopSapp via AlterarServico + */ + async topsappOnuExecutarVinculo(chatId, userId, session) { + try { + const { idCliente, idServico, ftthData, location } = session.ftthPayload; + + await this.bot.sendMessage(chatId, '⏳ Salvando dados no TopSapp...'); + + const resultado = await this.topsapp.atualizarFTTH(idCliente, idServico, ftthData, location); + + if (resultado.success) { + await this.bot.sendMessage(chatId, + `✅ *ONU vinculada com sucesso no TopSapp!*\n\n` + + `📱 *Serial:* \`${ftthData.serial}\`\n` + + `🆔 *Serviço ID:* ${idServico}\n` + + `👤 *Cliente ID:* ${idCliente}`, + { parse_mode: 'Markdown' } + ); + } else { + await this.bot.sendMessage(chatId, + `❌ *Erro ao vincular no TopSapp*\n\n${resultado.error}`, + { parse_mode: 'Markdown' } + ); + } + + } catch (error) { + console.error('❌ Erro ao executar vínculo TopSapp:', error); + await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); + } finally { + this.userSessions.delete(userId); + } + } + + /** + * Callback: Seleção de cliente via botão inline no fluxo de suporte + */ + async handleSuporteClienteCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const userId = callbackQuery.from.id; + + await this.bot.answerCallbackQuery(callbackQuery.id); + + const index = parseInt(data.replace('suporte_cliente_', '')); + + const sessaoAtiva = Array.from(this.suporteSessions.values()).find( + session => session.chatId === chatId && session.userId === userId + ); + + if (!sessaoAtiva) { + await this.bot.sendMessage(chatId, + `❌ **Sessão expirada**\n\nUse o comando "abrir suporte" para iniciar novamente.`, + { parse_mode: 'Markdown' } + ); + return; + } + + const clientes = sessaoAtiva.dados.clientesEncontrados; + if (!clientes || index < 0 || index >= clientes.length) { + await this.bot.sendMessage(chatId, `❌ **Cliente inválido.** Por favor, tente novamente.`, { parse_mode: 'Markdown' }); + return; + } + + sessaoAtiva.dados.clienteSelecionado = clientes[index]; + sessaoAtiva.etapa = 'selecionar_servico'; + await this.processarEtapaSelecionarServico(sessaoAtiva.id); + } + + /** + * Callback: Seleção de serviço via botão inline no fluxo de suporte + */ + async handleSuporteServicoCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const userId = callbackQuery.from.id; + + await this.bot.answerCallbackQuery(callbackQuery.id); + + const index = parseInt(data.replace('suporte_servico_', '')); + + const sessaoAtiva = Array.from(this.suporteSessions.values()).find( + session => session.chatId === chatId && session.userId === userId + ); + + if (!sessaoAtiva) { + await this.bot.sendMessage(chatId, + `❌ **Sessão expirada**\n\nUse o comando "abrir suporte" para iniciar novamente.`, + { parse_mode: 'Markdown' } + ); + return; + } + + const servicos = sessaoAtiva.dados.servicos; + if (!servicos || index < 0 || index >= servicos.length) { + await this.bot.sendMessage(chatId, `❌ **Serviço inválido.** Por favor, tente novamente.`, { parse_mode: 'Markdown' }); + return; + } + + sessaoAtiva.dados.servicoSelecionado = servicos[index]; + sessaoAtiva.etapa = 'selecionar_categoria'; + await this.processarEtapaSelecionarCategoria(sessaoAtiva.id); + } + + /** + * Callback: Seleção de técnico via botão inline no fluxo de suporte + */ + async handleSuporteTecnicoCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const userId = callbackQuery.from.id; + + const index = parseInt(data.replace('suporte_tecnico_', '')); + + const sessaoAtiva = Array.from(this.suporteSessions.values()).find( + session => session.chatId === chatId && session.userId === userId + ); + + if (!sessaoAtiva) { + await this.bot.sendMessage(chatId, + `❌ **Sessão expirada**\n\nUse o comando "abrir suporte" para iniciar novamente.`, + { parse_mode: 'Markdown' } + ); + return; + } + + const tecnicos = sessaoAtiva.dados.tecnicos; + if (!tecnicos || index < 0 || index >= tecnicos.length) { + await this.bot.sendMessage(chatId, `❌ **Técnico inválido.** Por favor, tente novamente.`, { parse_mode: 'Markdown' }); + return; + } + + sessaoAtiva.dados.tecnicoSelecionado = tecnicos[index]; + sessaoAtiva.etapa = 'definir_data_hora'; + await this.processarEtapaDefinirDataHora(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 " + */ + async handleVerRoteador(chatId, termo) { + if (!termo) { + await this.bot.sendMessage(chatId, + '📡 *Ver Roteador - GenieACS*\n\n' + + '❓ Informe o serial ou a TAG (login TopSapp) do dispositivo.\n' + + 'Exemplos:\n' + + '• `ver roteador ABCD12345678`\n' + + '• `ver roteador joao.silva`', + { parse_mode: 'Markdown' } + ); + return; + } + + // Escapa caracteres especiais do Markdown V1 em valores dinâmicos + const esc = (v) => { + if (v === null || v === undefined || v === '') return 'N/A'; + return String(v).replace(/[_*`\[]/g, '\\$&'); + }; + + await this.bot.sendMessage(chatId, `🔍 Consultando dispositivo \`${esc(termo)}\` no GenieACS...`, { parse_mode: 'Markdown' }); + + try { + const resultado = await this.genieacs.buscarRoteador(termo); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, + `❌ *Dispositivo não encontrado*\n\n` + + `🔍 Busca: \`${esc(termo)}\`\n` + + `⚠️ ${esc(resultado.error)}`, + { parse_mode: 'Markdown' } + ); + return; + } + + const d = resultado.device; + const tagLabel = d.tags && d.tags.length > 0 ? d.tags.map(esc).join(', ') : 'N/A'; + const buscadoPor = resultado.searchedBy === 'tag' ? 'TAG' : 'Serial'; + + let msg = + `📡 *Informações do Roteador*\n` + + `📌 Instância: ${esc(resultado.instance)}\n` + + `🔎 Encontrado por: ${buscadoPor}\n\n` + + `🖥️ *Dispositivo*\n` + + `├ Nome: ${esc(d.productClass)}\n` + + `├ Fabricante: ${esc(d.manufacturer)}\n` + + `├ OUI: ${esc(d.oui)}\n` + + `├ Serial: \`${esc(d.serial)}\`\n` + + `├ TAG (TopSapp): \`${tagLabel}\`\n` + + `├ HW: ${esc(d.hardwareVersion)}\n` + + `└ SW: ${esc(d.softwareVersion)}\n\n` + + `📶 *Wi-Fi 2.4 GHz*\n` + + `├ SSID: ${esc(d.ssid24)}\n` + + `├ Senha: \`${esc(d.senha24)}\`\n` + + `├ Canal: ${esc(d.channel24)}\n` + + `├ Auto-Canal: ${esc(d.autoChannel24)}\n` + + `└ Clientes: ${d.clients24 !== null && d.clients24 !== undefined ? esc(d.clients24) : 'N/A'}\n\n` + + `📶 *Wi-Fi 5 GHz*\n` + + `├ SSID: ${esc(d.ssid5)}\n` + + `├ Senha: \`${esc(d.senha5)}\`\n` + + `├ Canal: ${esc(d.channel5)}\n` + + `├ Auto-Canal: ${esc(d.autoChannel5)}\n` + + `└ Clientes: ${d.clients5 !== null && d.clients5 !== undefined ? esc(d.clients5) : 'N/A'}`; + + // PPPoE WAN + msg += `\n\n🌐 *PPPoE WAN*\n`; + msg += `├ Username: ${esc(d.pppoeUsername)}\n`; + msg += `├ Status: ${esc(d.pppoeStatus)}\n`; + msg += `├ IP: \`${esc(d.pppoeIP)}\`\n`; + msg += `├ Uptime: ${esc(d.pppoeUptime)}\n`; + msg += `├ Gateway: ${esc(d.gatewayWAN)}\n`; + msg += `├ DNS WAN: ${esc(d.dnsWAN)}\n`; + msg += `├ Link Físico: ${esc(d.linkFisicoWAN)}\n`; + msg += `├ Recebido: ${esc(d.trafegoRecebido)}\n`; + msg += `├ Enviado: ${esc(d.trafegoEnviado)}\n`; + if (d.taxaDownload) msg += `├ ↓ Download: ${esc(d.taxaDownload)}\n`; + if (d.taxaUpload) msg += `├ ↑ Upload: ${esc(d.taxaUpload)}\n`; + msg += `└ Último Erro: ${esc(d.pppoeLastError)}`; + + // DHCP LAN + msg += `\n\n🏠 *LAN/DHCP*\n`; + msg += `├ IP LAN: \`${esc(d.ipLAN)}\`\n`; + msg += `├ Máscara: ${esc(d.mascaraLAN)}\n`; + msg += `├ Início DHCP: ${esc(d.dhcpInicio)}\n`; + msg += `├ Fim DHCP: ${esc(d.dhcpFim)}\n`; + msg += `└ DNS LAN: ${esc(d.dnsLAN)}`; + + await this.bot.sendMessage(chatId, msg, { parse_mode: 'Markdown' }); + + } catch (error) { + logger.error('Erro em handleVerRoteador:', error); + await this.bot.sendMessage(chatId, `❌ Erro ao consultar o GenieACS: ${error.message}`); + } + } + + // ───────────────────────────────────────────────────────────────────────── + // CONFIGURAR ROTEADOR VIA GENIEACS + // ───────────────────────────────────────────────────────────────────────── + + /** + * Mapa de campos configuráveis: chave curta → detalhes do parâmetro TR-069. + * Centralizado aqui para ser usado tanto no menu quanto nos callbacks. + */ + _routerParamMap() { + return { + ssid24: { label: 'SSID 2.4GHz', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID', type: 'xsd:string', toggle: false }, + ssid5: { label: 'SSID 5GHz', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.SSID', type: 'xsd:string', toggle: false }, + senha24: { label: 'Senha Wi-Fi 2.4', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.KeyPassphrase', type: 'xsd:string', toggle: false }, + senha5: { label: 'Senha Wi-Fi 5', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.PreSharedKey.1.KeyPassphrase', type: 'xsd:string', toggle: false }, + ch24: { label: 'Canal 2.4GHz', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.Channel', type: 'xsd:unsignedInt', toggle: false }, + ch5: { label: 'Canal 5GHz', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.Channel', type: 'xsd:unsignedInt', toggle: false }, + auto24: { label: 'Auto-Canal 2.4', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.AutoChannelEnable', type: 'xsd:boolean', toggle: true }, + auto5: { label: 'Auto-Canal 5', param: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.AutoChannelEnable', type: 'xsd:boolean', toggle: true }, + dnswan: { label: 'DNS WAN', param: 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.DNSServers', type: 'xsd:string', toggle: false }, + dnslan: { label: 'DNS LAN', param: 'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.DNSServers', type: 'xsd:string', toggle: false }, + dhcpstart: { label: 'DHCP Início', param: 'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MinAddress', type: 'xsd:string', toggle: false }, + dhcpend: { label: 'DHCP Fim', param: 'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MaxAddress', type: 'xsd:string', toggle: false }, + iplan: { label: 'IP LAN', param: 'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceIPAddress', type: 'xsd:string', toggle: false }, + mascaralan: { label: 'Máscara LAN', param: 'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceSubnetMask', type: 'xsd:string', toggle: false }, + }; + } + + /** + * Comando "configurar roteador ". + * + * Fluxo: + * 1. Se o termo parece serial/tag GenieACS (alphanumérico sem espaços) → busca direto no GenieACS. + * 2. Caso contrário, busca no TopSapp por nome/CPF: + * a. Seleciona o cliente (se houver múltiplos) + * b. Seleciona o serviço com login PPPoE + * c. Usa o login PPPoE como TAG para buscar no GenieACS + * d. Se não encontrar TAG no GenieACS → informa que não é possível configurar + */ + async handleConfigurarRoteador(chatId, userId, termo) { + if (!termo) { + await this.bot.sendMessage(chatId, + '⚙️ *Configurar Roteador*\n\n' + + 'Informe o serial, TAG, nome do cliente ou CPF.\n' + + 'Exemplos:\n' + + '• `configurar roteador ABCD12345678`\n' + + '• `configurar roteador joao.silva`\n' + + '• `configurar roteador João da Silva`\n' + + '• `configurar roteador 123.456.789-10`', + { parse_mode: 'Markdown' } + ); + return; + } + + // CPF sem formatação: 11 dígitos numéricos → formata e encaminha para TopSapp + if (/^\d{11}$/.test(termo)) { + const cpfFormatado = `${termo.slice(0,3)}.${termo.slice(3,6)}.${termo.slice(6,9)}-${termo.slice(9,11)}`; + return this._configurarRoteadorBuscarTopSapp(chatId, userId, cpfFormatado); + } + + // Heurística: serial/tag GenieACS não tem espaço e não é CPF formatado + const pareceGenieACS = !/\s/.test(termo) && !/^\d{3}\.\d{3}\.\d{3}-\d{2}$/.test(termo); + + if (pareceGenieACS) { + // Caminho direto: buscar no GenieACS por serial ou TAG + await this._configurarRoteadorPorGenieACS(chatId, userId, termo); + } else { + // Caminho via TopSapp: buscar cliente por nome/CPF + await this._configurarRoteadorBuscarTopSapp(chatId, userId, termo); + } + } + + /** + * Busca o dispositivo diretamente no GenieACS e abre o menu de configuração. + */ + async _configurarRoteadorPorGenieACS(chatId, userId, termo) { + await this.bot.sendMessage(chatId, `🔍 Buscando dispositivo \`${termo}\` no GenieACS...`, { parse_mode: 'Markdown' }); + try { + const resultado = await this.genieacs.buscarRoteador(termo); + if (!resultado.success) { + await this.bot.sendMessage(chatId, + `❌ *Dispositivo não encontrado no GenieACS*\n\n` + + `🔍 Busca: \`${termo}\`\n` + + `⚠️ ${resultado.error || 'Nenhum resultado'}\n\n` + + `_Tente buscar pelo nome ou CPF do cliente._`, + { parse_mode: 'Markdown' } + ); + return; + } + this.userSessions.set(userId, { + action: 'router_cfg', + deviceId: resultado.device.deviceId, + device: resultado.device, + instance: resultado.instance || '', + timestamp: Date.now() + }); + await this._enviarMenuConfigurarRoteador(chatId, resultado.device, resultado.instance || ''); + } catch (error) { + logger.error('Erro em _configurarRoteadorPorGenieACS:', error); + await this.bot.sendMessage(chatId, `❌ Erro ao consultar o GenieACS: ${error.message}`); + } + } + + /** + * Busca o cliente no TopSapp por nome/CPF e apresenta seleção. + */ + async _configurarRoteadorBuscarTopSapp(chatId, userId, query) { + await this.bot.sendMessage(chatId, `🔍 Buscando cliente "${query}" no TopSapp...`); + try { + const resultado = await this.topsapp.buscarCliente(query); + if (!resultado.success) { + await this.bot.sendMessage(chatId, `❌ Erro na busca: ${resultado.error}`); + return; + } + if (!resultado.clientes || resultado.clientes.length === 0) { + await this.bot.sendMessage(chatId, `🔍 Nenhum cliente encontrado para: "${query}"`); + return; + } + + const instancia = resultado.instancia || ''; + + if (resultado.clientes.length === 1) { + await this._configurarRoteadorSelecionarServico(chatId, userId, resultado.clientes[0].id, instancia); + return; + } + + // Múltiplos clientes → seleção + let msg = `🔍 *${resultado.clientes.length} clientes encontrados:*\n\n`; + const keyboard = []; + resultado.clientes.slice(0, 8).forEach((c, i) => { + msg += `*${i + 1}.* ${c.nome}`; + if (c.cpf) msg += ` — \`${c.cpf}\``; + msg += '\n'; + keyboard.push([{ + text: `⚙️ ${c.nome.substring(0, 35)}`, + callback_data: `cfg_roteador_cli_${c.id}_${instancia}` + }]); + }); + + await this.bot.sendMessage(chatId, msg, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + } catch (error) { + logger.error('Erro em _configurarRoteadorBuscarTopSapp:', error); + await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); + } + } + + /** + * Busca os serviços do cliente e apresenta seleção (ou vai direto se só há 1). + */ + async _configurarRoteadorSelecionarServico(chatId, userId, idCliente, location) { + const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviços: ${servicosResult.error}`); + return; + } + + const servicos = (servicosResult.servicos || []).filter(s => { + const st = s.id_status || s.statusNumerico; + return st === 1 || st === '1' || + String(s.status).toLowerCase() === 'liberado' || + String(s.status).toLowerCase() === 'ativo'; + }); + + if (servicos.length === 0) { + await this.bot.sendMessage(chatId, `⚠️ Cliente sem serviços ativos.`); + return; + } + + if (servicos.length === 1) { + await this._configurarRoteadorPorServico(chatId, userId, servicos[0], location); + return; + } + + let msg = `⚙️ *Selecione o serviço:*\n\n`; + const keyboard = []; + servicos.forEach((s, i) => { + const nome = s.descricao || s.nome || `Serviço ${i + 1}`; + const login = s.usuario || s.pppoe_login || s.login || ''; + msg += `*${i + 1}.* ${nome}${login ? ` — \`${login}\`` : ''}\n`; + keyboard.push([{ + text: `⚙️ ${nome.substring(0, 35)}`, + callback_data: `cfg_roteador_srv_${idCliente}_${s.id_servico || s.id}_${location}` + }]); + }); + + await this.bot.sendMessage(chatId, msg, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + } + + /** + * A partir de um serviço TopSapp, extrai o login PPPoE, busca a TAG no GenieACS e abre o menu. + */ + async _configurarRoteadorPorServico(chatId, userId, servico, location) { + const pppoeLogin = servico.usuario || servico.pppoe_login || servico.login || null; + + if (!pppoeLogin) { + await this.bot.sendMessage(chatId, + `⚠️ O serviço selecionado não possui login PPPoE cadastrado.\n` + + `Não é possível localizar o roteador no GenieACS sem esse dado.` + ); + return; + } + + await this.bot.sendMessage(chatId, `🔍 Localizando roteador pela TAG \`${pppoeLogin}\` no GenieACS...`, { parse_mode: 'Markdown' }); + + try { + const resultado = await this.genieacs.buscarRoteador(pppoeLogin, location); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, + `⚠️ *Roteador não encontrado no GenieACS*\n\n` + + `🏷️ TAG pesquisada: \`${pppoeLogin}\`\n\n` + + `Não é possível realizar alterações no roteador deste cliente pelo bot.\n` + + `_O dispositivo pode não estar cadastrado no GenieACS ou a TAG ainda não foi sincronizada._`, + { parse_mode: 'Markdown' } + ); + return; + } + + this.userSessions.set(userId, { + action: 'router_cfg', + deviceId: resultado.device.deviceId, + device: resultado.device, + instance: resultado.instance || '', + timestamp: Date.now() + }); + + await this._enviarMenuConfigurarRoteador(chatId, resultado.device, resultado.instance || ''); + } catch (error) { + logger.error('Erro em _configurarRoteadorPorServico:', error); + await this.bot.sendMessage(chatId, `❌ Erro ao consultar o GenieACS: ${error.message}`); + } + } + + /** + * Envia/atualiza a mensagem de menu de configuração do roteador. + */ + async _enviarMenuConfigurarRoteador(chatId, device, instance) { + const esc = (v) => (v === null || v === undefined || v === '') ? 'N/A' : String(v).replace(/[_*`\[]/g, '\\$&'); + + const auto24On = device.autoChannel24 === 'Sim'; + const auto5On = device.autoChannel5 === 'Sim'; + + const sumario = + `⚙️ *Configurar Roteador*\n` + + `📌 ${esc(instance)} — \`${esc(device.serial)}\`\n\n` + + `📶 *Wi-Fi 2.4 GHz*\n` + + `├ SSID: ${esc(device.ssid24)}\n` + + `├ Senha: \`${esc(device.senha24)}\`\n` + + `├ Canal: ${esc(device.channel24)} (Auto: ${esc(device.autoChannel24)})\n\n` + + `📶 *Wi-Fi 5 GHz*\n` + + `├ SSID: ${esc(device.ssid5)}\n` + + `├ Senha: \`${esc(device.senha5)}\`\n` + + `├ Canal: ${esc(device.channel5)} (Auto: ${esc(device.autoChannel5)})\n\n` + + `🌐 *WAN/PPPoE*\n` + + `├ DNS WAN: ${esc(device.dnsWAN)}\n\n` + + `🏠 *LAN/DHCP*\n` + + `├ IP LAN: ${esc(device.ipLAN)}\n` + + `├ Máscara: ${esc(device.mascaraLAN)}\n` + + `├ DNS LAN: ${esc(device.dnsLAN)}\n` + + `├ DHCP Início: ${esc(device.dhcpInicio)}\n` + + `└ DHCP Fim: ${esc(device.dhcpFim)}\n\n` + + `_Selecione o que deseja alterar:_`; + + const keyboard = { + inline_keyboard: [ + [ + { text: '📶 SSID 2.4GHz', callback_data: 'router_cfg_ssid24' }, + { text: '📶 SSID 5GHz', callback_data: 'router_cfg_ssid5' }, + ], + [ + { text: '🔑 Senha 2.4GHz', callback_data: 'router_cfg_senha24' }, + { text: '🔑 Senha 5GHz', callback_data: 'router_cfg_senha5' }, + ], + [ + { text: `${auto24On ? '✅' : '⬜'} Auto-Canal 2.4`, callback_data: 'router_cfg_auto24' }, + { text: `${auto5On ? '✅' : '⬜'} Auto-Canal 5`, callback_data: 'router_cfg_auto5' }, + ], + [ + { text: '📡 Canal 2.4GHz', callback_data: 'router_cfg_ch24' }, + { text: '📡 Canal 5GHz', callback_data: 'router_cfg_ch5' }, + ], + [ + { text: '🌐 DNS WAN', callback_data: 'router_cfg_dnswan' }, + { text: '🏠 DNS LAN', callback_data: 'router_cfg_dnslan' }, + ], + [ + { text: '▶ DHCP Início', callback_data: 'router_cfg_dhcpstart' }, + { text: '◀ DHCP Fim', callback_data: 'router_cfg_dhcpend' }, + ], + [ + { text: '🖥️ IP LAN', callback_data: 'router_cfg_iplan' }, + { text: '🔢 Máscara LAN', callback_data: 'router_cfg_mascaralan'}, + ], + [ + { text: '🔄 Atualizar leitura', callback_data: 'router_cfg_refresh' }, + { text: '❌ Fechar', callback_data: 'cancel_action' }, + ], + ] + }; + + await this.bot.sendMessage(chatId, sumario, { parse_mode: 'Markdown', reply_markup: keyboard }); + } + + /** + * Dispatcher de todos os callbacks com prefixo "router_cfg_". + */ + async handleRouterCfgCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const userId = callbackQuery.from.id; + const msgId = callbackQuery.message.message_id; + + await this.bot.answerCallbackQuery(callbackQuery.id); + + const session = this.userSessions.get(userId); + if (!session || session.action !== 'router_cfg') { + await this.bot.sendMessage(chatId, '⚠️ Sessão expirada. Use `configurar roteador ` novamente.', { parse_mode: 'Markdown' }); + return; + } + + const field = data.replace('router_cfg_', ''); // ex.: "ssid24", "auto24", "ch24_6", "refresh" + + // ── Atualizar leitura ──────────────────────────────────────────────── + if (field === 'refresh') { + await this.bot.sendMessage(chatId, '🔄 Relendo dados do dispositivo...'); + const fresh = await this.genieacs.buscarRoteadorPorId(session.deviceId, session.instance); + if (!fresh.success) { + await this.bot.sendMessage(chatId, `❌ Falha ao reler dispositivo: ${fresh.error}`); + return; + } + session.device = fresh.device; + this.userSessions.set(userId, session); + await this._enviarMenuConfigurarRoteador(chatId, fresh.device, session.instance); + return; + } + + // ── Seleção de canal vinda do picker inline (ch24_N ou ch5_N) ─────── + const chPickMatch = field.match(/^(ch24|ch5)_(\d+)$/); + if (chPickMatch) { + const band = chPickMatch[1]; // 'ch24' ou 'ch5' + const canal = parseInt(chPickMatch[2], 10); + const paramMap = this._routerParamMap(); + const meta = paramMap[band]; + + await this.bot.sendMessage(chatId, `⏳ Aplicando ${meta.label} → canal ${canal}...`); + const res = await this.genieacs.configurarRoteador( + session.deviceId, + [[meta.param, canal, 'xsd:unsignedInt']], + session.instance + ); + if (!res.success) { + await this.bot.sendMessage(chatId, `❌ Falha: ${res.error}`); + } else { + if (band === 'ch24') session.device.channel24 = String(canal); + else session.device.channel5 = String(canal); + this.userSessions.set(userId, session); + const okMsg = res.queued + ? `⏳ *${meta.label}* → canal *${canal}* enfileirado. O roteador aplicará na próxima conexão.` + : `✅ *${meta.label}* definido para o canal *${canal}*.`; + await this.bot.sendMessage(chatId, okMsg, { parse_mode: 'Markdown' }); + } + await this._enviarMenuConfigurarRoteador(chatId, session.device, session.instance); + return; + } + + const paramMap = this._routerParamMap(); + + // ── Toggle de auto-canal ───────────────────────────────────────────── + if (field === 'auto24' || field === 'auto5') { + const meta = paramMap[field]; + const atualSim = field === 'auto24' + ? session.device.autoChannel24 === 'Sim' + : session.device.autoChannel5 === 'Sim'; + const novoValor = !atualSim; // inverte + + await this.bot.sendMessage(chatId, `⏳ ${atualSim ? 'Desativando' : 'Ativando'} ${meta.label}...`); + const res = await this.genieacs.configurarRoteador( + session.deviceId, + [[meta.param, novoValor, 'xsd:boolean']], + session.instance + ); + + if (!res.success) { + await this.bot.sendMessage(chatId, `❌ Falha: ${res.error}`); + return; + } + // Atualiza o device local + if (field === 'auto24') session.device.autoChannel24 = novoValor ? 'Sim' : 'Não'; + else session.device.autoChannel5 = novoValor ? 'Sim' : 'Não'; + this.userSessions.set(userId, session); + + const autoMsg = res.queued + ? `⏳ ${meta.label} ${novoValor ? 'ativação' : 'desativação'} enfileirada. O roteador aplicará na próxima conexão.` + : `✅ ${meta.label} ${novoValor ? 'ativado' : 'desativado'} com sucesso.`; + await this.bot.sendMessage(chatId, autoMsg); + await this._enviarMenuConfigurarRoteador(chatId, session.device, session.instance); + return; + } + + // ── Picker de canal com botões inline ──────────────────────────────── + if (field === 'ch24' || field === 'ch5') { + const meta = paramMap[field]; + const canalAtual = field === 'ch24' ? (session.device.channel24 || '') : (session.device.channel5 || ''); + const is24 = field === 'ch24'; + + const canais24 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; + const canais5 = [36, 40, 44, 48, 52, 60, 64, 149, 153, 157, 161]; + const canais = is24 ? canais24 : canais5; + + // Monta rows de 4 botões por linha + const rows = []; + for (let i = 0; i < canais.length; i += 4) { + rows.push( + canais.slice(i, i + 4).map(ch => ({ + text: String(ch) === String(canalAtual) ? `✅ ${ch}` : String(ch), + callback_data: `router_cfg_${field}_${ch}` + })) + ); + } + rows.push([{ text: '↩️ Voltar', callback_data: 'router_cfg_refresh' }]); + + await this.bot.sendMessage(chatId, + `📡 *${meta.label}*\nCanal atual: \`${canalAtual || 'N/A'}\`\n\nEscolha o novo canal:`, + { parse_mode: 'Markdown', reply_markup: { inline_keyboard: rows } } + ); + return; + } + + // ── Campos de texto/número: solicita input ─────────────────────────── + const meta = paramMap[field]; + if (!meta) { + await this.bot.sendMessage(chatId, `❓ Campo desconhecido: \`${field}\``, { parse_mode: 'Markdown' }); + return; + } + + // Valor atual para mostrar ao usuário + const fieldToCurrent = { + ssid24: 'ssid24', ssid5: 'ssid5', + senha24: 'senha24', senha5: 'senha5', + dnswan: 'dnsWAN', dnslan: 'dnsLAN', + dhcpstart: 'dhcpInicio', dhcpend: 'dhcpFim', + iplan: 'ipLAN', mascaralan: 'mascaraLAN', + }; + const atualKey = fieldToCurrent[field]; + const valorAtual = atualKey ? (session.device[atualKey] || 'N/A') : 'N/A'; + + let hint = ''; + if (field.startsWith('dns')) hint = ' (ex.: 8.8.8.8,8.8.4.4)'; + else if (field.startsWith('dhcp')) hint = ' (ex.: 192.168.1.100)'; + else if (field === 'iplan') hint = ' (ex.: 192.168.1.1)'; + else if (field === 'mascaralan') hint = ' (ex.: 255.255.255.0)'; + + // Promove o estado de sessão para aguardar o valor + session.action = `router_cfg_wait_${field}`; + this.userSessions.set(userId, session); + + await this.bot.sendMessage(chatId, + `⚙️ *Alterar ${meta.label}*\n\n` + + `Valor atual: \`${valorAtual}\`\n\n` + + `Digite o novo valor${hint}:\n` + + `_(ou envie /cancelar para abortar)_`, + { parse_mode: 'Markdown' } + ); + } + + /** + * Processa o valor digitado pelo usuário para alterar um parâmetro do roteador. + * Chamado por handleUserSessionInput quando session.action começa com "router_cfg_wait_". + */ + async _processarInputConfigurarRoteador(chatId, userId, session, text) { + const field = session.action.replace('router_cfg_wait_', ''); // ex.: "ssid24" + const paramMap = this._routerParamMap(); + const meta = paramMap[field]; + + if (!meta) { + this.userSessions.delete(userId); + await this.bot.sendMessage(chatId, '❌ Campo desconhecido. Operação cancelada.'); + return; + } + + // Cancela se o usuário enviou /cancelar + if (text.trim().toLowerCase() === '/cancelar') { + session.action = 'router_cfg'; + this.userSessions.set(userId, session); + await this.bot.sendMessage(chatId, '🚫 Alteração cancelada.'); + await this._enviarMenuConfigurarRoteador(chatId, session.device, session.instance); + return; + } + + // Converte para o tipo correto + let valorFinal; + if (meta.type === 'xsd:unsignedInt') { + const n = parseInt(text.trim(), 10); + if (isNaN(n) || n < 0) { + await this.bot.sendMessage(chatId, `❌ Valor inválido. Informe um número inteiro positivo.`); + return; // mantém sessão aguardando nova tentativa + } + valorFinal = n; + } else { + valorFinal = text.trim(); + if (!valorFinal) { + await this.bot.sendMessage(chatId, `❌ Valor não pode ser vazio.`); + return; + } + } + + await this.bot.sendMessage(chatId, `⏳ Aplicando ${meta.label}...`); + + const res = await this.genieacs.configurarRoteador( + session.deviceId, + [[meta.param, valorFinal, meta.type]], + session.instance + ); + + if (!res.success) { + await this.bot.sendMessage(chatId, `❌ Falha ao aplicar: ${res.error}`); + session.action = 'router_cfg'; + this.userSessions.set(userId, session); + return; + } + + const statusMsg = res.queued + ? `⏳ *${meta.label}* → \`${valorFinal}\`\n_Task enfileirada. O roteador aplicará na próxima conexão com o servidor._` + : `✅ *${meta.label}* atualizado para: \`${valorFinal}\``; + await this.bot.sendMessage(chatId, statusMsg, { parse_mode: 'Markdown' }); + + // Atualiza o device local com o novo valor para o menu refletir + const fieldToDeviceKey = { + ssid24: 'ssid24', ssid5: 'ssid5', + senha24: 'senha24', senha5: 'senha5', + ch24: 'channel24', ch5: 'channel5', + dnswan: 'dnsWAN', dnslan: 'dnsLAN', + dhcpstart: 'dhcpInicio', dhcpend: 'dhcpFim', + iplan: 'ipLAN', mascaralan: 'mascaraLAN', + }; + const devKey = fieldToDeviceKey[field]; + if (devKey) session.device[devKey] = String(valorFinal); + session.action = 'router_cfg'; + this.userSessions.set(userId, session); + + await this._enviarMenuConfigurarRoteador(chatId, session.device, session.instance); + } + + // ========== IPoE ========== + + /** + * Handler para comando "mudar para ipoe" + * Busca o cliente e inicia fluxo de seleção de serviço + */ + async handleMudarIPoE(chatId, target) { + if (!target) { + await this.bot.sendMessage(chatId, + `🔗 *Mudar para IPoE*\n\n` + + `💡 *Como usar:*\n` + + `• "mudar para ipoe João da Silva"\n` + + `• "ipoe 123.456.789-10"\n` + + `• "mudar ipoe 12345" (ID do cliente)\n\n` + + `O comando gera os dados de DHCP Option 82 para o TopSapp e configura a VLAN 977 (IPoE) na OLT automaticamente.`, + { parse_mode: 'Markdown' } + ); + return; + } + + try { + await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); + + let location = ''; + let query = target; + + if (/guarai|colmeia/i.test(target)) { + location = 'Guarai/Colmeia'; + query = target.replace(/(guarai|colmeia)/gi, '').trim(); + } else if (/pedro afonso/i.test(target)) { + location = 'Pedro Afonso'; + query = target.replace(/pedro afonso/gi, '').trim(); + } + + const resultado = await this.topsapp.buscarCliente(query, location); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, `❌ Erro na busca: ${resultado.error}`); + return; + } + + if (!resultado.clientes || resultado.clientes.length === 0) { + await this.bot.sendMessage(chatId, `🔍 Nenhum cliente encontrado para: "${query}"`); + return; + } + + const instancia = resultado.instancia || location; + + if (resultado.clientes.length === 1) { + await this._mostrarServicosParaIPoE(chatId, resultado.clientes[0].id, instancia); + return; + } + + // Múltiplos clientes → teclado de seleção + let message = `🔗 *Mudar para IPoE — Selecione o cliente:*\n\n`; + const keyboard = []; + + resultado.clientes.slice(0, 8).forEach((cliente, index) => { + message += `*${index + 1}.* ${cliente.nome}`; + if (cliente.cpf) message += ` — CPF: \`${cliente.cpf}\``; + message += `\n`; + keyboard.push([{ + text: `🔗 ${cliente.nome.substring(0, 30)}`, + callback_data: `ipoe_cliente_${cliente.id}_${instancia}` + }]); + }); + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + + } catch (error) { + console.error('❌ Erro em handleMudarIPoE:', error); + await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); + } + } + + /** + * Callback quando o usuário seleciona um cliente para IPoE + */ + async handleIPoEClienteCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('ipoe_cliente_', '').split('_'); + const idCliente = parts[0]; + const location = parts.slice(1).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this._mostrarServicosParaIPoE(chatId, idCliente, location); + } + + /** + * Exibe serviços ativos do cliente com endereço e login para seleção (IPoE) + */ + async _mostrarServicosParaIPoE(chatId, idCliente, location) { + await this.bot.sendMessage(chatId, '📋 Carregando serviços do cliente...'); + + const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); + + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviços: ${servicosResult.error}`); + return; + } + + const servicos = (servicosResult.servicos || []).filter(s => { + const st = s.id_status || s.statusNumerico; + return st === 1 || st === '1' || String(s.status).toLowerCase() === 'liberado'; + }); + + if (servicos.length === 0) { + await this.bot.sendMessage(chatId, `⚠️ Cliente sem serviços ativos para configurar IPoE.`); + return; + } + + let message = `🔗 *Mudar para IPoE — Serviços do cliente:*\n\n`; + const keyboard = []; + const esc = (v) => String(v).replace(/_/g, '\\_'); + + servicos.forEach((s, i) => { + const nome = s.descricao || s.nome || `Serviço ${i + 1}`; + const serial = s.serial || s.mac || 'sem serial'; + const login = s.usuario || s.pppoe_login || s.login || 'sem login'; + const endereco = s.instalacao_endereco || ''; + const numero = s.instalacao_numero || ''; + const bairro = s.instalacao_bairro || ''; + const cidade = s.instalacao_cidade || ''; + + message += `📦 *${i + 1}. ${esc(nome)}*\n`; + message += ` 👤 Login: \`${login}\`\n`; + message += ` 📟 Serial: \`${serial}\`\n`; + if (endereco) { + message += ` 📍 ${endereco}${numero ? `, ${numero}` : ''}`; + if (bairro) message += ` — ${bairro}`; + if (cidade) message += `, ${cidade}`; + message += `\n`; + } + message += `\n`; + + keyboard.push([{ + text: `🔗 ${(i + 1)}. ${nome.substring(0, 30)}`, + callback_data: `ipoe_servico_${idCliente}_${s.id_servico || s.id}_${location}` + }]); + }); + + message += `💡 Selecione o serviço para configurar IPoE:`; + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + } + + /** + * Callback quando o usuário seleciona um serviço para IPoE + */ + async handleIPoEServicoCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('ipoe_servico_', '').split('_'); + const idCliente = parts[0]; + const idServico = parts[1]; + const location = parts.slice(2).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + + // Recarregar serviço para ter dados atualizados + const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviço: ${servicosResult.error}`); + return; + } + + const servico = (servicosResult.servicos || []).find(s => + String(s.id_servico || s.id) === String(idServico) + ); + + if (!servico) { + await this.bot.sendMessage(chatId, `❌ Serviço #${idServico} não encontrado.`); + return; + } + + await this._confirmarMudarIPoE(chatId, idCliente, servico, location); + } + + /** + * Mostra preview do que será alterado e pede confirmação + */ + async _confirmarMudarIPoE(chatId, idCliente, servico, location) { + const serial = servico.serial || servico.mac || ''; + const baseInfo = servico.base || ''; + const prefixo = this.topsapp._resolverPrefixoIPoE + ? this.topsapp._resolverPrefixoIPoE(baseInfo) + : null; + + if (!serial) { + await this.bot.sendMessage(chatId, + `❌ *Serviço sem serial de ONU cadastrado.*\n\n` + + `Não é possível gerar o Circuit-ID sem o serial.\n` + + `Verifique o cadastro do serviço no TopSapp.`, + { parse_mode: 'Markdown' } + ); + return; + } + + if (!prefixo) { + await this.bot.sendMessage(chatId, + `❌ *Não foi possível identificar a OLT/base deste serviço.*\n\n` + + `Base cadastrada: "${baseInfo || 'vazio'}"\n` + + `Verifique o cadastro do serviço no TopSapp.`, + { parse_mode: 'Markdown' } + ); + return; + } + + const circuitId = `${prefixo}:${serial}`; + const nome = servico.descricao || servico.nome || 'N/A'; + const idServico = servico.id_servico || servico.id; + + // Escapar underscores para Telegram Markdown + const esc = (v) => String(v).replace(/_/g, '\\_'); + + let message = `🔗 *Confirmar mudança para IPoE*\n\n`; + message += `📦 *Serviço:* ${esc(nome)}\n`; + message += `🆔 *ID Serviço:* ${idServico}\n`; + message += `📟 *Serial ONU:* \`${serial}\`\n`; + message += `🏢 *Base/OLT:* ${esc(baseInfo)}\n`; + message += `🏷️ *Prefixo:* ${esc(prefixo)}\n\n`; + message += `*Será configurado:*\n`; + message += `📝 DHCP Username: \`${circuitId}\`\n`; + message += `🔗 Agent Circuit ID: \`${circuitId}\`\n`; + message += `✅ DHCP Liberar: *Sim*\n`; + message += `📡 VLAN OLT: *977* (IPoE)\n\n`; + + if (prefixo === 'FH_GRI') { + message += `🔧 _OLT FiberHome — WAN Service será reconfigurado para VLAN 977_\n`; + } else { + message += `🔧 _OLT Datacom — Ethernet e Service-Port serão reconfigurados para VLAN 977_\n`; + } + message += `Deseja confirmar?`; + + const keyboard = [ + [ + { text: '✅ Confirmar', callback_data: `confirm_ipoe_${idCliente}_${idServico}_${location}` }, + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ]; + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + } + + /** + * Callback de confirmação: executa a mudança para IPoE + */ + async handleConfirmIPoECallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('confirm_ipoe_', '').split('_'); + const idCliente = parts[0]; + const idServico = parts[1]; + const location = parts.slice(2).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.sendMessage(chatId, '⏳ Processando mudança para IPoE...'); + + const resultado = await this.topsapp.mudarParaIPoE(idCliente, idServico, location); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, + `❌ *Erro ao gerar configuração IPoE*\n\n⚠️ ${resultado.error}`, + { parse_mode: 'Markdown' } + ); + return; + } + + const esc = (v) => String(v).replace(/_/g, '\\_'); + + // Mostrar dados do TopSapp para configuração manual + let msg = `📋 *Configuração IPoE — Dados para o TopSapp*\n\n`; + msg += `📦 *Serviço:* ${esc(resultado.servico)}\n`; + msg += `👤 *Login:* \`${resultado.login || 'N/A'}\`\n`; + msg += `📟 *Serial:* \`${resultado.serial}\`\n`; + msg += `🏢 *OLT:* ${esc(resultado.baseInfo)} (${esc(resultado.prefixo)})\n\n`; + msg += `✏️ *Configure no painel TopSapp:*\n`; + msg += `▸ DHCP Username: \`${resultado.circuitId}\`\n`; + msg += `▸ Agent Circuit ID: \`${resultado.circuitId}\`\n`; + msg += `▸ DHCP Liberar: *Sim*\n\n`; + msg += `⚠️ _A API não suporta campos DHCP — configure manualmente no painel._\n`; + + await this.bot.sendMessage(chatId, msg, { parse_mode: 'Markdown' }); + + // Configuração OLT automática + await this.bot.sendMessage(chatId, '📡 Configurando VLAN 977 na OLT...'); + + try { + let oltResult; + + if (resultado.prefixo === 'FH_GRI') { + // FiberHome — SET-WANSERVICE com VLAN 977 + oltResult = await this.fiberhomeTL1.configurarIPoEFiberhome(resultado.serial); + } else { + // Datacom — precisa descobrir slot/onuId pelo serial + const prefixoParaOlt = { + 'DIT': 'olt_dito', + 'CLM01': 'olt_colmeia', + 'CAN01': 'olt_canaa', + 'CAN02': 'olt_canaa2', + 'PA01': 'olt_pedro_afonso', + 'PA02': 'olt_canavieiras' + }; + + const oltId = prefixoParaOlt[resultado.prefixo]; + if (!oltId) { + await this.bot.sendMessage(chatId, `❌ OLT não mapeada para prefixo: ${esc(resultado.prefixo)}`); + return; + } + + // Buscar ONU na OLT pelo serial + const onuInfo = await this.ssh.findOnuBySerial(oltId, resultado.serial); + if (!onuInfo.success || !onuInfo.onuInfo?.found) { + await this.bot.sendMessage(chatId, + `❌ *ONU não encontrada na OLT*\n\n` + + `Serial: \`${resultado.serial}\`\n` + + `OLT: ${esc(oltId)}\n\n` + + `Verifique se o serial está correto no TopSapp.`, + { parse_mode: 'Markdown' } + ); + return; + } + + const { slot, onuId } = onuInfo.onuInfo; + await this.bot.sendMessage(chatId, `🔍 ONU encontrada: Slot ${slot}, ONU ${onuId}`); + + oltResult = await this.ssh.configIPoEDatacom(oltId, slot, onuId); + } + + if (oltResult.success) { + let successMsg = `✅ *VLAN alterada para 977 na OLT!*\n\n`; + if (resultado.prefixo === 'FH_GRI') { + successMsg += `📡 FiberHome — WAN Service reconfigurado\n`; + if (oltResult.details) { + successMsg += `🔌 Portas configuradas: ${oltResult.details.portas}\n`; + } + } else { + successMsg += `📡 Datacom — Ethernet + Service-Port reconfigurados\n`; + if (oltResult.oldServicePorts) { + successMsg += `🔌 Service-ports antigos removidos: ${oltResult.oldServicePorts.join(', ')}\n`; + } + if (oltResult.newServicePortId) { + successMsg += `🆕 Novo service-port: ${oltResult.newServicePortId}\n`; + } + } + successMsg += `\n✅ *IPoE configurado com sucesso!*`; + await this.bot.sendMessage(chatId, successMsg, { parse_mode: 'Markdown' }); + } else { + await this.bot.sendMessage(chatId, + `❌ *Erro ao configurar OLT*\n\n⚠️ ${oltResult.message || oltResult.error}`, + { parse_mode: 'Markdown' } + ); + } + } catch (error) { + console.error('❌ Erro na configuração OLT IPoE:', error); + await this.bot.sendMessage(chatId, + `❌ *Erro ao configurar OLT*\n\n⚠️ ${error.message}`, + { parse_mode: 'Markdown' } + ); + } + } +} + +module.exports = BotHandler; + diff --git a/src/fiberhome/manager_tl1.js b/src/fiberhome/manager_tl1.js index 1ba54fe..043b272 100644 --- a/src/fiberhome/manager_tl1.js +++ b/src/fiberhome/manager_tl1.js @@ -884,6 +884,78 @@ class FiberhomeManager { return diagnostico; } + + /** + * Configura IPoE em ONU FiberHome: SET-WANSERVICE com VLAN 977 + * Busca a ONU registrada pela serial e reconfigura as portas WAN + * @param {string} serial - MAC/Serial da ONU + * @param {string} onuType - Tipo da ONU (para determinar número de portas) + * @returns {Promise} - Promise com resultado + */ + async configurarIPoEFiberhome(serial, onuType = 'AN5506-02-B') { + try { + const VLAN_IPOE = 977; + this.logger.info('Iniciando configuração IPoE FiberHome via TL1', { serial, vlan: VLAN_IPOE }); + + const conectado = await this.conectarUNM2000(); + if (!conectado) { + throw new Error('Não foi possível conectar ao UNM2000'); + } + + // Buscar ONU registrada pelo serial (retorna o objeto diretamente ou lança exceção) + const onuInfo = await this.buscarOnuRegistrada(serial); + + const nameOlt = onuInfo.nameOlt || this.config.olt.name; + const portAlias = onuInfo.portAlias; + + this.logger.info('ONU encontrada, reconfigurando WAN Service', { nameOlt, portAlias, serial, vlan: VLAN_IPOE }); + + // SET-WANSERVICE com VLAN 977 + const numPortas = onuType === 'AN5506-02-B' ? 2 : 1; + + for (let porta = 1; porta <= numPortas; porta++) { + const setWanCmd = `SET-WANSERVICE::OLTID=${nameOlt},PONID=NA-NA-${portAlias},ONUIDTYPE=MAC,ONUID=${serial}:${this.commandId++}::STATUS=1,MODE=2,CONNTYPE=1,VLAN=${VLAN_IPOE},COS=0,QOS=2,UPORT=${porta};`; + this.logger.debug(`Configurando WAN Service IPoE porta ${porta}`, { comando: setWanCmd }); + + const resultado = await this.executarComandoTL1(setWanCmd); + if (!resultado.success) { + this.logger.warn(`Aviso ao configurar WAN porta ${porta}: ${resultado.error}`); + } + + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + // Reiniciar ONU para aplicar + await new Promise(resolve => setTimeout(resolve, 2000)); + const resetCmd = `RESET-ONU::OLTID=${nameOlt},PONID=NA-NA-${portAlias},ONUIDTYPE=MAC,ONUID=${serial}:${this.commandId++}::;`; + this.logger.debug('Reiniciando ONU após IPoE', { comando: resetCmd }); + const resultadoReset = await this.executarComandoTL1(resetCmd); + if (!resultadoReset.success) { + this.logger.warn(`Aviso ao reiniciar ONU: ${resultadoReset.error}`); + } + + this.logger.info('IPoE FiberHome configurado com sucesso', { serial, vlan: VLAN_IPOE }); + + return { + success: true, + message: `VLAN alterada para ${VLAN_IPOE} (IPoE)`, + details: { + olt: nameOlt, + porta: portAlias, + serial, + vlan: VLAN_IPOE, + portas: numPortas + } + }; + + } catch (error) { + this.logger.error('Erro ao configurar IPoE FiberHome', { serial, error: error.message }); + return { + success: false, + error: error.message + }; + } + } } module.exports = FiberhomeManager; diff --git a/src/ssh/manager.js b/src/ssh/manager.js index 04decc3..256de6d 100644 --- a/src/ssh/manager.js +++ b/src/ssh/manager.js @@ -1832,6 +1832,196 @@ class SSHManager { }; } } + + /** + * Configura IPoE em ONU Datacom: altera VLAN da ethernet e service-ports para 977 + * 1. Busca service-ports existentes + * 2. Busca a ONU (slot/onuId) pelo serial + * 3. Remove service-ports antigos + * 4. Altera ethernet native vlan para 977 + * 5. Cria novos service-ports com VLAN 977 + * 6. Commit + * @param {string} equipmentId - ID do equipamento OLT (ex: olt_dito) + * @param {string} slot - Slot da ONU (ex: 1/1/1) + * @param {string} onuId - ID da ONU (ex: 5) + * @returns {Promise} - Promise com resultado da operação + */ + async configIPoEDatacom(equipmentId, slot, onuId) { + console.log(`🔗 [IPoE Datacom] Iniciando - OLT: ${equipmentId}, Slot: ${slot}, ONU: ${onuId}`); + + return new Promise(async (resolve, reject) => { + try { + const connection = await this.connect(equipmentId); + if (!connection.success) { + throw new Error(`Falha na conexão: ${connection.message}`); + } + + // Buscar service-ports existentes + const spCommand = `show running-config service-port | context-match "gpon ${slot} onu ${onuId}"`; + const spResult = await this.executeCommand(connection.connectionId, spCommand); + const servicePorts = spResult.success ? this.parseServicePorts(spResult.output) : []; + + if (servicePorts.length === 0) { + this.disconnect(connection.connectionId); + return resolve({ + success: false, + message: `Nenhum service-port encontrado para ONU ${onuId} no slot ${slot}` + }); + } + + console.log(`🔌 [IPoE Datacom] Service-ports encontrados: ${servicePorts.join(', ')}`); + + const conn = this.connections.get(connection.connectionId); + const VLAN_IPOE = 977; + + conn.connection.shell((err, stream) => { + if (err) { + this.disconnect(connection.connectionId); + reject(new Error(`Erro ao abrir shell: ${err.message}`)); + return; + } + + let output = ''; + let step = 0; + let isComplete = false; + let newServicePortId = null; + + // Sequência de comandos: + // 0: config + // 1..N: no service-port X (remover antigos) + // N+1: interface gpon {slot} + // N+2: onu {onuId} + // N+3: ethernet 1 + // N+4: native vlan vlan-id 977 + // N+5: top + // N+6: service-port new + // N+7: gpon {slot} onu {onuId} gem 1 match vlan vlan-id 977 action vlan replace vlan-id 977 + // N+8: commit + // N+9: top + // N+10: exit + // N+11: exit + const commands = [ + 'config', + ...servicePorts.map(sp => `no service-port ${sp}`), + `interface gpon ${slot}`, + `onu ${onuId}`, + 'ethernet 1', + `native vlan vlan-id ${VLAN_IPOE}`, + 'top', + 'service-port new', + `gpon ${slot} onu ${onuId} gem 1 match vlan vlan-id ${VLAN_IPOE} action vlan replace vlan-id ${VLAN_IPOE}`, + 'commit', + 'top', + 'exit', + 'exit' + ]; + + const removeCount = servicePorts.length; + const ifaceIdx = 1 + removeCount; // interface gpon + const onuIdx = ifaceIdx + 1; // onu + const ethIdx = onuIdx + 1; // ethernet 1 + const vlanIdx = ethIdx + 1; // native vlan + const topAfterVlanIdx = vlanIdx + 1; // top (back to config) + const spNewIdx = topAfterVlanIdx + 1; // service-port new + const spCfgIdx = spNewIdx + 1; // gpon ... match vlan + const commitIdx = spCfgIdx + 1; // commit + const topAfterCommitIdx = commitIdx + 1; // top + const exit1Idx = topAfterCommitIdx + 1; // exit + const exit2Idx = exit1Idx + 1; // exit + + stream.on('data', (data) => { + const dataStr = data.toString(); + output += dataStr; + console.log(`📨 [IPoE] Recebido: ${dataStr.trim()}`); + + // Capturar novo service-port ID + if (dataStr.includes('(config-service-port-')) { + const match = dataStr.match(/\(config-service-port-(\d+)\)/); + if (match && !newServicePortId) { + newServicePortId = match[1]; + console.log(`✅ [IPoE] Novo service-port ID: ${newServicePortId}`); + } + } + + if (step < commands.length) { + let shouldSend = false; + + if (step === 0 && dataStr.includes('#')) { + shouldSend = true; // prompt inicial → config + } else if (step >= 1 && step <= removeCount && dataStr.includes('(config)#')) { + shouldSend = true; // modo config → no service-port X + } else if (step === ifaceIdx && dataStr.includes('(config)#')) { + shouldSend = true; // modo config → interface gpon + } else if (step === onuIdx && dataStr.includes('(config-gpon')) { + shouldSend = true; // modo gpon → onu + } else if (step === ethIdx && dataStr.includes('(config-gpon-onu')) { + shouldSend = true; // modo onu → ethernet 1 + } else if (step === vlanIdx && dataStr.includes('(config-ethernet')) { + shouldSend = true; // modo ethernet → native vlan + } else if (step === topAfterVlanIdx && dataStr.includes('(config-ethernet')) { + shouldSend = true; // após vlan → top + } else if (step === spNewIdx && dataStr.includes('(config)#')) { + shouldSend = true; // config → service-port new + } else if (step === spCfgIdx && dataStr.includes('(config-service-port')) { + shouldSend = true; // service-port → gpon ... match vlan + } else if (step === commitIdx && dataStr.includes('(config-service-port')) { + shouldSend = true; // service-port → commit + } else if (step === topAfterCommitIdx && dataStr.includes('(config-service-port')) { + shouldSend = true; // service-port → top + } else if (step === exit1Idx && dataStr.includes('(config)#')) { + shouldSend = true; // config → exit + } else if (step === exit2Idx && dataStr.includes('#') && !dataStr.includes('(config')) { + shouldSend = true; // prompt → exit + } + + if (shouldSend) { + console.log(`📝 [IPoE] Comando ${step + 1}/${commands.length}: ${commands[step]}`); + stream.write(commands[step] + '\n'); + step++; + } + } else if (!isComplete && dataStr.includes('#') && !dataStr.includes('(config')) { + isComplete = true; + setTimeout(() => { stream.end(); }, 1000); + } + }); + + stream.on('close', () => { + this.disconnect(connection.connectionId); + console.log(`✅ [IPoE Datacom] Concluído - ONU ${onuId} no slot ${slot}`); + + resolve({ + success: true, + message: `VLAN alterada para ${VLAN_IPOE} (IPoE)`, + output: output.trim(), + slot, + onuId, + vlanId: VLAN_IPOE, + oldServicePorts: servicePorts, + newServicePortId + }); + }); + + stream.on('error', (err) => { + this.disconnect(connection.connectionId); + reject(new Error(`Erro no shell: ${err.message}`)); + }); + + setTimeout(() => { + if (!stream.destroyed && !isComplete) { + console.log(`⏰ [IPoE Datacom] Timeout`); + stream.end(); + this.disconnect(connection.connectionId); + reject(new Error('Timeout na operação IPoE')); + } + }, 90000); + }); + + } catch (error) { + console.error(`❌ [IPoE Datacom] Erro:`, error); + reject(new Error(`Erro IPoE Datacom: ${error.message}`)); + } + }); + } } module.exports = SSHManager; diff --git a/src/topsapp/api.js b/src/topsapp/api.js index b958f7e..6fc36cb 100644 --- a/src/topsapp/api.js +++ b/src/topsapp/api.js @@ -916,26 +916,9 @@ class TopSappAPI { } } - /** - * Configurar serviço para IPoE (DHCP Option 82) - * Define dhcp_username, dhcp_agent_circuit_id e dhcp_liberar no serviço - */ - async configurarIPoE(idCliente, idServico, dhcpAgentCircuitId, location = '', credentials) { - const instance = this.getInstanceByLocation(location); - try { - const session = await this.getValidSession(instance, credentials); - return await this.makeRequest('AlterarServico', { - idUsuario: session.idUsuario || 1, - idCliente: parseInt(idCliente), - idServico: parseInt(idServico), - dhcp_username: dhcpAgentCircuitId, - dhcp_agent_circuit_id: dhcpAgentCircuitId, - dhcp_liberar: '1' - }, instance, credentials); - } catch (error) { - return { success: false, error: error.message }; - } - } + // Nota: AlterarServico não suporta campos DHCP (dhcp_username, dhcp_agent_circuit_id, dhcp_liberar). + // Esses campos só existem no CadastrarClienteServico (criação de serviço novo). + // A configuração IPoE é gerada pelo bot e deve ser aplicada manualmente no painel TopSapp. /** * Listar todas as instâncias disponíveis diff --git a/src/topsapp/manager.js b/src/topsapp/manager.js index 25fcf91..18426ea 100644 --- a/src/topsapp/manager.js +++ b/src/topsapp/manager.js @@ -1309,23 +1309,21 @@ class TopSappManager { } const circuitId = `${prefixo}:${serial}`; + const login = servico.usuario || servico.pppoe_login || servico.login || ''; - logger.info(`[IPoE] Configurando serviço ${idServico} do cliente ${idCliente}: circuit_id=${circuitId}`); + logger.info(`[IPoE] Dados gerados para serviço ${idServico} do cliente ${idCliente}: circuit_id=${circuitId}`); - const resultado = await this.api.configurarIPoE(idCliente, idServico, circuitId, location, credentials); - - if (resultado.success) { - return { - success: true, - circuitId, - prefixo, - serial, - baseInfo, - servico: servico.descricao || servico.nome || 'N/A' - }; - } - - return resultado; + // A API AlterarServico não suporta campos DHCP. + // Retornamos os dados calculados para configuração manual no painel TopSapp. + return { + success: true, + circuitId, + prefixo, + serial, + baseInfo, + login, + servico: servico.descricao || servico.nome || 'N/A' + }; } catch (error) { return { success: false, error: error.message }; }